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: "direct deepseek", model: Model{Provider: "deepseek", Reasoning: true}, want: []string{"", "low", "high", "max"}},
17 {name: "responses", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.5"}, want: []string{"", "low", "medium", "high", "xhigh"}},
18 {name: "responses native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
19 {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
20 {name: "sol native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-sol"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
21 {name: "luna native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-luna"}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
22 {name: "adaptive", model: Model{Reasoning: true, AdaptiveThinking: true}, want: []string{"", "low", "medium", "high", "xhigh", "max"}},
23 {name: "adaptive compatible", model: Model{Reasoning: true, AdaptiveThinkingCompat: true}, want: []string{"", "high"}},
24 {name: "gemini pro", model: Model{Provider: "google", ID: "gemini-3-pro-preview", Reasoning: true}, want: []string{"", "low", "high"}},
25 {name: "gemini flash", model: Model{Provider: "google", ID: "gemini-3-flash-preview", Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high"}},
26 {name: "gemini 2.5 budget", model: Model{Provider: "google", ID: "gemini-2.5-flash", Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high", "xhigh"}},
27 {name: "gemini alias without control", model: Model{Provider: "google", ID: "gemini-flash-latest", Reasoning: true}, want: []string{""}},
28 {name: "anthropic budget", model: Model{Provider: "anthropic", Reasoning: true}, want: []string{"", "minimum", "low", "medium", "high", "xhigh"}},
29 {name: "bedrock without effort control", model: Model{Provider: "amazon-bedrock", Reasoning: true}, want: []string{""}},
30 {
31 name: "per-model overrides",
32 model: Model{
33 Provider: "anthropic", Reasoning: true,
34 ReasoningLevelMap: map[string]string{"minimum": "low", "high": "", "max": "max"},
35 },
36 want: []string{"", "low", "medium", "xhigh", "max"},
37 },
38 {
39 name: "explicit levels extend generic defaults",
40 model: Model{
41 Provider: "custom", Reasoning: true,
42 ReasoningLevelMap: map[string]string{
43 "minimum": "", "low": "", "medium": "", "high": "high", "xhigh": "xhigh", "max": "max",
44 },
45 },
46 want: []string{"", "high", "xhigh", "max"},
47 },
48 }
49 for _, tt := range tests {
50 t.Run(tt.name, func(t *testing.T) {
51 if got := AvailableReasoningLevels(tt.model); !slices.Equal(got, tt.want) {
52 t.Fatalf("levels = %q, want %q", got, tt.want)
53 }
54 })
55 }
56}
57
58func TestClampReasoningForModel(t *testing.T) {
59 tests := []struct {
60 name string
61 model Model
62 level string
63 want string
64 }{
65 {name: "unsupported", model: Model{}, level: "high", want: ""},
66 {name: "generic max", model: Model{Reasoning: true}, level: "max", want: "high"},
67 {name: "deepseek xhigh maps to high", model: Model{Provider: "deepseek", Reasoning: true}, level: "xhigh", want: "high"},
68 {name: "deepseek max stays max", model: Model{Provider: "deepseek", Reasoning: true}, level: "max", want: "max"},
69 {name: "adaptive minimum", model: Model{Reasoning: true, AdaptiveThinking: true}, level: "minimum", want: "low"},
70 {name: "gemini pro medium", model: Model{Provider: "google", ID: "gemini-3-pro", Reasoning: true}, level: "medium", want: "high"},
71 {name: "native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-5.6-sol"}, level: "max", want: "max"},
72 {name: "astra native max", model: Model{Reasoning: true, API: APIResponses, ID: "gpt-6-astra"}, level: "max", want: "max"},
73 {
74 name: "explicit equivalent",
75 model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"minimum": "high"}},
76 level: "minimum",
77 want: "high",
78 },
79 {
80 name: "explicit max extends generic defaults",
81 model: Model{Reasoning: true, ReasoningLevelMap: map[string]string{"max": "max"}},
82 level: "max",
83 want: "max",
84 },
85 }
86 for _, tt := range tests {
87 t.Run(tt.name, func(t *testing.T) {
88 if got := ClampReasoningForModel(tt.model, tt.level); got != tt.want {
89 t.Fatalf("level = %q, want %q", got, tt.want)
90 }
91 })
92 }
93}
94
95func TestDeepSeekReasoningEffort(t *testing.T) {
96 client := NewDeepSeek("test", "").(*openaiClient)
97 for _, model := range []string{"deepseek-v4-pro", "deepseek-v4-flash"} {
98 m, err := FindModel("deepseek", model)
99 if err != nil {
100 t.Fatal(err)
101 }
102 if got := AvailableReasoningLevels(m); !slices.Equal(got, []string{"", "low", "high", "max"}) {
103 t.Fatalf("%s: levels = %q, want off/low/high/max", model, got)
104 }
105 for _, tc := range []struct{ requested, want string }{
106 {"low", "low"},
107 {"medium", "high"},
108 {"high", "high"},
109 {"xhigh", "high"},
110 {"max", "max"},
111 } {
112 wire, err := client.buildRequest(Request{Model: model, Reasoning: tc.requested})
113 if err != nil {
114 t.Fatal(err)
115 }
116 if wire.ReasoningEffort != tc.want {
117 t.Errorf("%s %s: reasoning_effort = %q, want %q", model, tc.requested, wire.ReasoningEffort, tc.want)
118 }
119 }
120 }
121}
122
123func TestOpenAIRequestUsesReasoningLevelMap(t *testing.T) {
124 SetLiveModels([]Model{{
125 Provider: "custom",
126 ID: "mapped-reasoning-model",
127 Reasoning: true,
128 ReasoningLevelMap: map[string]string{"high": "low", "max": "max"},
129 }})
130 t.Cleanup(func() { SetLiveModels(nil) })
131
132 client := NewOpenAICompat("custom", "test", "", "").(*openaiClient)
133 for _, tt := range []struct {
134 requested string
135 want string
136 }{
137 {requested: "high", want: "low"},
138 {requested: "max", want: "max"},
139 } {
140 request, err := client.buildRequest(Request{Model: "mapped-reasoning-model", Reasoning: tt.requested})
141 if err != nil {
142 t.Fatal(err)
143 }
144 if request.ReasoningEffort != tt.want {
145 t.Errorf("reasoning effort for %q = %q, want %q", tt.requested, request.ReasoningEffort, tt.want)
146 }
147 }
148}
149
150func TestReasoningEffortMappings(t *testing.T) {
151 cases := []struct {
152 level string
153 model string
154 openai string
155 anthCompat string
156 codex string
157 budget int
158 normalized string
159 }{
160 {"off", "gpt-5.6-sol", "", "", "", 0, ""},
161 {"minimum", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
162 {"minimal", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"},
163 {"low", "gpt-5.6-sol", "low", "low", "low", 2048, "low"},
164 {"medium", "gpt-5.6-sol", "medium", "medium", "medium", 8192, "medium"},
165 {"high", "gpt-5.6-sol", "high", "high", "high", 16384, "high"},
166 {"maximum", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
167 {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"},
168 {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"},
169 {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"},
170 {"max", "gpt-6-astra", "high", "max", "max", 32768, "max"},
171 {"max", "GPT-6-ASTRA", "high", "max", "max", 32768, "max"},
172 {"max", "gpt-6-sol", "high", "max", "max", 32768, "max"},
173 {"max", "gpt-6-luna", "high", "max", "max", 32768, "max"},
174 {"max", "gpt-6-unknown", "high", "max", "xhigh", 32768, "max"},
175 }
176 for _, tc := range cases {
177 if got := NormalizeReasoning(tc.level); got != tc.normalized {
178 t.Errorf("NormalizeReasoning(%q)=%q want %q", tc.level, got, tc.normalized)
179 }
180 if got := OpenAIReasoningEffort(tc.level); got != tc.openai {
181 t.Errorf("OpenAIReasoningEffort(%q)=%q want %q", tc.level, got, tc.openai)
182 }
183 if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat {
184 t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat)
185 }
186 if got := OpenAICodexReasoningEffort(tc.level, tc.model); got != tc.codex {
187 t.Errorf("OpenAICodexReasoningEffort(%q, %q)=%q want %q", tc.level, tc.model, got, tc.codex)
188 }
189 if got := ReasoningBudget(tc.level); got != tc.budget {
190 t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget)
191 }
192 }
193}