v0.14.0 75d4151

models.go RawHistoryFolder

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

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