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:
Jack ThomasandJason authored and GitHub committed 2026-09-04 12:50:28 +08:00
1 parent bc4ed66d3a
commit e47b5fca15
5 files changed
+348 -67

No files matched your search

+80 -6
View File
@@ -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");
+184 -3
View File
@@ -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]
+45 -38
View File
@@ -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",
+30 -16
View File
@@ -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"]],