Skip to content

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

Open
HandSonic wants to merge 2 commits into
QuantumNous:mainfrom
HandSonic:fix/relayconvert-request-reasoning
Open

fix(relayconvert): preserve assistant reasoning across request protocol conversions#6593
HandSonic wants to merge 2 commits 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, text, and media from being lost or misordered during conversion.
    • Excluded reasoning from user messages and preserved tool interactions.
    • Prevented reasoning content from being incorrectly converted into user messages.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The request conversion layer preserves assistant reasoning across Claude, OpenAI Chat, OpenAI Responses, and Gemini 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 and golden fixtures 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 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 PR adds Responses→Chat reasoning changes and tests, but [#6592] explicitly excludes that direction and #6396 is not listed as linked. Link #6396 or split the Responses→Chat changes into a separate PR; keep this PR limited to the four directions defined by [#6592].
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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
Linked Issues check ✅ Passed The changes implement all four reasoning-preservation paths required by [#6592], with ordering, role filtering, and Claude handling covered by tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes preserving assistant reasoning across request protocol conversions, which is the main change.
✨ 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.
ClaudeMessagesRequestToOpenAIChat only set the converted media content
when the turn had no tool calls, so the text of a mixed 'text + tool_use'
Claude assistant turn (a very common shape, e.g. '我来查一下' followed by
a tool call) was silently dropped to content=null. The upstream model
then loses the assistant's stated intent across multi-turn tool loops.

Set the media content whenever media messages exist, regardless of tool
calls. Pure tool_use turns keep the previous null-content shape.

Closes QuantumNous#6615

AI-assisted: implementation and tests were produced with AI assistance.
@AmPlace

AmPlace commented Aug 4, 2026

Copy link
Copy Markdown

Thanks for continuing the Responses-to-Chat reasoning-history fix.

The PR description already notes that this direction matches #6396, follows the implementation approach from #6395, and ports three tests from #6395. Since the original issue analysis, minimal reproduction, implementation, and regression tests originated in #6395, please preserve the contribution attribution when merging—for example by retaining the original commit, adding co-author attribution, or explicitly recording the source in the final commit message.

I am also available to help compare the current implementation against the original regression cases.

ishalumi added a commit to ishalumi/new-api that referenced this pull request Aug 20, 2026
…tool_search proxy + tool_choice namespace

QuantumNous#6593 剩余方向:
- OpenAI→Claude 请求: thinking 块 replay (非最新 assistant 轮)
- OpenAI→Responses 请求: reasoning_content → reasoning input item
- OpenAI→Gemini 请求: reasoning_content → thought part

QuantumNous#5939 补充:
- tool_search → function 代理 (Codex MCP 兼容)
- tool_choice: server_label/namespace → flattened name
- tool_choice: mcp/mcp_server/namespace → function choice
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)

2 participants