Skip to content

Commit 2bac9a7

Browse files
authored
revert(vendor-channels): 还原至 v0.3.0 基线,修复 failover 级联 server_tool_use.id 400 错误 (#218)
* revert(vendor-channels): 还原至 v0.3.0 基线,修复 failover 级联 server_tool_use.id 400 错误; - 还原 vendor_channels.py 至 v0.3.0(移除 _flatten_tool_blocks、_inject_tool_result_id_for_zhipu、 _enforce_pairing_sanity_pass、zhipu 自清理通道、anthropic→zhipu 通道等 v0.3.0 后引入的特殊处理) - 修复 _determine_source_vendor Priority 1 逻辑:仅当 failed_tier 有已注册转换通道时才返回, 否则降级到 session history / body inference,确保 copilot→anthropic failover 时能通过 session 找到 zhipu 源并正确清理 server_tool_use 块 - 还原 model/vendor.py(移除 TOOL_RESULTS capability) - 部分还原 error_classifier.py(保留 CN 语义拒绝标记,移除 has_tool_results) - 还原 executor.py(移除 zhipu 500 特殊检测,保留 invalid_tool_call_format 检测) - 同步更新测试用例,全部 1399 测试通过 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com> * docs(issue): 恢复与 vendor-channels 回退无关的 Issue #1 文档; 回退 vendor-channels 至 v0.3.0 基线时误删了 docs/issue.md 全部内容, 其中 Issue #1(streaming usage parse failed)描述的 null 防护修复 仍保留在 usage_parser.py 中,属于无关文档,不应被连带删除。 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
1 parent 43488a1 commit 2bac9a7

11 files changed

Lines changed: 493 additions & 2628 deletions

File tree

‎docs/issue.md‎

Lines changed: 0 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -45,171 +45,3 @@ if "usage" in data: # 仅判断 key 存在
4545

4646
- 本仓库内 `parse_usage_from_chunk` 的 Gemini `usageMetadata` 分支 (line ~219) 已经使用 `isinstance(um, dict)` 防御, 不受影响, 可作为参考实现。
4747
- 检查其他解析器 (如 routing / vendor adapter 层) 是否还有 `if "key" in data: v = data["key"]; v.get(...)` 这种模式, 必要时同步加固。
48-
49-
---
50-
51-
## zhipu 自循环 400 + tool_results 偶发降级
52-
53-
**问题描述**
54-
55-
生产日志反复出现下述链路: 请求一开始命中 zhipu 主 tier, 但在含 `tool_results` 的多轮工具调用场景下偶发返回 400, 触发到 copilot 二级 tier。具体日志特征:
56-
57-
```
58-
WARNING Tier zhipu likely format incompatibility (400 + tool_results), trying next tier without recording failure
59-
WARNING Tier zhipu semantic rejection (400), trying next tier without recording failure
60-
DEBUG Applied transition channel zhipu → copilot: rewritten_38_srvtoolu_ids, stripped_16_thinking_blocks, removed_3_cache_control_fields, misplaced_tool_result_relocated
61-
```
62-
63-
zhipu → copilot 通道的 adaptations 列表暴露了上一轮 zhipu 响应中存在的非标准产物 (`srvtoolu_*` ID、自签 thinking、错位的 `tool_result`)。
64-
65-
**表因**
66-
67-
zhipu 自身偶发返回 400, 但错误体非 JSON 结构, 由 `_is_likely_request_format_error()` 判定为「格式不兼容」并跳过当前 tier。
68-
69-
**根因**
70-
71-
1. zhipu 是 `NativeAnthropicVendor` 薄透传供应商, **不做任何请求体预处理**。
72-
2. `executor._determine_source_vendor` 三条优先级路径均以 `source != target_name` 过滤掉了同 vendor 自转换。
73-
3. `VENDOR_TRANSITIONS` 注册表中无 `("zhipu", "zhipu")` 条目。
74-
75-
后果: GLM-5 偶发产出非标准产物 (assistant 内联 `tool_result`、`server_tool_use_delta` 流式残块) 后, Claude Code 把这些产物原样回送下一轮请求时, **没有任何清洗发生**, 直接被转发到 zhipu 自身, 命中 zhipu 端的输入校验返回 400。
76-
77-
**处理方式**
78-
79-
- 在 `vendor_channels.py` 新增 `prepare_zhipu_self_cleanup` 函数, 仅修复 zhipu 自身拒绝的两类产物:
80-
1. 剥离 `server_tool_use_delta` 流式残块
81-
2. `enforce_anthropic_tool_pairing` 把 assistant 内联 `tool_result` 重定位到紧随 user 消息
82-
- 显式 **保留** zhipu 原生支持的特性: `srvtoolu_*` ID、`server_tool_use` 类型、自签 thinking signature、`cache_control` (cache_read 已在生产实证)、顶层 `thinking` 参数。
83-
- 在 `VENDOR_TRANSITIONS` 注册 `("zhipu", "zhipu") = prepare_zhipu_self_cleanup`。
84-
- 在 `executor._determine_source_vendor` 三条优先级路径中, 把「`source != target`」过滤替换为「通道已注册」门控 (`get_transition_channel(...) is not None`), 让自转换通道在显式注册时启用, 未注册时退化为原行为。
85-
86-
**后续防范**
87-
88-
- 新增 `NativeAnthropicVendor` 子类 (minimax / kimi / doubao / xiaomi / alibaba 等) 时, 若上游 vendor 偶发产出违反 Anthropic 规范的产物, 可按需注册同名自清理通道, executor 无需任何额外改动。
89-
- 同 vendor 自转换通道应**精确剪裁**: 仅修复 vendor 自身拒绝的产物, 不要套用跨 vendor 通道的全量清理 (会误伤 vendor 原生支持的特性, 如 cache_control 损失带来 cache_read miss)。
90-
91-
**同类问题影响与处理注意事项**
92-
93-
- `enforce_anthropic_tool_pairing` 仅识别 `tool_use` 类型 (不含 `server_tool_use`), 因为 `server_tool_use` 由 vendor 自身执行, 不需要客户端 `tool_result`。构造测试或类似清洗逻辑时需注意此差别。
94-
- `_is_likely_request_format_error()` 把「400 + tool_results + 非结构化错误体」一律标记为格式不兼容并跳过 tier 不计熔断器, 这层兜底虽能维持可用性但会**掩盖** vendor 自身的间歇性问题, 让根因更难发现。处理类似 400 偶发时, 应优先看 `Applied transition channel` 日志中的 adaptations 列表, 它能精确暴露上游响应中的非标准产物。
95-
96-
---
97-
98-
## anthropic 报 messages.X tool_use 缺 tool_result (zhipu→anthropic 故障转移失败)
99-
100-
**问题描述**
101-
102-
zhipu 完成响应后, executor 故障转移至 anthropic 时反复失败 (HTTP 400):
103-
104-
```
105-
DEBUG Applied transition channel zhipu → anthropic: rewritten_86_srvtoolu_ids, misplaced_tool_result_relocated, stripped_18_thinking_blocks
106-
WARNING anthropic stream error: status=400 ... messages.3: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_normalized_3.
107-
INFO Failover: anthropic → zhipu (reason: HTTP 400)
108-
```
109-
110-
adaptations 列表显示 `misplaced_tool_result_relocated` 但**没有** `orphaned_tool_use_repaired`, 即 enforce 单遍扫描视角下认为所有 tool_use 都已配对; 但 anthropic 仍报 messages.X 缺 tool_result, 导致请求级 cascade failover 反复回到 zhipu。
111-
112-
**表因**
113-
114-
`prepare_zhipu_to_anthropic` 链路输出的请求体中, 某个 assistant 的 `tool_use` 在紧邻的 user 消息中没有匹配的 `tool_result` 块。
115-
116-
**根因**
117-
118-
`_rewrite_srvtoolu_ids` 采用单遍正向扫描: 在同一次循环中一边收集 srvtoolu_* → toolu_normalized_* 的 id_map, 一边改写遇到的 `tool_result.tool_use_id`。GLM-5 流式偶发将 inline tool_result 输出在本消息 `server_tool_use` 之前 (block 顺序异常), 导致:
119-
120-
1. 处理 inline tool_result 时, id_map 尚未填入对应 srvtoolu_* → 漏改名, inline 仍保留 `srvtoolu_X`
121-
2. 处理本消息 server_tool_use 时, 填入 id_map 并把 tool_use 改名为 `toolu_normalized_X`
122-
3. 进入 `enforce_anthropic_tool_pairing` 时:
123-
- A 步 extracted dict key = `srvtoolu_X` (inline 保留的旧 ID)
124-
- B 步 tool_use_ids = `[toolu_normalized_X]` (已改名)
125-
- F 步 `uid in extracted` 检查失败 (key 错位), 但若 next user 已含其他 stale tool_result 让 existing_result_ids "巧合" 命中, F 步会跳过 synth → 不触发 orphan 标签
126-
- 最终 anthropic 看到 messages.X 真的缺 toolu_normalized_X 的 tool_result → 400
127-
128-
**处理方式**
129-
130-
- `_rewrite_srvtoolu_ids` 改为**两遍扫描**: Pass 1 仅遍历 assistant 消息, 收集 id_map 并改写 tool_use 自身的 id 与 type; Pass 2 全量遍历所有消息 (含 user / 异常 assistant 内联), 统一改写所有 `tool_result.tool_use_id` 引用。彻底消除 block 顺序敏感性。
131-
- `enforce_anthropic_tool_pairing` 主循环结束后追加**全局 sanity check pass**: 重新遍历每条 assistant, 验证其 tool_use_ids 全部在 next user 的 tool_result 中存在; 发现遗漏直接合成 is_error 占位并打 `pairing_sanity_repaired` 标签。作为防御深度抵御未来其他主循环边角错位。
132-
- A 步对 `tool_use_id` 缺失的破损 inline tool_result 也计入 relocated_count (避免 silent drop 影响 adaptations 标签可观测性)。
133-
134-
**后续防范**
135-
136-
- 任何"按出现顺序填充字典 + 同遍引用查询"的两阶段操作都应警惕**顺序耦合**问题。两遍扫描 (collect → apply) 是消除此类 bug 的标准 pattern。
137-
- 关键校验函数应有**主循环 + 全局 sanity check** 的双层结构, 单层校验在边角场景下容易被绕过。
138-
- 处理 anthropic `tool_use ids without tool_result blocks immediately after` 类 cascade failover 时, **adaptations 标签能否复现日志**是定位 root cause 的强信号: 若 enforce 视角与 anthropic 视角不一致 (有 misplaced 但无 orphan, anthropic 仍报错), 必有上游 _rewrite / id 改写阶段的隐藏漏洞。
139-
140-
**同类问题影响与处理注意事项**
141-
142-
- 任何对 messages 进行 ID 重写的转换链 (如 `_rewrite_srvtoolu_ids`、`anthropic_to_openai`、`anthropic_to_gemini`) 都应使用两遍扫描或一次性收集后再批量改写, 以保证 block 顺序无关性。
143-
- enforce 类校验函数若依赖 dict key 与 list 元素的**等同性**, 必须先确保两者在同一参考系下 (改名前 vs 改名后); 否则错位会以 "看起来 OK 实际有漏" 的方式静默泄漏到下游。
144-
145-
---
146-
147-
## zhipu 500 `'ClaudeContentBlockToolResult' object has no attribute 'id'`
148-
149-
**问题描述**
150-
151-
zhipu GLM-5 在处理含 `tool_result` 块的会话时持续返回 500 错误,每次请求都触发故障转移至 copilot,zhipu 完全无法承接含工具调用的多轮对话:
152-
153-
```
154-
WARNING zhipu stream error: status=500 body='...message":"\'ClaudeContentBlockToolResult\' object has no attribute \'id\'"}'
155-
```
156-
157-
**表因**
158-
159-
zhipu 后端在解析 `tool_result` 内容块时错误地访问 `.id` 属性。但 Anthropic API 规范中 `tool_result` 块只有 `tool_use_id` 字段(用于关联对应的 `tool_use`),没有 `id` 字段。
160-
161-
**根因**(2026-04-29 第二次复盘更新)
162-
163-
**第一次诊断**(已推翻):认为 `_inject_tool_result_id_for_zhipu` 注入 `id` 可绕过。实证:注入 114 个块后 500 依旧。
164-
165-
**第二次诊断**(已推翻):认为 `enforce_anthropic_tool_pairing` 搬迁 tool_result 到 user 消息是触发条件。实证:移除 tool pairing 后 500 依旧(日志显示 `copilot → zhipu: stripped_19_thinking_blocks, removed_thinking_param`,无 `misplaced_tool_result_relocated`)。
166-
167-
**实际根因**:zhipu 后端的 `ClaudeContentBlockToolResult` Python 类**没有 `id` 属性**,但 zhipu 代码在处理**所有** `tool_result` 块时都访问 `obj.id`,无论块位于 assistant 还是 user 消息。三层因果链:
168-
169-
1. **zhipu 后端 Bug**(不可修复 — 上游代码):`ClaudeContentBlockToolResult` 类缺少 `id` 属性,zhipu 代码访问时触发 `AttributeError` → 500。
170-
2. **JSON 注入无效**(已实证):`_inject_tool_result_id_for_zhipu` 往 JSON dict 注入 `id = tool_use_id`,但 zhipu 反序列化框架不读取此字段,Python 对象仍无 `id` 属性。
171-
3. **无预防机制**(proxy 层可修复):tier 门控系统不检查请求是否含 `tool_result` 块 → 每次请求先发 zhipu → 必然 500 → failover → 额外 ~2 秒延迟。
172-
173-
**实证依据**:
174-
- 有注入(114 个块)→ 500;无注入 → 500。结论:注入无效。
175-
- 有 tool pairing → 500;无 tool pairing → 500。结论:tool pairing 不是触发条件。
176-
- 首次请求(无 tool_result 块)→ zhipu 正常。结论:500 由 tool_result 块本身触发。
177-
178-
**处理方式**(2026-04-29 第二次更新)
179-
180-
从所有 zhipu 目标转换通道中移除有害/无效步骤(`enforce_anthropic_tool_pairing`、`_inject_tool_result_id_for_zhipu`、`_strip_cache_control`),并在 `ZhipuVendor.supports_request` 中增加 `has_tool_results` 门控:当请求包含 `tool_result` 块时主动拒绝 zhipu tier,避免「尝试 → 500 → failover」的无效延迟。
181-
182-
| 变更项 | 说明 |
183-
|--------|------|
184-
| `RequestCapabilities.has_tool_results` | 新增字段,检测请求中是否含 `tool_result` 块 |
185-
| `CapabilityLossReason.TOOL_RESULTS` | 新增枚举值,标记 tool_result 兼容性问题 |
186-
| `ZhipuVendor.supports_request` | 覆写方法,`has_tool_results=True` 时拒绝请求 |
187-
| `build_request_capabilities` | 扩展 tool_result 块检测逻辑 |
188-
189-
保留的 zhipu 目标转换通道精简步骤:
190-
191-
| 保留项 | 原因 |
192-
|--------|------|
193-
| `strip_thinking_blocks` | copilot/anthropic 的 thinking 签名 zhipu 无法验证 |
194-
| 移除 `thinking`/`extended_thinking` 顶层参数 | zhipu 不支持 |
195-
| `_remove_vendor_blocks(server_tool_use_delta)` | zhipu 自身流式残块 |
196-
| `_remove_vendor_blocks(server_tool_use)` | Anthropic beta 块,zhipu 不支持 |
197-
198-
**涉及变更的转换通道**:
199-
- `prepare_copilot_to_zhipu` — 移除 cache_control / tool pairing / id 注入
200-
- `prepare_anthropic_to_zhipu` — 移除 cache_control / tool pairing / id 注入
201-
- `prepare_zhipu_self_cleanup` — 移除 tool pairing / id 注入
202-
203-
**注意**: `prepare_zhipu_to_anthropic` 和 `prepare_zhipu_to_copilot` 不受影响(目标是 anthropic/copilot,不是 zhipu),仍保留 `enforce_anthropic_tool_pairing`。
204-
205-
**后续防范**
206-
207-
- **转换通道的「最小干预」原则**:跨供应商转换应仅清理目标供应商**确认不支持**的特性。未经验证的「预防性清理」(如剥离 cache_control)可能误伤供应商原生支持的功能,甚至引入新的故障。
208-
- **workaround 须验证有效**:`_inject_tool_result_id_for_zhipu` 虽有注释说明目的,但未经验证其有效性即合入。后续 workaround 须附带验证证据(如 curl 复现、上游确认)。
209-
- **zhipu 后端 bug 跟踪**:`ClaudeContentBlockToolResult` 类缺少 `id` 属性是 zhipu 上游 bug。若 zhipu 修复此 bug,可考虑恢复 tool pairing 以获得更严格的消息结构校验。
210-
211-
**同类问题影响与处理注意事项**
212-
213-
- `NativeAnthropicVendor` 子类的自清理通道应**精确剪裁**:仅修复 vendor 自身拒绝的产物,不做跨供应商的全量清理。
214-
- 当 zhipu 后端出现新的 400 拒绝(如 inline tool_result 再次被拒),应优先调查是 zhipu 后端变更还是请求格式问题,而非立即加回 tool pairing(可能重新触发 500)。
215-
- `_inject_tool_result_id_for_zhipu` 函数暂时保留在代码中(未删除),标记为 deprecated,待确认不需要后清理。

0 commit comments

Comments
 (0)