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}