v0.3.75 64117f3

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 zot'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	// ---- Gondola ----
258	// Seed entries only: kimi-k3 is the default returned by
259	// defaultModelForProvider so it resolves offline. Gondola is a USDC-paid
260	// marketplace gateway for Venice AI inference; prices/limits pulled live
261	// from https://api.gondola-ai.com/v1/models (2026-08-11).
262	{
263		Provider: "gondola", ID: "kimi-k3", DisplayName: "Kimi K3 (Gondola)",
264		ContextWindow: 1000000, MaxOutput: 131072, Reasoning: true,
265		PriceInput: 1.1475, PriceOutput: 5.7375, PriceCacheRead: 0.11475,
266		BaseURL: gondolaDefaultBaseURL,
267	},
268	{
269		Provider: "gondola", ID: "claude-opus-5", DisplayName: "Claude Opus 5 (Gondola)",
270		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
271		PriceInput: 1.836, PriceOutput: 9.18, PriceCacheRead: 0.1836, PriceCacheWrite: 2.295,
272		BaseURL: gondolaDefaultBaseURL,
273	},
274
275	// ---- Anthropic / newer families ----
276	{
277		Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
278		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
279		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
280		Speculative: true,
281	},
282	{
283		Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
284		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
285		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
286		Speculative: true,
287	},
288	{
289		Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
290		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
291		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
292		Speculative: true,
293	},
294	{
295		Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8",
296		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
297		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
298		Speculative: true,
299	},
300	{
301		Provider: "anthropic", ID: "claude-opus-5", DisplayName: "Claude Opus 5",
302		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
303		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
304	},
305	{
306		Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
307		ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
308		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
309		Speculative: true,
310	},
311	{
312		Provider: "anthropic", ID: "claude-sonnet-5", DisplayName: "Claude Sonnet 5",
313		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
314		PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5,
315	},
316	{
317		Provider: "anthropic", ID: "claude-fable-5", DisplayName: "Claude Fable 5",
318		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
319		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5,
320	},
321	{
322		Provider: "anthropic", ID: "claude-fable-5-1", DisplayName: "Claude Fable 5.1",
323		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
324		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.25, PriceCacheWrite: 12.5,
325	},
326
327	// ---- Speculative: OpenAI ----
328	// Public OpenAI API route. The ChatGPT/Codex subscription route is
329	// represented separately below as provider "openai-codex".
330	{
331		Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
332		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
333		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.13,
334		Speculative: true,
335	},
336	{
337		Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
338		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
339		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
340		Speculative: true,
341	},
342	{
343		Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
344		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
345		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
346		Speculative: true,
347	},
348	{
349		Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
350		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
351		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
352		Speculative: true,
353	},
354	{
355		Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
356		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
357		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
358		Speculative: true,
359	},
360	{
361		Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", API: APIResponses,
362		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
363		PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
364		Speculative: true,
365	},
366	{
367		Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", API: APIResponses,
368		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
369		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
370		Speculative: true,
371	},
372	{
373		Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", API: APIResponses,
374		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
375		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
376		Speculative: true,
377	},
378
379	// ---- OpenAI / GPT-6 ----
380	{
381		Provider: "openai", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses,
382		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
383		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5,
384		PriceTierInputTokens: 272000,
385		PriceInputAbove:      20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25,
386	},
387
388	// ---- OpenAI Codex / ChatGPT subscription backend ----
389	// Same model ids as the OpenAI family, but routed through the
390	// ChatGPT Codex OAuth backend rather than api.openai.com.
391	{
392		Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
393		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
394		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
395		Speculative: true,
396	},
397	{
398		Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4",
399		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
400		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
401		Speculative: true,
402	},
403	{
404		Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
405		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
406		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
407		Speculative: true,
408	},
409	{
410		Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5",
411		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
412		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
413		Speculative: true,
414	},
415	{
416		Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna",
417		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
418		PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
419		Speculative: true,
420	},
421	{
422		Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol",
423		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
424		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
425		Speculative: true,
426	},
427	{
428		Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra",
429		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
430		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
431		Speculative: true,
432	},
433	{
434		Provider: "openai-codex", ID: "gpt-6-astra", DisplayName: "GPT-6 Astra", API: APIResponses,
435		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
436		PriceInput: 10, PriceOutput: 50, PriceCacheRead: 1, PriceCacheWrite: 12.5,
437		PriceTierInputTokens: 272000,
438		PriceInputAbove:      20, PriceOutputAbove: 75, PriceCacheReadAbove: 2, PriceCacheWriteAbove: 25,
439	},
440}
441
442// DefaultModel is used when the user does not specify one.
443var DefaultModel = Catalog[0] // claude-sonnet-4-5
444
445// ----- active (merged) catalog -----
446//
447// Callers should use Active() / FindModel / ModelsForProvider for
448// lookups. They return the baked-in Catalog merged with any live
449// models loaded via SetLiveModels.
450
451var (
452	activeMu          sync.RWMutex
453	active            []Model                        // live overlay merged in via SetLiveModels; nil = none yet
454	activeSet         bool                           // true once SetLiveModels has run (even with empty live)
455	managedModels     []Model                        // ephemeral models exposed by local model managers
456	modelAvailability = map[string]map[string]bool{} // account-specific, memory only
457)
458
459// SetLiveModels replaces the "live" overlay used by the active catalog.
460// Typically called after a successful /v1/models discovery or on load
461// from the on-disk cache.
462func SetLiveModels(live []Model) {
463	activeMu.Lock()
464	defer activeMu.Unlock()
465	activeSet = true
466	if len(live) == 0 {
467		active = nil
468		return
469	}
470	active = MergeCatalog(live)
471}
472
473// Active returns the current merged catalog.
474//
475// When no live overlay has been set it returns the fully-assembled
476// static Catalog. Reading Catalog here (rather than capturing it into
477// a package-level var initializer) is load-bearing: the extended
478// catalog in catalog_builtin.go / extra_models.go is appended from
479// init() functions, which run AFTER package-level var initializers.
480// Snapshotting Catalog at var-init time would freeze the picker to the
481// curated seed list and drop every extra provider (openrouter, groq,
482// xai, ...). Deferring the read to call time avoids that ordering trap.
483func Active() []Model { return activeModels(true) }
484
485func activeModels(availableOnly bool) []Model {
486	activeMu.RLock()
487	defer activeMu.RUnlock()
488	src := active
489	if !activeSet || src == nil {
490		src = Catalog
491	}
492	out := make([]Model, len(src))
493	copy(out, src)
494	for i := range out {
495		out[i].ReasoningLevelMap = maps.Clone(out[i].ReasoningLevelMap)
496	}
497	if len(managedModels) == 0 {
498		if availableOnly {
499			return filterAvailableModels(out)
500		}
501		return out
502	}
503	index := make(map[string]int, len(out))
504	for i, model := range out {
505		index[model.Provider+"\x00"+model.ID] = i
506	}
507	for _, model := range managedModels {
508		model.ReasoningLevelMap = maps.Clone(model.ReasoningLevelMap)
509		key := model.Provider + "\x00" + model.ID
510		if i, ok := index[key]; ok {
511			if out[i].Source == "user" {
512				continue
513			}
514			out[i] = model
515			continue
516		}
517		index[key] = len(out)
518		out = append(out, model)
519	}
520	if availableOnly {
521		return filterAvailableModels(out)
522	}
523	return out
524}
525
526// SetModelAvailability restricts a provider's visible catalog to the supplied
527// account model IDs. A nil slice clears the restriction; an empty non-nil slice
528// hides all models. This snapshot is not persisted in the shared model cache.
529func SetModelAvailability(provider string, ids []string) {
530	activeMu.Lock()
531	defer activeMu.Unlock()
532	if ids == nil {
533		delete(modelAvailability, provider)
534		return
535	}
536	allowed := make(map[string]bool, len(ids))
537	for _, id := range ids {
538		allowed[id] = true
539	}
540	modelAvailability[provider] = allowed
541}
542
543// Caller holds activeMu. Models is already a private copy.
544func filterAvailableModels(models []Model) []Model {
545	out := models[:0]
546	for _, m := range models {
547		allowed, restricted := modelAvailability[m.Provider]
548		if !restricted || allowed[m.ID] {
549			out = append(out, m)
550		}
551	}
552	return out
553}
554
555// SetManagedModels replaces the ephemeral catalog entries supplied by local
556// model managers. Unlike SetLiveModels, this does not disturb provider model
557// discovery or its on-disk cache.
558func SetManagedModels(models []Model) {
559	activeMu.Lock()
560	defer activeMu.Unlock()
561	managedModels = append([]Model(nil), models...)
562}
563
564// SetManagedModelsForProvider replaces only one local server's entries.
565func SetManagedModelsForProvider(providerID string, models []Model) {
566	activeMu.Lock()
567	defer activeMu.Unlock()
568	kept := managedModels[:0]
569	for _, m := range managedModels {
570		if m.Provider != providerID {
571			kept = append(kept, m)
572		}
573	}
574	managedModels = kept
575	for _, m := range models {
576		if m.Provider == providerID {
577			managedModels = append(managedModels, m)
578		}
579	}
580}
581
582// FindModel returns a Model by id, optionally constrained by provider.
583// If provider is empty, the first matching id is returned. Looks up
584// against the merged catalog, including models hidden by account availability.
585// Existing sessions still need stable wire and pricing metadata after a refresh.
586func FindModel(provider, id string) (Model, error) {
587	for _, m := range activeModels(false) {
588		if m.ID == id && (provider == "" || m.Provider == provider) {
589			return m, nil
590		}
591	}
592	return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
593}
594
595// ModelsForProvider returns all models for the given provider, from the
596// merged active catalog.
597func ModelsForProvider(provider string) []Model {
598	var out []Model
599	for _, m := range Active() {
600		if m.Provider == provider {
601			out = append(out, m)
602		}
603	}
604	return out
605}
606
607// ComputeCost returns the USD cost for the given usage on model m.
608func ComputeCost(m Model, u Usage) float64 {
609	inputPrice := m.PriceInput
610	outputPrice := m.PriceOutput
611	cacheReadPrice := m.PriceCacheRead
612	cacheWritePrice := m.PriceCacheWrite
613	promptTokens := u.InputTokens + u.CacheReadTokens + u.CacheWriteTokens
614	if m.PriceTierInputTokens > 0 && promptTokens > m.PriceTierInputTokens {
615		inputPrice = m.PriceInputAbove
616		outputPrice = m.PriceOutputAbove
617		cacheReadPrice = m.PriceCacheReadAbove
618		cacheWritePrice = m.PriceCacheWriteAbove
619	}
620
621	const per = 1_000_000.0
622	return float64(u.InputTokens)*inputPrice/per +
623		float64(u.OutputTokens)*outputPrice/per +
624		float64(u.CacheReadTokens)*cacheReadPrice/per +
625		float64(u.CacheWriteTokens)*cacheWritePrice/per
626}

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