v0.3.1 5c1a9ec

reasoning.go RawHistoryFolder

1package provider
2
3import "strings"
4
5var reasoningLevelOrder = []string{"", "minimum", "low", "medium", "high", "xhigh", "max"}
6
7// AvailableReasoningLevels returns the distinct reasoning levels supported by
8// a model. Protocol defaults are adjusted by optional per-model overrides. The
9// empty string represents off.
10func AvailableReasoningLevels(model Model) []string {
11	defaults := defaultReasoningLevels(model)
12	if !model.Reasoning || len(model.ReasoningLevelMap) == 0 {
13		return defaults
14	}
15
16	available := map[string]bool{"": true}
17	for _, level := range reasoningLevelOrder[1:] {
18		if !containsReasoningLevel(defaults, level) {
19			if _, overridden := model.ReasoningLevelMap[level]; !overridden {
20				continue
21			}
22		}
23		effective := level
24		if mapped, overridden := model.ReasoningLevelMap[level]; overridden {
25			effective = NormalizeReasoning(mapped)
26		}
27		if effective != "" && !containsReasoningLevel(defaults, effective) {
28			effective = nearestReasoningLevel(defaults, effective)
29		}
30		if reasoningLevelRank(effective) > 0 {
31			available[effective] = true
32		}
33	}
34
35	levels := []string{""}
36	for _, level := range reasoningLevelOrder[1:] {
37		if available[level] {
38			levels = append(levels, level)
39		}
40	}
41	return levels
42}
43
44func defaultReasoningLevels(model Model) []string {
45	if !model.Reasoning {
46		return []string{""}
47	}
48
49	id := strings.ToLower(model.ID)
50	if (model.Provider == "google" || model.Provider == "google-vertex") && strings.Contains(id, "gemini-3") {
51		if strings.Contains(id, "-pro") {
52			return []string{"", "low", "high"}
53		}
54		return []string{"", "minimum", "low", "medium", "high"}
55	}
56	if model.AdaptiveThinkingCompat {
57		return []string{"", "high"}
58	}
59	if model.AdaptiveThinking {
60		return []string{"", "low", "medium", "high", "xhigh", "max"}
61	}
62	if model.API == APIResponses || model.Provider == "openai-codex" || model.Provider == "openai-responses" || model.Provider == "azure-openai-responses" {
63		levels := []string{"", "low", "medium", "high", "xhigh"}
64		if strings.HasPrefix(id, "gpt-5.6-") {
65			levels = append(levels, "max")
66		}
67		return levels
68	}
69	if model.Provider == "google" || model.Provider == "google-vertex" {
70		if strings.Contains(id, "gemini-2.5") {
71			return []string{"", "minimum", "low", "medium", "high", "xhigh"}
72		}
73		return []string{""}
74	}
75	if usesReasoningBudget(model) {
76		return []string{"", "minimum", "low", "medium", "high", "xhigh"}
77	}
78	if model.Provider == "amazon-bedrock" {
79		return []string{""}
80	}
81	return []string{"", "low", "medium", "high"}
82}
83
84func containsReasoningLevel(levels []string, target string) bool {
85	for _, level := range levels {
86		if level == target {
87			return true
88		}
89	}
90	return false
91}
92
93// ClampReasoningForModel maps a configured level to the nearest level exposed
94// for the active model. Ties prefer the higher level.
95func ClampReasoningForModel(model Model, level string) string {
96	normalized := NormalizeReasoning(level)
97	available := AvailableReasoningLevels(model)
98	if mapped, overridden := model.ReasoningLevelMap[normalized]; overridden {
99		target := NormalizeReasoning(mapped)
100		if target != "" && containsReasoningLevel(available, target) {
101			return target
102		}
103	}
104	for _, candidate := range available {
105		if candidate == normalized {
106			return candidate
107		}
108	}
109	return nearestReasoningLevel(available, normalized)
110}
111
112func nearestReasoningLevel(available []string, requested string) string {
113	if requested == "" || len(available) == 1 {
114		return ""
115	}
116	requestedRank := reasoningLevelRank(requested)
117	if requestedRank == 0 {
118		return ""
119	}
120	best, bestDistance := available[1], len(reasoningLevelOrder)
121	for _, candidate := range available[1:] {
122		distance := reasoningLevelRank(candidate) - requestedRank
123		if distance < 0 {
124			distance = -distance
125		}
126		if distance <= bestDistance {
127			best, bestDistance = candidate, distance
128		}
129	}
130	return best
131}
132
133func reasoningLevelRank(level string) int {
134	for rank, candidate := range reasoningLevelOrder {
135		if candidate == level {
136			return rank
137		}
138	}
139	return 0
140}
141
142func usesReasoningBudget(model Model) bool {
143	switch model.Provider {
144	case "anthropic", "fireworks", "kimi", "minimax", "minimax-cn", "vercel-ai-gateway":
145		return true
146	}
147	return model.API == "anthropic"
148}
149
150// NormalizeReasoning canonicalizes zut's user-facing reasoning levels.
151// Empty string means reasoning is disabled. "maximum" remains
152// an alias for xhigh; "max" is the separate opt-in tier above it.
153func NormalizeReasoning(level string) string {
154	switch strings.ToLower(strings.TrimSpace(level)) {
155	case "", "off", "none", "no", "false", "disabled":
156		return ""
157	case "min", "minimal", "minimum":
158		return "minimum"
159	case "low":
160		return "low"
161	case "med", "medium":
162		return "medium"
163	case "hi", "high":
164		return "high"
165	case "xhigh", "maximum":
166		return "xhigh"
167	case "max":
168		return "max"
169	default:
170		return strings.ToLower(strings.TrimSpace(level))
171	}
172}
173
174// ReasoningBudget returns zut's approximate token budget for reasoning-capable
175// providers that accept explicit budgets.
176func ReasoningBudget(level string) int {
177	switch NormalizeReasoning(level) {
178	case "minimum":
179		return 1024
180	case "low":
181		return 2048
182	case "medium":
183		return 8192
184	case "high":
185		return 16384
186	case "xhigh", "max":
187		return 32768
188	default:
189		return 0
190	}
191}
192
193// AnthropicAdaptiveEffort maps zut's user-facing reasoning levels onto the
194// effort enum used by adaptive-thinking models. These models reject explicit
195// thinking budgets; reasoning depth is controlled by output_config.effort.
196func AnthropicAdaptiveEffort(level string) string {
197	switch NormalizeReasoning(level) {
198	case "minimum", "low":
199		return "low"
200	case "medium":
201		return "medium"
202	case "high":
203		return "high"
204	case "xhigh":
205		return "xhigh"
206	case "max":
207		return "max"
208	default:
209		return ""
210	}
211}
212
213// OpenAIReasoningEffort maps zut's thinking setting onto the effort enum
214// accepted by generic OpenAI-compatible chat-completions endpoints.
215func OpenAIReasoningEffort(level string) string {
216	switch NormalizeReasoning(level) {
217	case "minimum", "low":
218		// Many compatible endpoints only accept low/medium/high.
219		return "low"
220	case "medium":
221		return "medium"
222	case "high", "xhigh", "max":
223		return "high"
224	default:
225		return ""
226	}
227}
228
229// OpenAICompatAnthropicEffort maps zut's thinking setting when an adaptive
230// Anthropic model is served over an OpenAI-compatible chat-completions wire.
231// Adaptive models accept native xhigh and max effort values.
232func OpenAICompatAnthropicEffort(level string) string {
233	switch NormalizeReasoning(level) {
234	case "minimum", "low":
235		return "low"
236	case "medium":
237		return "medium"
238	case "high":
239		return "high"
240	case "xhigh":
241		return "xhigh"
242	case "max":
243		return "max"
244	default:
245		return ""
246	}
247}
248
249// OpenAICodexReasoningEffort maps zut levels onto the Responses API effort
250// enum. GPT-5.6 supports native max; other models clamp max to xhigh.
251func OpenAICodexReasoningEffort(level, model string) string {
252	switch NormalizeReasoning(level) {
253	case "minimum", "low":
254		return "low"
255	case "medium":
256		return "medium"
257	case "high":
258		return "high"
259	case "xhigh":
260		return "xhigh"
261	case "max":
262		if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") {
263			return "max"
264		}
265		return "xhigh"
266	default:
267		return ""
268	}
269}

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