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