mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 19:31:28 +08:00
feat(console): dual-protocol provider presets + fix sub-path model discovery
Console — provider presets inside the model editor: - Preset chips for five coding-plan providers (Zhipu GLM / Kimi / DeepSeek / Volcengine Ark / MiniMax) rendered at the top of the add/edit model dialog - All five providers expose both an Anthropic-protocol and an OpenAI-protocol endpoint; a click fills the endpoint matching the current model type, and switching the type automatically swaps to the same provider's other endpoint (no stale URL/protocol mismatches) - Switching to a different provider clears the API key; switching protocols within the same provider keeps it - Fills base URL (full-request-URL form for irregular paths), request protocol, claude-cli custom headers for Anthropic endpoints, token limits, remark, and the default model id + display name; known preset model ids merge into the model-name combobox Server — model discovery for sub-path endpoints: - model_discovery_url previously replaced the whole path with /v1/models, which strips the /api/anthropic, /coding, /api/paas/v4 style prefixes every coding-plan provider uses, so discovery always 404'd for them. It now appends to the existing path and normalizes endpoint/version suffixes (/chat/completions, /responses, /messages, /v1, /v3, /v4) - Verified live against Zhipu (10 models) and Kimi (4 models) coding plans; existing tests updated to the append semantics plus new URL cases Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5450fc76e2
commit
75babb7334
@@ -659,7 +659,32 @@ fn model_discovery_url(base_url: &str) -> Result<Url> {
|
||||
"model request URL must contain a host".into(),
|
||||
));
|
||||
}
|
||||
url.set_path("/v1/models");
|
||||
// 在现有路径上追加,而不是整段替换:多数编程套餐的 API 挂在子路径下
|
||||
// (/api/anthropic、/coding、/api/paas/v4 等),直接 set_path("/v1/models")
|
||||
// 会把这些前缀吃掉,发现请求必然 404
|
||||
let path = url.path().trim_end_matches('/');
|
||||
let last = path.rsplit('/').next().unwrap_or("");
|
||||
let versioned = last.len() > 1
|
||||
&& last.starts_with('v')
|
||||
&& last[1..].bytes().all(|byte| byte.is_ascii_digit());
|
||||
let new_path = if let Some(parent) = path.strip_suffix("/chat/completions") {
|
||||
// 完整请求 URL:剥掉端点段(chat/completions 是两段),换成 models
|
||||
format!("{parent}/models")
|
||||
} else if let Some(parent) = path
|
||||
.strip_suffix("/responses")
|
||||
.or_else(|| path.strip_suffix("/messages"))
|
||||
.or_else(|| path.strip_suffix("/completions"))
|
||||
{
|
||||
format!("{parent}/models")
|
||||
} else if path.is_empty() {
|
||||
"/v1/models".to_string()
|
||||
} else if versioned {
|
||||
// 已带版本段(/v1、/api/v3、/api/paas/v4):只补 models
|
||||
format!("{path}/models")
|
||||
} else {
|
||||
format!("{path}/v1/models")
|
||||
};
|
||||
url.set_path(&new_path);
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
Ok(url)
|
||||
@@ -797,7 +822,25 @@ mod tests {
|
||||
store::Store,
|
||||
};
|
||||
|
||||
use super::ControlService;
|
||||
use super::{model_discovery_url, ControlService};
|
||||
|
||||
#[test]
|
||||
fn model_discovery_url_appends_to_path() {
|
||||
let cases = [
|
||||
("https://api.deepseek.com", "https://api.deepseek.com/v1/models"),
|
||||
("https://open.bigmodel.cn/api/anthropic", "https://open.bigmodel.cn/api/anthropic/v1/models"),
|
||||
("https://api.kimi.com/coding", "https://api.kimi.com/coding/v1/models"),
|
||||
("https://api.moonshot.cn/v1", "https://api.moonshot.cn/v1/models"),
|
||||
("https://ark.cn-beijing.volces.com/api/v3", "https://ark.cn-beijing.volces.com/api/v3/models"),
|
||||
(
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4/models",
|
||||
),
|
||||
];
|
||||
for (base, expected) in cases {
|
||||
assert_eq!(model_discovery_url(base).unwrap().as_str(), expected, "base: {base}");
|
||||
}
|
||||
}
|
||||
|
||||
struct TestProvider {
|
||||
invocation: Arc<Mutex<Option<ModelInvocation>>>,
|
||||
@@ -899,12 +942,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_discovery_url_uses_only_the_provider_origin() {
|
||||
fn model_discovery_url_keeps_provider_path_prefix() {
|
||||
assert_eq!(
|
||||
super::model_discovery_url("https://example.com:8443/arbitrary/v1/chat/completions")
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
"https://example.com:8443/v1/models"
|
||||
"https://example.com:8443/arbitrary/v1/models"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -933,7 +976,7 @@ mod tests {
|
||||
|
||||
let (sender, mut requests) = tokio::sync::mpsc::unbounded_channel();
|
||||
let app = axum::Router::new()
|
||||
.route("/v1/models", axum::routing::get(models))
|
||||
.route("/custom/models", axum::routing::get(models))
|
||||
.with_state(sender);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
@@ -970,7 +1013,8 @@ mod tests {
|
||||
assert_eq!(result.models, vec!["model-a"]);
|
||||
let (method, uri, headers, body) = requests.recv().await.unwrap();
|
||||
assert_eq!(method, axum::http::Method::GET);
|
||||
assert_eq!(uri.path(), "/v1/models");
|
||||
// /custom/responses 剥掉端点段后是 /custom,发现地址为 /custom/models
|
||||
assert_eq!(uri.path(), "/custom/models");
|
||||
assert!(body.is_empty());
|
||||
assert!(headers.get(axum::http::header::USER_AGENT).is_none());
|
||||
assert_eq!(headers.get("x-tenant").unwrap(), "tenant-a");
|
||||
|
||||
Reference in New Issue
Block a user