From e891f5c876b010bc223d587a62fe83e59b2def43 Mon Sep 17 00:00:00 2001 From: makoMakoGo <48956204+makoMakoGo@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:53:57 +0800 Subject: [PATCH] [codex] fix Zhipu coding plan presets (#3524) * fix(presets): update Zhipu coding plan endpoints * fix(model-fetch): probe /models on versioned /vN base URLs The model-list probe assumed any base URL not ending in /v1 needs /v1/models appended. For providers whose base URL already ends in a version segment like /v4 (Zhipu/Z.AI GLM Coding Plan at .../api/coding/paas/v4), this produced .../v4/v1/models which 404s, so the "Fetch models" button always failed. Detect a trailing /v{N} version segment and probe {base}/models first, keeping /v1/models as a fallback candidate for non-/v1 versions. Fixes Codex/OpenCode/OpenClaw/Hermes GLM presets and any other vN-style endpoint, with no behavior change for /v1 or non-versioned URLs. --------- Co-authored-by: Jason --- src-tauri/src/services/model_fetch.rs | 74 +++++++++++++++++-- src/config/codexProviderPresets.ts | 8 +- src/config/hermesProviderPresets.ts | 4 +- src/config/openclawProviderPresets.ts | 12 +-- src/config/opencodeProviderPresets.ts | 12 +-- tests/config/codexChatProviderPresets.test.ts | 4 +- 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/services/model_fetch.rs b/src-tauri/src/services/model_fetch.rs index 580ddda88..b495c9fb9 100644 --- a/src-tauri/src/services/model_fetch.rs +++ b/src-tauri/src/services/model_fetch.rs @@ -121,9 +121,10 @@ pub async fn fetch_models( /// /// 候选顺序: /// 1. `models_url_override` 非空 → 只返回它 -/// 2. baseURL 直接拼 `/v1/models`(若已有 `/v1` 结尾则拼 `/models`) -/// 3. 若 baseURL 命中 [`KNOWN_COMPAT_SUFFIXES`],剥离后缀再拼 `/v1/models` -/// 4. 同上,但拼 `/models`(部分站点如 DeepSeek 官方只暴露 `/models`) +/// 2. baseURL 拼 `/v1/models`;若已以版本段 `/v{N}` 结尾(`/v1`、智谱 +/// `/api/coding/paas/v4` 等),版本号已在路径里,改拼 `/models` +/// 3. 版本段非 `/v1`(如 `/v4`)时再追加 `/v1/models` 作为兜底次候选 +/// 4. 若 baseURL 命中 [`KNOWN_COMPAT_SUFFIXES`],剥离后缀再拼 `/v1/models`、`/models` /// /// 结果已去重且保持首次出现顺序。 pub fn build_models_url_candidates( @@ -160,12 +161,18 @@ pub fn build_models_url_candidates( return Ok(candidates); } - let primary = if trimmed.ends_with("/v1") { - format!("{trimmed}/models") + // baseURL 已以版本段 /v{N} 结尾时(如 `/v1`、智谱 `/api/coding/paas/v4`), + // OpenAI 惯例的模型端点是 `{base}/models`,不能再补 `/v1` + // (否则 .../coding/paas/v4/v1/models → 404)。 + if ends_with_version_segment(trimmed) { + candidates.push(format!("{trimmed}/models")); + // 版本段非 /v1 时,保留旧的 /v1/models 作为兜底次候选(正确路径已在前)。 + if !trimmed.ends_with("/v1") { + candidates.push(format!("{trimmed}/v1/models")); + } } else { - format!("{trimmed}/v1/models") - }; - candidates.push(primary); + candidates.push(format!("{trimmed}/v1/models")); + } if let Some(stripped) = strip_compat_suffix(trimmed) { let root = stripped.trim_end_matches('/'); @@ -210,6 +217,15 @@ fn strip_compat_suffix(base_url: &str) -> Option<&str> { None } +/// 判断 baseURL 是否以 OpenAI 风格的版本段 `/v{N}` 结尾(`N` 为一个或多个数字), +/// 例如 `/v1`、`.../paas/v4`。这类 URL 版本号已在路径中,模型端点应为 +/// `{base}/models`,不能再补 `/v1`(智谱 Coding Plan 即 `.../coding/paas/v4`)。 +fn ends_with_version_segment(url: &str) -> bool { + let last = url.rsplit('/').next().unwrap_or(""); + last.strip_prefix('v') + .is_some_and(|digits| !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())) +} + #[cfg(test)] mod tests { use super::*; @@ -232,6 +248,48 @@ mod tests { assert_eq!(c, vec!["https://api.example.com/v1/models"]); } + #[test] + fn test_candidates_zhipu_coding_paas_v4() { + // 智谱 Coding Plan 端点以 /v4 版本段结尾:模型端点是 {base}/models, + // 正确路径必须排在 .../v4/v1/models(404)之前。 + let c = + build_models_url_candidates("https://open.bigmodel.cn/api/coding/paas/v4", false, None) + .unwrap(); + assert_eq!( + c, + vec![ + "https://open.bigmodel.cn/api/coding/paas/v4/models", + "https://open.bigmodel.cn/api/coding/paas/v4/v1/models", + ] + ); + } + + #[test] + fn test_candidates_zai_coding_paas_v4() { + let c = build_models_url_candidates("https://api.z.ai/api/coding/paas/v4", false, None) + .unwrap(); + assert_eq!( + c, + vec![ + "https://api.z.ai/api/coding/paas/v4/models", + "https://api.z.ai/api/coding/paas/v4/v1/models", + ] + ); + } + + #[test] + fn test_ends_with_version_segment() { + assert!(ends_with_version_segment("https://x.com/v1")); + assert!(ends_with_version_segment( + "https://open.bigmodel.cn/api/coding/paas/v4" + )); + assert!(ends_with_version_segment("https://x.com/v10")); + assert!(!ends_with_version_segment("https://x.com/api")); + assert!(!ends_with_version_segment("https://x.com/vX")); + assert!(!ends_with_version_segment("https://x.com/models")); + assert!(!ends_with_version_segment("https://api.siliconflow.cn")); + } + #[test] fn test_candidates_full_url() { let c = build_models_url_candidates( diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index d2a7bc39c..4e906ada8 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -286,10 +286,10 @@ requires_openai_auth = true`, auth: generateThirdPartyAuth(""), config: generateThirdPartyConfig( "zhipu_glm", - "https://open.bigmodel.cn/api/paas/v4", + "https://open.bigmodel.cn/api/coding/paas/v4", "glm-5.1", ), - endpointCandidates: ["https://open.bigmodel.cn/api/paas/v4"], + endpointCandidates: ["https://open.bigmodel.cn/api/coding/paas/v4"], apiFormat: "openai_chat", modelCatalog: modelCatalog([ { model: "glm-5.1", displayName: "GLM-5.1", contextWindow: 200000 }, @@ -312,10 +312,10 @@ requires_openai_auth = true`, auth: generateThirdPartyAuth(""), config: generateThirdPartyConfig( "zhipu_glm_en", - "https://api.z.ai/api/paas/v4", + "https://api.z.ai/api/coding/paas/v4", "glm-5.1", ), - endpointCandidates: ["https://api.z.ai/api/paas/v4"], + endpointCandidates: ["https://api.z.ai/api/coding/paas/v4"], apiFormat: "openai_chat", modelCatalog: modelCatalog([ { model: "glm-5.1", displayName: "GLM-5.1", contextWindow: 200000 }, diff --git a/src/config/hermesProviderPresets.ts b/src/config/hermesProviderPresets.ts index d81df42c3..31e9f3658 100644 --- a/src/config/hermesProviderPresets.ts +++ b/src/config/hermesProviderPresets.ts @@ -393,7 +393,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [ apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII", settingsConfig: { name: "zhipu_glm", - base_url: "https://open.bigmodel.cn/api/paas/v4", + base_url: "https://open.bigmodel.cn/api/coding/paas/v4", api_key: "", api_mode: "chat_completions", models: [{ id: "glm-5.1", name: "GLM-5.1" }], @@ -411,7 +411,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [ apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB", settingsConfig: { name: "zhipu_glm_en", - base_url: "https://api.z.ai/api/paas/v4", + base_url: "https://api.z.ai/api/coding/paas/v4", api_key: "", api_mode: "chat_completions", models: [{ id: "glm-5.1", name: "GLM-5.1" }], diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index 54b04007c..9fc704e94 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -306,7 +306,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://open.bigmodel.cn", apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII", settingsConfig: { - baseUrl: "https://open.bigmodel.cn/api/paas/v4", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", apiKey: "", api: "openai-completions", models: [ @@ -324,8 +324,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ templateValues: { baseUrl: { label: "Base URL", - placeholder: "https://open.bigmodel.cn/api/paas/v4", - defaultValue: "https://open.bigmodel.cn/api/paas/v4", + placeholder: "https://open.bigmodel.cn/api/coding/paas/v4", + defaultValue: "https://open.bigmodel.cn/api/coding/paas/v4", editorValue: "", }, apiKey: { @@ -344,7 +344,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ websiteUrl: "https://z.ai", apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB", settingsConfig: { - baseUrl: "https://api.z.ai/v1", + baseUrl: "https://api.z.ai/api/coding/paas/v4", apiKey: "", api: "openai-completions", models: [ @@ -362,8 +362,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [ templateValues: { baseUrl: { label: "Base URL", - placeholder: "https://api.z.ai/v1", - defaultValue: "https://api.z.ai/v1", + placeholder: "https://api.z.ai/api/coding/paas/v4", + defaultValue: "https://api.z.ai/api/coding/paas/v4", editorValue: "", }, apiKey: { diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 5bd1d2e27..8e043e8c2 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -442,7 +442,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ npm: "@ai-sdk/openai-compatible", name: "Zhipu GLM", options: { - baseURL: "https://open.bigmodel.cn/api/paas/v4", + baseURL: "https://open.bigmodel.cn/api/coding/paas/v4", apiKey: "", setCacheKey: true, }, @@ -456,8 +456,8 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ templateValues: { baseURL: { label: "Base URL", - placeholder: "https://open.bigmodel.cn/api/paas/v4", - defaultValue: "https://open.bigmodel.cn/api/paas/v4", + placeholder: "https://open.bigmodel.cn/api/coding/paas/v4", + defaultValue: "https://open.bigmodel.cn/api/coding/paas/v4", editorValue: "", }, apiKey: { @@ -475,7 +475,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ npm: "@ai-sdk/openai-compatible", name: "Zhipu GLM en", options: { - baseURL: "https://api.z.ai/v1", + baseURL: "https://api.z.ai/api/coding/paas/v4", apiKey: "", setCacheKey: true, }, @@ -489,8 +489,8 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ templateValues: { baseURL: { label: "Base URL", - placeholder: "https://api.z.ai/v1", - defaultValue: "https://api.z.ai/v1", + placeholder: "https://api.z.ai/api/coding/paas/v4", + defaultValue: "https://api.z.ai/api/coding/paas/v4", editorValue: "", }, apiKey: { diff --git a/tests/config/codexChatProviderPresets.test.ts b/tests/config/codexChatProviderPresets.test.ts index 23fe9cb58..61fa416cd 100644 --- a/tests/config/codexChatProviderPresets.test.ts +++ b/tests/config/codexChatProviderPresets.test.ts @@ -44,14 +44,14 @@ const expectedChatPresets = new Map< [ "Zhipu GLM", { - baseUrl: "https://open.bigmodel.cn/api/paas/v4", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", contextWindows: { "glm-5.1": 200000 }, }, ], [ "Zhipu GLM en", { - baseUrl: "https://api.z.ai/api/paas/v4", + baseUrl: "https://api.z.ai/api/coding/paas/v4", contextWindows: { "glm-5.1": 200000 }, }, ],