Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/codex-lite-reasoning-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### English

- Codex Responses-Lite models (such as GPT-5.6) now send `reasoning.context: "all_turns"`, fixing the upstream 400 "requires reasoning.context to be all_turns".

### 中文

- Codex Responses-Lite 模型(如 GPT-5.6)现在会发送 `reasoning.context: "all_turns"`,修复上游返回 400 “requires reasoning.context to be all_turns” 的问题。
44 changes: 44 additions & 0 deletions internal/providers/codex/responses_lite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package codex

import (
"encoding/json"
"testing"
)

// Regression for #300: requests sent under the Responses-Lite header must
// carry reasoning.context=all_turns, or the backend rejects them with 400.
func TestNormalizeCodexUpstreamLiteForcesReasoningContext(t *testing.T) {
for name, raw := range map[string]string{
"missing": "",
"effort": `{"effort":"high","summary":"auto"}`,
"override": `{"context":"current_turn"}`,
} {
t.Run(name, func(t *testing.T) {
fields := map[string]json.RawMessage{"input": json.RawMessage(`[]`)}
if raw != "" {
fields["reasoning"] = json.RawMessage(raw)
}
if err := normalizeCodexUpstream(fields, "gpt-5.6-sol", true, ""); err != nil {
t.Fatal(err)
}
var reasoning map[string]string
if err := json.Unmarshal(fields["reasoning"], &reasoning); err != nil {
t.Fatalf("reasoning %s: %v", fields["reasoning"], err)
}
if reasoning["context"] != "all_turns" || string(fields["parallel_tool_calls"]) != "false" {
t.Fatalf("reasoning=%s parallel_tool_calls=%s", fields["reasoning"], fields["parallel_tool_calls"])
}
if name == "effort" && (reasoning["effort"] != "high" || reasoning["summary"] != "auto") {
t.Fatalf("existing members lost: %s", fields["reasoning"])
}
})
}

fields := map[string]json.RawMessage{"input": json.RawMessage(`[]`)}
if err := normalizeCodexUpstream(fields, "gpt-5.5", false, ""); err != nil {
t.Fatal(err)
}
if _, ok := fields["reasoning"]; ok {
t.Fatalf("non-lite request gained reasoning: %s", fields["reasoning"])
}
}
19 changes: 19 additions & 0 deletions internal/providers/codex/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ func normalizeCodexUpstream(fields map[string]json.RawMessage, model string, lit
// except on lite where the backend still expects the explicit false.
if lite {
fields["parallel_tool_calls"] = json.RawMessage(`false`)
// The lite header also requires reasoning.context=all_turns (#300).
reasoning, err := withReasoningContextAllTurns(fields["reasoning"])
if err != nil {
return err
}
fields["reasoning"] = reasoning
} else if !emptyJSONArray(fields["tools"]) {
fields["parallel_tool_calls"] = json.RawMessage(`true`)
} else {
Expand All @@ -122,6 +128,19 @@ func normalizeCodexUpstream(fields map[string]json.RawMessage, model string, lit
return nil
}

// withReasoningContextAllTurns sets reasoning.context to all_turns and keeps
// every other reasoning member as sent.
func withReasoningContextAllTurns(raw json.RawMessage) (json.RawMessage, error) {
members := map[string]json.RawMessage{}
if !isNullJSON(raw) {
if err := json.Unmarshal(raw, &members); err != nil {
return nil, err
}
}
members["context"] = json.RawMessage(`"all_turns"`)
return json.Marshal(members)
}

// normalizeServiceTier keeps only the tiers the ChatGPT Codex backend accepts.
// "fast" is the client spelling of "priority" and is rewritten; anything else
// is removed rather than forwarded.
Expand Down
Loading