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 // Prices are USD per 1M tokens.
18 PriceInput float64
19 PriceOutput float64
20 PriceCacheRead float64
21 PriceCacheWrite float64
22
23 // Speculative marks models whose ids are known from the upstream
24 // vendor's CLI but not yet live on their public API. They'll 404
25 // today but start working the moment the provider flips the switch.
26 Speculative bool
27
28 // BaseURL overrides the provider's default API endpoint for this
29 // model. Optional; when empty the provider's default (or the
30 // --base-url flag) is used. Useful for local models served by
31 // ollama, vLLM, LM Studio, etc.
32 BaseURL string
33
34 // Source is where this model entry came from: "catalog" (baked in),
35 // "live" (discovered via /v1/models), or "cache" (loaded from the
36 // on-disk cache). Informational.
37 Source string
38}
39
40// Catalog is the hardcoded, read-only list of supported models.
41// Prices are USD per 1M tokens. The list is curated to what zot's
42// clients (Anthropic Messages + OpenAI Chat Completions) can actually
43// talk to; models that are only reachable through the OpenAI Responses
44// API (o1-pro, o3-pro, gpt-5-pro) are omitted.
45var Catalog = []Model{
46 // ---- Anthropic / Claude 4.x ----
47 {
48 Provider: "anthropic", ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5",
49 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
50 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
51 },
52 {
53 Provider: "anthropic", ID: "claude-opus-4-1", DisplayName: "Claude Opus 4.1",
54 ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
55 PriceInput: 15.00, PriceOutput: 75.00, PriceCacheRead: 1.50, PriceCacheWrite: 18.75,
56 },
57 {
58 Provider: "anthropic", ID: "claude-opus-4-0", DisplayName: "Claude Opus 4",
59 ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
60 PriceInput: 15.00, PriceOutput: 75.00, PriceCacheRead: 1.50, PriceCacheWrite: 18.75,
61 },
62 {
63 Provider: "anthropic", ID: "claude-sonnet-4-0", DisplayName: "Claude Sonnet 4",
64 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
65 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
66 },
67 {
68 Provider: "anthropic", ID: "claude-haiku-4-5", DisplayName: "Claude Haiku 4.5",
69 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
70 PriceInput: 1.00, PriceOutput: 5.00, PriceCacheRead: 0.10, PriceCacheWrite: 1.25,
71 },
72
73 // ---- Anthropic / Claude 3.x (legacy) ----
74 {
75 Provider: "anthropic", ID: "claude-3-7-sonnet-20250219", DisplayName: "Claude Sonnet 3.7",
76 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
77 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
78 },
79 {
80 Provider: "anthropic", ID: "claude-3-5-sonnet-20241022", DisplayName: "Claude Sonnet 3.5 v2",
81 ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
82 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
83 },
84 {
85 Provider: "anthropic", ID: "claude-3-5-haiku-latest", DisplayName: "Claude Haiku 3.5",
86 ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
87 PriceInput: 0.80, PriceOutput: 4.00, PriceCacheRead: 0.08, PriceCacheWrite: 1.00,
88 },
89 {
90 Provider: "anthropic", ID: "claude-3-opus-20240229", DisplayName: "Claude Opus 3",
91 ContextWindow: 200000, MaxOutput: 4096, Reasoning: false,
92 PriceInput: 15.00, PriceOutput: 75.00, PriceCacheRead: 1.50, PriceCacheWrite: 18.75,
93 },
94
95 // ---- DeepSeek ----
96 // The current public DeepSeek API exposes the V4 family on
97 // api.deepseek.com/v1. Pro is the flagship reasoning model;
98 // Flash is the cheaper/faster sibling. Both accept image inputs
99 // (multimodal parts: image_url) in addition to text.
100 {
101 Provider: "deepseek", ID: "deepseek-v4-pro", DisplayName: "DeepSeek V4 Pro",
102 ContextWindow: 128000, MaxOutput: 8192, Reasoning: true,
103 PriceInput: 0.55, PriceOutput: 2.19, PriceCacheRead: 0.14,
104 BaseURL: "https://api.deepseek.com/v1",
105 },
106 {
107 Provider: "deepseek", ID: "deepseek-v4-flash", DisplayName: "DeepSeek V4 Flash",
108 ContextWindow: 128000, MaxOutput: 8192, Reasoning: false,
109 PriceInput: 0.27, PriceOutput: 1.10, PriceCacheRead: 0.07,
110 BaseURL: "https://api.deepseek.com/v1",
111 },
112
113 // ---- Kimi / Kimi Code ----
114 {
115 Provider: "kimi", ID: "kimi-for-coding", DisplayName: "Kimi-k2.6",
116 ContextWindow: 262144, MaxOutput: 32000, Reasoning: true,
117 // Kimi Coding's public model metadata currently reports zero-priced
118 // usage for this endpoint; keep the cost meter explicit instead of
119 // relying on the struct's zero defaults.
120 PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, PriceCacheWrite: 0,
121 BaseURL: "https://api.kimi.com/coding/v1",
122 },
123
124 // ---- OpenAI / GPT-5 family ----
125 {
126 Provider: "openai", ID: "gpt-5", DisplayName: "GPT-5",
127 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
128 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125,
129 },
130 {
131 Provider: "openai", ID: "gpt-5-mini", DisplayName: "GPT-5 mini",
132 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
133 PriceInput: 0.25, PriceOutput: 2.00, PriceCacheRead: 0.025,
134 },
135 {
136 Provider: "openai", ID: "gpt-5-nano", DisplayName: "GPT-5 nano",
137 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
138 PriceInput: 0.05, PriceOutput: 0.40, PriceCacheRead: 0.005,
139 },
140
141 // ---- OpenAI / GPT-4.1 family ----
142 {
143 Provider: "openai", ID: "gpt-4.1", DisplayName: "GPT-4.1",
144 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
145 PriceInput: 2.00, PriceOutput: 8.00, PriceCacheRead: 0.50,
146 },
147 {
148 Provider: "openai", ID: "gpt-4.1-mini", DisplayName: "GPT-4.1 mini",
149 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
150 PriceInput: 0.40, PriceOutput: 1.60, PriceCacheRead: 0.10,
151 },
152 {
153 Provider: "openai", ID: "gpt-4.1-nano", DisplayName: "GPT-4.1 nano",
154 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
155 PriceInput: 0.10, PriceOutput: 0.40, PriceCacheRead: 0.03,
156 },
157
158 // ---- OpenAI / GPT-4o family ----
159 {
160 Provider: "openai", ID: "gpt-4o", DisplayName: "GPT-4o",
161 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
162 PriceInput: 2.50, PriceOutput: 10.00, PriceCacheRead: 1.25,
163 },
164 {
165 Provider: "openai", ID: "gpt-4o-mini", DisplayName: "GPT-4o mini",
166 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
167 PriceInput: 0.15, PriceOutput: 0.60, PriceCacheRead: 0.08,
168 },
169
170 // ---- OpenAI / reasoning models ----
171 {
172 Provider: "openai", ID: "o4-mini", DisplayName: "o4-mini",
173 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
174 PriceInput: 1.10, PriceOutput: 4.40, PriceCacheRead: 0.275,
175 },
176 {
177 Provider: "openai", ID: "o3", DisplayName: "o3",
178 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
179 PriceInput: 2.00, PriceOutput: 8.00, PriceCacheRead: 0.50,
180 },
181 {
182 Provider: "openai", ID: "o3-mini", DisplayName: "o3-mini",
183 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
184 PriceInput: 1.10, PriceOutput: 4.40, PriceCacheRead: 0.55,
185 },
186 {
187 Provider: "openai", ID: "o1", DisplayName: "o1",
188 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
189 PriceInput: 15.00, PriceOutput: 60.00, PriceCacheRead: 7.50,
190 },
191
192 // ---- Google / Gemini ----
193 {
194 Provider: "google", ID: "gemini-2.5-pro", DisplayName: "Gemini 2.5 Pro",
195 ContextWindow: 1_048_576, MaxOutput: 65536, Reasoning: true,
196 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.31, PriceCacheWrite: 0,
197 },
198 {
199 Provider: "google", ID: "gemini-2.5-flash", DisplayName: "Gemini 2.5 Flash",
200 ContextWindow: 1_048_576, MaxOutput: 65536, Reasoning: true,
201 PriceInput: 0.30, PriceOutput: 2.50, PriceCacheRead: 0.075, PriceCacheWrite: 0,
202 },
203 {
204 Provider: "google", ID: "gemini-2.5-flash-lite", DisplayName: "Gemini 2.5 Flash-Lite",
205 ContextWindow: 1_048_576, MaxOutput: 65536, Reasoning: true,
206 PriceInput: 0.10, PriceOutput: 0.40, PriceCacheRead: 0.025, PriceCacheWrite: 0,
207 },
208 {
209 Provider: "google", ID: "gemini-2.0-flash", DisplayName: "Gemini 2.0 Flash",
210 ContextWindow: 1_048_576, MaxOutput: 8192, Reasoning: false,
211 PriceInput: 0.10, PriceOutput: 0.40, PriceCacheRead: 0.025, PriceCacheWrite: 0,
212 },
213 {
214 Provider: "google", ID: "gemini-2.0-flash-lite", DisplayName: "Gemini 2.0 Flash-Lite",
215 ContextWindow: 1_048_576, MaxOutput: 8192, Reasoning: false,
216 PriceInput: 0.075, PriceOutput: 0.30, PriceCacheRead: 0, PriceCacheWrite: 0,
217 },
218
219 // ---- Speculative: Anthropic ----
220 {
221 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5",
222 // 200k ctx / 64k maxOutput per Anthropic's published sizing
223 // for the opus-4-5 family; the 1M context is a 4.6+ thing.
224 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
225 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
226 Speculative: true,
227 },
228 {
229 Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
230 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
231 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
232 Speculative: true,
233 },
234 {
235 Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
236 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
237 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
238 Speculative: true,
239 },
240 {
241 Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
242 ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
243 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
244 Speculative: true,
245 },
246
247 // ---- Speculative: OpenAI ----
248 // Public OpenAI API route. The ChatGPT/Codex subscription route is
249 // represented separately below as provider "openai-codex".
250 {
251 Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
252 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
253 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125,
254 Speculative: true,
255 },
256 {
257 Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
258 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
259 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
260 Speculative: true,
261 },
262 {
263 Provider: "openai", ID: "gpt-5.3", DisplayName: "GPT-5.3",
264 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
265 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
266 Speculative: true,
267 },
268 {
269 Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
270 // ContextWindow: 272k across every route we support (OpenAI
271 // direct API and the ChatGPT Codex OAuth backend).
272 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
273 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
274 Speculative: true,
275 },
276 {
277 Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
278 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
279 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
280 Speculative: true,
281 },
282 {
283 Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
284 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
285 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
286 Speculative: true,
287 },
288 {
289 Provider: "openai", ID: "gpt-5.5-mini", DisplayName: "GPT-5.5 mini",
290 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
291 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
292 Speculative: true,
293 },
294
295 // ---- OpenAI Codex / ChatGPT subscription backend ----
296 // Same model ids as the OpenAI family, but routed through the
297 // ChatGPT Codex OAuth backend rather than api.openai.com.
298 {
299 Provider: "openai-codex", ID: "gpt-5.2", DisplayName: "GPT-5.2 Codex",
300 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
301 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
302 Speculative: true,
303 },
304 {
305 Provider: "openai-codex", ID: "gpt-5.3-codex", DisplayName: "GPT-5.3 Codex",
306 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
307 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
308 Speculative: true,
309 },
310 {
311 Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
312 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
313 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
314 Speculative: true,
315 },
316 {
317 Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4 Codex",
318 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
319 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
320 Speculative: true,
321 },
322 {
323 Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini Codex",
324 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
325 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
326 Speculative: true,
327 },
328 {
329 Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5 Codex",
330 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
331 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
332 Speculative: true,
333 },
334 {
335 Provider: "openai-codex", ID: "gpt-5.5-mini", DisplayName: "GPT-5.5 mini Codex",
336 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
337 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
338 Speculative: true,
339 },
340}
341
342// DefaultModel is used when the user does not specify one.
343var DefaultModel = Catalog[0] // claude-sonnet-4-5
344
345// ----- active (merged) catalog -----
346//
347// Callers should use Active() / FindModel / ModelsForProvider for
348// lookups. They return the baked-in Catalog merged with any live
349// models loaded via SetLiveModels.
350
351var (
352 activeMu sync.RWMutex
353 active []Model = Catalog // default: just the static catalog
354)
355
356// SetLiveModels replaces the "live" overlay used by the active catalog.
357// Typically called after a successful /v1/models discovery or on load
358// from the on-disk cache.
359func SetLiveModels(live []Model) {
360 activeMu.Lock()
361 defer activeMu.Unlock()
362 if len(live) == 0 {
363 active = Catalog
364 return
365 }
366 active = MergeCatalog(live)
367}
368
369// Active returns the current merged catalog.
370func Active() []Model {
371 activeMu.RLock()
372 defer activeMu.RUnlock()
373 out := make([]Model, len(active))
374 copy(out, active)
375 return out
376}
377
378// FindModel returns a Model by id, optionally constrained by provider.
379// If provider is empty, the first matching id is returned. Looks up
380// against the merged active catalog.
381func FindModel(provider, id string) (Model, error) {
382 for _, m := range Active() {
383 if m.ID == id && (provider == "" || m.Provider == provider) {
384 return m, nil
385 }
386 }
387 return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
388}
389
390// ModelsForProvider returns all models for the given provider, from the
391// merged active catalog.
392func ModelsForProvider(provider string) []Model {
393 var out []Model
394 for _, m := range Active() {
395 if m.Provider == provider {
396 out = append(out, m)
397 }
398 }
399 return out
400}
401
402// ComputeCost returns the USD cost for the given usage on model m.
403func ComputeCost(m Model, u Usage) float64 {
404 const per = 1_000_000.0
405 return float64(u.InputTokens)*m.PriceInput/per +
406 float64(u.OutputTokens)*m.PriceOutput/per +
407 float64(u.CacheReadTokens)*m.PriceCacheRead/per +
408 float64(u.CacheWriteTokens)*m.PriceCacheWrite/per
409}