fix(relayconvert): preserve assistant reasoning across request protocol conversions - #6593
fix(relayconvert): preserve assistant reasoning across request protocol conversions#6593HandSonic wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
🚧 Files skipped from review as they are similar to previous changes (15)
WalkthroughThe 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. ChangesReasoning preservation
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
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
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winConsider a short comment above the compound gate condition.
The condition at Line 359 combines four checks (
message.Role != "assistant", empty reasoning, andformatSrcIdx[i] == lastAssistantIdx) to decide between the plain-string path and the thinking-block path. The intent is documented at lines 369-374, inside theelsebranch, 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
📒 Files selected for processing (15)
relaykit/dto/claude.gorelaykit/relayconvert/internal/claude_messages/to_oai_chat_req.gorelaykit/relayconvert/internal/claude_messages/to_oai_chat_req_test.gorelaykit/relayconvert/internal/oai_chat/to_claude_messages_req.gorelaykit/relayconvert/internal/oai_chat/to_claude_messages_req_test.gorelaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.gorelaykit/relayconvert/internal/oai_chat/to_gemini_chat_req_test.gorelaykit/relayconvert/internal/oai_chat/to_oai_responses_req.gorelaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.gorelaykit/relayconvert/internal/oai_responses/to_oai_chat_req.gorelaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.gorelaykit/relayconvert/reasoning_roundtrip_test.gorelaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.jsonrelaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.jsonrelaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json
aca9c98 to
db504bb
Compare
…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.
db504bb to
986de4c
Compare
📝 变更描述 / 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 相同,一并修复):oai_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 顺序。oai_chat/to_oai_responses_req.go):assistant 历史消息的reasoning_content生成reasoning输入项(summary数组携带文本),置于 message/function_call 项之前(对齐 modelbridge 微信注册没了 #7 的 encode 语义)。claude_messages/to_oai_chat_req.go):assistant 回合的thinking块文本写入reasoning_content(仅限 assistant 角色,其他角色忽略);纯 thinking 的 assistant 消息不再被丢弃。redacted_thinking携带的是不透明加密负载而非可读文本,经评估刻意不保留(放入 chat 格式会在回传时变异为非法的普通 thinking 块);dto.ClaudeMediaMessage仍补充Data字段以便后续签名工作使用。oai_chat/to_claude_messages_req.go):assistant 的reasoning_content转为 thinking 块,置于 text/tool_use 块之前。关键设计:仅对非最新 assistant 回合输出 thinking 块——Anthropic 在工具续轮时会对最新 assistant 消息中的 thinking 块做签名校验,而 chat 格式无法携带签名,合成块会被 400 拒绝;最新回合保持修复前的可接受形状(text 占位 + tool_use),较早回合官方允许省略/忽略,可安全携带。连续同角色消息合并时推理一并合并(修复合并丢推理问题);"..."占位逻辑保持原行为以维持合并机制,thinking 块存在时抑制占位文本输出。oai_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
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
📸 运行证明 / Proof of Work
新增回归测试(每个方向 + 端到端往返 + 对抗边界)
对抗性审查(5 视角独立审查 + 逐条反驳验证)
构建与测试
Golden 快照:仅
claude_to_openai/claude_to_gemini/claude_to_openai_responses三个请求转换快照有 diff,且 diff 内容恰为被保留的推理(reasoning_content/thoughtpart /reasoning输入项)——证明无推理请求的输出零变化,行为变更严格收敛在"保留推理"上。Summary by CodeRabbit
New Features
Bug Fixes