v0.2.66 d5769b9

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

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