v0.3.28 b58450d

models.go RawHistoryFolder

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

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