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