v0.1.99 fb3e1af

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 (latest)",
49		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
50		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
51	},
52	{
53		Provider: "anthropic", ID: "claude-opus-4-1", DisplayName: "Claude Opus 4.1 (latest)",
54		ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
55		PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
56	},
57	{
58		Provider: "anthropic", ID: "claude-opus-4-0", DisplayName: "Claude Opus 4 (latest)",
59		ContextWindow: 200000, MaxOutput: 32000, Reasoning: true,
60		PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, PriceCacheWrite: 18.75,
61	},
62	{
63		Provider: "anthropic", ID: "claude-sonnet-4-0", DisplayName: "Claude Sonnet 4 (latest)",
64		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
65		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
66	},
67	{
68		Provider: "anthropic", ID: "claude-haiku-4-5", DisplayName: "Claude Haiku 4.5 (latest)",
69		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
70		PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, 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, PriceOutput: 15, PriceCacheRead: 0.3, 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, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
83	},
84	{
85		Provider: "anthropic", ID: "claude-3-5-haiku-latest", DisplayName: "Claude Haiku 3.5 (latest)",
86		ContextWindow: 200000, MaxOutput: 8192, Reasoning: false,
87		PriceInput: 0.8, PriceOutput: 4, PriceCacheRead: 0.08, PriceCacheWrite: 1,
88	},
89	{
90		Provider: "anthropic", ID: "claude-3-opus-20240229", DisplayName: "Claude Opus 3",
91		ContextWindow: 200000, MaxOutput: 4096, Reasoning: false,
92		PriceInput: 15, PriceOutput: 75, PriceCacheRead: 1.5, 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: 1000000, MaxOutput: 384000, Reasoning: true,
103		PriceInput: 0.435, PriceOutput: 0.87, PriceCacheRead: 0.003625,
104		BaseURL: "https://api.deepseek.com",
105	},
106	{
107		Provider: "deepseek", ID: "deepseek-v4-flash", DisplayName: "DeepSeek V4 Flash",
108		ContextWindow: 1000000, MaxOutput: 384000, Reasoning: true,
109		PriceInput: 0.14, PriceOutput: 0.28, PriceCacheRead: 0.0028,
110		BaseURL: "https://api.deepseek.com",
111	},
112
113	// ---- Kimi / Kimi Code ----
114	// Anthropic-messages on https://api.kimi.com/coding (no /v1 suffix;
115	// the Anthropic client appends /v1/messages itself).
116	{
117		Provider: "kimi", ID: "kimi-for-coding", DisplayName: "Kimi For Coding",
118		ContextWindow: 262144, MaxOutput: 32768, Reasoning: true,
119		PriceInput: 0, PriceOutput: 0, PriceCacheRead: 0,
120		BaseURL: "https://api.kimi.com/coding",
121	},
122
123	// ---- OpenAI / GPT-5 family ----
124	{
125		Provider: "openai", ID: "gpt-5", DisplayName: "GPT-5",
126		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
127		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.125,
128	},
129	{
130		Provider: "openai", ID: "gpt-5-mini", DisplayName: "GPT-5 Mini",
131		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
132		PriceInput: 0.25, PriceOutput: 2, PriceCacheRead: 0.025,
133	},
134	{
135		Provider: "openai", ID: "gpt-5-nano", DisplayName: "GPT-5 Nano",
136		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
137		PriceInput: 0.05, PriceOutput: 0.4, PriceCacheRead: 0.005,
138	},
139
140	// ---- OpenAI / GPT-4.1 family ----
141	{
142		Provider: "openai", ID: "gpt-4.1", DisplayName: "GPT-4.1",
143		ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
144		PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5,
145	},
146	{
147		Provider: "openai", ID: "gpt-4.1-mini", DisplayName: "GPT-4.1 mini",
148		ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
149		PriceInput: 0.4, PriceOutput: 1.6, PriceCacheRead: 0.1,
150	},
151	{
152		Provider: "openai", ID: "gpt-4.1-nano", DisplayName: "GPT-4.1 nano",
153		ContextWindow: 1047576, MaxOutput: 32768, Reasoning: false,
154		PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.03,
155	},
156
157	// ---- OpenAI / GPT-4o family ----
158	{
159		Provider: "openai", ID: "gpt-4o", DisplayName: "GPT-4o",
160		ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
161		PriceInput: 2.5, PriceOutput: 10, PriceCacheRead: 1.25,
162	},
163	{
164		Provider: "openai", ID: "gpt-4o-mini", DisplayName: "GPT-4o mini",
165		ContextWindow: 128000, MaxOutput: 16384, Reasoning: false,
166		PriceInput: 0.15, PriceOutput: 0.6, PriceCacheRead: 0.08,
167	},
168
169	// ---- OpenAI / reasoning models ----
170	{
171		Provider: "openai", ID: "o4-mini", DisplayName: "o4-mini",
172		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
173		PriceInput: 1.1, PriceOutput: 4.4, PriceCacheRead: 0.28,
174	},
175	{
176		Provider: "openai", ID: "o3", DisplayName: "o3",
177		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
178		PriceInput: 2, PriceOutput: 8, PriceCacheRead: 0.5,
179	},
180	{
181		Provider: "openai", ID: "o3-mini", DisplayName: "o3-mini",
182		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
183		PriceInput: 1.1, PriceOutput: 4.4, PriceCacheRead: 0.55,
184	},
185	{
186		Provider: "openai", ID: "o1", DisplayName: "o1",
187		ContextWindow: 200000, MaxOutput: 100000, Reasoning: true,
188		PriceInput: 15, PriceOutput: 60, PriceCacheRead: 7.5,
189	},
190
191	// ---- Google / Gemini ----
192	{
193		Provider: "google", ID: "gemini-2.5-pro", DisplayName: "Gemini 2.5 Pro",
194		ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
195		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.125,
196	},
197	{
198		Provider: "google", ID: "gemini-2.5-flash", DisplayName: "Gemini 2.5 Flash",
199		ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
200		PriceInput: 0.3, PriceOutput: 2.5, PriceCacheRead: 0.03,
201	},
202	{
203		Provider: "google", ID: "gemini-2.5-flash-lite", DisplayName: "Gemini 2.5 Flash-Lite",
204		ContextWindow: 1048576, MaxOutput: 65536, Reasoning: true,
205		PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.01,
206	},
207	{
208		Provider: "google", ID: "gemini-2.0-flash", DisplayName: "Gemini 2.0 Flash",
209		ContextWindow: 1048576, MaxOutput: 8192, Reasoning: false,
210		PriceInput: 0.1, PriceOutput: 0.4, PriceCacheRead: 0.025,
211	},
212	{
213		Provider: "google", ID: "gemini-2.0-flash-lite", DisplayName: "Gemini 2.0 Flash-Lite",
214		ContextWindow: 1048576, MaxOutput: 8192, Reasoning: false,
215		PriceInput: 0.075, PriceOutput: 0.3, PriceCacheRead: 0,
216	},
217
218	// ---- Speculative: Anthropic ----
219	{
220		Provider: "anthropic", ID: "claude-opus-4-5", DisplayName: "Claude Opus 4.5 (latest)",
221		ContextWindow: 200000, MaxOutput: 64000, Reasoning: true,
222		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
223		Speculative: true,
224	},
225	{
226		Provider: "anthropic", ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6",
227		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
228		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
229		Speculative: true,
230	},
231	{
232		Provider: "anthropic", ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7",
233		ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true,
234		PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25,
235		Speculative: true,
236	},
237	{
238		Provider: "anthropic", ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6",
239		ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true,
240		PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75,
241		Speculative: true,
242	},
243
244	// ---- Speculative: OpenAI ----
245	// Public OpenAI API route. The ChatGPT/Codex subscription route is
246	// represented separately below as provider "openai-codex".
247	{
248		Provider: "openai", ID: "gpt-5.1", DisplayName: "GPT-5.1",
249		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
250		PriceInput: 1.25, PriceOutput: 10, PriceCacheRead: 0.13,
251		Speculative: true,
252	},
253	{
254		Provider: "openai", ID: "gpt-5.2", DisplayName: "GPT-5.2",
255		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
256		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
257		Speculative: true,
258	},
259	{
260		Provider: "openai", ID: "gpt-5.4", DisplayName: "GPT-5.4",
261		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
262		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
263		Speculative: true,
264	},
265	{
266		Provider: "openai", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
267		ContextWindow: 400000, MaxOutput: 128000, Reasoning: true,
268		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
269		Speculative: true,
270	},
271	{
272		Provider: "openai", ID: "gpt-5.5", DisplayName: "GPT-5.5",
273		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
274		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
275		Speculative: true,
276	},
277
278	// ---- OpenAI Codex / ChatGPT subscription backend ----
279	// Same model ids as the OpenAI family, but routed through the
280	// ChatGPT Codex OAuth backend rather than api.openai.com.
281	{
282		Provider: "openai-codex", ID: "gpt-5.2", DisplayName: "GPT-5.2",
283		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
284		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
285		Speculative: true,
286	},
287	{
288		Provider: "openai-codex", ID: "gpt-5.3-codex", DisplayName: "GPT-5.3 Codex",
289		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
290		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
291		Speculative: true,
292	},
293	{
294		Provider: "openai-codex", ID: "gpt-5.3-codex-spark", DisplayName: "GPT-5.3 Codex Spark",
295		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
296		PriceInput: 1.75, PriceOutput: 14, PriceCacheRead: 0.175,
297		Speculative: true,
298	},
299	{
300		Provider: "openai-codex", ID: "gpt-5.4", DisplayName: "GPT-5.4",
301		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
302		PriceInput: 2.5, PriceOutput: 15, PriceCacheRead: 0.25,
303		Speculative: true,
304	},
305	{
306		Provider: "openai-codex", ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 mini",
307		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
308		PriceInput: 0.75, PriceOutput: 4.5, PriceCacheRead: 0.075,
309		Speculative: true,
310	},
311	{
312		Provider: "openai-codex", ID: "gpt-5.5", DisplayName: "GPT-5.5",
313		ContextWindow: 272000, MaxOutput: 128000, Reasoning: true,
314		PriceInput: 5, PriceOutput: 30, PriceCacheRead: 0.5,
315		Speculative: true,
316	},
317}
318
319// DefaultModel is used when the user does not specify one.
320var DefaultModel = Catalog[0] // claude-sonnet-4-5
321
322// ----- active (merged) catalog -----
323//
324// Callers should use Active() / FindModel / ModelsForProvider for
325// lookups. They return the baked-in Catalog merged with any live
326// models loaded via SetLiveModels.
327
328var (
329	activeMu sync.RWMutex
330	active   []Model = Catalog // default: just the static catalog
331)
332
333// SetLiveModels replaces the "live" overlay used by the active catalog.
334// Typically called after a successful /v1/models discovery or on load
335// from the on-disk cache.
336func SetLiveModels(live []Model) {
337	activeMu.Lock()
338	defer activeMu.Unlock()
339	if len(live) == 0 {
340		active = Catalog
341		return
342	}
343	active = MergeCatalog(live)
344}
345
346// Active returns the current merged catalog.
347func Active() []Model {
348	activeMu.RLock()
349	defer activeMu.RUnlock()
350	out := make([]Model, len(active))
351	copy(out, active)
352	return out
353}
354
355// FindModel returns a Model by id, optionally constrained by provider.
356// If provider is empty, the first matching id is returned. Looks up
357// against the merged active catalog.
358func FindModel(provider, id string) (Model, error) {
359	for _, m := range Active() {
360		if m.ID == id && (provider == "" || m.Provider == provider) {
361			return m, nil
362		}
363	}
364	return Model{}, fmt.Errorf("unknown model %q (provider=%q)", id, provider)
365}
366
367// ModelsForProvider returns all models for the given provider, from the
368// merged active catalog.
369func ModelsForProvider(provider string) []Model {
370	var out []Model
371	for _, m := range Active() {
372		if m.Provider == provider {
373			out = append(out, m)
374		}
375	}
376	return out
377}
378
379// ComputeCost returns the USD cost for the given usage on model m.
380func ComputeCost(m Model, u Usage) float64 {
381	const per = 1_000_000.0
382	return float64(u.InputTokens)*m.PriceInput/per +
383		float64(u.OutputTokens)*m.PriceOutput/per +
384		float64(u.CacheReadTokens)*m.PriceCacheRead/per +
385		float64(u.CacheWriteTokens)*m.PriceCacheWrite/per
386}

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