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 // Source is where this model entry came from: "catalog" (baked in),
29 // "live" (discovered via /v1/models), or "cache" (loaded from the
30 // on-disk cache). Informational.
31 Source string
32}
33
34// Catalog is the hardcoded, read-only list of supported models.
35// Prices are USD per 1M tokens. The list is curated to what zot's
36// clients (Anthropic Messages + OpenAI Chat Completions) can actually
37// talk to; models that are only reachable through the OpenAI Responses
38// API (o1-pro, o3-pro, gpt-5-pro) are omitted.
39var Catalog = []Model{
40 // ---- Anthropic / Claude 4.x ----
41 {
42 Provider: "anthropic", ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5",
43 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
44 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
45 },
46 {
47 Provider: "anthropic", ID: "claude-opus-4-1", DisplayName: "Claude Opus 4.1",
48 ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
49 PriceInput: 15.00, PriceOutput: 75.00, PriceCacheRead: 1.50, PriceCacheWrite: 18.75,
50 },
51 {
52 Provider: "anthropic", ID: "claude-opus-4-0", DisplayName: "Claude Opus 4",
53 ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
54 PriceInput: 15.00, PriceOutput: 75.00, PriceCacheRead: 1.50, PriceCacheWrite: 18.75,
55 },
56 {
57 Provider: "anthropic", ID: "claude-sonnet-4-0", DisplayName: "Claude Sonnet 4",
58 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
59 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
60 },
61 {
62 Provider: "anthropic", ID: "claude-haiku-4-5", DisplayName: "Claude Haiku 4.5",
63 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
64 PriceInput: 1.00, PriceOutput: 5.00, PriceCacheRead: 0.10, PriceCacheWrite: 1.25,
65 },
66
67 // ---- Anthropic / Claude 3.x (legacy) ----
68 {
69 Provider: "anthropic", ID: "claude-3-7-sonnet-20250219", DisplayName: "Claude Sonnet 3.7",
70 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
71 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
72 },
73 {
74 Provider: "anthropic", ID: "claude-3-5-sonnet-20241022", DisplayName: "Claude Sonnet 3.5 v2",
75 ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
76 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
77 },
78 {
79 Provider: "anthropic", ID: "claude-3-5-haiku-latest", DisplayName: "Claude Haiku 3.5",
80 ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
81 PriceInput: 0.80, PriceOutput: 4.00, PriceCacheRead: 0.08, PriceCacheWrite: 1.00,
82 },
83 {
84 Provider: "anthropic", ID: "claude-3-opus-20240229", DisplayName: "Claude Opus 3",
85 ContextWindow: 200000, MaxOutput: 4096, Reasoning: false,
86 PriceInput: 15.00, PriceOutput: 75.00, PriceCacheRead: 1.50, PriceCacheWrite: 18.75,
87 },
88
89 // ---- OpenAI / GPT-5 family ----
90 {
91 Provider: "openai", ID: "gpt-5", DisplayName: "GPT-5",
92 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
93 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125,
94 },
95 {
96 Provider: "openai", ID: "gpt-5-mini", DisplayName: "GPT-5 mini",
97 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
98 PriceInput: 0.25, PriceOutput: 2.00, PriceCacheRead: 0.025,
99 },
100 {
101 Provider: "openai", ID: "gpt-5-nano", DisplayName: "GPT-5 nano",
102 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
103 PriceInput: 0.05, PriceOutput: 0.40, PriceCacheRead: 0.005,
104 },
105
106 // ---- OpenAI / GPT-4.1 family ----
107 {
108 Provider: "openai", ID: "gpt-4.1", DisplayName: "GPT-4.1",
109 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
110 PriceInput: 2.00, PriceOutput: 8.00, PriceCacheRead: 0.50,
111 },
112 {
113 Provider: "openai", ID: "gpt-4.1-mini", DisplayName: "GPT-4.1 mini",
114 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
115 PriceInput: 0.40, PriceOutput: 1.60, PriceCacheRead: 0.10,
116 },
117 {
118 Provider: "openai", ID: "gpt-4.1-nano", DisplayName: "GPT-4.1 nano",
119 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
120 PriceInput: 0.10, PriceOutput: 0.40, PriceCacheRead: 0.03,
121 },
122
123 // ---- OpenAI / GPT-4o family ----
124 {
125 Provider: "openai", ID: "gpt-4o", DisplayName: "GPT-4o",
126 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
127 PriceInput: 2.50, PriceOutput: 10.00, PriceCacheRead: 1.25,
128 },
129 {
130 Provider: "openai", ID: "gpt-4o-mini", DisplayName: "GPT-4o mini",
131 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
132 PriceInput: 0.15, PriceOutput: 0.60, PriceCacheRead: 0.08,
133 },
134
135 // ---- OpenAI / reasoning models ----
136 {
137 Provider: "openai", ID: "o4-mini", DisplayName: "o4-mini",
138 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
139 PriceInput: 1.10, PriceOutput: 4.40, PriceCacheRead: 0.275,
140 },
141 {
142 Provider: "openai", ID: "o3", DisplayName: "o3",
143 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
144 PriceInput: 2.00, PriceOutput: 8.00, PriceCacheRead: 0.50,
145 },
146 {
147 Provider: "openai", ID: "o3-mini", DisplayName: "o3-mini",
148 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
149 PriceInput: 1.10, PriceOutput: 4.40, PriceCacheRead: 0.55,
150 },
151 {
152 Provider: "openai", ID: "o1", DisplayName: "o1",
153 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
154 PriceInput: 15.00, PriceOutput: 60.00, PriceCacheRead: 7.50,
155 },
156
157 // ---- Speculative: Anthropic ----
158 {
159 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5",
160 // 200k ctx / 64k maxOutput per Anthropic's published sizing
161 // for the opus-4-5 family; the 1M context is a 4.6+ thing.
162 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
163 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
164 Speculative: true,
165 },
166 {
167 Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
168 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
169 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
170 Speculative: true,
171 },
172 {
173 Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
174 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
175 PriceInput: 5.00, PriceOutput: 25.00, PriceCacheRead: 0.50, PriceCacheWrite: 6.25,
176 Speculative: true,
177 },
178 {
179 Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
180 ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
181 PriceInput: 3.00, PriceOutput: 15.00, PriceCacheRead: 0.30, PriceCacheWrite: 3.75,
182 Speculative: true,
183 },
184
185 // ---- Speculative: OpenAI ----
186 // Context windows on the OpenAI gpt-5.x family differ by route:
187 // the direct API advertises 400k, the ChatGPT Codex OAuth backend
188 // caps at 272k. zot serves both auth modes from one catalog row
189 // per id, so we pin to the smaller number to keep the context-usage
190 // meter honest under subscription auth. Users on the direct API
191 // simply see a conservative headroom estimate.
192 {
193 Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
194 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
195 PriceInput: 1.25, PriceOutput: 10.00, PriceCacheRead: 0.125,
196 Speculative: true,
197 },
198 {
199 Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
200 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
201 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
202 Speculative: true,
203 },
204 {
205 Provider: "openai", ID: "gpt-5.3", DisplayName: "GPT-5.3",
206 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
207 PriceInput: 1.75, PriceOutput: 14.00, PriceCacheRead: 0.175,
208 Speculative: true,
209 },
210 {
211 Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
212 // ContextWindow: 272k across every route we support (OpenAI
213 // direct API and the ChatGPT Codex OAuth backend).
214 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
215 PriceInput: 2.50, PriceOutput: 15.00, PriceCacheRead: 0.25,
216 Speculative: true,
217 },
218 {
219 Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
220 // ContextWindow: 400k on the OpenAI direct API, 272k on the
221 // ChatGPT Codex OAuth backend. We pin to the smaller Codex
222 // cap so the context-usage meter is honest under subscription
223 // auth; direct-API users simply see a conservative headroom
224 // estimate rather than an inflated one.
225 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
226 PriceInput: 0.75, PriceOutput: 4.50, PriceCacheRead: 0.075,
227 Speculative: true,
228 },
229}
230
231// DefaultModel is used when the user does not specify one.
232var DefaultModel = Catalog[0] // claude-sonnet-4-5
233
234// ----- active (merged) catalog -----
235//
236// Callers should use Active() / FindModel / ModelsForProvider for
237// lookups. They return the baked-in Catalog merged with any live
238// models loaded via SetLiveModels.
239
240var (
241 activeMu sync.RWMutex
242 active []Model = Catalog // default: just the static catalog
243)
244
245// SetLiveModels replaces the "live" overlay used by the active catalog.
246// Typically called after a successful /v1/models discovery or on load
247// from the on-disk cache.
248func SetLiveModels(live []Model) {
249 activeMu.Lock()
250 defer activeMu.Unlock()
251 if len(live) == 0 {
252 active = Catalog
253 return
254 }
255 active = MergeCatalog(live)
256}
257
258// Active returns the current merged catalog.
259func Active() []Model {
260 activeMu.RLock()
261 defer activeMu.RUnlock()
262 out := make([]Model, len(active))
263 copy(out, active)
264 return out
265}
266
267// FindModel returns a Model by id, optionally constrained by provider.
268// If provider is empty, the first matching id is returned. Looks up
269// against the merged active catalog.
270func FindModel(provider, id string) (Model, error) {
271 for _, m := range Active() {
272 if m.ID == id && (provider == "" || m.Provider == provider) {
273 return m, nil
274 }
275 }
276 return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
277}
278
279// ModelsForProvider returns all models for the given provider, from the
280// merged active catalog.
281func ModelsForProvider(provider string) []Model {
282 var out []Model
283 for _, m := range Active() {
284 if m.Provider == provider {
285 out = append(out, m)
286 }
287 }
288 return out
289}
290
291// ComputeCost returns the USD cost for the given usage on model m.
292func ComputeCost(m Model, u Usage) float64 {
293 const per = 1_000_000.0
294 return float64(u.InputTokens)*m.PriceInput/per +
295 float64(u.OutputTokens)*m.PriceOutput/per +
296 float64(u.CacheReadTokens)*m.PriceCacheRead/per +
297 float64(u.CacheWriteTokens)*m.PriceCacheWrite/per
298}