v0.4.7 2ff82fa

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

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