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