v0.1.81 1aea23e

models.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close