v0.48.1 deaab59

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

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