Conversation
Greptile SummaryThis PR centralizes outbound reasoning field handling by adding an explicit Confidence Score: 5/5Safe to merge — all providers correctly wire their reasoning field mode through both config and any direct The previously flagged P1 bypasses (Gemini, OpenRouter) are fully fixed. All 12 provider transformers show consistent reasoning field configuration. The single remaining finding is a P2 stale comment on llm/transformer/openai/outbound.go — minor stale comment on Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
LLMReq["llm.Request"] --> TF["TransformRequest\n(per-provider)"]
TF --> RFL{"ReasoningField\nconfig / explicit"}
RFL -->|ReasoningFieldContent| RFC["reasoning_content only\n(DeepSeek, Gemini, Fireworks,\nBailian, Moonshot, Zai,\nXAI, Doubao, Longcat, ModelScope)"]
RFL -->|ReasoningFieldReasoning| RFR["reasoning only\n(NanoGPT, OpenRouter)"]
RFL -->|ReasoningFieldNone| RFN["strip both\n(default when unset)"]
RFL -->|ReasoningFieldAll| RFA["preserve both\n(Copilot, client responses\nvia MessageFromLLM)"]
RFC --> OAIReq["openai.Request → HTTP body"]
RFR --> OAIReq
RFN --> OAIReq
RFA --> OAIReq
LLMResp["llm.Response"] --> CI["ChoiceFromLLM\n(inbound_convert.go)"]
CI --> MFLLM["MessageFromLLM\n→ ReasoningFieldAll"]
MFLLM --> ClientResp["Client-facing OpenAI response\n(both reasoning fields preserved)"]
Reviews (7): Last reviewed commit: "opt: openai outbound reasoning field con..." | Re-trigger Greptile |
There was a problem hiding this comment.
Code Review
This pull request introduces a ReasoningField configuration to the OpenAI outbound transformer to standardize how reasoning fields are handled across different LLM providers. The review highlights a breaking change where legacy conversion helpers now default to stripping reasoning fields, which negatively impacts providers like Gemini and OpenRouter that haven't been updated to use the new configuration-aware methods. Feedback also notes an inconsistency in the Fireworks provider setup and suggests a correction for a misleading test name in the inbound conversion suite.
|
|
||
| // RequestFromLLM creates OpenAI Request from unified llm.Request. | ||
| func RequestFromLLM(r *llm.Request) *Request { | ||
| return RequestFromLLMWithConfig(r, ReasoningFieldNone) |
There was a problem hiding this comment.
Changing the default behavior of RequestFromLLM to ReasoningFieldNone is a breaking change. Any transformer that overrides TransformRequest and relies on this helper (such as Gemini and OpenRouter in this repository) will now silently strip reasoning fields from outbound requests.
Please ensure all callers of RequestFromLLM and MessageFromLLM (line 104) are updated to use RequestFromLLMWithConfig with the appropriate field configuration, or consider maintaining the previous behavior (preserving both fields) as the default for these legacy helpers.
| PlatformType: openai.PlatformGoogle, | ||
| BaseURL: baseURL, | ||
| APIKeyProvider: config.APIKeyProvider, | ||
| ReasoningField: openai.ReasoningFieldContent, |
There was a problem hiding this comment.
The TransformRequest override in this file (line 344 in the full file) calls openai.RequestFromLLM, which now defaults to ReasoningFieldNone. This will cause reasoning fields to be stripped in outbound requests, ignoring the ReasoningFieldContent configuration set here. You should update TransformRequest to use openai.RequestFromLLMWithConfig with the configured field.
| PlatformType: openai.PlatformOpenAI, | ||
| BaseURL: config.BaseURL, | ||
| APIKeyProvider: config.APIKeyProvider, | ||
| ReasoningField: openai.ReasoningFieldReasoning, |
There was a problem hiding this comment.
The TransformRequest override in this file (line 104 in the full file) calls openai.RequestFromLLM, which now defaults to ReasoningFieldNone. This will cause reasoning fields to be stripped in outbound requests, ignoring the ReasoningFieldReasoning configuration set here. You should update TransformRequest to use openai.RequestFromLLMWithConfig with the configured field.
| PlatformType: openai.PlatformOpenAI, | ||
| BaseURL: baseURL, | ||
| APIKeyProvider: config.APIKeyProvider, | ||
| ReasoningField: openai.ReasoningFieldContent, |
There was a problem hiding this comment.
This configuration is inconsistent with the Fireworks provider's capabilities and the implementation in TransformRequest (line 104), which explicitly uses ReasoningFieldNone. Since Fireworks does not support reasoning fields, this should be set to None to avoid confusion.
| ReasoningField: openai.ReasoningFieldContent, | |
| ReasoningField: openai.ReasoningFieldNone, |
| }, | ||
| { | ||
| name: "Nil reasoning field - should not populate ReasoningContent", | ||
| name: "Empty reasoning_content with non-empty reasoning - should sync from Reasoning", |
There was a problem hiding this comment.
The test name is misleading. The test case actually verifies that the field is not synced when it is an empty string (as opposed to nil), which matches the implementation logic.
| name: "Empty reasoning_content with non-empty reasoning - should sync from Reasoning", | |
| name: "Empty reasoning_content with non-empty reasoning - should not sync from Reasoning", |
42fa818 to
c36bd7e
Compare
|
@gemini-code-assist review |
There was a problem hiding this comment.
Code Review
This pull request introduces a ReasoningField configuration to the OpenAI outbound transformer, enabling granular control over the handling of reasoning fields across different LLM providers. The Fireworks transformer has been refactored to leverage the base OpenAI implementation. Review feedback identifies a breaking change in the default reasoning field behavior and recommends defaulting to ReasoningFieldAll to maintain backward compatibility. Inconsistencies were also noted in the Doubao, Moonshot, and Zai transformers where explicit transformation calls do not align with their respective configurations. Furthermore, improvements are suggested for the message conversion logic to ensure proper field syncing and empty string handling.
| // Determine which reasoning field to use, default to ReasoningFieldNone | ||
| reasoningField := t.config.ReasoningField | ||
| if reasoningField == "" { | ||
| reasoningField = ReasoningFieldNone | ||
| } |
There was a problem hiding this comment.
Defaulting to ReasoningFieldNone when the configuration is empty is a breaking change for the base OpenAI transformer. Previously, reasoning fields were preserved and synced. It should default to ReasoningFieldAll to maintain backward compatibility for users of the generic OpenAI transformer who expect reasoning content to be passed through.
| // Determine which reasoning field to use, default to ReasoningFieldNone | |
| reasoningField := t.config.ReasoningField | |
| if reasoningField == "" { | |
| reasoningField = ReasoningFieldNone | |
| } | |
| // Determine which reasoning field to use, default to ReasoningFieldAll | |
| reasoningField := t.config.ReasoningField | |
| if reasoningField == "" { | |
| reasoningField = ReasoningFieldAll | |
| } |
| switch reasoningField { | ||
| case ReasoningFieldContent: | ||
| // Only use reasoning_content field | ||
| // Prefer ReasoningContent, fallback to Reasoning if ReasoningContent is nil | ||
| reasoningContent = m.ReasoningContent | ||
| if reasoningContent == nil && m.Reasoning != nil { | ||
| reasoningContent = m.Reasoning | ||
| } | ||
| reasoning = nil | ||
| case ReasoningFieldReasoning: | ||
| // Only use reasoning field | ||
| // Prefer Reasoning, fallback to ReasoningContent if Reasoning is nil | ||
| reasoning = m.Reasoning | ||
| if reasoning == nil && m.ReasoningContent != nil { | ||
| reasoning = m.ReasoningContent | ||
| } | ||
| reasoningContent = nil | ||
| case ReasoningFieldNone: | ||
| // Strip all reasoning fields | ||
| reasoningContent = nil | ||
| reasoning = nil | ||
| default: // ReasoningFieldAll | ||
| // Preserve both reasoning fields as-is | ||
| reasoningContent = m.ReasoningContent | ||
| reasoning = m.Reasoning | ||
| } |
There was a problem hiding this comment.
The new implementation of reasoning field handling has two issues:
- It omits the empty string check (
* != "") when performing fallbacks inReasoningFieldContentandReasoningFieldReasoningcases. This might lead to syncing empty reasoning strings which were previously ignored. - The
ReasoningFieldAll(default) case currently just copies the fields without the syncing/fallback logic that existed in the previous implementation. This is a regression for the default behavior.
Restoring the syncing logic ensures that if only one field is provided, it is copied to the other to maximize compatibility with various providers.
switch reasoningField {
case ReasoningFieldContent:
// Only use reasoning_content field
// Prefer ReasoningContent, fallback to Reasoning if ReasoningContent is nil
reasoningContent = m.ReasoningContent
if reasoningContent == nil && m.Reasoning != nil && *m.Reasoning != "" {
reasoningContent = m.Reasoning
}
reasoning = nil
case ReasoningFieldReasoning:
// Only use reasoning field
// Prefer Reasoning, fallback to ReasoningContent if Reasoning is nil
reasoning = m.Reasoning
if reasoning == nil && m.ReasoningContent != nil && *m.ReasoningContent != "" {
reasoning = m.ReasoningContent
}
reasoningContent = nil
case ReasoningFieldNone:
// Strip all reasoning fields
reasoningContent = nil
reasoning = nil
default: // ReasoningFieldAll
// Preserve both reasoning fields and sync them if one is missing
reasoningContent = m.ReasoningContent
reasoning = m.Reasoning
if reasoningContent == nil && reasoning != nil && *reasoning != "" {
reasoningContent = reasoning
}
if reasoning == nil && reasoningContent != nil && *reasoningContent != "" {
reasoning = reasoningContent
}
}|
|
||
| // Convert llm.Request to openai.Request first | ||
| oaiReq := openai.RequestFromLLM(llmReq) | ||
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldContent) |
There was a problem hiding this comment.
There is an inconsistency between the transformer configuration (which sets ReasoningFieldNone at line 72) and this explicit call to RequestFromLLM which uses ReasoningFieldContent. Since Doubao is configured to strip reasoning fields, this call should use openai.ReasoningFieldNone to be consistent and avoid sending unsupported fields to the provider.
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldContent) | |
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldNone) |
|
|
||
| // Convert llm.Request to openai.Request first | ||
| oaiReq := openai.RequestFromLLM(llmReq) | ||
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldContent) |
There was a problem hiding this comment.
There is an inconsistency between the transformer configuration (which sets ReasoningFieldNone at line 47) and this explicit call to RequestFromLLM which uses ReasoningFieldContent. Since Moonshot is configured to strip reasoning fields, this call should use openai.ReasoningFieldNone to align with the intended configuration.
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldContent) | |
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldNone) |
|
|
||
| // ReasoningField specifies which reasoning field to use in outbound messages. | ||
| // Use ReasoningFieldContent for DeepSeek/Gemini, ReasoningFieldReasoning for NanoGPT/OpenRouter, | ||
| // or ReasoningFieldNone (default) to strip all reasoning fields. |
There was a problem hiding this comment.
If the default behavior is changed to ReasoningFieldAll to maintain backward compatibility (as suggested in the TransformRequest logic), this comment should be updated accordingly.
| // or ReasoningFieldNone (default) to strip all reasoning fields. | |
| // or ReasoningFieldAll (default) to preserve both reasoning fields. |
|
|
||
| // Convert llm.Request to openai.Request first | ||
| oaiReq := openai.RequestFromLLM(llmReq) | ||
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldContent) |
There was a problem hiding this comment.
There is an inconsistency between the transformer configuration (which sets ReasoningFieldNone at line 51) and this explicit call to RequestFromLLM which uses ReasoningFieldContent. Since Zai is configured to strip reasoning fields, this call should use openai.ReasoningFieldNone.
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldContent) | |
| oaiReq := openai.RequestFromLLM(llmReq, openai.ReasoningFieldNone) |
c177ccd to
1898d1e
Compare
No description provided.