mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(codex): point Zhipu GLM presets at the official Responses endpoint (#6957)
Zhipu documents three separate base URLs per site (Anthropic /api/anthropic, Chat /api/coding/paas/v4, Responses /api/v1) and warns that the wrong one cannot use Coding Plan quota. Codex direct-connect speaks Responses, so the presets must target /api/v1; the 400 in #6944 (`unknown variant custom`) came from the Chat endpoint's strict legacy gateway. - Switch the Zhipu GLM / Zhipu GLM en presets to apiFormat openai_responses (NativeResponses catalog: shell_command edits, no freeform apply_patch) - Point base_url/endpointCandidates at /api/v1 and mirror the official models.json: glm-5.3 (1M context, low/high/max, default max) plus glm-5-turbo on the CN site; drop glm-5.2 and the `none` level - Host fallback: rows saved with a stale openai_chat apiFormat but a native Responses base_url get the NativeResponses catalog without a re-save; Chat-endpoint paths (/paas/v4, /chat/completions) are excluded - Match vendor hosts on DNS label boundaries via a shared helper, so `z.ai` no longer captures xyz.ai; add bigmodel.cn / z.ai and the glm prefix to the Codex web_search reject list Existing Zhipu providers still point at the Chat endpoint; re-import the preset to pick up /api/v1. Fixes #6944 Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
1 parent
bc4ed66d3a
commit
e47b5fca15
5 files changed
+348
-67
No files matched your search
@@ -68,6 +68,13 @@ const CODEX_WEB_SEARCH_REJECT_HOSTS: &[&str] = &[
|
||||
"longcat.chat", // Meituan LongCat (api.longcat.chat)
|
||||
"minimax.io", // MiniMax global (api.minimax.io)
|
||||
"minimaxi.com", // MiniMax CN (api.minimaxi.com)
|
||||
// Zhipu GLM CN / global (open.bigmodel.cn, api.z.ai): the native Responses
|
||||
// gateway's tool-type enum is `function | web_search_preview |
|
||||
// code_interpreter | mcp` (verbatim from the #6944 400 body) — Codex's
|
||||
// `web_search` hosted tool is not in it. Matched on host labels (see
|
||||
// `codex_url_host_matches_any`), so `xyz.ai` never collides with `z.ai`.
|
||||
"bigmodel.cn",
|
||||
"z.ai",
|
||||
];
|
||||
|
||||
/// Brand prefixes of models whose native gateways reject `web_search`, matched
|
||||
@@ -75,7 +82,41 @@ const CODEX_WEB_SEARCH_REJECT_HOSTS: &[&str] = &[
|
||||
/// `MiniMaxAI/MiniMax-M3` are caught. Exact brand names (not a fuzzy heuristic),
|
||||
/// so a supporting gateway is never wrongly matched.
|
||||
const CODEX_WEB_SEARCH_REJECT_MODEL_PREFIXES: &[&str] =
|
||||
&["mimo", "longcat", "minimax", "qwen3-coder"];
|
||||
&["mimo", "longcat", "minimax", "qwen3-coder", "glm"];
|
||||
|
||||
/// Host component of a base URL (or a bare host), lowercased, without scheme,
|
||||
/// userinfo, port, path or query. Tolerates the loose forms users paste into
|
||||
/// the provider form (`example.com`, `https://user@Example.com:8443/v1`).
|
||||
pub(crate) fn codex_url_host(url_or_host: &str) -> String {
|
||||
let trimmed = url_or_host.trim();
|
||||
let rest = trimmed
|
||||
.split_once("://")
|
||||
.map_or(trimmed, |(_scheme, rest)| rest);
|
||||
let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
|
||||
let host_port = authority.rsplit('@').next().unwrap_or(authority);
|
||||
let host = if let Some(ipv6) = host_port.strip_prefix('[') {
|
||||
ipv6.split(']').next().unwrap_or(ipv6)
|
||||
} else {
|
||||
host_port.split(':').next().unwrap_or(host_port)
|
||||
};
|
||||
host.trim_end_matches('.').to_ascii_lowercase()
|
||||
}
|
||||
|
||||
/// Whether the URL's host IS one of `hosts` or a subdomain of it, matched on
|
||||
/// DNS label boundaries. Vendor host lists must go through this rather than a
|
||||
/// substring `contains`: a 4-char entry like `z.ai` would otherwise also match
|
||||
/// `api.xyz.ai` / `viz.ai` and silently push an unrelated provider onto a
|
||||
/// vendor-specific code path.
|
||||
pub(crate) fn codex_url_host_matches_any(url_or_host: &str, hosts: &[&str]) -> bool {
|
||||
let host = codex_url_host(url_or_host);
|
||||
if host.is_empty() {
|
||||
return false;
|
||||
}
|
||||
hosts.iter().any(|candidate| {
|
||||
let candidate = candidate.trim_start_matches('.').to_ascii_lowercase();
|
||||
host == candidate || host.ends_with(&format!(".{candidate}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Top-level `model` id from a Codex `config.toml`.
|
||||
fn codex_top_level_model(config_text: &str) -> Option<String> {
|
||||
@@ -92,11 +133,7 @@ fn codex_top_level_model(config_text: &str) -> Option<String> {
|
||||
/// the live `config.toml`, so it applies to existing providers without a re-save.
|
||||
fn codex_native_gateway_rejects_web_search(config_text: &str) -> bool {
|
||||
if let Some(base_url) = extract_codex_base_url(config_text) {
|
||||
let base_url = base_url.to_ascii_lowercase();
|
||||
if CODEX_WEB_SEARCH_REJECT_HOSTS
|
||||
.iter()
|
||||
.any(|host| base_url.contains(host))
|
||||
{
|
||||
if codex_url_host_matches_any(&base_url, CODEX_WEB_SEARCH_REJECT_HOSTS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7250,6 +7287,8 @@ web_search = "disabled"
|
||||
("LongCat-2.0", "https://api.longcat.chat/openai/v1"),
|
||||
("MiniMax-M3", "https://api.minimax.io/v1"),
|
||||
("MiniMax-M3", "https://api.minimaxi.com/v1"),
|
||||
("glm-5.3", "https://open.bigmodel.cn/api/v1"),
|
||||
("glm-5.3", "https://api.z.ai/api/v1"),
|
||||
] {
|
||||
assert!(
|
||||
codex_native_gateway_rejects_web_search(&cfg(model, host)),
|
||||
@@ -7263,6 +7302,7 @@ web_search = "disabled"
|
||||
("MiniMax-M3", "https://api.siliconflow.cn/v1"),
|
||||
("MiniMaxAI/MiniMax-M3", "https://api.siliconflow.cn/v1"),
|
||||
("mimo-v2.5-pro", "https://some-aggregator.example/v1"),
|
||||
("zai-org/glm-5.3", "https://some-aggregator.example/v1"),
|
||||
(
|
||||
"qwen/qwen3-coder-plus",
|
||||
"https://some-aggregator.example/v1",
|
||||
@@ -7296,6 +7336,12 @@ web_search = "disabled"
|
||||
"https://ark.cn-beijing.volces.com/api/v3",
|
||||
),
|
||||
("Pro/moonshotai/Kimi-K2.6", "https://api.siliconflow.cn/v1"),
|
||||
// Host-label matching: `z.ai` / `bigmodel.cn` must not swallow
|
||||
// unrelated domains that merely contain them as a substring.
|
||||
("gpt-5.5", "https://api.xyz.ai/v1"),
|
||||
("gpt-5.5", "https://viz.ai/v1"),
|
||||
("gpt-5.5", "https://notbigmodel.cn/v1"),
|
||||
("gpt-5.5", "https://z.ai.example.com/v1"),
|
||||
] {
|
||||
assert!(
|
||||
!codex_native_gateway_rejects_web_search(&cfg(model, host)),
|
||||
@@ -7304,6 +7350,34 @@ web_search = "disabled"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_host_matcher_uses_label_boundaries() {
|
||||
let hosts = &["z.ai", "bigmodel.cn"];
|
||||
for url in [
|
||||
"https://api.z.ai/api/v1",
|
||||
"https://open.bigmodel.cn/api/v1",
|
||||
"https://Open.BigModel.cn/api/coding/paas/v4",
|
||||
"https://user:pw@api.z.ai:8443/api/v1?x=1#f",
|
||||
"z.ai",
|
||||
"api.z.ai.",
|
||||
] {
|
||||
assert!(codex_url_host_matches_any(url, hosts), "{url}");
|
||||
}
|
||||
for url in [
|
||||
"https://api.xyz.ai/v1",
|
||||
"https://viz.ai/v1",
|
||||
"https://z.ai.example.com/v1",
|
||||
"https://notbigmodel.cn/v1",
|
||||
"https://example.com/z.ai/v1",
|
||||
"https://example.com/?next=https://api.z.ai",
|
||||
"",
|
||||
] {
|
||||
assert!(!codex_url_host_matches_any(url, hosts), "{url}");
|
||||
}
|
||||
assert_eq!(codex_url_host("https://[::1]:8080/v1"), "::1");
|
||||
assert_eq!(codex_url_host("HTTP://Example.COM:80"), "example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_catalog_path_returns_none_when_config_missing_field() {
|
||||
let base = PathBuf::from("/tmp/.codex");
|
||||
|
||||
@@ -333,6 +333,47 @@ pub fn is_codex_official_provider(provider: &Provider) -> bool {
|
||||
is_fixed_official_id || provider.category.as_deref() == Some("official")
|
||||
}
|
||||
|
||||
/// Vendors whose OFFICIAL Codex integration is a native `/responses` gateway that
|
||||
/// rejects Codex's freeform custom tools (`apply_patch` with `type: "custom"`,
|
||||
/// #6944). Same vendor set as `CODEX_WEB_SEARCH_REJECT_HOSTS` in `codex_config`
|
||||
/// (kept separate: that list also gates aggregators by model brand). Matched on
|
||||
/// host labels via `codex_url_host_matches_any`, never by substring.
|
||||
const CODEX_NATIVE_RESPONSES_HOSTS: &[&str] = &[
|
||||
"bigmodel.cn",
|
||||
"z.ai",
|
||||
"xiaomimimo.com",
|
||||
"minimaxi.com",
|
||||
"minimax.io",
|
||||
"longcat.chat",
|
||||
];
|
||||
|
||||
/// Path markers of a listed vendor's OpenAI *Chat Completions* endpoint, which is
|
||||
/// NOT its Responses endpoint. Zhipu documents three separate base URLs per site
|
||||
/// (Anthropic `/api/anthropic`, Chat `/api/coding/paas/v4` + pay-as-you-go
|
||||
/// `/api/paas/v4`, Responses `/api/v1`) and warns that the wrong one cannot use
|
||||
/// Coding Plan quota. A stored provider still pointing at a Chat path is a
|
||||
/// pre-2026-09 Chat-route record: it keeps its `ProxyChat` catalog (the proxy
|
||||
/// route converts it correctly; direct connect fails loudly with the #6944 400
|
||||
/// until the preset is re-imported) instead of being silently steered onto the
|
||||
/// wrong endpoint with a native catalog.
|
||||
const CODEX_NATIVE_RESPONSES_CHAT_PATH_MARKERS: &[&str] = &["/paas/v4"];
|
||||
|
||||
/// Whether `base_url` points at a listed vendor's native Responses gateway, so a
|
||||
/// provider whose stored `apiFormat` predates the preset's switch to
|
||||
/// `openai_responses` still gets the `NativeResponses` catalog without a re-save.
|
||||
pub fn is_codex_native_responses_url(base_url: &str) -> bool {
|
||||
if !crate::codex_config::codex_url_host_matches_any(base_url, CODEX_NATIVE_RESPONSES_HOSTS) {
|
||||
return false;
|
||||
}
|
||||
if is_chat_completions_url(base_url) {
|
||||
return false;
|
||||
}
|
||||
let lower = base_url.to_ascii_lowercase();
|
||||
!CODEX_NATIVE_RESPONSES_CHAT_PATH_MARKERS
|
||||
.iter()
|
||||
.any(|marker| lower.contains(marker))
|
||||
}
|
||||
|
||||
/// Resolve the model-catalog tool profile for a Codex provider using the SAME
|
||||
/// Anthropic detection as the proxy router ([`codex_provider_uses_anthropic`]), so the
|
||||
/// generated catalog never disagrees with the routed transform. A provider whose
|
||||
@@ -355,9 +396,49 @@ pub fn resolve_codex_catalog_tool_profile(
|
||||
if codex_provider_uses_anthropic(provider) {
|
||||
return CodexCatalogToolProfile::Anthropic;
|
||||
}
|
||||
CodexCatalogToolProfile::from_api_format(
|
||||
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
|
||||
)
|
||||
|
||||
// Defensive fallback for providers saved in SQLite before their preset
|
||||
// switched to `openai_responses` (the #6944 reporter reinstalled to no
|
||||
// effect precisely because the stale `apiFormat` lives in the DB row): a
|
||||
// base_url on a listed vendor's native Responses gateway forces the
|
||||
// NativeResponses catalog. Chat-endpoint paths are deliberately excluded —
|
||||
// see `CODEX_NATIVE_RESPONSES_CHAT_PATH_MARKERS`.
|
||||
if let Some(base_url) = provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(extract_codex_base_url_from_toml)
|
||||
.or_else(|| {
|
||||
provider
|
||||
.settings_config
|
||||
.get("base_url")
|
||||
.or_else(|| provider.settings_config.get("baseURL"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
{
|
||||
if is_codex_native_responses_url(&base_url) {
|
||||
return CodexCatalogToolProfile::NativeResponses;
|
||||
}
|
||||
}
|
||||
|
||||
let api_format = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.api_format.as_deref())
|
||||
.or_else(|| {
|
||||
provider
|
||||
.settings_config
|
||||
.get("api_format")
|
||||
.and_then(|v| v.as_str())
|
||||
})
|
||||
.or_else(|| {
|
||||
provider
|
||||
.settings_config
|
||||
.get("apiFormat")
|
||||
.and_then(|v| v.as_str())
|
||||
});
|
||||
CodexCatalogToolProfile::from_api_format(api_format)
|
||||
}
|
||||
|
||||
/// Extract the real upstream model configured for a Codex provider.
|
||||
@@ -1457,6 +1538,106 @@ wire_api = "anthropic"
|
||||
resolve_codex_catalog_tool_profile(&chat),
|
||||
CodexCatalogToolProfile::ProxyChat
|
||||
);
|
||||
|
||||
// Host fallback (#6944): a DB row saved while the Zhipu preset was still
|
||||
// `openai_chat` but whose base_url is the vendor's native Responses
|
||||
// gateway (`/api/v1`) resolves to NativeResponses without a re-save —
|
||||
// whether the URL lives in the TOML or in settings `baseURL`.
|
||||
let legacy_chat_toml = |base_url: &str| {
|
||||
create_provider(json!({
|
||||
"apiFormat": "openai_chat",
|
||||
"config": format!(
|
||||
"model_provider = \"custom\"\n\n[model_providers.custom]\nname = \"zhipu_glm\"\nbase_url = \"{base_url}\"\nwire_api = \"responses\"\n"
|
||||
)
|
||||
}))
|
||||
};
|
||||
for base_url in [
|
||||
"https://open.bigmodel.cn/api/v1",
|
||||
"https://api.z.ai/api/v1",
|
||||
"https://Open.BigModel.cn/api/v1/",
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_codex_catalog_tool_profile(&legacy_chat_toml(base_url)),
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
"{base_url}"
|
||||
);
|
||||
}
|
||||
let legacy_chat_settings = create_provider(json!({
|
||||
"apiFormat": "openai_chat",
|
||||
"baseURL": "https://api.z.ai/api/v1"
|
||||
}));
|
||||
assert_eq!(
|
||||
resolve_codex_catalog_tool_profile(&legacy_chat_settings),
|
||||
CodexCatalogToolProfile::NativeResponses
|
||||
);
|
||||
|
||||
// The vendor's Chat Completions endpoints are NOT its Responses gateway
|
||||
// (Zhipu: `/api/coding/paas/v4` Coding Plan, `/api/paas/v4` pay-as-you-go)
|
||||
// — a stale row there keeps ProxyChat so it is never steered onto the
|
||||
// endpoint Zhipu documents as unable to use Coding Plan quota.
|
||||
for base_url in [
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
"https://api.z.ai/api/coding/paas/v4",
|
||||
"https://open.bigmodel.cn/api/paas/v4",
|
||||
"https://open.bigmodel.cn/api/paas/v4/chat/completions",
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_codex_catalog_tool_profile(&legacy_chat_toml(base_url)),
|
||||
CodexCatalogToolProfile::ProxyChat,
|
||||
"{base_url}"
|
||||
);
|
||||
}
|
||||
|
||||
// Host matching is on DNS labels: `xyz.ai` must not be captured by the
|
||||
// 4-char `z.ai` entry (it would lose apply_patch and web_search).
|
||||
for base_url in [
|
||||
"https://api.xyz.ai/v1",
|
||||
"https://viz.ai/v1",
|
||||
"https://z.ai.example.com/v1",
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_codex_catalog_tool_profile(&legacy_chat_toml(base_url)),
|
||||
CodexCatalogToolProfile::ProxyChat,
|
||||
"{base_url}"
|
||||
);
|
||||
}
|
||||
|
||||
// An explicit `openai_responses` on an unlisted host is untouched by the
|
||||
// fallback (it only ever widens toward NativeResponses for listed hosts).
|
||||
let explicit_native = create_provider(json!({
|
||||
"apiFormat": "openai_responses",
|
||||
"baseURL": "https://api.xyz.ai/v1"
|
||||
}));
|
||||
assert_eq!(
|
||||
resolve_codex_catalog_tool_profile(&explicit_native),
|
||||
CodexCatalogToolProfile::NativeResponses
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_responses_url_guard_matches_gateway_not_chat_paths() {
|
||||
for url in [
|
||||
"https://open.bigmodel.cn/api/v1",
|
||||
"https://api.z.ai/api/v1",
|
||||
"https://api.xiaomimimo.com/v1",
|
||||
"https://token-plan-cn.xiaomimimo.com/v1",
|
||||
"https://api.minimaxi.com/v1",
|
||||
"https://api.minimax.io/v1",
|
||||
"https://api.longcat.chat/openai/v1",
|
||||
] {
|
||||
assert!(is_codex_native_responses_url(url), "{url}");
|
||||
}
|
||||
for url in [
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
"https://api.z.ai/api/coding/paas/v4",
|
||||
"https://open.bigmodel.cn/api/paas/v4",
|
||||
"https://api.minimaxi.com/v1/chat/completions",
|
||||
"https://api.xyz.ai/v1",
|
||||
"https://api.deepseek.com",
|
||||
"",
|
||||
] {
|
||||
assert!(!is_codex_native_responses_url(url), "{url}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1172,32 +1172,45 @@ requires_openai_auth = true`,
|
||||
websiteUrl: "https://open.bigmodel.cn",
|
||||
apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII",
|
||||
auth: generateThirdPartyAuth(""),
|
||||
// 智谱三端点分立(docs.bigmodel.cn/cn/coding-plan/tool/others):Anthropic
|
||||
// /api/anthropic、OpenAI Chat /api/coding/paas/v4、OpenAI Responses /api/v1,
|
||||
// 并明示「错误配置端点将导致无法使用 GLM Coding Plan 套餐额度」。Codex 直连
|
||||
// 发的是 Responses wire,base_url 必须是 /api/v1;Chat 端点上的 /responses
|
||||
// 是严格旧网关(拒 type=custom 工具 → #6944 的 400)
|
||||
config: generateThirdPartyConfig(
|
||||
"zhipu_glm",
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
"glm-5.2",
|
||||
"https://open.bigmodel.cn/api/v1",
|
||||
"glm-5.3",
|
||||
),
|
||||
endpointCandidates: ["https://open.bigmodel.cn/api/coding/paas/v4"],
|
||||
apiFormat: "openai_chat",
|
||||
endpointCandidates: ["https://open.bigmodel.cn/api/v1"],
|
||||
// 官方 Codex 接入页(docs.bigmodel.cn/cn/coding-plan/tool/codex,2026-09-04
|
||||
// 核对):wire_api=responses 对自家 /api/v1,与 MiMo/MiniMax 同为原生直连
|
||||
// → NativeResponses profile(shell_command 编辑、不发 freeform apply_patch;
|
||||
// 官方目录虽声明 freeform,无真机验证前按保守口径,不引入 400 风险)
|
||||
apiFormat: "openai_responses",
|
||||
// 档位/上下文/模态照抄官方 models.json:glm-5.3 low/high/max 默认 max;
|
||||
// glm-5-turbo 官方档位为空、默认 max——cc-switch 表达不了空档位(回落会得到
|
||||
// 模板 none/high,none 在原生直连下没有转换层兜底、会原样发给严格网关),
|
||||
// 按官方默认收成单档 max。两模型 input_modalities=["text"]、并行工具调用 true
|
||||
modelCatalog: modelCatalog([
|
||||
// Chat 路由 supportsEffort:false:档位值不进 wire,none=注入
|
||||
// thinking:{type:"disabled"} 关思考,其余档一律等价于开思考。只暴露真实
|
||||
// 两态;不填的话 gpt5_5 模板默认 low/medium/high/xhigh 全是假差异档,
|
||||
// 且没有 none,用户在 Codex 里反而关不掉思考
|
||||
{
|
||||
model: "glm-5.2",
|
||||
displayName: "GLM-5.2",
|
||||
contextWindow: 200000,
|
||||
reasoningLevels: ["none", "high"],
|
||||
model: "glm-5.3",
|
||||
displayName: "GLM-5.3",
|
||||
contextWindow: 1048576,
|
||||
inputModalities: ["text"],
|
||||
supportsParallelToolCalls: true,
|
||||
reasoningLevels: ["low", "high", "max"],
|
||||
defaultReasoningLevel: "max",
|
||||
},
|
||||
{
|
||||
model: "glm-5-turbo",
|
||||
displayName: "GLM-5-Turbo",
|
||||
contextWindow: 204800,
|
||||
inputModalities: ["text"],
|
||||
supportsParallelToolCalls: true,
|
||||
reasoningLevels: ["max"],
|
||||
},
|
||||
]),
|
||||
codexChatReasoning: {
|
||||
supportsThinking: true,
|
||||
supportsEffort: false,
|
||||
thinkingParam: "thinking",
|
||||
effortParam: "none",
|
||||
outputFormat: "reasoning_content",
|
||||
},
|
||||
category: "cn_official",
|
||||
icon: "zhipu",
|
||||
iconColor: "#0F62FE",
|
||||
@@ -1207,32 +1220,26 @@ requires_openai_auth = true`,
|
||||
websiteUrl: "https://z.ai",
|
||||
apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB",
|
||||
auth: generateThirdPartyAuth(""),
|
||||
// 国际站同上(docs.z.ai/devpack/tool/others + devpack/tool/codex,2026-09-04
|
||||
// 核对):Responses 端点 /api/v1,官方 models.json 仅列 glm-5.3
|
||||
config: generateThirdPartyConfig(
|
||||
"zhipu_glm_en",
|
||||
"https://api.z.ai/api/coding/paas/v4",
|
||||
"glm-5.2",
|
||||
"https://api.z.ai/api/v1",
|
||||
"glm-5.3",
|
||||
),
|
||||
endpointCandidates: ["https://api.z.ai/api/coding/paas/v4"],
|
||||
apiFormat: "openai_chat",
|
||||
endpointCandidates: ["https://api.z.ai/api/v1"],
|
||||
apiFormat: "openai_responses",
|
||||
modelCatalog: modelCatalog([
|
||||
// Chat 路由 supportsEffort:false:档位值不进 wire,none=注入
|
||||
// thinking:{type:"disabled"} 关思考,其余档一律等价于开思考。只暴露真实
|
||||
// 两态;不填的话 gpt5_5 模板默认 low/medium/high/xhigh 全是假差异档,
|
||||
// 且没有 none,用户在 Codex 里反而关不掉思考
|
||||
{
|
||||
model: "glm-5.2",
|
||||
displayName: "GLM-5.2",
|
||||
contextWindow: 200000,
|
||||
reasoningLevels: ["none", "high"],
|
||||
model: "glm-5.3",
|
||||
displayName: "GLM-5.3",
|
||||
contextWindow: 1048576,
|
||||
inputModalities: ["text"],
|
||||
supportsParallelToolCalls: true,
|
||||
reasoningLevels: ["low", "high", "max"],
|
||||
defaultReasoningLevel: "max",
|
||||
},
|
||||
]),
|
||||
codexChatReasoning: {
|
||||
supportsThinking: true,
|
||||
supportsEffort: false,
|
||||
thinkingParam: "thinking",
|
||||
effortParam: "none",
|
||||
outputFormat: "reasoning_content",
|
||||
},
|
||||
category: "cn_official",
|
||||
icon: "zhipu",
|
||||
iconColor: "#0F62FE",
|
||||
|
||||
@@ -10,22 +10,8 @@ const expectedChatPresets = new Map<
|
||||
string,
|
||||
{ baseUrl: string; contextWindows: Record<string, number> }
|
||||
>([
|
||||
// 火山 Agent Plan / Coding Plan 与 BytePlus 国际站(coding/v3)均已切
|
||||
// 火山 Agent Plan / Coding Plan 与 BytePlus 国际站(coding/v3)、智谱 GLM 均已切
|
||||
// 原生 Responses,见下方 native 清单
|
||||
[
|
||||
"Zhipu GLM",
|
||||
{
|
||||
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
contextWindows: { "glm-5.2": 200000 },
|
||||
},
|
||||
],
|
||||
[
|
||||
"Zhipu GLM en",
|
||||
{
|
||||
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
||||
contextWindows: { "glm-5.2": 200000 },
|
||||
},
|
||||
],
|
||||
[
|
||||
"Baidu Qianfan Coding Plan",
|
||||
{
|
||||
@@ -168,7 +154,7 @@ describe("Codex Chat provider presets", () => {
|
||||
it("uses native Responses API for migrated CN providers without local route mapping", () => {
|
||||
const nativeResponsesPresets = new Map<
|
||||
string,
|
||||
{ contextWindows: Record<string, number> }
|
||||
{ baseUrl?: string; contextWindows: Record<string, number> }
|
||||
>([
|
||||
// 官方 Codex 文档确认 Agent Plan /api/plan/v3 与 Coding Plan
|
||||
// /api/coding/v3 均支持 Responses API;BytePlus 国际站 coding/v3
|
||||
@@ -218,6 +204,25 @@ describe("Codex Chat provider presets", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
// 智谱三端点分立(Anthropic /api/anthropic、Chat /api/coding/paas/v4、
|
||||
// Responses /api/v1),官方明示错误端点无法使用 Coding Plan 套餐额度——
|
||||
// 原生 Responses 预设必须锁在 /api/v1(#6944;docs.bigmodel.cn/cn/coding-plan/
|
||||
// tool/codex 与 docs.z.ai/devpack/tool/codex 自带 models.json,2026-09-04 核对:
|
||||
// 国内站 glm-5.3 + glm-5-turbo,国际站仅 glm-5.3)
|
||||
[
|
||||
"Zhipu GLM",
|
||||
{
|
||||
baseUrl: "https://open.bigmodel.cn/api/v1",
|
||||
contextWindows: { "glm-5.3": 1048576, "glm-5-turbo": 204800 },
|
||||
},
|
||||
],
|
||||
[
|
||||
"Zhipu GLM en",
|
||||
{
|
||||
baseUrl: "https://api.z.ai/api/v1",
|
||||
contextWindows: { "glm-5.3": 1048576 },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
for (const [name, expected] of nativeResponsesPresets) {
|
||||
@@ -225,6 +230,15 @@ describe("Codex Chat provider presets", () => {
|
||||
|
||||
expect(preset, `${name} preset`).toBeDefined();
|
||||
expect(preset?.apiFormat).toBe("openai_responses");
|
||||
if (expected.baseUrl) {
|
||||
// 直连预设的 base_url 必须是厂商的 Responses 端点本身(不是同站的
|
||||
// Chat 端点);endpointCandidates 与主地址同路径档
|
||||
expect(extractCodexBaseUrl(preset?.config)).toBe(expected.baseUrl);
|
||||
expect(preset?.endpointCandidates).toContain(expected.baseUrl);
|
||||
expect(extractCodexModelName(preset?.config)).toBe(
|
||||
preset?.modelCatalog?.[0]?.model,
|
||||
);
|
||||
}
|
||||
// 原生 Responses 预设现在带 modelCatalog:cc-switch 直连时据此生成
|
||||
// ~/.codex 的 model-catalogs.json(shell_command 编辑、不发 freeform
|
||||
// apply_patch)。带 catalog 不再强制开“本地路由映射”——前端已按
|
||||
|
||||
@@ -93,10 +93,15 @@ describe("Codex preset pre-filled reasoning levels", () => {
|
||||
["Xiaomi MiMo", "mimo-v2.5", ["none", "high"]],
|
||||
["Xiaomi MiMo Token Plan (China)", "mimo-v2.5-pro", ["none", "high"]],
|
||||
["Xiaomi MiMo Token Plan (China)", "mimo-v2.5", ["none", "high"]],
|
||||
// GLM 走 Chat 路由(supportsEffort:false):none=真实关思考开关,其余档
|
||||
// 等价开思考;只暴露两态,顺带补上模板四档里缺失的 none(关思考入口)
|
||||
["Zhipu GLM", "glm-5.2", ["none", "high"]],
|
||||
["Zhipu GLM en", "glm-5.2", ["none", "high"]],
|
||||
// 智谱官方 Codex 接入页自带 models.json(docs.bigmodel.cn/cn/coding-plan/tool/
|
||||
// codex、docs.z.ai/devpack/tool/codex,2026-09-04 核对):glm-5.3 档位
|
||||
// low/high/max、默认 max(≠ 后端回落的模板默认 high,故显式声明);
|
||||
// glm-5-turbo 官方档位为空、默认 max——cc-switch 表达不了空档位(会回落
|
||||
// 到模板 none/high,而 none 在原生直连下没有转换层兜底、会原样发给严格
|
||||
// 网关),按官方默认收成单档 max
|
||||
["Zhipu GLM", "glm-5.3", ["low", "high", "max"], "max"],
|
||||
["Zhipu GLM", "glm-5-turbo", ["max"]],
|
||||
["Zhipu GLM en", "glm-5.3", ["low", "high", "max"], "max"],
|
||||
// SiliconFlow .com 的 M3:平台级 enable_thinking 布尔开关(后端按平台
|
||||
// 推断兜底),M3 官方可关思考 → 两态
|
||||
["SiliconFlow en", "MiniMaxAI/MiniMax-M3", ["none", "high"]],
|
||||
|
||||
Reference in new issue
Block a user