v0.3.98 ff10c69

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

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