1package provider
2
3import (
4 "slices"
5 "testing"
6)
7
8func TestAvailableReasoningLevels(t *testing.T) {
9 tests := []struct {
10 name string
11 model Model
12 want []string
13 }{
14 {name: "unsupported", model: Model{}, want: []string{""}},
15 {name: "generic openai", model: Model{Reasoning: true}, want: []string{"", "low", "medium", "high"}},
16 {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}},
17 {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
18 {name: "adaptive", model: Model{Reasoning: true, AdaptiveThinking: true}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
19 {name: "adaptive compatible", model: Model{Reasoning: true, AdaptiveThinkingCompat: true}, want: []string{"", "high"}},
20 {name: "gemini pro", model: Model{Provider: "google", ID: "gemini-3-pro-preview", Reasoning: true}, want: []string{"", "low", "high"}},
21 {name: "gemini flash", model: Model{Provider: "google", ID: "gemini-3-flash-preview", Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high"}},
22 {name: "gemini 2.5 budget", model: Model{Provider: "google", ID: "gemini-2.5-flash", Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high", "xhigh"}},
23 {name: "gemini alias without control", model: Model{Provider: "google", ID: "gemini-flash-latest", Reasoning: true}, want: []string{""}},
24 {name: "anthropic budget", model: Model{Provider: "anthropic", Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high", "xhigh"}},
25 {name: "anthropic messages adapter", model: Model{API: APIAnthropicMessages, Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high", "xhigh"}},
26 {name: "bedrock without effort control", model: Model{Provider: "amazon-bedrock", Reasoning: true}, want: []string{""}},
27 {
28 name: "per-model overrides",
29 model: Model{
30 Provider: "anthropic", Reasoning: true,
31 ReasoningLevelMap: map[string]string{"minimum": "low", "high": "", "max": "max"},
32 },
33 want: []string{"", "low", "medium", "xhigh", "max"},
34 },
35 {
36 name: "explicit levels extend generic defaults",
37 model: Model{
38 Provider: "custom", Reasoning: true,
39 ReasoningLevelMap: map[string]string{
40 "minimum": "", "low": "", "medium": "", "high": "high", "xhigh": "xhigh", "max": "max",
41 },
42 },
43 want: []string{"", "high", "xhigh", "max"},
44 },
45 }
46 for _, tt := range tests {
47 t.Run(tt.name, func(t *testing.T) {
48 if got := AvailableReasoningLevels(tt.model); !slices.Equal(got, tt.want) {
49 t.Fatalf("levels = %q, want %q", got, tt.want)
50 }
51 })
52 }
53}
54
55func TestClampReasoningForModel(t *testing.T) {
56 tests := []struct {
57 name string
58 model Model
59 level string
60 want string
61 }{
62 {name: "unsupported", model: Model{}, level: "high", want: ""},
63 {name: "generic max", model: Model{Reasoning: true}, level: "max", want: "high"},
64 {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"},
65 {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"},
66 {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"},
67 {
68 name: "explicit equivalent",
69 model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"minimum": "high"}},
70 level: "minimum",
71 want: "high",
72 },
73 {
74 name: "explicit max extends generic defaults",
75 model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"max": "max"}},
76 level: "max",
77 want: "max",
78 },
79 }
80 for _, tt := range tests {
81 t.Run(tt.name, func(t *testing.T) {
82 if got := ClampReasoningForModel(tt.model, tt.level); got != tt.want {
83 t.Fatalf("level = %q, want %q", got, tt.want)
84 }
85 })
86 }
87}
88
89func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) {
90 SetLiveModels([]Model{{
91 Provider: "custom",
92 ID: "mapped-reasoning-model",
93 Reasoning: true,
94 ReasoningLevelMap: map[string]string{"high": "low", "max": "max"},
95 }})
96 t.Cleanup(func() { SetLiveModels(nil) })
97
98 client := NewOpenAICompat("custom", "test", "", "").(*openaiClient)
99 for _, tt := range []struct {
100 requested string
101 want string
102 }{
103 {requested: "high", want: "low"},
104 {requested: "max", want: "max"},
105 } {
106 request, err := client.buildRequest(Request{Model: "mapped-reasoning-model", Reasoning: tt.requested})
107 if err != nil {
108 t.Fatal(err)
109 }
110 if request.ReasoningEffort != tt.want {
111 t.Errorf("reasoning effort for %q = %q, want %q", tt.requested, request.ReasoningEffort, tt.want)
112 }
113 }
114}
115
116func TestOpenAIRequestUsesExactReasoningEffortMap(t *testing.T) {
117 preserveActiveCatalog(t)
118 SetLiveModels([]Model{{
119 Provider: "custom-compatible",
120 ID: "compatible-reasoning-model",
121 Reasoning: true,
122 ReasoningLevelMap: map[string]string{"minimum": "minimum"},
123 ReasoningEffortMap: map[string]string{"minimum": "minimal"},
124 }})
125
126 client := NewOpenAICompat("custom-compatible", "test", "", "").(*openaiClient)
127 request, err := client.buildRequest(Request{Model: "compatible-reasoning-model", Reasoning: "minimum"})
128 if err != nil {
129 t.Fatal(err)
130 }
131 if request.ReasoningEffort != "minimal" {
132 t.Fatalf("reasoning effort = %q, want minimal", request.ReasoningEffort)
133 }
134}
135
136func TestModelsDevReasoningMapsRestrictResponsesDefaults(t *testing.T) {
137 levelMap, effortMap := modelsDevReasoningMaps(true, []modelsDevReasoningOption{{
138 Type: "effort",
139 Values: []string{"low", "medium", "high"},
140 }})
141 model := Model{
142 Provider: "opencode-go",
143 ID: "gpt-5.6-restricted",
144 API: APIResponses,
145 Reasoning: true,
146 ReasoningLevelMap: levelMap,
147 ReasoningEffortMap: effortMap,
148 }
149 wantLevels := []string{"", "low", "medium", "high"}
150 if got := AvailableReasoningLevels(model); !slices.Equal(got, wantLevels) {
151 t.Fatalf("available levels = %q, want %q", got, wantLevels)
152 }
153 if got := ClampReasoningForModel(model, "max"); got != "high" {
154 t.Fatalf("clamped max = %q, want high", got)
155 }
156
157 preserveActiveCatalog(t)
158 SetLiveModels([]Model{model})
159 named := NewOpenAIResponsesNamed("token", "https://example.test/v1", "opencode-go").(*renamedClient)
160 client := named.inner.(*codexClient)
161 wire, err := client.buildRequest(Request{Model: model.ID, Reasoning: "max"})
162 if err != nil {
163 t.Fatal(err)
164 }
165 if wire.Reasoning == nil || wire.Reasoning.Effort != "high" {
166 t.Fatalf("wire reasoning = %+v, want high", wire.Reasoning)
167 }
168}
169
170func TestReasoningEffortMappings(t *testing.T) {
171 cases := []struct {
172 level string
173 model string
174 openai string
175 anthCompat string
176 codex string
177 budget int
178 normalized string
179 }{
180 {"off", "gpt-5.6-sol", "", "", "", 0, ""},
181 {"minimum", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
182 {"minimal", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
183 {"low", "gpt-5.6-sol", "low", "low", "low", 2048, "low"},
184 {"medium", "gpt-5.6-sol", "medium", "medium", "medium", 8192, "medium"},
185 {"high", "gpt-5.6-sol", "high", "high", "high", 16384, "high"},
186 {"maximum", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
187 {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
188 {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"},
189 {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"},
190 }
191 for _, tc := range cases {
192 if got := NormalizeReasoning(tc.level); got != tc.normalized {
193 t.Errorf("NormalizeReasoning(%q)=%q want %q", tc.level, got, tc.normalized)
194 }
195 if got := OpenAIReasoningEffort(tc.level); got != tc.openai {
196 t.Errorf("OpenAIReasoningEffort(%q)=%q want %q", tc.level, got, tc.openai)
197 }
198 if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat {
199 t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat)
200 }
201 if got := OpenAICodexReasoningEffort(tc.level, tc.model); got != tc.codex {
202 t.Errorf("OpenAICodexReasoningEffort(%q, %q)=%q want %q", tc.level, tc.model, got, tc.codex)
203 }
204 if got := ReasoningBudget(tc.level); got != tc.budget {
205 t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget)
206 }
207 }
208}