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 // ---- Speculative: Anthropic ----
238 {
239 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
240 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
241 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
242 Speculative: true,
243 },
244 {
245 Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
246 ContextWindow: 1000000, MaxOutput: 128000, 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-7", DisplayName: "Claude Opus 4.7",
252 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
253 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
254 Speculative: true,
255 },
256 {
257 Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8",
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-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
264 ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
265 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
266 Speculative: true,
267 },
268 {
269 Provider: "anthropic", ID: "claude-fable-5", DisplayName: "Claude Fable 5",
270 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
271 PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
272 Speculative: true,
273 },
274
275 // ---- Speculative: OpenAI ----
276 // Public OpenAI API route. The ChatGPT/Codex subscription route is
277 // represented separately below as provider "openai-codex".
278 {
279 Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
280 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
281 PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.13,
282 Speculative: true,
283 },
284 {
285 Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
286 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
287 PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
288 Speculative: true,
289 },
290 {
291 Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
292 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
293 PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
294 Speculative: true,
295 },
296 {
297 Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
298 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
299 PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
300 Speculative: true,
301 },
302 {
303 Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
304 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
305 PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
306 Speculative: true,
307 },
308
309 // ---- OpenAI Codex / ChatGPT subscription backend ----
310 // Same model ids as the OpenAI family, but routed through the
311 // ChatGPT Codex OAuth backend rather than api.openai.com.
312 {
313 Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
314 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
315 PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
316 Speculative: true,
317 },
318 {
319 Provider: "openai-codex", 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-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
326 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
327 PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
328 Speculative: true,
329 },
330 {
331 Provider: "openai-codex", 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
338// DefaultModel is used when the user does not specify one.
339var DefaultModel = Catalog[0] // claude-sonnet-4-5
340
341// ----- active (merged) catalog -----
342//
343// Callers should use Active() / FindModel / ModelsForProvider for
344// lookups. They return the baked-in Catalog merged with any live
345// models loaded via SetLiveModels.
346
347var (
348 activeMu sync.RWMutex
349 active []Model // live overlay merged in via SetLiveModels; nil = none yet
350 activeSet bool // true once SetLiveModels has run (even with empty live)
351)
352
353// SetLiveModels replaces the "live" overlay used by the active catalog.
354// Typically called after a successful /v1/models discovery or on load
355// from the on-disk cache.
356func SetLiveModels(live []Model) {
357 activeMu.Lock()
358 defer activeMu.Unlock()
359 activeSet = true
360 if len(live) == 0 {
361 active = nil
362 return
363 }
364 active = MergeCatalog(live)
365}
366
367// Active returns the current merged catalog.
368//
369// When no live overlay has been set it returns the fully-assembled
370// static Catalog. Reading Catalog here (rather than capturing it into
371// a package-level var initializer) is load-bearing: the extended
372// catalog in catalog_builtin.go / extra_models.go is appended from
373// init() functions, which run AFTER package-level var initializers.
374// Snapshotting Catalog at var-init time would freeze the picker to the
375// curated seed list and drop every extra provider (openrouter, groq,
376// xai, ...). Deferring the read to call time avoids that ordering trap.
377func Active() []Model {
378 activeMu.RLock()
379 defer activeMu.RUnlock()
380 src := active
381 if !activeSet || src == nil {
382 src = Catalog
383 }
384 out := make([]Model, len(src))
385 copy(out, src)
386 return out
387}
388
389// FindModel returns a Model by id, optionally constrained by provider.
390// If provider is empty, the first matching id is returned. Looks up
391// against the merged active catalog.
392func FindModel(provider, id string) (Model, error) {
393 for _, m := range Active() {
394 if m.ID == id && (provider == "" || m.Provider == provider) {
395 return m, nil
396 }
397 }
398 return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
399}
400
401// ModelsForProvider returns all models for the given provider, from the
402// merged active catalog.
403func ModelsForProvider(provider string) []Model {
404 var out []Model
405 for _, m := range Active() {
406 if m.Provider == provider {
407 out = append(out, m)
408 }
409 }
410 return out
411}
412
413// ComputeCost returns the USD cost for the given usage on model m.
414func ComputeCost(m Model, u Usage) float64 {
415 const per = 1_000_000.0
416 return float64(u.InputTokens)*m.PriceInput/per +
417 float64(u.OutputTokens)*m.PriceOutput/per +
418 float64(u.CacheReadTokens)*m.PriceCacheRead/per +
419 float64(u.CacheWriteTokens)*m.PriceCacheWrite/per
420}