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