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