-
-
Notifications
You must be signed in to change notification settings - Fork 7.7k
fix: preserve reasoning_content for OpenAI-compatible providers #3427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
shenshuoyaoyouguang
wants to merge
9
commits into
router-for-me:dev
from
shenshuoyaoyouguang:fix/preserve-reasoning-content
Closed
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
dde5021
fix: preserve reasoning_content for OpenAI-compatible providers
xiaobendan483600 b197807
refactor: simplify reasonEntry to plain string, add message count saf…
xiaobendan483600 e5dd3cd
test: cover translated reasoning_content preservation when original l…
xiaobendan483600 1e83302
fix: remove reasoning_content inheritance to avoid fabricating stale …
xiaobendan483600 6297ace
perf: cache array results and pre-allocate map per Gemini review feed…
xiaobendan483600 845721e
fix: preserve reasoning_content across message count mismatches and R…
xiaobendan483600 5ca1322
refactor: address review feedback on reasoning_content preservation
xiaobendan483600 14c560d
fix: preserve reasoning items in follow-up requests and respect paylo…
xiaobendan483600 d2ddc4c
fix(responses): preserve reasoning across tool follow-ups
xiaobendan483600 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package executor | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/tidwall/gjson" | ||
| "github.com/tidwall/sjson" | ||
| ) | ||
|
|
||
| // preserveReasoningContent ensures assistant messages in the translated OpenAI-format | ||
| // payload retain reasoning_content from the original source payload. | ||
| // | ||
| // DeepSeek and other providers that support thinking mode require reasoning_content | ||
| // to be passed back verbatim in multi-turn conversations. Without this, the API returns | ||
| // a 400 error: "The reasoning_content in the thinking mode must be passed back to the API." | ||
| // | ||
| // Matching strategy: instead of requiring identical message counts (which breaks when | ||
| // translation inserts/splits messages like Claude tool_result → tool role), we match | ||
| // assistant messages by their ordinal position within the assistant-only sequence. | ||
| // This is robust because translation never reorders or drops assistant messages — | ||
| // it only inserts non-assistant messages (tool, system) around them. | ||
| // | ||
| // When the translated payload already carries reasoning_content at a given assistant | ||
| // ordinal (e.g. from a payload override or from translation), that value is preserved — | ||
| // the user or translator has explicitly set it and their intent takes precedence. | ||
| // Only when reasoning_content is missing do we fall back to the original value. | ||
| // | ||
| // Error contract: on sjson.SetBytes failure, the function discards any partial writes | ||
| // and returns the unmodified translated input along with the error, so the caller never | ||
| // receives a partially-patched payload. | ||
| func preserveReasoningContent(original, translated []byte) ([]byte, error) { | ||
| if len(original) == 0 || len(translated) == 0 { | ||
| return translated, nil | ||
| } | ||
| if !gjson.ValidBytes(original) || !gjson.ValidBytes(translated) { | ||
| return translated, nil | ||
| } | ||
|
|
||
| origMsgs := gjson.GetBytes(original, "messages") | ||
| if !origMsgs.Exists() || !origMsgs.IsArray() { | ||
| return translated, nil | ||
| } | ||
| origMsgArr := origMsgs.Array() | ||
|
|
||
| transMsgs := gjson.GetBytes(translated, "messages") | ||
| if !transMsgs.Exists() || !transMsgs.IsArray() { | ||
| return translated, nil | ||
| } | ||
| transMsgArr := transMsgs.Array() | ||
|
|
||
| origReasoning := collectAssistantReasoning(origMsgArr) | ||
| if len(origReasoning) == 0 { | ||
| return translated, nil | ||
| } | ||
|
|
||
| out := translated | ||
| assistantOrdinal := 0 | ||
| for i, msg := range transMsgArr { | ||
| if strings.TrimSpace(msg.Get("role").String()) != "assistant" { | ||
| continue | ||
| } | ||
|
|
||
| origText, origOK := origReasoning[assistantOrdinal] | ||
| transRC := msg.Get("reasoning_content") | ||
| if origOK && !transRC.Exists() { | ||
| path := fmt.Sprintf("messages.%d.reasoning_content", i) | ||
| next, err := sjson.SetBytes(out, path, origText) | ||
| if err != nil { | ||
| return translated, fmt.Errorf("preserveReasoningContent: failed to set reasoning_content at index %d: %w", i, err) | ||
| } | ||
| out = next | ||
| } | ||
| assistantOrdinal++ | ||
| } | ||
|
|
||
| return out, nil | ||
| } | ||
|
|
||
| // collectAssistantReasoning extracts reasoning_content from assistant messages, | ||
| // keyed by their ordinal position in the assistant-only sequence (0, 1, 2, ...). | ||
| // Empty-string reasoning_content is preserved because DeepSeek requires it. | ||
| func collectAssistantReasoning(messages []gjson.Result) map[int]string { | ||
| reasoning := make(map[int]string) | ||
| ordinal := 0 | ||
| for _, msg := range messages { | ||
| if strings.TrimSpace(msg.Get("role").String()) != "assistant" { | ||
| continue | ||
| } | ||
| if rc := msg.Get("reasoning_content"); rc.Exists() { | ||
| reasoning[ordinal] = rc.String() | ||
| } | ||
| ordinal++ | ||
| } | ||
| return reasoning | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user configures
payload.filteror an override to remove/replacemessages.N.reasoning_contentfor an OpenAI-compatible backend, this secondpreserveReasoningContentcall runs afterApplyPayloadConfigWithRootand unconditionally re-adds the original value. That makes the explicit payload config ineffective (the same ordering exists in the streaming path), so a provider that rejects or needs sanitized reasoning content can no longer be worked around via config.Useful? React with 👍 / 👎.