v0.4.11 e5c9e04

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

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 477

↑↓ moveenter openesc close