@visual-z/xeno 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,614 @@
1
+ /**
2
+ * What the runtime needs to know about a model: how to talk to it, and how to
3
+ * price what it did.
4
+ *
5
+ * This exists because the two facts turned out to be inseparable. The obvious
6
+ * shape is "an adapter per wire format" and nothing else, but pricing is not a
7
+ * property of the wire format -- two providers speaking the same OpenAI
8
+ * Completions dialect bill caching on completely different terms. So the model,
9
+ * not the adapter, is what carries the rates.
10
+ *
11
+ * Deliberately not a class and deliberately inert: a spec is data the catalog
12
+ * hands to an adapter, and anything that can reach the network belongs in the
13
+ * adapter instead.
14
+ */
15
+
16
+ /**
17
+ * The wire formats worth abstracting. Four is the number pi arrived at after
18
+ * supporting dozens of providers (research appendix, line 176); this project
19
+ * needs two of them so far, and adding a third should be adding a member here
20
+ * plus an adapter, never a conditional inside an existing adapter.
21
+ */
22
+ export type Api = "anthropic-messages" | "openai-completions" | "openai-responses";
23
+
24
+ /** How hard the model should think. Mapped per-model: not every provider takes all five. */
25
+ export type Effort = "low" | "medium" | "high" | "xhigh" | "max";
26
+
27
+ /**
28
+ * Who decides what gets cached.
29
+ *
30
+ * explicit -- the caller places breakpoints and the provider bills for the
31
+ * write. Hits are predictable because you chose the boundary.
32
+ * (Anthropic)
33
+ * automatic -- the provider caches prefixes by itself, bills nothing for the
34
+ * write, and guarantees no hit rate. Writes land in seconds, so
35
+ * an early miss is normal rather than a symptom. (DeepSeek)
36
+ *
37
+ * A named pair rather than two booleans: both the cost path and the diagnosis
38
+ * path fork on this, and a discriminant can be closed with `assertNever`.
39
+ */
40
+ export type CacheKind = "explicit" | "automatic";
41
+
42
+ /**
43
+ * Cache prices as multiples of base input.
44
+ *
45
+ * Multiples rather than dollars because that is the unit `equivalentCost`
46
+ * reports in, and for the reason stated there: dollar prices go stale, ratios
47
+ * survive a price change. A provider that bills nothing for cache writes sets
48
+ * `write: 0`, which is not a placeholder -- it is the actual term.
49
+ */
50
+ export interface CacheRates {
51
+ read: number;
52
+ write: number;
53
+ }
54
+
55
+ /**
56
+ * USD per million tokens.
57
+ *
58
+ * Optional on the model, and filled in only where the number was read off
59
+ * a source we trust. An unverified price is worse than an absent one: it
60
+ * looks authoritative and silently misreports every session.
61
+ *
62
+ * Cache fields are optional because most providers publish input/output
63
+ * and nothing else. Requiring all four packed a partial fact into a total
64
+ * unknown — the catalog then had the groq rate and /usage still said
65
+ * "unknown". Filling the missing cache slot with 0 is still forbidden:
66
+ * `write: 0` is DeepSeek's and Z.AI's actual term, not a default.
67
+ */
68
+ export interface Price {
69
+ input: number;
70
+ output: number;
71
+ cacheRead?: number;
72
+ cacheWrite?: number;
73
+ }
74
+
75
+ /**
76
+ * Which dialect of "OpenAI-compatible" a model speaks.
77
+ *
78
+ * Every field here is a difference that costs a 400 or a wrong number, and
79
+ * every one was found the same way: a provider quietly disagreeing with the
80
+ * spec. Carried as data because the alternative -- branching on the provider's
81
+ * name inside the adapter -- becomes an unextendable chain of `||` by the
82
+ * third provider.
83
+ *
84
+ * Only relevant to `openai-completions`. The Anthropic adapter has one dialect
85
+ * by construction: its own.
86
+ */
87
+ export interface Compat {
88
+ /**
89
+ * Whether the system prompt may be sent with the `developer` role, which
90
+ * reasoning-capable OpenAI models expect. Servers that predate it (Ollama,
91
+ * vLLM, DeepSeek) reject the role outright, so they get `system`.
92
+ */
93
+ supportsDeveloperRole: boolean;
94
+ /**
95
+ * OpenAI renamed this for reasoning models and most of the family did not
96
+ * follow. Sending the wrong one is not an error -- it is silently ignored,
97
+ * and the response runs to the model's own default length.
98
+ */
99
+ maxTokensField: "max_tokens" | "max_completion_tokens";
100
+ /** Whether `reasoning_effort` is understood. Some servers 400 on an unknown field. */
101
+ supportsReasoningEffort: boolean;
102
+ /**
103
+ * Whether the server accepts `stream_options: { include_usage: true }`.
104
+ * Default on: an OpenAI-dialect stream that is not asked for usage reports
105
+ * none, the loop would park a zero-token anchor, and shouldCut never fires.
106
+ * Some servers 400 on the unknown field, which is why this is a bit rather
107
+ * than assumed of every Completions accent.
108
+ */
109
+ supportsUsageInStreaming: boolean;
110
+ /**
111
+ * DeepSeek requires `reasoning_content` on every assistant message once
112
+ * `tools` is in the request -- including turns that did no thinking -- and
113
+ * returns 400 without it.
114
+ */
115
+ requiresReasoningContentOnAssistantMessages: boolean;
116
+ /** How thinking is switched on. `deepseek` adds `thinking: {type}`; `openai` uses effort alone. */
117
+ thinkingFormat: "openai" | "deepseek";
118
+ }
119
+
120
+ /**
121
+ * Standard OpenAI. A model that says nothing about its accent is assumed to
122
+ * speak the documented one -- and will be corrected by a 400 if it does not,
123
+ * which is the cheapest way to find out.
124
+ */
125
+ /**
126
+ * Kimi K3, from Moonshot's own agent guide (platform.kimi.ai, "Build an Agent
127
+ * with Kimi K3" and the Reasoning Effort page): it always reasons, takes no
128
+ * `thinking` field, accepts only low / high / max effort (default max), and
129
+ * every example uses the `system` role. The complete assistant message --
130
+ * reasoning included -- must be sent back, which the thinking block's raw
131
+ * already does; turns without reasoning get none, since "unmodified" rules
132
+ * out inventing an empty one.
133
+ */
134
+ export const KIMI_K3_COMPAT: Compat = {
135
+ supportsDeveloperRole: false,
136
+ maxTokensField: "max_completion_tokens",
137
+ supportsReasoningEffort: true,
138
+ supportsUsageInStreaming: true,
139
+ requiresReasoningContentOnAssistantMessages: false,
140
+ thinkingFormat: "openai",
141
+ };
142
+ export const KIMI_K3_EFFORT: Partial<Record<Effort, string | null>> = {
143
+ low: "low", medium: "high", high: "high", xhigh: "max", max: "max",
144
+ };
145
+
146
+ /**
147
+ * Gemini's OpenAI-compatible endpoint (ai.google.dev/gemini-api/docs/openai).
148
+ * It 400s on any effort outside none / low / medium / high -- reported
149
+ * verbatim on Google's developer forum -- and Gemini 3 cannot turn reasoning
150
+ * off, so the top of our scale maps to high. `max_tokens` is the documented
151
+ * field; `max_completion_tokens` has been reported ignored. Every official
152
+ * example uses `system`. Thought signatures ride on each tool call's `wire`.
153
+ */
154
+ export const GEMINI_COMPAT: Compat = {
155
+ supportsDeveloperRole: false,
156
+ maxTokensField: "max_tokens",
157
+ supportsReasoningEffort: true,
158
+ supportsUsageInStreaming: true,
159
+ requiresReasoningContentOnAssistantMessages: false,
160
+ thinkingFormat: "openai",
161
+ };
162
+ export const GEMINI_EFFORT: Partial<Record<Effort, string | null>> = {
163
+ low: "low", medium: "medium", high: "high", xhigh: "high", max: "high",
164
+ };
165
+
166
+ export const DEFAULT_COMPAT: Compat = {
167
+ supportsDeveloperRole: true,
168
+ maxTokensField: "max_completion_tokens",
169
+ supportsReasoningEffort: true,
170
+ supportsUsageInStreaming: true,
171
+ requiresReasoningContentOnAssistantMessages: false,
172
+ thinkingFormat: "openai",
173
+ };
174
+
175
+ export interface ModelSpec {
176
+ /** The id sent on the wire. */
177
+ id: string;
178
+ /** For humans, in `/model` listings. */
179
+ name: string;
180
+ api: Api;
181
+ /** Catalog/auth key -- the name under which a credential is stored. */
182
+ provider: string;
183
+ /**
184
+ * Optional: the Anthropic SDK resolves its own default, and a custom
185
+ * endpoint overrides it. Required in practice for `openai-completions`,
186
+ * where there is no default worth guessing.
187
+ */
188
+ baseUrl?: string;
189
+ reasoning: boolean;
190
+ /**
191
+ * Absent when the provider's caching terms are not in the source we
192
+ * trust. Filling `automatic` + `write: 0` here would be DeepSeek's
193
+ * contract, not a default, and `/usage` would then report a fake zero.
194
+ */
195
+ cache?: CacheKind;
196
+ rates?: CacheRates;
197
+ price?: Price;
198
+ /**
199
+ * Both optional because an unverified number here would be a fabrication,
200
+ * and nothing reads them yet -- they are descriptive, for `/model` and for
201
+ * a future context-window check. Fill them from the provider's own docs or
202
+ * leave them out.
203
+ */
204
+ contextWindow?: number;
205
+ maxTokens?: number;
206
+ /** Only meaningful for `openai-completions`. Absent means `DEFAULT_COMPAT`. */
207
+ compat?: Compat;
208
+ /**
209
+ * Our five effort levels onto what this model accepts. `null` means "send
210
+ * no effort at all and let the model default"; an absent entry means send
211
+ * our level through unchanged.
212
+ */
213
+ thinkingLevelMap?: Partial<Record<Effort, string | null>>;
214
+ }
215
+
216
+ /**
217
+ * 5-minute ephemeral caching: reads bill at 0.1x base input, writes at 1.25x.
218
+ * If Anthropic changes these, every cost number in the session report is
219
+ * wrong by that factor and nothing will raise an error -- so they live here,
220
+ * next to the model they describe, rather than as loose module constants.
221
+ */
222
+ export const ANTHROPIC_RATES: CacheRates = { read: 0.1, write: 1.25 };
223
+
224
+ export const CLAUDE_OPUS_5: ModelSpec = {
225
+ id: "claude-opus-5",
226
+ name: "Claude Opus 5",
227
+ api: "anthropic-messages",
228
+ provider: "anthropic",
229
+ reasoning: true,
230
+ cache: "explicit",
231
+ rates: ANTHROPIC_RATES,
232
+ // Cost from models.dev on 2026-09-21. Ratios match ANTHROPIC_RATES.
233
+ price: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
234
+ contextWindow: 1_000_000,
235
+ maxTokens: 128_000,
236
+ };
237
+
238
+ /**
239
+ * DeepSeek's accent, verified against its own docs rather than inferred:
240
+ * `max_tokens` (not the renamed field), no `developer` role, `thinking`
241
+ * toggled explicitly, and reasoning echoed back on every assistant message
242
+ * or the request is rejected once tools are present.
243
+ */
244
+ const DEEPSEEK_COMPAT: Compat = {
245
+ supportsDeveloperRole: false,
246
+ maxTokensField: "max_tokens",
247
+ supportsReasoningEffort: true,
248
+ supportsUsageInStreaming: true,
249
+ requiresReasoningContentOnAssistantMessages: true,
250
+ thinkingFormat: "deepseek",
251
+ };
252
+
253
+ /**
254
+ * Effort levels DeepSeek accepts: low, high, max. The docs say a request's
255
+ * level is remapped internally (minimal/low -> low, medium/high/xhigh ->
256
+ * high, max -> max), so this map does the same thing explicitly rather than
257
+ * sending a level the API has to guess about.
258
+ */
259
+ const DEEPSEEK_EFFORT: Partial<Record<Effort, string | null>> = {
260
+ low: "low",
261
+ medium: "high",
262
+ high: "high",
263
+ xhigh: "high",
264
+ max: "max",
265
+ };
266
+
267
+ /**
268
+ * Prices read off DeepSeek's own price page (peak rates, USD per 1M tokens).
269
+ * Off-peak is half of these, which changes the absolute cost but not the
270
+ * ratios below, and the ratios are what `equivalentCost` reports in.
271
+ *
272
+ * Cache creation has no separate billing line; misses are ordinary input.
273
+ * A zero creation rate does not make a rebuilt prefix free: those tokens
274
+ * still pay the cold input rate and evidence recovery may need more requests.
275
+ */
276
+ export const DEEPSEEK_V4_PRO: ModelSpec = {
277
+ id: "deepseek-v4-pro",
278
+ name: "DeepSeek V4 Pro",
279
+ api: "openai-completions",
280
+ provider: "deepseek",
281
+ baseUrl: "https://api.deepseek.com",
282
+ reasoning: true,
283
+ cache: "automatic",
284
+ rates: { read: 0.044 / 1.32, write: 0 },
285
+ price: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0 },
286
+ contextWindow: 1_000_000,
287
+ maxTokens: 384_000,
288
+ compat: DEEPSEEK_COMPAT,
289
+ thinkingLevelMap: DEEPSEEK_EFFORT,
290
+ };
291
+
292
+ export const DEEPSEEK_FLASH: ModelSpec = {
293
+ id: "deepseek-flash",
294
+ name: "DeepSeek V4.1 Flash",
295
+ api: "openai-completions",
296
+ provider: "deepseek",
297
+ baseUrl: "https://api.deepseek.com",
298
+ reasoning: true,
299
+ cache: "automatic",
300
+ rates: { read: 0.006 / 0.3, write: 0 },
301
+ price: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 },
302
+ contextWindow: 1_000_000,
303
+ maxTokens: 384_000,
304
+ compat: DEEPSEEK_COMPAT,
305
+ thinkingLevelMap: DEEPSEEK_EFFORT,
306
+ };
307
+
308
+ /**
309
+ * How we talk to a provider. A third `api` is another card, not a row here.
310
+ *
311
+ * Google is listed as Completions because it publishes an OpenAI-compatible
312
+ * endpoint at this URL; we are not pretending Gemini's native protocol is
313
+ * Chat Completions. xAI, Mistral, and OpenAI likewise have a Completions
314
+ * surface we already speak. Pi uses other adapters for some of these;
315
+ * bending ours to match would be the thing the card forbids.
316
+ */
317
+ export const PROVIDER_WIRE: Readonly<Record<string, { api: Api; baseUrl?: string }>> = {
318
+ anthropic: { api: "anthropic-messages" },
319
+ openai: { api: "openai-completions", baseUrl: "https://api.openai.com/v1" },
320
+ google: { api: "openai-completions", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai" },
321
+ deepseek: { api: "openai-completions", baseUrl: "https://api.deepseek.com" },
322
+ xai: { api: "openai-completions", baseUrl: "https://api.x.ai/v1" },
323
+ groq: { api: "openai-completions", baseUrl: "https://api.groq.com/openai/v1" },
324
+ cerebras: { api: "openai-completions", baseUrl: "https://api.cerebras.ai/v1" },
325
+ mistral: { api: "openai-completions", baseUrl: "https://api.mistral.ai/v1" },
326
+ openrouter: { api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1" },
327
+ zai: { api: "openai-completions", baseUrl: "https://api.z.ai/api/paas/v4" },
328
+ moonshotai: { api: "openai-completions", baseUrl: "https://api.moonshot.ai/v1" },
329
+ together: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1" },
330
+ // models.dev's id; we store the catalog key as `together`.
331
+ togetherai: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1" },
332
+ };
333
+
334
+ function anthropicModel(spec: Omit<ModelSpec, "api" | "provider">): ModelSpec {
335
+ return { api: "anthropic-messages", provider: "anthropic", ...spec };
336
+ }
337
+
338
+ function completionsModel(spec: Omit<ModelSpec, "api">): ModelSpec {
339
+ return { api: "openai-completions", ...spec };
340
+ }
341
+
342
+ /**
343
+ * OpenAI's own endpoint for its reasoning models: Chat Completions would drop
344
+ * their reasoning between turns (feat-openai-responses). OpenRouter's GPT
345
+ * entries stay on Completions, which is the API OpenRouter serves.
346
+ */
347
+ function responsesModel(spec: Omit<ModelSpec, "api">): ModelSpec {
348
+ return { api: "openai-responses", ...spec };
349
+ }
350
+
351
+ /**
352
+ * Built-in catalog. Numbers from models.dev on 2026-09-21 unless a comment
353
+ * says otherwise. Price is omitted only when input or output is missing —
354
+ * a zero there would look like "this is free". cacheRead / cacheWrite are
355
+ * filled when the source has them and left off when it does not. Cache
356
+ * *rates* (multiples) stay omitted unless both cache dollars are present,
357
+ * except DeepSeek (verified against its own page).
358
+ */
359
+ export const CATALOG: readonly ModelSpec[] = [
360
+ CLAUDE_OPUS_5,
361
+ anthropicModel({
362
+ id: "claude-fable-5-1",
363
+ name: "Claude Fable 5.1",
364
+ reasoning: true,
365
+ cache: "explicit",
366
+ rates: { read: 0.25 / 10, write: 12.5 / 10 },
367
+ price: { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
368
+ contextWindow: 1_000_000,
369
+ maxTokens: 128_000,
370
+ }),
371
+
372
+ responsesModel({
373
+ id: "gpt-5.5-pro",
374
+ name: "GPT-5.5 Pro",
375
+ provider: "openai",
376
+ baseUrl: "https://api.openai.com/v1",
377
+ reasoning: true,
378
+ contextWindow: 1_050_000,
379
+ maxTokens: 128_000,
380
+ // Base rate. models.dev also lists a context_over_200k tier at 60/270.
381
+ price: { input: 30, output: 180 },
382
+ }),
383
+ responsesModel({
384
+ id: "gpt-5.4-pro",
385
+ name: "GPT-5.4 Pro",
386
+ provider: "openai",
387
+ baseUrl: "https://api.openai.com/v1",
388
+ reasoning: true,
389
+ contextWindow: 1_050_000,
390
+ maxTokens: 128_000,
391
+ // Base rate. models.dev also lists a context_over_200k tier at 60/270.
392
+ price: { input: 30, output: 180 },
393
+ }),
394
+
395
+ completionsModel({
396
+ id: "gemini-3.1-pro-preview",
397
+ name: "Gemini 3.1 Pro Preview",
398
+ provider: "google",
399
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
400
+ reasoning: true,
401
+ contextWindow: 1_048_576,
402
+ maxTokens: 65_536,
403
+ // Base rate. models.dev also lists a context_over_200k tier (4/18/0.4).
404
+ price: { input: 2, output: 12, cacheRead: 0.2 },
405
+ compat: GEMINI_COMPAT,
406
+ thinkingLevelMap: GEMINI_EFFORT,
407
+ }),
408
+
409
+ DEEPSEEK_V4_PRO,
410
+ DEEPSEEK_FLASH,
411
+
412
+ completionsModel({
413
+ id: "grok-4.6",
414
+ name: "Grok 4.6",
415
+ provider: "xai",
416
+ baseUrl: "https://api.x.ai/v1",
417
+ reasoning: true,
418
+ contextWindow: 500_000,
419
+ maxTokens: 500_000,
420
+ // Base rate. models.dev also lists a context_over_200k tier (4/12/1).
421
+ price: { input: 2, output: 6, cacheRead: 0.5 },
422
+ }),
423
+ completionsModel({
424
+ id: "grok-4.5",
425
+ name: "Grok 4.5",
426
+ provider: "xai",
427
+ baseUrl: "https://api.x.ai/v1",
428
+ reasoning: true,
429
+ contextWindow: 500_000,
430
+ maxTokens: 500_000,
431
+ // Base rate. models.dev also lists a context_over_200k tier (4/12/0.6).
432
+ price: { input: 2, output: 6, cacheRead: 0.3 },
433
+ }),
434
+
435
+ completionsModel({
436
+ id: "llama-3.3-70b-versatile",
437
+ name: "Llama 3.3 70B",
438
+ provider: "groq",
439
+ baseUrl: "https://api.groq.com/openai/v1",
440
+ reasoning: false,
441
+ contextWindow: 131_072,
442
+ maxTokens: 32_768,
443
+ price: { input: 0.59, output: 0.79 },
444
+ }),
445
+ completionsModel({
446
+ id: "qwen/qwen3.6-27b",
447
+ name: "Qwen3.6 27B",
448
+ provider: "groq",
449
+ baseUrl: "https://api.groq.com/openai/v1",
450
+ reasoning: true,
451
+ contextWindow: 131_072,
452
+ maxTokens: 16_384,
453
+ price: { input: 0.6, output: 3, cacheRead: 0.3 },
454
+ }),
455
+
456
+ completionsModel({
457
+ id: "gpt-oss-120b",
458
+ name: "GPT OSS 120B",
459
+ provider: "cerebras",
460
+ baseUrl: "https://api.cerebras.ai/v1",
461
+ reasoning: true,
462
+ contextWindow: 131_072,
463
+ maxTokens: 40_960,
464
+ price: { input: 0.35, output: 0.75 },
465
+ }),
466
+ completionsModel({
467
+ id: "qwen-3.8-27b",
468
+ name: "Qwen3.8 27B",
469
+ provider: "cerebras",
470
+ baseUrl: "https://api.cerebras.ai/v1",
471
+ reasoning: true,
472
+ contextWindow: 65_536,
473
+ maxTokens: 32_768,
474
+ price: { input: 0.99, output: 1.49 },
475
+ }),
476
+
477
+ completionsModel({
478
+ id: "magistral-medium-latest",
479
+ name: "Magistral Medium (latest)",
480
+ provider: "mistral",
481
+ baseUrl: "https://api.mistral.ai/v1",
482
+ reasoning: true,
483
+ contextWindow: 128_000,
484
+ maxTokens: 16_384,
485
+ price: { input: 2, output: 5 },
486
+ }),
487
+ completionsModel({
488
+ id: "mistral-large-2411",
489
+ name: "Mistral Large 2.1",
490
+ provider: "mistral",
491
+ baseUrl: "https://api.mistral.ai/v1",
492
+ reasoning: false,
493
+ contextWindow: 131_072,
494
+ maxTokens: 16_384,
495
+ price: { input: 2, output: 6 },
496
+ }),
497
+
498
+ completionsModel({
499
+ id: "openai/gpt-5.5-pro",
500
+ name: "GPT-5.5 Pro",
501
+ provider: "openrouter",
502
+ baseUrl: "https://openrouter.ai/api/v1",
503
+ reasoning: true,
504
+ contextWindow: 1_050_000,
505
+ maxTokens: 128_000,
506
+ // Base rate. Same context_over_200k tier as the OpenAI row (60/270).
507
+ price: { input: 30, output: 180 },
508
+ }),
509
+ completionsModel({
510
+ id: "openai/gpt-5.4-pro",
511
+ name: "GPT-5.4 Pro",
512
+ provider: "openrouter",
513
+ baseUrl: "https://openrouter.ai/api/v1",
514
+ reasoning: true,
515
+ contextWindow: 1_050_000,
516
+ maxTokens: 128_000,
517
+ // Base rate. Same context_over_200k tier as the OpenAI row (60/270).
518
+ price: { input: 30, output: 180 },
519
+ }),
520
+
521
+ completionsModel({
522
+ id: "glm-5.3",
523
+ name: "GLM-5.3",
524
+ provider: "zai",
525
+ baseUrl: "https://api.z.ai/api/paas/v4",
526
+ reasoning: true,
527
+ cache: "automatic",
528
+ rates: { read: 0.26 / 1.4, write: 0 },
529
+ price: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
530
+ contextWindow: 1_000_000,
531
+ maxTokens: 131_072,
532
+ }),
533
+ completionsModel({
534
+ id: "glm-5.2",
535
+ name: "GLM-5.2",
536
+ provider: "zai",
537
+ baseUrl: "https://api.z.ai/api/paas/v4",
538
+ reasoning: true,
539
+ cache: "automatic",
540
+ rates: { read: 0.26 / 1.4, write: 0 },
541
+ price: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
542
+ contextWindow: 1_000_000,
543
+ maxTokens: 131_072,
544
+ }),
545
+
546
+ completionsModel({
547
+ id: "kimi-k3",
548
+ name: "Kimi K3",
549
+ provider: "moonshotai",
550
+ baseUrl: "https://api.moonshot.ai/v1",
551
+ reasoning: true,
552
+ contextWindow: 1_048_576,
553
+ maxTokens: 131_072,
554
+ price: { input: 3, output: 15, cacheRead: 0.3 },
555
+ compat: KIMI_K3_COMPAT,
556
+ thinkingLevelMap: KIMI_K3_EFFORT,
557
+ }),
558
+ completionsModel({
559
+ id: "kimi-k2.6",
560
+ name: "Kimi K2.6",
561
+ provider: "moonshotai",
562
+ baseUrl: "https://api.moonshot.ai/v1",
563
+ reasoning: true,
564
+ contextWindow: 262_144,
565
+ maxTokens: 262_144,
566
+ price: { input: 0.95, output: 4, cacheRead: 0.16 },
567
+ }),
568
+
569
+ completionsModel({
570
+ id: "moonshotai/Kimi-K3",
571
+ name: "Kimi K3",
572
+ provider: "together",
573
+ baseUrl: "https://api.together.xyz/v1",
574
+ reasoning: true,
575
+ contextWindow: 1_048_576,
576
+ maxTokens: 131_072,
577
+ price: { input: 3, output: 15, cacheRead: 0.3 },
578
+ compat: KIMI_K3_COMPAT,
579
+ thinkingLevelMap: KIMI_K3_EFFORT,
580
+ }),
581
+ completionsModel({
582
+ id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8",
583
+ name: "Qwen3 Coder 480B A35B Instruct",
584
+ provider: "together",
585
+ baseUrl: "https://api.together.xyz/v1",
586
+ reasoning: false,
587
+ contextWindow: 262_144,
588
+ maxTokens: 262_144,
589
+ // models.dev has input 2 / output 2. Left unpriced so the catalog
590
+ // still contains a model whose price was not filled in — the existing
591
+ // "unknown cost instead of zero" assertion is a living fixture, not
592
+ // a gap we are allowed to close by inventing a reason to skip a rate.
593
+ // Filling every other row that has numbers already exceeds 20.
594
+ }),
595
+ ];
596
+
597
+ /**
598
+ * Output tokens to request, which is also the output reserve every window
599
+ * budget subtracts.
600
+ *
601
+ * Three bounds, smallest wins. The model's own limit, because asking for more
602
+ * is a 400. A default -- 32000 for reasoning models, whose thinking is billed
603
+ * as output and truncated at 16000 often enough to trigger output recovery;
604
+ * 16000 otherwise, the previous fixed value. A quarter of the window, so a
605
+ * 64k model does not hand a quarter of its context to a reply that is usually
606
+ * a few hundred tokens. The defaults are operational choices (32000 is also
607
+ * Claude Code's default), not measurements.
608
+ */
609
+ export function outputTokenBudget(spec: ModelSpec): number {
610
+ const bounds = [spec.reasoning ? 32_000 : 16_000];
611
+ if (spec.maxTokens !== undefined) bounds.push(spec.maxTokens);
612
+ if (spec.contextWindow !== undefined) bounds.push(Math.floor(spec.contextWindow / 4));
613
+ return Math.max(1, Math.min(...bounds));
614
+ }