v0.29.0 73d6864

models.go RawHistoryFolder

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

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