v0.4.3 2a19df1

reasoning_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close