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