Skip to content

fix(responses): 补齐 reasoning item 转换缺口 + 上游看门基线推进 - #16

Merged
3304711297 merged 1 commit into
mainfrom
fix/responses-reasoning-items
Sep 18, 2026
Merged

3304711297 merged 1 commit into
mainfrom
fix/responses-reasoning-items

Conversation

@3304711297

Copy link
Copy Markdown
Owner

这个 PR 做了什么

补齐一个真实的协议缺口,并顺手推进上游看门基线以收口 Issue #14

一、Responses 多轮历史里的 reasoning item 此前被静默丢弃

Responses 协议客户端(Codex CLI 等)会把上一轮的思维链以 {"type":"reasoning", ...} item 形式随多轮历史一并回传。本仓 responses_compat._convert_input_items 原先没有该类型的分支,而该 item 也不带 role —— 两个条件都不命中,于是落进末尾「其他类型保底」分支,因 if role: 为假被整个跳过:

无消息产出、无告警、无日志。

表现为多轮对话后思维链凭空消失,客户端侧只能看到正文。

复现(修复前)

payload = {"model": "deepseek-v4-pro", "input": [
    {"role": "user", "content": "solve math"},
    {"type": "reasoning", "id": "rs_1",
     "summary": [{"type": "summary_text", "text": "let me think about 2+2"}]},
    {"type": "message", "role": "assistant", "content": "4"},
]}
# 修复前:
# [{"role":"user","content":"solve math"}, {"role":"assistant","content":"4"}]
#                                ↑ reasoning 整个消失,且无任何日志
# 修复后:
# [..., {"role":"assistant","content":"4","reasoning_content":"let me think about 2+2"}]

同生态上游 ardeyouxipianyi/workbuddy2api-hub v1.4.0(fixes #17)修的是同一问题;本 PR 按同语义在本地独立实现(非搬运),并额外覆盖了上游用例未涉及的边界(孤儿 reasoning 防漂移、同轮多条拼接、空 reasoning 不注入伪字段)。

二、实现要点(responses_compat.py

变更 说明
新增 _extract_reasoning_text(item) 按规范从 summary(部件数组 / 字符串)提取,缺失时回退 content(字符串 / 部件数组,兼容 reasoning_text / text / output_text / summary_text 四种部件名)
新增 pending_reasoning 暂存 reasoning 紧邻其所属 assistant 轮次之前,由 _flush_assistant 随之落地为 reasoning_content
同轮多条 reasoning 按出现顺序 \n 拼接,不覆盖
孤儿 reasoning 其后紧跟 user 消息、无归属轮次时在简单消息分支显式作废,防止漂移附着到下游毫不相干的 assistant 上
空 reasoning 不注入空 reasoning_content,不制造伪字段

与既有 deepseek_thinking.backfill_reasoning_content 的关系:该函数在「历史中已存在带 reasoning 的 assistant 消息」时才补齐同轮一致性。此前 reasoning 在转换阶段就被丢掉,回填根本无从触发 —— 本修复让转换阶段先保住数据,两条链路互补而非重复。

三、测试(+7 条,先写失败再实现)

tests/test_responses_api.py

  • reasoning 位于 assistant 之前 → 挂到该消息(规范输出序)
  • assistant 仍 pending 时到来的 reasoning → 归属同一轮,不漂到下一轮
  • 孤儿 reasoning → 作废,不漂到下游 assistant(防误附着)
  • 同一轮多条 reasoning → 顺序拼接
  • summary 数组逐段拼接 / summary 缺失时 content 回退
  • 空 reasoning → 不注入空字段
  • reasoning 后跟 function_call 轮次 → 挂到 tool_calls 消息上

RED 证据:实现前 4 条按预期失败于 KeyError: 'reasoning_content'

变异验证(证明用例真能抓回归,不是「碰巧绿」):

变异 结果
删掉 reasoning 分支(还原修复前行为) 5 条变红
多条 reasoning 改回「只取最后一条」 拼接用例变红
还原 14 passed ✓

四、上游看门基线推进(收口 Issue #14

tools/upstream-sources.jsonwb2api-upstream-ardeyoulast_synced_commit / last_synced_sha 推进至 67d2352aabsorbednote 记录借鉴出处与本地落地形态;updated_at 保持 2026-09-18。

五、验证(LF 检出,本地全量)

门禁 结果
pytest -q 431 passed(423 → 431)
tests/run_isolated_tests.py 431 passed / exit 0(无网络、无 DNS)
npm test 202 passed
cargo test 85 passed
check_premarked_sync.py ✓ 键集一致
check-upstream.py --lint ✓ 19 个源
check-upstream.py实跑 0 项待评估 / 0 项查询失败 → 满足工作流自动收口条件

未做(如实声明):未打真实 CodeBuddy 上游请求(本地无法复现该 wire 场景);Rust 侧本轮未改动,cargo test 仅作为回归确认。

评审时请留意

  1. 孤儿 reasoning 的作废是刻意的:暂存语义下若不清理,reasoning 会漂到后面某个毫不相干的 assistant 上 —— 那比丢弃更糟(错误归因比缺失更难排查)。已有专门用例锁定。
  2. content 回退路径的多部件名兼容是刻意的:上游 Issue 🔔 [Upstream Watch] 社区反代借鉴项目有新提交(9 项待评估) #17 的日志显示不同客户端/实现会在 summary / content 间摇摆,部件名也不统一;漏一个就又是一次静默丢失。
  3. 本 PR 不含 cargo fmt 或其它无关重排 —— 三个文件均以纯 LF 入库,与仓库既有约定一致。

一、Responses 多轮历史的 reasoning item 此前被静默丢弃(真实缺口)

Responses 协议客户端(Codex CLI 等)会把上一轮的思维链以
`{"type":"reasoning", ...}` item 形式随多轮历史一并回传。本仓
`responses_compat._convert_input_items` 没有该类型的分支,而该 item 也不带
`role` → 两个条件都不命中,于是落进末尾「其他类型保底」的分支里,因
`if role:` 为假而被整个跳过:**无消息产出、无告警、无日志**。表现为多轮对话
后思维链凭空消失,客户端侧只能看到正文。

同生态上游 ardeyouxipianyi/workbuddy2api-hub v1.4.0(fixes #17)修的是同一
问题,本提交按同语义在本地独立落地(非搬运)。

实现(`responses_compat.py`):
- 新增 `_extract_reasoning_text(item)`:按 Responses 规范从 `summary`
  (部件数组 / 字符串)提取,缺失时回退 `content`(字符串 / 部件数组,
  兼容 reasoning_text / text / output_text / summary_text 四种部件名);
- 新增 `pending_reasoning` 暂存:reasoning 紧邻其所属 assistant 轮次之前,
  由 `_flush_assistant` 随之落地为 `reasoning_content`;
- 同一轮多条 reasoning 按出现顺序 `\n` 拼接,不覆盖;
- 孤儿 reasoning(其后紧跟 user 消息、无归属轮次)在简单消息分支显式作废,
  防止漂移附着到下游毫不相干的 assistant 上;
- 空 reasoning 不注入空 `reasoning_content`(避免制造伪字段)。

与既有 `deepseek_thinking.backfill_reasoning_content` 的关系:该函数在
「历史中已存在带 reasoning 的 assistant 消息」时才补齐同轮一致性。此前
reasoning 在转换阶段就被丢掉,回填无从触发;本修复让转换阶段先保住数据,
两条链路互补而非重复。

测试(`tests/test_responses_api.py`,+7 条,先写失败再实现):
- reasoning 在 assistant 之前 → 挂到该消息(规范输出序)
- assistant 仍 pending 时到来的 reasoning → 归属同一轮,不漂到下一轮
- 孤儿 reasoning → 作废,不漂到下游 assistant(防误附着)
- 同一轮多条 reasoning → 顺序拼接
- summary 数组逐段拼接 / summary 缺失时 content 回退
- 空 reasoning → 不注入空字段
- reasoning 后跟 function_call 轮次 → 挂到 tool_calls 消息上

RED 证据:实现前 4 条按预期失败于 `KeyError: 'reasoning_content'`。
变异验证:① 删掉 reasoning 分支 → 5 条变红;② 多条改回「只取最后一条」
→ 拼接用例变红;还原后 14 passed。

二、上游看门基线推进(收口 Issue #14)

`tools/upstream-sources.json`:`wb2api-upstream-ardeyou` 的
last_synced_commit / last_synced_sha 推进至 `67d2352a`,`absorbed` 与 `note`
记录该机制的借鉴出处与本地落地形态。

`updated_at` 保持 2026-09-18。

验证(LF 检出,本地全量)
- `pytest -q`                     431 passed(423 → 431,+7 新用例 +1 既有)
- `tests/run_isolated_tests.py`   431 passed / exit 0(无网络、无 DNS)
- `npm test`                      202 passed
- `cargo test`                    85 passed
- `check_premarked_sync.py`       ✓ 键集一致
- `check-upstream.py --lint`      ✓ 19 个源
- `check-upstream.py`(实跑)      0 项待评估 / 0 项查询失败 → 满足工作流自动收口条件

未做:未打真实 CodeBuddy 上游请求(无法在本地复现该 wire 场景);Rust 侧本轮
未改动。
@3304711297
3304711297 merged commit 6716d2a into main Sep 18, 2026
2 checks passed
@3304711297
3304711297 deleted the fix/responses-reasoning-items branch September 18, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant