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 API string // wire API override for providers that support multiple protocols
14 ContextWindow int
15 MaxOutput int
16 Reasoning bool // supports reasoning/thinking
17
18 // AdaptiveThinking marks Anthropic models that only support the
19 // adaptive thinking mode (Opus 4.7+). These reject explicit
20 // thinking budgets (thinking:{type:"enabled",budget_tokens:N} -> 400)
21 // and also reject non-default sampling params (temperature/top_p/
22 // top_k). The Anthropic client sends thinking:{type:"adaptive"} plus
23 // output_config.effort and omits temperature for these models.
24 AdaptiveThinking bool
25
26 // Prices are USD per 1M tokens.
27 PriceInput float64
28 PriceOutput float64
29 PriceCacheRead float64
30 PriceCacheWrite float64
31
32 // Speculative marks models whose ids are known from the upstream
33 // vendor's CLI but not yet live on their public API. They'll 404
34 // today but start working the moment the provider flips the switch.
35 Speculative bool
36
37 // BaseURL overrides the provider's default API endpoint for this
38 // model. Optional; when empty the provider's default (or the
39 // --base-url flag) is used. Useful for local models served by
40 // ollama, vLLM, LM Studio, etc.
41 BaseURL string
42
43 // Source is where this model entry came from: "catalog" (baked in),
44 // "live" (discovered via /v1/models), or "cache" (loaded from the
45 // on-disk cache). Informational.
46 Source string
47}
48
49// Catalog is the hardcoded, read-only list of supported models.
50// Prices are USD per 1M tokens. The list is curated to what zot's
51// clients (Anthropic Messages + OpenAI Chat Completions) can actually
52// talk to; models that are only reachable through the OpenAI Responses
53// API (o1-pro, o3-pro, gpt-5-pro) are omitted.
54var Catalog = []Model{
55 // ---- Anthropic / Claude 4.x ----
56 {
57 Provider: "anthropic", ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5 (latest)",
58 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
59 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
60 },
61 {
62 Provider: "anthropic", ID: "claude-opus-4-1", DisplayName: "Claude Opus 4.1 (latest)",
63 ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
64 PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
65 },
66 {
67 Provider: "anthropic", ID: "claude-opus-4-0", DisplayName: "Claude Opus 4 (latest)",
68 ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
69 PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
70 },
71 {
72 Provider: "anthropic", ID: "claude-sonnet-4-0", DisplayName: "Claude Sonnet 4 (latest)",
73 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
74 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
75 },
76 {
77 Provider: "anthropic", ID: "claude-haiku-4-5", DisplayName: "Claude Haiku 4.5 (latest)",
78 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
79 PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
80 },
81
82 // ---- Anthropic / Claude 3.x (legacy) ----
83 {
84 Provider: "anthropic", ID: "claude-3-7-sonnet-20250219", DisplayName: "Claude Sonnet 3.7",
85 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
86 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
87 },
88 {
89 Provider: "anthropic", ID: "claude-3-5-sonnet-20241022", DisplayName: "Claude Sonnet 3.5 v2",
90 ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
91 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
92 },
93 {
94 Provider: "anthropic", ID: "claude-3-5-haiku-latest", DisplayName: "Claude Haiku 3.5 (latest)",
95 ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
96 PriceInput: 0.8, PriceOutput: 4, PriceCacheRead: 0.08, PriceCacheWrite: 1,
97 },
98 {
99 Provider: "anthropic", ID: "claude-3-opus-20240229", DisplayName: "Claude Opus 3",
100 ContextWindow: 200000, MaxOutput: 4096, Reasoning: false,
101 PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
102 },
103
104 // ---- DeepSeek ----
105 // The current public DeepSeek API exposes the V4 family on
106 // api.deepseek.com/v1. Pro is the flagship reasoning model;
107 // Flash is the cheaper/faster sibling. Both accept image inputs
108 // (multimodal parts: image_url) in addition to text.
109 {
110 Provider: "deepseek", ID: "deepseek-v4-pro", DisplayName: "DeepSeek V4 Pro",
111 ContextWindow: 1000000, MaxOutput: 384000, Reasoning: true,
112 PriceInput: 0.435, PriceOutput: 0.87, PriceCacheRead: 0.003625,
113 BaseURL: "https://api.deepseek.com",
114 },
115 {
116 Provider: "deepseek", ID: "deepseek-v4-flash", DisplayName: "DeepSeek V4 Flash",
117 ContextWindow: 1000000, MaxOutput: 384000, Reasoning: true,
118 PriceInput: 0.14, PriceOutput: 0.28, PriceCacheRead: 0.0028,
119 BaseURL: "https://api.deepseek.com",
120 },
121
122 // ---- Kimi / Kimi Code ----
123 // Anthropic-messages on https://api.kimi.com/coding (no /v1 suffix;
124 // the Anthropic client appends /v1/messages itself).
125 {
126 Provider: "kimi", ID: "kimi-for-coding", DisplayName: "Kimi For Coding",
127 ContextWindow: 262144, MaxOutput: 32768, Reasoning: true,
128 PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0,
129 BaseURL: "https://api.kimi.com/coding",
130 },
131
132 // ---- OpenAI / GPT-5 family ----
133 {
134 Provider: "openai", ID: "gpt-5", DisplayName: "GPT-5",
135 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
136 PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.125,
137 },
138 {
139 Provider: "openai", ID: "gpt-5-mini", DisplayName: "GPT-5 Mini",
140 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
141 PriceInput: 0.25, PriceOutput: 2, PriceCacheRead: 0.025,
142 },
143 {
144 Provider: "openai", ID: "gpt-5-nano", DisplayName: "GPT-5 Nano",
145 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
146 PriceInput: 0.05, PriceOutput: 0.4, PriceCacheRead: 0.005,
147 },
148
149 // ---- OpenAI / GPT-4.1 family ----
150 {
151 Provider: "openai", ID: "gpt-4.1", DisplayName: "GPT-4.1",
152 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
153 PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5,
154 },
155 {
156 Provider: "openai", ID: "gpt-4.1-mini", DisplayName: "GPT-4.1 mini",
157 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
158 PriceInput: 0.4, PriceOutput: 1.6, PriceCacheRead: 0.1,
159 },
160 {
161 Provider: "openai", ID: "gpt-4.1-nano", DisplayName: "GPT-4.1 nano",
162 ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
163 PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.03,
164 },
165
166 // ---- OpenAI / GPT-4o family ----
167 {
168 Provider: "openai", ID: "gpt-4o", DisplayName: "GPT-4o",
169 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
170 PriceInput: 2.5, PriceOutput: 10, PriceCacheRead: 1.25,
171 },
172 {
173 Provider: "openai", ID: "gpt-4o-mini", DisplayName: "GPT-4o mini",
174 ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
175 PriceInput: 0.15, PriceOutput: 0.6, PriceCacheRead: 0.08,
176 },
177
178 // ---- OpenAI / reasoning models ----
179 {
180 Provider: "openai", ID: "o4-mini", DisplayName: "o4-mini",
181 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
182 PriceInput: 1.1, PriceOutput: 4.4, PriceCacheRead: 0.28,
183 },
184 {
185 Provider: "openai", ID: "o3", DisplayName: "o3",
186 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
187 PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5,
188 },
189 {
190 Provider: "openai", ID: "o3-mini", DisplayName: "o3-mini",
191 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
192 PriceInput: 1.1, PriceOutput: 4.4, PriceCacheRead: 0.55,
193 },
194 {
195 Provider: "openai", ID: "o1", DisplayName: "o1",
196 ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
197 PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5,
198 },
199
200 // ---- Google / Gemini ----
201 {
202 Provider: "google", ID: "gemini-2.5-pro", DisplayName: "Gemini 2.5 Pro",
203 ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
204 PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.125,
205 },
206 {
207 Provider: "google", ID: "gemini-2.5-flash", DisplayName: "Gemini 2.5 Flash",
208 ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
209 PriceInput: 0.3, PriceOutput: 2.5, PriceCacheRead: 0.03,
210 },
211 {
212 Provider: "google", ID: "gemini-2.5-flash-lite", DisplayName: "Gemini 2.5 Flash-Lite",
213 ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
214 PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.01,
215 },
216 {
217 Provider: "google", ID: "gemini-2.0-flash", DisplayName: "Gemini 2.0 Flash",
218 ContextWindow: 1048576, MaxOutput: 8192, Reasoning: false,
219 PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.025,
220 },
221 {
222 Provider: "google", ID: "gemini-2.0-flash-lite", DisplayName: "Gemini 2.0 Flash-Lite",
223 ContextWindow: 1048576, MaxOutput: 8192, Reasoning: false,
224 PriceInput: 0.075, PriceOutput: 0.3, PriceCacheRead: 0,
225 },
226
227 // ---- OpenRouter ----
228 // Seed entry only: the default model returned by defaultModelForProvider
229 // so it resolves offline. The full OpenRouter catalog is discovered live
230 // (DiscoverOpenRouter) and overlays this on refresh.
231 {
232 Provider: "openrouter", ID: "anthropic/claude-sonnet-4.5", DisplayName: "Claude Sonnet 4.5 (OpenRouter)",
233 ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
234 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
235 BaseURL: openrouterDefaultBaseURL,
236 },
237 {
238 Provider: "openrouter", ID: "anthropic/claude-sonnet-5", DisplayName: "Claude Sonnet 5 (OpenRouter)",
239 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
240 PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5,
241 BaseURL: openrouterDefaultBaseURL,
242 },
243
244 // ---- Speculative: Anthropic ----
245 {
246 Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
247 ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
248 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
249 Speculative: true,
250 },
251 {
252 Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
253 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
254 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
255 Speculative: true,
256 },
257 {
258 Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
259 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
260 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
261 Speculative: true,
262 },
263 {
264 Provider: "anthropic", ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8",
265 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
266 PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
267 Speculative: true,
268 },
269 {
270 Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
271 ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
272 PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
273 Speculative: true,
274 },
275 {
276 Provider: "anthropic", ID: "claude-sonnet-5", DisplayName: "Claude Sonnet 5",
277 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
278 PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5,
279 },
280 {
281 Provider: "anthropic", ID: "claude-fable-5", DisplayName: "Claude Fable 5",
282 ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true,
283 PriceInput: 10, PriceOutput: 50, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
284 Speculative: true,
285 },
286
287 // ---- Speculative: OpenAI ----
288 // Public OpenAI API route. The ChatGPT/Codex subscription route is
289 // represented separately below as provider "openai-codex".
290 {
291 Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
292 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
293 PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.13,
294 Speculative: true,
295 },
296 {
297 Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
298 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
299 PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
300 Speculative: true,
301 },
302 {
303 Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
304 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
305 PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
306 Speculative: true,
307 },
308 {
309 Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
310 ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
311 PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
312 Speculative: true,
313 },
314 {
315 Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
316 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
317 PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
318 Speculative: true,
319 },
320 {
321 Provider: "openai", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna", API: APIResponses,
322 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
323 PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
324 Speculative: true,
325 },
326 {
327 Provider: "openai", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol", API: APIResponses,
328 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
329 PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
330 Speculative: true,
331 },
332 {
333 Provider: "openai", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra", API: APIResponses,
334 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
335 PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
336 Speculative: true,
337 },
338
339 // ---- OpenAI Codex / ChatGPT subscription backend ----
340 // Same model ids as the OpenAI family, but routed through the
341 // ChatGPT Codex OAuth backend rather than api.openai.com.
342 {
343 Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
344 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
345 PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
346 Speculative: true,
347 },
348 {
349 Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4",
350 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
351 PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
352 Speculative: true,
353 },
354 {
355 Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
356 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
357 PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
358 Speculative: true,
359 },
360 {
361 Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5",
362 ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
363 PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
364 Speculative: true,
365 },
366 {
367 Provider: "openai-codex", ID: "gpt-5.6-luna", DisplayName: "GPT-5.6 Luna",
368 ContextWindow: 372000, MaxOutput: 128000, Reasoning: true,
369 PriceInput: 1, PriceOutput: 6, PriceCacheRead: 0.1, PriceCacheWrite: 1.25,
370 Speculative: true,
371 },
372 {
373 Provider: "openai-codex", ID: "gpt-5.6-sol", DisplayName: "GPT-5.6 Sol",
374 ContextWindow: 372000, MaxOutput: 128000, Reasoning: true,
375 PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
376 Speculative: true,
377 },
378 {
379 Provider: "openai-codex", ID: "gpt-5.6-terra", DisplayName: "GPT-5.6 Terra",
380 ContextWindow: 372000, MaxOutput: 128000, Reasoning: true,
381 PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25, PriceCacheWrite: 3.125,
382 Speculative: true,
383 },
384}
385
386// DefaultModel is used when the user does not specify one.
387var DefaultModel = Catalog[0] // claude-sonnet-4-5
388
389// ----- active (merged) catalog -----
390//
391// Callers should use Active() / FindModel / ModelsForProvider for
392// lookups. They return the baked-in Catalog merged with any live
393// models loaded via SetLiveModels.
394
395var (
396 activeMu sync.RWMutex
397 active []Model // live overlay merged in via SetLiveModels; nil = none yet
398 activeSet bool // true once SetLiveModels has run (even with empty live)
399 managedModels []Model // ephemeral models exposed by local model managers
400)
401
402// SetLiveModels replaces the "live" overlay used by the active catalog.
403// Typically called after a successful /v1/models discovery or on load
404// from the on-disk cache.
405func SetLiveModels(live []Model) {
406 activeMu.Lock()
407 defer activeMu.Unlock()
408 activeSet = true
409 if len(live) == 0 {
410 active = nil
411 return
412 }
413 active = MergeCatalog(live)
414}
415
416// Active returns the current merged catalog.
417//
418// When no live overlay has been set it returns the fully-assembled
419// static Catalog. Reading Catalog here (rather than capturing it into
420// a package-level var initializer) is load-bearing: the extended
421// catalog in catalog_builtin.go / extra_models.go is appended from
422// init() functions, which run AFTER package-level var initializers.
423// Snapshotting Catalog at var-init time would freeze the picker to the
424// curated seed list and drop every extra provider (openrouter, groq,
425// xai, ...). Deferring the read to call time avoids that ordering trap.
426func Active() []Model {
427 activeMu.RLock()
428 defer activeMu.RUnlock()
429 src := active
430 if !activeSet || src == nil {
431 src = Catalog
432 }
433 out := make([]Model, len(src))
434 copy(out, src)
435 if len(managedModels) == 0 {
436 return out
437 }
438 index := make(map[string]int, len(out))
439 for i, model := range out {
440 index[model.Provider+"\x00"+model.ID] = i
441 }
442 for _, model := range managedModels {
443 key := model.Provider + "\x00" + model.ID
444 if i, ok := index[key]; ok {
445 out[i] = model
446 continue
447 }
448 index[key] = len(out)
449 out = append(out, model)
450 }
451 return out
452}
453
454// SetManagedModels replaces the ephemeral catalog entries supplied by local
455// model managers. Unlike SetLiveModels, this does not disturb provider model
456// discovery or its on-disk cache.
457func SetManagedModels(models []Model) {
458 activeMu.Lock()
459 defer activeMu.Unlock()
460 managedModels = append([]Model(nil), models...)
461}
462
463// FindModel returns a Model by id, optionally constrained by provider.
464// If provider is empty, the first matching id is returned. Looks up
465// against the merged active catalog.
466func FindModel(provider, id string) (Model, error) {
467 for _, m := range Active() {
468 if m.ID == id && (provider == "" || m.Provider == provider) {
469 return m, nil
470 }
471 }
472 return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
473}
474
475// ModelsForProvider returns all models for the given provider, from the
476// merged active catalog.
477func ModelsForProvider(provider string) []Model {
478 var out []Model
479 for _, m := range Active() {
480 if m.Provider == provider {
481 out = append(out, m)
482 }
483 }
484 return out
485}
486
487// ComputeCost returns the USD cost for the given usage on model m.
488func ComputeCost(m Model, u Usage) float64 {
489 const per = 1_000_000.0
490 return float64(u.InputTokens)*m.PriceInput/per +
491 float64(u.OutputTokens)*m.PriceOutput/per +
492 float64(u.CacheReadTokens)*m.PriceCacheRead/per +
493 float64(u.CacheWriteTokens)*m.PriceCacheWrite/per
494}