fix(executor): inherit reasoning_content from conversation history for OpenAI-compatible providers#3543
fix(executor): inherit reasoning_content from conversation history for OpenAI-compatible providers#3543gwdgithubnom wants to merge 2 commits into
Conversation
…r OpenAI-compatible providers
There was a problem hiding this comment.
Code Review
This pull request introduces a new utility function, PreserveReasoningContent, in the helps package to ensure that reasoning_content is maintained in assistant messages during multi-turn tool-call scenarios. This logic is required by providers like MiMo and DeepSeek to maintain model context when client SDKs might otherwise strip it. The function is integrated into the OpenAICompatExecutor for both standard and streaming execution paths and is accompanied by comprehensive unit tests covering various injection scenarios and edge cases. I have no feedback to provide as no review comments were present.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e150c06029
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
fix: preserve
reasoning_contentacross multi-turn tool-call conversations for all OpenAI-compatible providers (DeepSeek, Kimi, MiMo, etc.).Problem: When thinking mode is enabled and tool calls are present, providers like DeepSeek and MiMo require
reasoning_contentto be passed back verbatim in subsequent requests. Some client SDKs (e.g.@ai-sdk/openai-compatible) may strip this field from conversation history, especially in older versions, causing 400 errors:{"error":{"message":"The `reasoning_content` in the thinking mode must be passed back to the API.","type":"invalid_request_error"}}See MiMo official documentation for the full requirement.
Fix: Add
PreserveReasoningContent()in the executor layer that scans conversation history to inherit the latest non-emptyreasoning_contentinto assistant messages withtool_callsthat are missing it.Key design principles:
reasoning_contentfrom conversation history. No fallback to content text or placeholders.reasoning_content: ""does not overwrite a prior non-empty value.reasoning_content, this function becomes a no-op (only iterates messages for existence checks, no JSON modifications, negligible overhead).Why executor layer, not translator layer: The
translator-path-guardCI blocks any PR that touchesinternal/translator/. This executor-layer approach avoids that restriction while covering all source formats (OpenAI, Claude, Codex) that route throughOpenAICompatExecutor.Changes
internal/runtime/executor/helps/reasoning_preserve.goPreserveReasoningContent()functioninternal/runtime/executor/helps/reasoning_preserve_test.gointernal/runtime/executor/openai_compat_executor.goPreserveReasoningContentin Execute/ExecuteStreamTest Coverage (11 cases)
Validation
Related
Relationship to PR #3523
PR #3523 (
Feature/support deepseek reasoning content) is model-limited to DeepSeek V4 and handlesResponses API reasoning items. This PR is complementary, not duplicate:
Both are needed for full coverage. This PR works as a safety net even if PR #3523 is merged.
Critical: Translator Layer Gaps (Requires Maintainer @luispater Action)
This PR fixes the executor-layer gap. However, there are additional gaps in the translator layer that cause
reasoning_contentto be lost BEFORE the executor can preserve it. All translator fixes are blocked bytranslator-path-guardCI and require maintainer action.Gap A: Protocol Translators Missing Reasoning Type Handling (e.g., Claude → OpenAI)
Example file:
internal/translator/openai/claude/openai_claude_request.go(line ~146)Problem: When a client (e.g., Claude Code, Kilo Code, Gemini CLI) routes through CPA to DeepSeek/MiMo, the response flow is:
The Claude→OpenAI translator handles
"thinking"(Anthropic's native type) but NOT"reasoning"(the type used when translating OpenAI responses back to Claude format):Impact: The same gap exists across multiple protocol translators (Claude→OpenAI, Gemini→OpenAI, etc.). Any client routing through CPA to OpenAI-compatible providers will lose
reasoning_contenton subsequent tool-call rounds.Fix (Claude→OpenAI example): Add
case "reasoning":handler identical tocase "thinking"::Other protocol translators need the same pattern applied to their respective reasoning types (e.g.,
thoughtfor Gemini).Gap B: Responses API Translator Missing
reasoningInput ItemsFile:
internal/translator/openai/openai/responses/openai_openai-responses_request.goProblem:
ConvertOpenAIResponsesRequestToOpenAIChatCompletions()does not handlereasoninginput items. When Codex CLI sends conversation history containing reasoning items from previous turns, the reasoning data is silently dropped during translation to Chat Completions format.Fix: Add
case "reasoning":branch to extractsummary_text(andencrypted_contentas fallback), inject into next assistant message asreasoning_content.Why These Must Be Fixed Separately
All translator fixes are blocked by
translator-path-guardCI which rejects any PR touchinginternal/translator/. This PR intentionally avoids that restriction by working at the executor layer.Recommendation: The maintainer should fix Gap A and Gap B directly, or temporarily allow translator changes for this specific fix. Without these translator fixes, the executor-layer
PreserveReasoningContentcan only help when thereasoning_contentfield already exists in the request body (i.e., when the client SDK preserved it but other processing stripped it). When the translator drops reasoning data during format conversion, there is nothing for the executor to preserve.Full Coverage Requires All Three Layers
reasoningtype not handledreasoninginput items dropped