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}