v0.30.0 4a12e83

models.go RawHistoryFolder

1package provider
2
3import (
4	"fmt"
5	"maps"
6	"sync"
7)
8
9// Deliberate working budget, not the models' advertised maximum context.
10const openAIContextWindowTarget = 500_000
11
12// Model describes a single LLM we know about.
13type Model struct {
14	Provider          string // "anthropic" | "openai"
15	ID                string // API id
16	DisplayName       string
17	API               string // wire API override for providers that support multiple protocols
18	ContextWindow     int
19	MaxOutput         int
20	Reasoning         bool              // supports reasoning
21	ReasoningLevelMap map[string]string // optional level overrides; empty values remove a level
22
23	// AdaptiveThinking marks Anthropic models that only support the
24	// adaptive thinking mode (Opus 4.7+). These reject explicit
25	// thinking budgets (thinking:{type:"enabled",budget_tokens:N} -> 400)
26	// and also reject non-default sampling params (temperature/top_p/
27	// top_k). The Anthropic client sends thinking:{type:"adaptive"} plus
28	// output_config.effort and omits temperature for these models.
29	AdaptiveThinking bool
30
31	// AdaptiveThinkingCompat marks Anthropic-compatible models that require
32	// thinking:{type:"adaptive"} but still accept sampling parameters and do
33	// not support Anthropic's output_config.effort extension.
34	AdaptiveThinkingCompat bool
35
36	// Prices are USD per 1M tokens. Price*Above fields apply to all token
37	// classes when the total prompt size exceeds PriceTierInputTokens.
38	PriceInput           float64
39	PriceOutput          float64
40	PriceCacheRead       float64
41	PriceCacheWrite      float64
42	PriceTierInputTokens int
43	PriceInputAbove      float64
44	PriceOutputAbove     float64
45	PriceCacheReadAbove  float64
46	PriceCacheWriteAbove float64
47
48	// Speculative marks models whose ids are known from the upstream
49	// vendor's CLI but not yet live on their public API. They'll 404
50	// today but start working the moment the provider flips the switch.
51	Speculative bool
52
53	// BaseURL overrides the provider's default API endpoint for this
54	// model. Optional; when empty the provider's default (or the
55	// --base-url flag) is used. Useful for local models served by
56	// ollama, vLLM, LM Studio, etc.
57	BaseURL string
58
59	// Source is where this model entry came from: "catalog" (baked in),
60	// "live" (discovered via /v1/models), or "cache" (loaded from the
61	// on-disk cache). Informational.
62	Source string
63}
64
65// Catalog is the hardcoded, read-only list of supported models.
66// Prices are USD per 1M tokens. The list is curated to what zut's
67// clients (Anthropic Messages + OpenAI Chat Completions) can actually
68// talk to; models that are only reachable through the OpenAI Responses
69// API (o1-pro, o3-pro, gpt-5-pro) are omitted.
70var Catalog = []Model{
71	// ---- Anthropic / Claude 4.x ----
72	{
73		Provider: "anthropic", ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5 (latest)",
74		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
75		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
76	},
77	{
78		Provider: "anthropic", ID: "claude-opus-4-1", DisplayName: "Claude Opus 4.1 (latest)",
79		ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
80		PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
81	},
82	{
83		Provider: "anthropic", ID: "claude-opus-4-0", DisplayName: "Claude Opus 4 (latest)",
84		ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
85		PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
86	},
87	{
88		Provider: "anthropic", ID: "claude-sonnet-4-0", DisplayName: "Claude Sonnet 4 (latest)",
89		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
90		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
91	},
92	{
93		Provider: "anthropic", ID: "claude-haiku-4-5", DisplayName: "Claude Haiku 4.5 (latest)",
94		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
95		PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
96	},
97
98	// ---- Anthropic / Claude 3.x (legacy) ----
99	{
100		Provider: "anthropic", ID: "claude-3-7-sonnet-20250219", DisplayName: "Claude Sonnet 3.7",
101		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
102		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
103	},
104	{
105		Provider: "anthropic", ID: "claude-3-5-sonnet-20241022", DisplayName: "Claude Sonnet 3.5 v2",
106		ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
107		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
108	},
109	{
110		Provider: "anthropic", ID: "claude-3-5-haiku-latest", DisplayName: "Claude Haiku 3.5 (latest)",
111		ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
112		PriceInput: 0.8, PriceOutput: 4, PriceCacheRead: 0.08, PriceCacheWrite: 1,
113	},
114	{
115		Provider: "anthropic", ID: "claude-3-opus-20240229", DisplayName: "Claude Opus 3",
116		ContextWindow: 200000, MaxOutput: 4096, Reasoning: false,
117		PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
118	},
119
120	// ---- DeepSeek ----
121	// The current public DeepSeek API exposes the V4 family on
122	// api.deepseek.com/v1. Pro is the flagship reasoning model;
123	// Flash is the cheaper/faster sibling. Both accept image inputs
124	// (multimodal parts: image_url) in addition to text.
125	{
126		Provider: "deepseek", ID: "deepseek-v4-pro", DisplayName: "DeepSeek V4 Pro",
127		ContextWindow: 1000000, MaxOutput: 384000, Reasoning: true,
128		PriceInput: 0.435, PriceOutput: 0.87, PriceCacheRead: 0.003625,
129		BaseURL: "https://api.deepseek.com",
130	},
131	{
132		Provider: "deepseek", ID: "deepseek-v4-flash", DisplayName: "DeepSeek V4 Flash",
133		ContextWindow: 1000000, MaxOutput: 384000, Reasoning: true,
134		PriceInput: 0.14, PriceOutput: 0.28, PriceCacheRead: 0.0028,
135		BaseURL: "https://api.deepseek.com",
136	},
137
138	// ---- Kimi / Kimi Code ----
139	// Anthropic-messages on https://api.kimi.com/coding (no /v1 suffix;
140	// the Anthropic client appends /v1/messages itself).
141	{
142		Provider: "kimi", ID: "kimi-for-coding", DisplayName: "Kimi For Coding",
143		ContextWindow: 262144, MaxOutput: 32768, Reasoning: true,
144		PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0,
145		BaseURL: "https://api.kimi.com/coding",
146	},
147
148	// ---- OpenAI / GPT-5 family ----
149	{
150		Provider: "openai", ID: "gpt-5", DisplayName: "GPT-5",
151		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
152		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.125,
153	},
154	{
155		Provider: "openai", ID: "gpt-5-mini", DisplayName: "GPT-5 Mini",
156		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
157		PriceInput: 0.25, PriceOutput: 2, PriceCacheRead: 0.025,
158	},
159	{
160		Provider: "openai", ID: "gpt-5-nano", DisplayName: "GPT-5 Nano",
161		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
162		PriceInput: 0.05, PriceOutput: 0.4, PriceCacheRead: 0.005,
163	},
164
165	// ---- OpenAI / GPT-4.1 family ----
166	{
167		Provider: "openai", ID: "gpt-4.1", DisplayName: "GPT-4.1",
168		ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
169		PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5,
170	},
171	{
172		Provider: "openai", ID: "gpt-4.1-mini", DisplayName: "GPT-4.1 mini",
173		ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
174		PriceInput: 0.4, PriceOutput: 1.6, PriceCacheRead: 0.1,
175	},
176	{
177		Provider: "openai", ID: "gpt-4.1-nano", DisplayName: "GPT-4.1 nano",
178		ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
179		PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.03,
180	},
181
182	// ---- OpenAI / GPT-4o family ----
183	{
184		Provider: "openai", ID: "gpt-4o", DisplayName: "GPT-4o",
185		ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
186		PriceInput: 2.5, PriceOutput: 10, PriceCacheRead: 1.25,
187	},
188	{
189		Provider: "openai", ID: "gpt-4o-mini", DisplayName: "GPT-4o mini",
190		ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
191		PriceInput: 0.15, PriceOutput: 0.6, PriceCacheRead: 0.08,
192	},
193
194	// ---- OpenAI / reasoning models ----
195	{
196		Provider: "openai", ID: "o4-mini", DisplayName: "o4-mini",
197		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
198		PriceInput: 1.1, PriceOutput: 4.4, PriceCacheRead: 0.28,
199	},
200	{
201		Provider: "openai", ID: "o3", DisplayName: "o3",
202		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
203		PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5,
204	},
205	{
206		Provider: "openai", ID: "o3-mini", DisplayName: "o3-mini",
207		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
208		PriceInput: 1.1, PriceOutput: 4.4, PriceCacheRead: 0.55,
209	},
210	{
211		Provider: "openai", ID: "o1", DisplayName: "o1",
212		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
213		PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5,
214	},
215
216	// ---- Google / Gemini ----
217	{
218		Provider: "google", ID: "gemini-2.5-pro", DisplayName: "Gemini 2.5 Pro",
219		ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
220		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.125,
221	},
222	{
223		Provider: "google", ID: "gemini-2.5-flash", DisplayName: "Gemini 2.5 Flash",
224		ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
225		PriceInput: 0.3, PriceOutput: 2.5, PriceCacheRead: 0.03,
226	},
227	{
228		Provider: "google", ID: "gemini-2.5-flash-lite", DisplayName: "Gemini 2.5 Flash-Lite",
229		ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
230		PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.01,
231	},
232	{
233		Provider: "google", ID: "gemini-2.0-flash", DisplayName: "Gemini 2.0 Flash",
234		ContextWindow: 1048576, MaxOutput: 8192, Reasoning: false,
235		PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.025,
236	},
237	{
238		Provider: "google", ID: "gemini-2.0-flash-lite", DisplayName: "Gemini 2.0 Flash-Lite",
239		ContextWindow: 1048576, MaxOutput: 8192, Reasoning: false,
240		PriceInput: 0.075, PriceOutput: 0.3, PriceCacheRead: 0,
241	},
242
243	// ---- OpenRouter ----
244	// Seed entry only: the default model returned by defaultModelForProvider
245	// so it resolves offline. The full OpenRouter catalog is discovered live
246	// (DiscoverOpenRouter) and overlays this on refresh.
247	{
248		Provider: "openrouter", ID: "anthropic/claude-sonnet-4.5", DisplayName: "Claude Sonnet 4.5 (OpenRouter)",
249		ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
250		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
251		BaseURL: openrouterDefaultBaseURL,
252	},
253	{
254		Provider: "openrouter", ID: "anthropic/claude-sonnet-5", DisplayName: "Claude Sonnet 5 (OpenRouter)",
255		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
256		PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5,
257		BaseURL: openrouterDefaultBaseURL,
258	},
259
260	// ---- Speculative: Anthropic ----
261	{
262		Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
263		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
264		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
265		Speculative: true,
266	},
267	{
268		Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
269		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
270		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
271		Speculative: true,
272	},
273	{
274		Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
275		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
276		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
277		Speculative: true,
278	},
279	{
280		Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8",
281		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
282		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
283		Speculative: true,
284	},
285	{
286		Provider: "anthropic", ID: "claude-opus-5", DisplayName: "Claude Opus 5",
287		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
288		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
289	},
290	{
291		Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
292		ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
293		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
294		Speculative: true,
295	},
296	{
297		Provider: "anthropic", ID: "claude-sonnet-5", DisplayName: "Claude Sonnet 5",
298		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
299		PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5,
300	},
301	{
302		Provider: "anthropic", ID: "claude-fable-5", DisplayName: "Claude Fable 5",
303		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
304		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
305		Speculative: true,
306	},
307
308	// ---- Speculative: OpenAI ----
309	// Public OpenAI API route. The ChatGPT/Codex subscription route is
310	// represented separately below as provider "openai-codex".
311	{
312		Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
313		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
314		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.13,
315		Speculative: true,
316	},
317	{
318		Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
319		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
320		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
321		Speculative: true,
322	},
323	{
324		Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
325		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
326		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
327		Speculative: true,
328	},
329	{
330		Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
331		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
332		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
333		Speculative: true,
334	},
335	{
336		Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
337		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
338		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
339		Speculative: true,
340	},
341	{
342		Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", API: APIResponses,
343		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
344		PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
345		Speculative: true,
346	},
347	{
348		Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", API: APIResponses,
349		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
350		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
351		Speculative: true,
352	},
353	{
354		Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", API: APIResponses,
355		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
356		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
357		Speculative: true,
358	},
359	{
360		Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses,
361		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
362		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5,
363		PriceTierInputTokens: 272000,
364		PriceInputAbove:      20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25,
365	},
366	// ---- OpenAI Codex / ChatGPT subscription backend ----
367	// Same model ids as the OpenAI family, but routed through the
368	// ChatGPT Codex OAuth backend rather than api.openai.com.
369	{
370		Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
371		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
372		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
373		Speculative: true,
374	},
375	{
376		Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4",
377		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
378		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
379		Speculative: true,
380	},
381	{
382		Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
383		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
384		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
385		Speculative: true,
386	},
387	{
388		Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5",
389		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
390		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
391		Speculative: true,
392	},
393	{
394		Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna",
395		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
396		PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
397	},
398	{
399		Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol",
400		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
401		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
402	},
403	{
404		Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra",
405		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
406		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
407	},
408	{
409		Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra",
410		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
411		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5,
412		PriceTierInputTokens: 272000,
413		PriceInputAbove:      20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25,
414	},
415}
416
417// DefaultModel is used when the user does not specify one.
418var DefaultModel = Catalog[0] // claude-sonnet-4-5
419
420// ----- active (merged) catalog -----
421//
422// Callers should use Active() / FindModel / ModelsForProvider for
423// lookups. They return the baked-in Catalog merged with any live
424// models loaded via SetLiveModels.
425
426var (
427	activeMu      sync.RWMutex
428	active        []Model // live overlay merged in via SetLiveModels; nil = none yet
429	activeSet     bool    // true once SetLiveModels has run (even with empty live)
430	managedModels []Model // ephemeral models exposed by local model managers
431)
432
433// SetLiveModels replaces the "live" overlay used by the active catalog.
434// Typically called after a successful /v1/models discovery or on load
435// from the on-disk cache.
436func SetLiveModels(live []Model) {
437	SetLiveModelsForProviders(live, nil)
438}
439
440// SetLiveModelsForProviders replaces the live overlay, treating the listed
441// providers' live catalogs as authoritative. For those providers, static
442// entries omitted by discovery are removed from the active catalog.
443func SetLiveModelsForProviders(live []Model, authoritativeProviders []string) {
444	activeMu.Lock()
445	defer activeMu.Unlock()
446	activeSet = true
447	if len(live) == 0 && len(authoritativeProviders) == 0 {
448		active = nil
449		return
450	}
451	active = MergeCatalogForProviders(live, authoritativeProviders)
452}
453
454// Active returns the current merged catalog.
455//
456// When no live overlay has been set it returns the fully-assembled
457// static Catalog. Reading Catalog here (rather than capturing it into
458// a package-level var initializer) is load-bearing: the extended
459// catalog in catalog_builtin.go / extra_models.go is appended from
460// init() functions, which run AFTER package-level var initializers.
461// Snapshotting Catalog at var-init time would freeze the picker to the
462// curated seed list and drop every extra provider (openrouter, groq,
463// xai, ...). Deferring the read to call time avoids that ordering trap.
464func Active() []Model {
465	activeMu.RLock()
466	defer activeMu.RUnlock()
467	src := active
468	if !activeSet || src == nil {
469		src = Catalog
470	}
471	out := make([]Model, len(src))
472	copy(out, src)
473	for i := range out {
474		out[i].ReasoningLevelMap = maps.Clone(out[i].ReasoningLevelMap)
475	}
476	if len(managedModels) == 0 {
477		return out
478	}
479	index := make(map[string]int, len(out))
480	for i, model := range out {
481		index[model.Provider+"\x00"+model.ID] = i
482	}
483	for _, model := range managedModels {
484		model.ReasoningLevelMap = maps.Clone(model.ReasoningLevelMap)
485		key := model.Provider + "\x00" + model.ID
486		if i, ok := index[key]; ok {
487			out[i] = model
488			continue
489		}
490		index[key] = len(out)
491		out = append(out, model)
492	}
493	return out
494}
495
496// SetManagedModels replaces the ephemeral catalog entries supplied by local
497// model managers. Unlike SetLiveModels, this does not disturb provider model
498// discovery or its on-disk cache.
499func SetManagedModels(models []Model) {
500	activeMu.Lock()
501	defer activeMu.Unlock()
502	managedModels = append([]Model(nil), models...)
503}
504
505// FindModel returns a Model by id, optionally constrained by provider.
506// If provider is empty, the first matching id is returned. Looks up
507// against the merged active catalog.
508func FindModel(provider, id string) (Model, error) {
509	for _, m := range Active() {
510		if m.ID == id && (provider == "" || m.Provider == provider) {
511			return m, nil
512		}
513	}
514	return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
515}
516
517// ModelsForProvider returns all models for the given provider, from the
518// merged active catalog.
519func ModelsForProvider(provider string) []Model {
520	var out []Model
521	for _, m := range Active() {
522		if m.Provider == provider {
523			out = append(out, m)
524		}
525	}
526	return out
527}
528
529// ComputeCost returns the USD cost for the given usage on model m.
530func ComputeCost(m Model, u Usage) float64 {
531	inputPrice := m.PriceInput
532	outputPrice := m.PriceOutput
533	cacheReadPrice := m.PriceCacheRead
534	cacheWritePrice := m.PriceCacheWrite
535	promptTokens := u.InputTokens + u.CacheReadTokens + u.CacheWriteTokens
536	if m.PriceTierInputTokens > 0 && promptTokens > m.PriceTierInputTokens {
537		inputPrice = m.PriceInputAbove
538		outputPrice = m.PriceOutputAbove
539		cacheReadPrice = m.PriceCacheReadAbove
540		cacheWritePrice = m.PriceCacheWriteAbove
541	}
542
543	const per = 1_000_000.0
544	return float64(u.InputTokens)*inputPrice/per +
545		float64(u.OutputTokens)*outputPrice/per +
546		float64(u.CacheReadTokens)*cacheReadPrice/per +
547		float64(u.CacheWriteTokens)*cacheWritePrice/per
548}

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