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