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 // ---- Kimi / Kimi Code ----
96 {
97 Provider: "kimi", ID: "kimi-for-coding", DisplayName: "Kimi-k2.6",
98 ContextWindow: 262144, MaxOutput: 32000, Reasoning: true,
99 // Kimi Coding's public model metadata currently reports zero-priced
100 // usage for this endpoint; keep the cost meter explicit instead of
101 // relying on the struct's zero defaults.
102 PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0, PriceCacheWrite: 0,
103 BaseURL: "https://api.kimi.com/coding/v1",
104 },
105
106 // ---- OpenAI / GPT-5 family ----
107 {
108 Provider: "openai", ID: "gpt-5", DisplayName: "GPT-5",
109 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
110 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125,
111 },
112 {
113 Provider: "openai", ID: "gpt-5-mini", DisplayName: "GPT-5 mini",
114 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
115 PriceInput: 0.25, PriceOutput: 2.00, PriceCacheRead: 0.025,
116 },
117 {
118 Provider: "openai", ID: "gpt-5-nano", DisplayName: "GPT-5 nano",
119 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
120 PriceInput: 0.05, PriceOutput: 0.40, PriceCacheRead: 0.005,
121 },
122
123 // ---- OpenAI / GPT-4.1 family ----
124 {
125 Provider: "openai", ID: "gpt-4.1", DisplayName: "GPT-4.1",
126 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
127 PriceInput: 2.00, PriceOutput: 8.00, PriceCacheRead: 0.50,
128 },
129 {
130 Provider: "openai", ID: "gpt-4.1-mini", DisplayName: "GPT-4.1 mini",
131 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
132 PriceInput: 0.40, PriceOutput: 1.60, PriceCacheRead: 0.10,
133 },
134 {
135 Provider: "openai", ID: "gpt-4.1-nano", DisplayName: "GPT-4.1 nano",
136 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
137 PriceInput: 0.10, PriceOutput: 0.40, PriceCacheRead: 0.03,
138 },
139
140 // ---- OpenAI / GPT-4o family ----
141 {
142 Provider: "openai", ID: "gpt-4o", DisplayName: "GPT-4o",
143 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
144 PriceInput: 2.50, PriceOutput: 10.00, PriceCacheRead: 1.25,
145 },
146 {
147 Provider: "openai", ID: "gpt-4o-mini", DisplayName: "GPT-4o mini",
148 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
149 PriceInput: 0.15, PriceOutput: 0.60, PriceCacheRead: 0.08,
150 },
151
152 // ---- OpenAI / reasoning models ----
153 {
154 Provider: "openai", ID: "o4-mini", DisplayName: "o4-mini",
155 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
156 PriceInput: 1.10, PriceOutput: 4.40, PriceCacheRead: 0.275,
157 },
158 {
159 Provider: "openai", ID: "o3", DisplayName: "o3",
160 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
161 PriceInput: 2.00, PriceOutput: 8.00, PriceCacheRead: 0.50,
162 },
163 {
164 Provider: "openai", ID: "o3-mini", DisplayName: "o3-mini",
165 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
166 PriceInput: 1.10, PriceOutput: 4.40, PriceCacheRead: 0.55,
167 },
168 {
169 Provider: "openai", ID: "o1", DisplayName: "o1",
170 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
171 PriceInput: 15.00, PriceOutput: 60.00, PriceCacheRead: 7.50,
172 },
173
174 // ---- Speculative: Anthropic ----
175 {
176 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5",
177 // 200k ctx / 64k maxOutput per Anthropic's published sizing
178 // for the opus-4-5 family; the 1M context is a 4.6+ thing.
179 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
180 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
181 Speculative: true,
182 },
183 {
184 Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
185 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
186 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
187 Speculative: true,
188 },
189 {
190 Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
191 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
192 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
193 Speculative: true,
194 },
195 {
196 Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
197 ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
198 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
199 Speculative: true,
200 },
201
202 // ---- Speculative: OpenAI ----
203 // Context windows on the OpenAI gpt-5.x family differ by route:
204 // the direct API advertises 400k, the ChatGPT Codex OAuth backend
205 // caps at 272k. zot serves both auth modes from one catalog row
206 // per id, so we pin to the smaller number to keep the context-usage
207 // meter honest under subscription auth. Users on the direct API
208 // simply see a conservative headroom estimate.
209 {
210 Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
211 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
212 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125,
213 Speculative: true,
214 },
215 {
216 Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
217 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
218 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
219 Speculative: true,
220 },
221 {
222 Provider: "openai", ID: "gpt-5.3", DisplayName: "GPT-5.3",
223 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
224 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
225 Speculative: true,
226 },
227 {
228 Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
229 // ContextWindow: 272k across every route we support (OpenAI
230 // direct API and the ChatGPT Codex OAuth backend).
231 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
232 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
233 Speculative: true,
234 },
235 {
236 Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
237 // ContextWindow: 400k on the OpenAI direct API, 272k on the
238 // ChatGPT Codex OAuth backend. We pin to the smaller Codex
239 // cap so the context-usage meter is honest under subscription
240 // auth; direct-API users simply see a conservative headroom
241 // estimate rather than an inflated one.
242 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
243 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
244 Speculative: true,
245 },
246 {
247 Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
248 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
249 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
250 Speculative: true,
251 },
252 {
253 Provider: "openai", ID: "gpt-5.5-mini", DisplayName: "GPT-5.5 mini",
254 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
255 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
256 Speculative: true,
257 },
258}
259
260// DefaultModel is used when the user does not specify one.
261var DefaultModel = Catalog[0] // claude-sonnet-4-5
262
263// ----- active (merged) catalog -----
264//
265// Callers should use Active() / FindModel / ModelsForProvider for
266// lookups. They return the baked-in Catalog merged with any live
267// models loaded via SetLiveModels.
268
269var (
270 activeMu sync.RWMutex
271 active []Model = Catalog // default: just the static catalog
272)
273
274// SetLiveModels replaces the "live" overlay used by the active catalog.
275// Typically called after a successful /v1/models discovery or on load
276// from the on-disk cache.
277func SetLiveModels(live []Model) {
278 activeMu.Lock()
279 defer activeMu.Unlock()
280 if len(live) == 0 {
281 active = Catalog
282 return
283 }
284 active = MergeCatalog(live)
285}
286
287// Active returns the current merged catalog.
288func Active() []Model {
289 activeMu.RLock()
290 defer activeMu.RUnlock()
291 out := make([]Model, len(active))
292 copy(out, active)
293 return out
294}
295
296// FindModel returns a Model by id, optionally constrained by provider.
297// If provider is empty, the first matching id is returned. Looks up
298// against the merged active catalog.
299func FindModel(provider, id string) (Model, error) {
300 for _, m := range Active() {
301 if m.ID == id && (provider == "" || m.Provider == provider) {
302 return m, nil
303 }
304 }
305 return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
306}
307
308// ModelsForProvider returns all models for the given provider, from the
309// merged active catalog.
310func ModelsForProvider(provider string) []Model {
311 var out []Model
312 for _, m := range Active() {
313 if m.Provider == provider {
314 out = append(out, m)
315 }
316 }
317 return out
318}
319
320// ComputeCost returns the USD cost for the given usage on model m.
321func ComputeCost(m Model, u Usage) float64 {
322 const per = 1_000_000.0
323 return float64(u.InputTokens)*m.PriceInput/per +
324 float64(u.OutputTokens)*m.PriceOutput/per +
325 float64(u.CacheReadTokens)*m.PriceCacheRead/per +
326 float64(u.CacheWriteTokens)*m.PriceCacheWrite/per
327}