v0.27.1 131edba

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	// ---- OpenAI Codex / ChatGPT subscription backend ----
359	// Same model ids as the OpenAI family, but routed through the
360	// ChatGPT Codex OAuth backend rather than api.openai.com.
361	{
362		Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
363		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
364		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
365		Speculative: true,
366	},
367	{
368		Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4",
369		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
370		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
371		Speculative: true,
372	},
373	{
374		Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
375		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
376		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
377		Speculative: true,
378	},
379	{
380		Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5",
381		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
382		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
383		Speculative: true,
384	},
385	{
386		Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna",
387		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
388		PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
389	},
390	{
391		Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol",
392		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
393		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
394	},
395	{
396		Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra",
397		ContextWindow: openAIContextWindowTarget, MaxOutput: 128000, Reasoning: true,
398		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
399	},
400}
401
402// DefaultModel is used when the user does not specify one.
403var DefaultModel = Catalog[0] // claude-sonnet-4-5
404
405// ----- active (merged) catalog -----
406//
407// Callers should use Active() / FindModel / ModelsForProvider for
408// lookups. They return the baked-in Catalog merged with any live
409// models loaded via SetLiveModels.
410
411var (
412	activeMu      sync.RWMutex
413	active        []Model // live overlay merged in via SetLiveModels; nil = none yet
414	activeSet     bool    // true once SetLiveModels has run (even with empty live)
415	managedModels []Model // ephemeral models exposed by local model managers
416)
417
418// SetLiveModels replaces the "live" overlay used by the active catalog.
419// Typically called after a successful /v1/models discovery or on load
420// from the on-disk cache.
421func SetLiveModels(live []Model) {
422	SetLiveModelsForProviders(live, nil)
423}
424
425// SetLiveModelsForProviders replaces the live overlay, treating the listed
426// providers' live catalogs as authoritative. For those providers, static
427// entries omitted by discovery are removed from the active catalog.
428func SetLiveModelsForProviders(live []Model, authoritativeProviders []string) {
429	activeMu.Lock()
430	defer activeMu.Unlock()
431	activeSet = true
432	if len(live) == 0 && len(authoritativeProviders) == 0 {
433		active = nil
434		return
435	}
436	active = MergeCatalogForProviders(live, authoritativeProviders)
437}
438
439// Active returns the current merged catalog.
440//
441// When no live overlay has been set it returns the fully-assembled
442// static Catalog. Reading Catalog here (rather than capturing it into
443// a package-level var initializer) is load-bearing: the extended
444// catalog in catalog_builtin.go / extra_models.go is appended from
445// init() functions, which run AFTER package-level var initializers.
446// Snapshotting Catalog at var-init time would freeze the picker to the
447// curated seed list and drop every extra provider (openrouter, groq,
448// xai, ...). Deferring the read to call time avoids that ordering trap.
449func Active() []Model {
450	activeMu.RLock()
451	defer activeMu.RUnlock()
452	src := active
453	if !activeSet || src == nil {
454		src = Catalog
455	}
456	out := make([]Model, len(src))
457	copy(out, src)
458	for i := range out {
459		out[i].ReasoningLevelMap = maps.Clone(out[i].ReasoningLevelMap)
460	}
461	if len(managedModels) == 0 {
462		return out
463	}
464	index := make(map[string]int, len(out))
465	for i, model := range out {
466		index[model.Provider+"\x00"+model.ID] = i
467	}
468	for _, model := range managedModels {
469		model.ReasoningLevelMap = maps.Clone(model.ReasoningLevelMap)
470		key := model.Provider + "\x00" + model.ID
471		if i, ok := index[key]; ok {
472			out[i] = model
473			continue
474		}
475		index[key] = len(out)
476		out = append(out, model)
477	}
478	return out
479}
480
481// SetManagedModels replaces the ephemeral catalog entries supplied by local
482// model managers. Unlike SetLiveModels, this does not disturb provider model
483// discovery or its on-disk cache.
484func SetManagedModels(models []Model) {
485	activeMu.Lock()
486	defer activeMu.Unlock()
487	managedModels = append([]Model(nil), models...)
488}
489
490// FindModel returns a Model by id, optionally constrained by provider.
491// If provider is empty, the first matching id is returned. Looks up
492// against the merged active catalog.
493func FindModel(provider, id string) (Model, error) {
494	for _, m := range Active() {
495		if m.ID == id && (provider == "" || m.Provider == provider) {
496			return m, nil
497		}
498	}
499	return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
500}
501
502// ModelsForProvider returns all models for the given provider, from the
503// merged active catalog.
504func ModelsForProvider(provider string) []Model {
505	var out []Model
506	for _, m := range Active() {
507		if m.Provider == provider {
508			out = append(out, m)
509		}
510	}
511	return out
512}
513
514// ComputeCost returns the USD cost for the given usage on model m.
515func ComputeCost(m Model, u Usage) float64 {
516	inputPrice := m.PriceInput
517	outputPrice := m.PriceOutput
518	cacheReadPrice := m.PriceCacheRead
519	cacheWritePrice := m.PriceCacheWrite
520	promptTokens := u.InputTokens + u.CacheReadTokens + u.CacheWriteTokens
521	if m.PriceTierInputTokens > 0 && promptTokens > m.PriceTierInputTokens {
522		inputPrice = m.PriceInputAbove
523		outputPrice = m.PriceOutputAbove
524		cacheReadPrice = m.PriceCacheReadAbove
525		cacheWritePrice = m.PriceCacheWriteAbove
526	}
527
528	const per = 1_000_000.0
529	return float64(u.InputTokens)*inputPrice/per +
530		float64(u.OutputTokens)*outputPrice/per +
531		float64(u.CacheReadTokens)*cacheReadPrice/per +
532		float64(u.CacheWriteTokens)*cacheWritePrice/per
533}

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