Skip to content

fix(relayconvert): preserve assistant reasoning across request protocol conversions - #6593

Open
HandSonic wants to merge 1 commit into
QuantumNous:mainfrom
HandSonic:fix/relayconvert-request-reasoning
Open

fix(relayconvert): preserve assistant reasoning across request protocol conversions#6593
HandSonic wants to merge 1 commit into
QuantumNous:mainfrom
HandSonic:fix/relayconvert-request-reasoning

Conversation

@HandSonic

@HandSonic HandSonic commented Aug 2, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

请求侧协议转换器在重建上游请求时会丢弃客户端回传的 assistant 推理内容(reasoning_content / thinking 块 / thought),导致推理模型(DeepSeek-R1/V4、Qwen-QwQ、Kimi-k2-thinking、小米 MiMo、Claude extended thinking 等)在多轮工具调用中丢失上一轮推理上下文;严格要求回传推理的上游会直接返回 HTTP 400(#4543#4408 均为同一根因的用户侧症状)。

逐一排查 relaykit/relayconvert 全部请求转换方向后,本 PR 修复其中 5 个丢失方向(Responses→Chat 方向与 #6396 相同,一并修复):

  1. Responses→Chatoai_responses/to_oai_chat_req.go):新增 reasoning 输入项识别——缓冲其 summary/content 文本,折叠进随后 function_call 所属的 assistant 消息 reasoning_content;同时识别 tool call 项上内联的 reasoning_content/reasoning 字段;无法归属的推理生成独立 assistant 消息。顺带修复了 reasoning 项因无 role 落入默认分支被错误转成 role:"user" 消息的问题(与 fix: Responses→Chat 转换丢失工具调用历史的 reasoning_content #6396 中作者补充的现象一致)。实现思路与 PR fix: preserve reasoning in Responses tool history #6395 一致,并在此基础上补充:无 function_call 跟随的推理不再被静默丢弃,而是保留为独立 assistant 消息(对齐 modelbridge 微信注册没了 #7 的 decode 语义);standalone 与 inline 推理的合并严格保持 wire 顺序。
  2. Chat→Responsesoai_chat/to_oai_responses_req.go):assistant 历史消息的 reasoning_content 生成 reasoning 输入项(summary 数组携带文本),置于 message/function_call 项之前(对齐 modelbridge 微信注册没了 #7 的 encode 语义)。
  3. Messages→Chatclaude_messages/to_oai_chat_req.go):assistant 回合的 thinking 块文本写入 reasoning_content(仅限 assistant 角色,其他角色忽略);纯 thinking 的 assistant 消息不再被丢弃。redacted_thinking 携带的是不透明加密负载而非可读文本,经评估刻意不保留(放入 chat 格式会在回传时变异为非法的普通 thinking 块);dto.ClaudeMediaMessage 仍补充 Data 字段以便后续签名工作使用。
  4. Chat→Claudeoai_chat/to_claude_messages_req.go):assistant 的 reasoning_content 转为 thinking 块,置于 text/tool_use 块之前。关键设计:仅对非最新 assistant 回合输出 thinking 块——Anthropic 在工具续轮时会对最新 assistant 消息中的 thinking 块做签名校验,而 chat 格式无法携带签名,合成块会被 400 拒绝;最新回合保持修复前的可接受形状(text 占位 + tool_use),较早回合官方允许省略/忽略,可安全携带。连续同角色消息合并时推理一并合并(修复合并丢推理问题);"..." 占位逻辑保持原行为以维持合并机制,thinking 块存在时抑制占位文本输出。
  5. Chat→Geminioai_chat/to_gemini_chat_req.go):assistant 的 reasoning_content 转为 thought:true 文本 part,置于 functionCall part 之前(与 modelbridge 的 gemini encode 行为一致);与现有 thoughtSignature bypass 逻辑兼容(bypass 仍落在 functionCall 上)。

为什么这样改能生效:丢失发生在转换层而非 DTO 层——dto.Message.ReasoningContent 字段一直存在且透传往返不丢字段,是各转换器从未读取/写入它。本 PR 只做"读取并搬运",不改变任何无推理请求的输出(85 案例 A/B 对比验证,见下方)。

已知限制:Chat→Claude 的最新回合推理保留需要签名贯通(响应侧先保留 signature,属于 #4763 范畴),本 PR 不涉及;Chat→Gemini 输出 parts 顺序为 thought→functionCall→text(functionCall 先于 text 是转换器原有行为,未改动);Messages→Chat 方向 tool_use 与 text 共存时 text 不进入 media 内容是转换器原有行为,未改动。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

声明:实现、测试与本描述由 AI 辅助完成,已经人工核对。

📸 运行证明 / Proof of Work

新增回归测试(每个方向 + 端到端往返 + 对抗边界)

relaykit/relayconvert/internal/oai_responses: 8 个用例(含 #6395 移植的 3 个、角色污染回归、
  孤儿推理、连续 assistant 合并、tool_output 前推理、多回合不串台、pending/inline 顺序)
relaykit/relayconvert/internal/oai_chat:      chat->responses 4 子用例;chat->claude 11 子用例
  (含最新回合门控、合并推理、nil-content 合并、首消息注入);chat->gemini 3 子用例
relaykit/relayconvert/internal/claude_messages: 4 个用例(thinking 保留、redacted 刻意丢弃、
  纯 thinking 消息保留、user 回合忽略)
relaykit/relayconvert: 3 个端到端往返测试(chat->responses->chat 字节级一致、
  claude->chat->claude 较早回合保留、最新回合刻意抑制)

对抗性审查(5 视角独立审查 + 逐条反驳验证)

  • 协议语义视角发现"chat→claude 无签名 thinking 块在最新回合被 Anthropic 签名校验 400"(经独立验证确认)→ 已修复为最新回合门控
  • 逻辑/往返视角发现:连续 assistant 合并丢推理、user 回合 thinking 误植、pending/inline 顺序反转、redacted_thinking 类型变异 → 全部修复并配测试
  • 回归视角:85 案例 A/B(修复前后)证明无推理流量字节级一致,仅 summary_text/reasoning_text 部件重解析为文本这一处有意变化

构建与测试

cd relaykit && GOWORK=off go build ./...          # 通过(模块独立性验证)
cd relaykit && GOWORK=off go test -count=1 ./...  # 全部通过
go build ./relay/... ./service/... ./controller/... # 根模块通过
go test ./service/... ./relay/...                  # 全部通过(service 包
  # TestObserveChannelAffinityUsageCacheByRelayFormat_UnsupportedModeKeepsEmpty 在
  # origin/main 基线上同样失败,为上游既有问题,与本 PR 无关)
go vet ./...                                       # 无告警

Golden 快照:仅 claude_to_openai / claude_to_gemini / claude_to_openai_responses 三个请求转换快照有 diff,且 diff 内容恰为被保留的推理(reasoning_content / thought part / reasoning 输入项)——证明无推理请求的输出零变化,行为变更严格收敛在"保留推理"上。

Summary by CodeRabbit

  • New Features

    • Preserved assistant reasoning across Claude, OpenAI Chat, OpenAI Responses, and Gemini conversions.
    • Maintained reasoning order around text, tool calls, and tool outputs.
    • Supported reasoning-only assistant messages and multi-turn reasoning replay.
    • Preserved compatible Claude thinking content while safely handling redacted payloads.
  • Bug Fixes

    • Prevented reasoning from being lost, misordered, or incorrectly converted into user content.
    • Excluded reasoning from user messages and preserved tool interactions during conversion.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1531cc67-b967-40d6-9c47-0e614228aa3f

📥 Commits

Reviewing files that changed from the base of the PR and between db504bb and 986de4c.

📒 Files selected for processing (15)
  • relaykit/dto/claude.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
  • relaykit/relayconvert/reasoning_roundtrip_test.go
  • relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json
🚧 Files skipped from review as they are similar to previous changes (15)
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json
  • relaykit/dto/claude.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req_test.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
  • relaykit/relayconvert/reasoning_roundtrip_test.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go

Walkthrough

The conversion layer now preserves assistant reasoning across Claude, OpenAI Chat, OpenAI Responses, and Gemini request formats. It handles redacted thinking, reasoning-only messages, message merging, tool calls, and latest-turn Claude constraints.

Changes

Reasoning preservation

Layer / File(s) Summary
Claude thinking to Chat reasoning
relaykit/dto/claude.go, relaykit/relayconvert/internal/claude_messages/...
Assistant thinking text becomes reasoning_content. Redacted and non-assistant thinking blocks are excluded.
Chat reasoning to Claude thinking
relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go, .../to_claude_messages_req_test.go
Earlier assistant reasoning becomes unsigned Claude thinking blocks. Latest-turn reasoning is withheld.
Chat reasoning to model APIs
relaykit/relayconvert/internal/oai_chat/..., .../testdata/golden/request/...
Reasoning becomes Gemini thought parts or Responses reasoning items before assistant output and tool calls.
Responses reasoning to Chat
relaykit/relayconvert/internal/oai_responses/...
Reasoning items are buffered, merged into assistant messages, or emitted as standalone assistant messages.
Cross-format validation
relaykit/relayconvert/reasoning_roundtrip_test.go
Round-trip tests verify reasoning and tool-call preservation across supported formats.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Issue 6592 — Directly covers the four request conversion paths addressed by this change.
  • Issue 6396 — Covers the related Responses-to-Chat reasoning preservation path.

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant ChatRequest
  participant ChatConverter
  participant ResponsesRequest
  participant ClaudeRequest
  participant GeminiRequest
  ChatRequest->>ChatConverter: assistant reasoning content
  ChatConverter->>ResponsesRequest: reasoning input item
  ChatConverter->>ClaudeRequest: unsigned thinking block
  ChatConverter->>GeminiRequest: thought text part
Loading

Poem

A rabbit carries thoughts through every stream,
Claude, Chat, Responses, Gemini gleam.
Reasoning stays with each tool-call trail,
Redacted blocks remain behind the veil.
Round trips keep the message whole.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The Responses-to-Chat conversion changes are outside issue #6592, which explicitly excludes that direction as separate work. Move the Responses-to-Chat changes and tests to the linked issue for that direction, or explicitly expand issue #6592 scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes preserving assistant reasoning during relay conversion, which is the primary change.
Linked Issues check ✅ Passed The required conversion paths in issue #6592 preserve assistant reasoning with ordering, tool-call, and Claude-specific handling covered by code and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go (1)

359-382: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a short comment above the compound gate condition.

The condition at Line 359 combines four checks (message.Role != "assistant", empty reasoning, and formatSrcIdx[i] == lastAssistantIdx) to decide between the plain-string path and the thinking-block path. The intent is documented at lines 369-374, inside the else branch, but not next to the gate itself. Add a short comment directly above Line 359 that states the gate's purpose, so a future edit to this condition does not silently break the latest-turn withholding behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go` around
lines 359 - 382, Add a concise comment immediately above the compound condition
in the message conversion flow explaining that it selects plain text while
withholding unsigned reasoning blocks on the latest assistant turn. Keep the
existing condition and surrounding branches unchanged, and anchor the comment to
the visible message.IsStringContent gate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go`:
- Around line 284-293: The assistant-turn conversion flow around
shouldAttachThoughtSignature must distinguish replayed reasoning from ordinary
text when applying AttachFirstTextThoughtSignature. Ensure the replayed
reasoning Thought part does not become the fallback signature target, while
preserving signature attachment for the subsequent function-call part and adding
only one signature check per replayed assistant turn.

---

Nitpick comments:
In `@relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go`:
- Around line 359-382: Add a concise comment immediately above the compound
condition in the message conversion flow explaining that it selects plain text
while withholding unsigned reasoning blocks on the latest assistant turn. Keep
the existing condition and surrounding branches unchanged, and anchor the
comment to the visible message.IsStringContent gate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d1053c3b-0345-4ef3-bed3-0cd97e6e5127

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab0202 and aca9c98.

📒 Files selected for processing (15)
  • relaykit/dto/claude.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
  • relaykit/relayconvert/reasoning_roundtrip_test.go
  • relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json

Comment thread relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
@HandSonic
HandSonic force-pushed the fix/relayconvert-request-reasoning branch from aca9c98 to db504bb Compare August 2, 2026 05:11
…ol conversions

Request-side converters dropped client-replayed assistant reasoning when
rebuilding upstream requests, breaking multi-turn tool-call context for
reasoning models (DeepSeek-R1/V4, Qwen-QwQ, Kimi-k2-thinking, MiMo,
Claude extended thinking):

- responses->chat: recognize reasoning input items (and inline
  reasoning_content on tool calls), fold them into the assistant message
  carrying the following function_call; orphan reasoning becomes a
  standalone assistant message instead of a bogus user message
- chat->responses: emit replayed assistant reasoning_content as a
  reasoning input item before the message/function_call items
- messages->chat: convert thinking/redacted_thinking blocks into
  reasoning_content
- chat->claude: emit replayed reasoning_content as an unsigned thinking
  block ahead of text/tool_use blocks
- chat->gemini: emit replayed reasoning_content as a thought part ahead
  of function_call parts

Adds per-direction regression tests plus chat->responses->chat and
claude->chat->claude round-trip tests; refreshes the three claude_to_*
request golden snapshots to the preserved-reasoning output.

Closes QuantumNous#6592
Refs QuantumNous#6396

AI-assisted: implementation and tests were produced with AI assistance.
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.

fix: 协议转换请求侧多方向丢失 assistant 推理内容(chat→Responses / Messages→Chat / chat→Claude / chat→Gemini)

1 participant