package thinking_test import ( "testing" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" _ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/codex" "github.com/tidwall/gjson") func TestExtractCodexReasoningEffortWithConfigurationUpdate(t *testing.T) { tests := []struct { name string provider string model string body string wantRequestEffort string wantTranslatedEffort string }{ { name: "codex extracts configuration_update effort over top-level", provider: "codex", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`, wantRequestEffort: "low", wantTranslatedEffort: "low", }, { name: "openai-response extracts configuration_update effort over top-level", provider: "openai-response", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`, wantRequestEffort: "low", wantTranslatedEffort: "low", }, { name: "codex picks latest configuration_update when multiple are present", provider: "codex", model: "gpt-6-astra", body: `{ "model":"gpt-6-astra", "reasoning":{"effort":"xhigh","summary":"auto"}, "input":[ {"type":"configuration_update","reasoning":{"effort":"low"}}, {"role":"user","content":"second turn"}, {"type":"configuration_update","reasoning":{"effort":"medium"}} ] }`, wantRequestEffort: "medium", wantTranslatedEffort: "medium", }, { name: "codex falls back to top-level when configuration_update has no reasoning effort", provider: "codex", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","tools":[]}]}`, wantRequestEffort: "xhigh", wantTranslatedEffort: "xhigh", }, { name: "codex handles configuration_update effort none", provider: "codex", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"type":"configuration_update","reasoning":{"effort":"none"}}]}`, wantRequestEffort: "none", wantTranslatedEffort: "none", }, { name: "codex handles configuration_update effort auto", provider: "codex", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"type":"configuration_update","reasoning":{"effort":"auto"}}]}`, wantRequestEffort: "auto", wantTranslatedEffort: "auto", }, { name: "trailing configuration_update without reasoning effort preserves earlier effort", provider: "codex", model: "gpt-6-astra", body: `{ "model":"gpt-6-astra", "reasoning":{"effort":"xhigh"}, "input":[ {"type":"configuration_update","reasoning":{"effort":"low"}}, {"type":"configuration_update","tools":[]} ] }`, wantRequestEffort: "low", wantTranslatedEffort: "low", }, { name: "invalid json returns empty effort", provider: "codex", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":`, wantRequestEffort: "", wantTranslatedEffort: "", }, { name: "codex falls back to top-level when input has no configuration_update", provider: "codex", model: "gpt-6-astra", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"role":"user","content":"hello"}]}`, wantRequestEffort: "xhigh", wantTranslatedEffort: "xhigh", }, { name: "model suffix takes precedence over configuration_update for request effort", provider: "codex", model: "gpt-6-astra(high)", body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`, wantRequestEffort: "high", wantTranslatedEffort: "low", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { gotRequest := thinking.ExtractReasoningEffort([]byte(tc.body), tc.provider, tc.model) if gotRequest != tc.wantRequestEffort { t.Fatalf("ExtractReasoningEffort() = %q, want %q", gotRequest, tc.wantRequestEffort) } gotTranslated := thinking.ExtractTranslatedReasoningEffort([]byte(tc.body), tc.provider) if gotTranslated != tc.wantTranslatedEffort { t.Fatalf("ExtractTranslatedReasoningEffort() = %q, want %q", gotTranslated, tc.wantTranslatedEffort) } }) }} func TestApplyThinkingPreservesCodexTopLevelReasoningEffortBaseline(t *testing.T) { // Preserving request-level baseline sent upstream ensures prompt prefix cache preservation. body := []byte(`{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`) applied, err := thinking.ApplyThinking(body, "gpt-6-astra", "codex", "codex", "codex") if err != nil { t.Fatalf("ApplyThinking() error = %v", err) } // Verify top-level request baseline is preserved for upstream prompt caching. if gotTopEffort := gjson.GetBytes(applied, "reasoning.effort").String(); gotTopEffort != "xhigh" { t.Fatalf("ApplyThinking() top-level reasoning.effort = %q, want %q", gotTopEffort, "xhigh") } if gotSummary := gjson.GetBytes(applied, "reasoning.summary").String(); gotSummary != "auto" { t.Fatalf("ApplyThinking() reasoning.summary = %q, want %q", gotSummary, "auto") } if gotInputEffort := gjson.GetBytes(applied, "input.0.reasoning.effort").String(); gotInputEffort != "low" { t.Fatalf("ApplyThinking() input[0].reasoning.effort = %q, want %q", gotInputEffort, "low") } // Verify usage reporting extracts the effective in-turn effort. effort := thinking.ExtractTranslatedReasoningEffort(applied, "codex") if effort != "low" { t.Fatalf("ExtractTranslatedReasoningEffort(applied) = %q, want %q", effort, "low") }}
package thinking_test
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking/provider/codex"
"github.com/tidwall/gjson"
)
func TestExtractCodexReasoningEffortWithConfigurationUpdate(t *testing.T) {
tests := []struct {
name string
provider string
model string
body string
wantRequestEffort string
wantTranslatedEffort string
}{
{
name: "codex extracts configuration_update effort over top-level",
provider: "codex",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`,
wantRequestEffort: "low",
wantTranslatedEffort: "low",
},
{
name: "openai-response extracts configuration_update effort over top-level",
provider: "openai-response",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`,
wantRequestEffort: "low",
wantTranslatedEffort: "low",
},
{
name: "codex picks latest configuration_update when multiple are present",
provider: "codex",
model: "gpt-6-astra",
body: `{
"model":"gpt-6-astra",
"reasoning":{"effort":"xhigh","summary":"auto"},
"input":[
{"type":"configuration_update","reasoning":{"effort":"low"}},
{"role":"user","content":"second turn"},
{"type":"configuration_update","reasoning":{"effort":"medium"}}
]
}`,
wantRequestEffort: "medium",
wantTranslatedEffort: "medium",
},
{
name: "codex falls back to top-level when configuration_update has no reasoning effort",
provider: "codex",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","tools":[]}]}`,
wantRequestEffort: "xhigh",
wantTranslatedEffort: "xhigh",
},
{
name: "codex handles configuration_update effort none",
provider: "codex",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"type":"configuration_update","reasoning":{"effort":"none"}}]}`,
wantRequestEffort: "none",
wantTranslatedEffort: "none",
},
{
name: "codex handles configuration_update effort auto",
provider: "codex",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"type":"configuration_update","reasoning":{"effort":"auto"}}]}`,
wantRequestEffort: "auto",
wantTranslatedEffort: "auto",
},
{
name: "trailing configuration_update without reasoning effort preserves earlier effort",
provider: "codex",
model: "gpt-6-astra",
body: `{
"model":"gpt-6-astra",
"reasoning":{"effort":"xhigh"},
"input":[
{"type":"configuration_update","reasoning":{"effort":"low"}},
{"type":"configuration_update","tools":[]}
]
}`,
wantRequestEffort: "low",
wantTranslatedEffort: "low",
},
{
name: "invalid json returns empty effort",
provider: "codex",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":`,
wantRequestEffort: "",
wantTranslatedEffort: "",
},
{
name: "codex falls back to top-level when input has no configuration_update",
provider: "codex",
model: "gpt-6-astra",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"role":"user","content":"hello"}]}`,
wantRequestEffort: "xhigh",
wantTranslatedEffort: "xhigh",
},
{
name: "model suffix takes precedence over configuration_update for request effort",
provider: "codex",
model: "gpt-6-astra(high)",
body: `{"model":"gpt-6-astra","reasoning":{"effort":"xhigh"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`,
wantRequestEffort: "high",
wantTranslatedEffort: "low",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
gotRequest := thinking.ExtractReasoningEffort([]byte(tc.body), tc.provider, tc.model)
if gotRequest != tc.wantRequestEffort {
t.Fatalf("ExtractReasoningEffort() = %q, want %q", gotRequest, tc.wantRequestEffort)
}
gotTranslated := thinking.ExtractTranslatedReasoningEffort([]byte(tc.body), tc.provider)
if gotTranslated != tc.wantTranslatedEffort {
t.Fatalf("ExtractTranslatedReasoningEffort() = %q, want %q", gotTranslated, tc.wantTranslatedEffort)
}
})
}
}
func TestApplyThinkingPreservesCodexTopLevelReasoningEffortBaseline(t *testing.T) {
// Preserving request-level baseline sent upstream ensures prompt prefix cache preservation.
body := []byte(`{"model":"gpt-6-astra","reasoning":{"effort":"xhigh","summary":"auto"},"input":[{"type":"configuration_update","reasoning":{"effort":"low"}}]}`)
applied, err := thinking.ApplyThinking(body, "gpt-6-astra", "codex", "codex", "codex")
if err != nil {
t.Fatalf("ApplyThinking() error = %v", err)
}
// Verify top-level request baseline is preserved for upstream prompt caching.
if gotTopEffort := gjson.GetBytes(applied, "reasoning.effort").String(); gotTopEffort != "xhigh" {
t.Fatalf("ApplyThinking() top-level reasoning.effort = %q, want %q", gotTopEffort, "xhigh")
}
if gotSummary := gjson.GetBytes(applied, "reasoning.summary").String(); gotSummary != "auto" {
t.Fatalf("ApplyThinking() reasoning.summary = %q, want %q", gotSummary, "auto")
}
if gotInputEffort := gjson.GetBytes(applied, "input.0.reasoning.effort").String(); gotInputEffort != "low" {
t.Fatalf("ApplyThinking() input[0].reasoning.effort = %q, want %q", gotInputEffort, "low")
}
// Verify usage reporting extracts the effective in-turn effort.
effort := thinking.ExtractTranslatedReasoningEffort(applied, "codex")
if effort != "low" {
t.Fatalf("ExtractTranslatedReasoningEffort(applied) = %q, want %q", effort, "low")
}
}