Skip to content

Commit 0bd7778

Browse files
committed
fix(codex): respect versioned GLM probe endpoints
Root cause: Codex provider protocol probing treated every non-/v1 base URL as an OpenAI root and appended /v1 before /responses or /chat/completions. Zhipu Coding Plan uses a versioned root such as https://open.bigmodel.cn/api/coding/paas/v4, so the probe hit /v4/v1/chat/completions and /v4/v1/responses and reported every GLM model as unavailable even though /v4/chat/completions works with the same key. Changes: preserve existing endpoint suffixes, detect trailing /vN API version segments, and append Responses or Chat Completions directly for versioned roots. The confirmation copy now says the corresponding endpoint is tested instead of claiming every provider uses /v1. Project memory records the live Zhipu probe evidence and the expected boundary: GLM is Chat Completions reachable while Responses currently 404s. Verification: direct live probes confirmed open.bigmodel.cn /api/paas/v4/chat/completions and /api/coding/paas/v4/chat/completions return 200, while the old /api/coding/paas/v4/v1/chat/completions path returns 404. Ran cargo fmt --manifest-path src-tauri/Cargo.toml --check; cargo test --manifest-path src-tauri/Cargo.toml probe_url --lib; pnpm vitest run tests/components/CodexFormFields.test.tsx; pnpm typecheck; git diff --check.
1 parent 9f1553b commit 0bd7778

3 files changed

Lines changed: 87 additions & 11 deletions

File tree

memory.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 2026-06-30 Codex GLM Model Context and Probe Guidance
44

5+
- 2026-07-01 用户用智谱 GLM key 实测“测试 Chat / Responses 全不通”时,真实网络和 key 都正常:`https://open.bigmodel.cn/api/paas/v4/chat/completions``https://open.bigmodel.cn/api/coding/paas/v4/chat/completions` 能返回 200,`/api/paas/v4/models``/api/coding/paas/v4/models` 也能返回模型列表;失败的 UI 报错路径是 `.../api/coding/paas/v4/v1/chat/completions` / `.../v4/v1/responses`,根因是新加的协议探测 URL 构造没有复用 `/models` 的版本段规则。修复边界:`probe_codex_responses_for_config` / `probe_codex_chat_for_config` 对已以 `/vN` 收尾的 Base URL 直接拼 `/responses` / `/chat/completions`,避免再追加 `/v1`;智谱仍只有 Chat Completions 路径可用,Responses 路径按官方当前接口返回 404,应由 UI 标为 Chat 可用而不是“全不通”。
56
- 2026-07-01 Codex provider 的“测试 Chat / Responses”不能只在按钮旁显示一条错误摘要;真实问题是每个模型可能有不同协议能力。`CodexFormFields` 的探测结果应按模型保存并在“模型映射”每行显示小 tag:`双协议``Responses``Chat``不可用`,tag title 保留 Responses/Chat 详细返回。汇总文案要列出双协议通过、仅 Responses、仅 Chat、双协议失败的模型,避免用户只看到第一个失败模型(如 `glm-4.5`)而不知道其它模型状态。
67
- 2026-07-01 “下一步”必须由真实探测结果驱动而不是模型名启发式:双协议通过和仅 Responses 通过的模型进入 Responses provider,只有 Chat 通过的模型进入 Chat provider,双失败模型不进入拆分建议。拆分成两个 provider 只在新增 provider 场景有 `onProviderSplitSuggestionChange` 回调时弹确认框;编辑已有 provider 时只显示行级协议 tag 和汇总,不弹一个确认后无实际保存效果的拆分对话框。
78
- 2026-06-30 复查用户提供的智谱 Coding Plan key 后确认:`https://open.bigmodel.cn/api/coding/paas/v4/models` 当前返回的 GLM 条目只有 `id/object/created/owned_by`,没有 `context_window``max_context_length` 等规格字段;因此自动获取模型列表若要补齐 GLM 上下文,不能只靠 `/models`。官方 Mintlify 文档提供稳定 markdown:`/cn/guide/start/model-overview.md` 的模型表给出 GLM-5.2 `1M`、GLM-5.1/5/5-Turbo/4.7/4.6 `200K`、GLM-4.5-Air `128K`,单模型页如 `/cn/guide/models/text/glm-4.5.md` 给出 GLM-4.5 `128K`。后端 `model_fetch.rs` 的正确策略是:先解析 `/models` 显式 metadata;若智谱/Z.AI endpoint 的 GLM 模型缺上下文,再从官方 docs markdown 补齐,且只填缺失值、不覆盖上游显式值。

src-tauri/src/commands/model_fetch.rs

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,29 +98,36 @@ pub async fn probe_codex_chat_for_config(
9898

9999
/// 构造 Responses 探测 URL。
100100
///
101-
/// 用户可能填写 provider 根地址、`/v1` 地址,也可能把完整 endpoint 当作 Base URL;
102-
/// 这里统一收敛到同一 host/prefix 下的 `/v1/responses`,避免探测请求打到错误路径。
101+
/// 用户可能填写 provider 根地址、`/v1` 地址、智谱这类 `/v4` 版本化根地址,
102+
/// 也可能把完整 endpoint 当作 Base URL;这里会保留已有版本段,避免把
103+
/// `.../api/coding/paas/v4` 错拼成 `.../v4/v1/responses`。
103104
fn build_responses_probe_url(base_url: &str, is_full_url: bool) -> Result<String, String> {
104105
let trimmed = base_url.trim().trim_end_matches('/');
105106
if trimmed.is_empty() {
106107
return Err("Base URL is empty".to_string());
107108
}
108-
if trimmed.ends_with("/v1/responses") {
109+
if trimmed.ends_with("/responses") {
109110
return Ok(trimmed.to_string());
110111
}
111112
if is_full_url {
112113
if let Some(index) = trimmed.find("/v1/") {
113114
return Ok(format!("{}/v1/responses", &trimmed[..index]));
114115
}
116+
if trimmed.ends_with("/chat/completions") {
117+
return Ok(format!(
118+
"{}/responses",
119+
trim_chat_completions_suffix(trimmed)
120+
));
121+
}
115122
if let Some(index) = trimmed.rfind('/') {
116123
let root = &trimmed[..index];
117124
if root.contains("://") {
118-
return Ok(format!("{root}/v1/responses"));
125+
return Ok(format!("{root}/responses"));
119126
}
120127
}
121128
return Err("Cannot derive /v1/responses endpoint from full URL".to_string());
122129
}
123-
if trimmed.ends_with("/v1") {
130+
if ends_with_version_segment(trimmed) {
124131
Ok(format!("{trimmed}/responses"))
125132
} else {
126133
Ok(format!("{trimmed}/v1/responses"))
@@ -129,34 +136,65 @@ fn build_responses_probe_url(base_url: &str, is_full_url: bool) -> Result<String
129136

130137
/// 构造 Chat Completions 探测 URL。
131138
///
132-
/// 与 Responses URL 生成规则保持同源,避免用户填写完整 endpoint 时两个探测打到不同前缀。
139+
/// 与 Responses URL 生成规则保持同源;对已经包含 `/v4` 等版本段的供应商,
140+
/// 直接拼 `/chat/completions`,避免智谱 Coding Plan 被错拼成 `/v4/v1/...`。
133141
fn build_chat_probe_url(base_url: &str, is_full_url: bool) -> Result<String, String> {
134142
let trimmed = base_url.trim().trim_end_matches('/');
135143
if trimmed.is_empty() {
136144
return Err("Base URL is empty".to_string());
137145
}
138-
if trimmed.ends_with("/v1/chat/completions") {
146+
if trimmed.ends_with("/chat/completions") {
139147
return Ok(trimmed.to_string());
140148
}
141149
if is_full_url {
142150
if let Some(index) = trimmed.find("/v1/") {
143151
return Ok(format!("{}/v1/chat/completions", &trimmed[..index]));
144152
}
153+
if trimmed.ends_with("/responses") {
154+
return Ok(format!(
155+
"{}/chat/completions",
156+
trim_endpoint_suffix(trimmed, "/responses")
157+
));
158+
}
145159
if let Some(index) = trimmed.rfind('/') {
146160
let root = &trimmed[..index];
147161
if root.contains("://") {
148-
return Ok(format!("{root}/v1/chat/completions"));
162+
return Ok(format!("{root}/chat/completions"));
149163
}
150164
}
151165
return Err("Cannot derive /v1/chat/completions endpoint from full URL".to_string());
152166
}
153-
if trimmed.ends_with("/v1") {
167+
if ends_with_version_segment(trimmed) {
154168
Ok(format!("{trimmed}/chat/completions"))
155169
} else {
156170
Ok(format!("{trimmed}/v1/chat/completions"))
157171
}
158172
}
159173

174+
/// 判断 URL 是否已经以 API 版本段收尾。
175+
///
176+
/// 供应商不总是使用 OpenAI 的 `/v1`;智谱 Coding Plan 的根地址是
177+
/// `/api/coding/paas/v4`,这类地址后面应直接追加 endpoint。
178+
fn ends_with_version_segment(url: &str) -> bool {
179+
let Some(segment) = url.rsplit('/').next() else {
180+
return false;
181+
};
182+
let Some(version) = segment.strip_prefix('v') else {
183+
return false;
184+
};
185+
!version.is_empty() && version.chars().all(|ch| ch.is_ascii_digit())
186+
}
187+
188+
/// 去掉 Chat Completions endpoint 后缀,返回同源 API 根路径。
189+
fn trim_chat_completions_suffix(url: &str) -> &str {
190+
trim_endpoint_suffix(url, "/chat/completions")
191+
}
192+
193+
/// 去掉指定 endpoint 后缀,调用方保证后缀已经匹配。
194+
fn trim_endpoint_suffix<'a>(url: &'a str, suffix: &str) -> &'a str {
195+
url.strip_suffix(suffix).unwrap_or(url)
196+
}
197+
160198
/// 发送真正的最小 Responses 请求。
161199
///
162200
/// HTTP 错误、网络错误和超时都以结构化 `ok=false` 返回,让前端状态机可以用同一套表格
@@ -331,4 +369,41 @@ mod tests {
331369
"https://example.com/v1/chat/completions"
332370
);
333371
}
372+
373+
#[test]
374+
fn responses_probe_url_respects_versioned_base_url() {
375+
assert_eq!(
376+
build_responses_probe_url("https://open.bigmodel.cn/api/coding/paas/v4", false)
377+
.unwrap(),
378+
"https://open.bigmodel.cn/api/coding/paas/v4/responses"
379+
);
380+
}
381+
382+
#[test]
383+
fn chat_probe_url_respects_versioned_base_url() {
384+
assert_eq!(
385+
build_chat_probe_url("https://open.bigmodel.cn/api/coding/paas/v4", false).unwrap(),
386+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions"
387+
);
388+
}
389+
390+
#[test]
391+
fn probe_urls_preserve_non_v1_full_endpoints() {
392+
assert_eq!(
393+
build_responses_probe_url(
394+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
395+
true
396+
)
397+
.unwrap(),
398+
"https://open.bigmodel.cn/api/coding/paas/v4/responses"
399+
);
400+
assert_eq!(
401+
build_chat_probe_url(
402+
"https://open.bigmodel.cn/api/coding/paas/v4/responses",
403+
true
404+
)
405+
.unwrap(),
406+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions"
407+
);
408+
}
334409
}

src/components/providers/forms/CodexFormFields.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,8 +1215,8 @@ export function CodexFormFields({
12151215
如果还没有模型目录,请先到下方“模型映射”右上角点击“获取模型列表”,或手动添加至少一个模型。
12161216
</span>
12171217
<span className="block">
1218-
每个模型会分别测试 /v1/responses 和
1219-
/v1/chat/completions,输出上限为
1218+
每个模型会分别测试对应的 Responses 和 Chat Completions
1219+
endpoint,输出上限为
12201220
1024。都不通时通常不是协议问题,而是 API Key、Base
12211221
URL、模型权限、额度、网络或上游故障。
12221222
</span>

0 commit comments

Comments
 (0)