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