zidane 5.13.26 → 5.14.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 (145) hide show
  1. package/dist/{acp-CjAoILwj.js → acp-DgKEJH3P.js} +7 -7
  2. package/dist/{acp-CjAoILwj.js.map → acp-DgKEJH3P.js.map} +1 -1
  3. package/dist/acp-cli.js +11 -6
  4. package/dist/acp-cli.js.map +1 -1
  5. package/dist/acp.d.ts +2 -2
  6. package/dist/acp.js +1 -1
  7. package/dist/{tools-B32lZXLE.js → agent-B-cKanSC.js} +8035 -9459
  8. package/dist/agent-B-cKanSC.js.map +1 -0
  9. package/dist/{agent-DQIL5BFk.d.ts → agent-BAgOgS37.d.ts} +367 -208
  10. package/dist/agent-BAgOgS37.d.ts.map +1 -0
  11. package/dist/agent.d.ts +3 -0
  12. package/dist/agent.js +5 -0
  13. package/dist/anthropic-RGqsJlC8.js +807 -0
  14. package/dist/anthropic-RGqsJlC8.js.map +1 -0
  15. package/dist/{atomic-write-Bgtr5JPu.js → atomic-write-B7zr1ddF.js} +2 -2
  16. package/dist/{atomic-write-Bgtr5JPu.js.map → atomic-write-B7zr1ddF.js.map} +1 -1
  17. package/dist/{auth-DPrSP1YV.js → auth-C-ms5N2x.js} +33 -5
  18. package/dist/{auth-DPrSP1YV.js.map → auth-C-ms5N2x.js.map} +1 -1
  19. package/dist/cache-telemetry-BKAwsNxm.js +169 -0
  20. package/dist/cache-telemetry-BKAwsNxm.js.map +1 -0
  21. package/dist/chat/pure.d.ts +3 -3
  22. package/dist/chat/pure.js +1 -1
  23. package/dist/chat.d.ts +21 -7
  24. package/dist/chat.d.ts.map +1 -1
  25. package/dist/chat.js +30 -5
  26. package/dist/chat.js.map +1 -1
  27. package/dist/contexts/daytona.d.ts +1 -1
  28. package/dist/contexts/docker.js +0 -1
  29. package/dist/contexts/docker.js.map +1 -1
  30. package/dist/contexts/e2b.d.ts +1 -1
  31. package/dist/errors-CkTvg97v.d.ts +203 -0
  32. package/dist/errors-CkTvg97v.d.ts.map +1 -0
  33. package/dist/errors.d.ts +3 -0
  34. package/dist/errors.js +3 -0
  35. package/dist/eval.d.ts +1 -1
  36. package/dist/eval.js +2 -2
  37. package/dist/headless.d.ts +1 -1
  38. package/dist/headless.js +406 -1
  39. package/dist/headless.js.map +1 -0
  40. package/dist/{index-YY3vp2F2.d.ts → index-CLuLFFLa.d.ts} +88 -418
  41. package/dist/index-CLuLFFLa.d.ts.map +1 -0
  42. package/dist/{index-C5qw3pj7.d.ts → index-CgBIl2mX.d.ts} +2 -2
  43. package/dist/{index-C5qw3pj7.d.ts.map → index-CgBIl2mX.d.ts.map} +1 -1
  44. package/dist/index.d.ts +9 -5
  45. package/dist/index.js +113 -167
  46. package/dist/index.js.map +1 -1
  47. package/dist/{logger-C2ngE8CY.d.ts → logger-CJ-lpKPy.d.ts} +2 -2
  48. package/dist/{logger-C2ngE8CY.d.ts.map → logger-CJ-lpKPy.d.ts.map} +1 -1
  49. package/dist/mcp.d.ts +1 -1
  50. package/dist/messages-YZQLKaz2.js +1157 -0
  51. package/dist/messages-YZQLKaz2.js.map +1 -0
  52. package/dist/oauth-8LqbeI_Q.js +292 -0
  53. package/dist/oauth-8LqbeI_Q.js.map +1 -0
  54. package/dist/{messages-Iy_vEycX.js → openai-compat-0Y7jcncn.js} +27 -1379
  55. package/dist/openai-compat-0Y7jcncn.js.map +1 -0
  56. package/dist/output/stream-json.d.ts +2 -2
  57. package/dist/output/stream-json.js +1 -1
  58. package/dist/output/terminal.d.ts +2 -2
  59. package/dist/{presets-B9nDwmaz.js → presets-Pr02ZNcy.js} +3 -2
  60. package/dist/{presets-B9nDwmaz.js.map → presets-Pr02ZNcy.js.map} +1 -1
  61. package/dist/presets.d.ts +2 -2
  62. package/dist/presets.js +1 -1
  63. package/dist/prompt.d.ts +2 -0
  64. package/dist/prompt.js +210 -0
  65. package/dist/prompt.js.map +1 -0
  66. package/dist/providers/anthropic.d.ts +2 -0
  67. package/dist/providers/anthropic.js +2 -0
  68. package/dist/providers/arcee.d.ts +2 -0
  69. package/dist/providers/arcee.js +42 -0
  70. package/dist/providers/arcee.js.map +1 -0
  71. package/dist/providers/baseten.d.ts +2 -0
  72. package/dist/providers/baseten.js +66 -0
  73. package/dist/providers/baseten.js.map +1 -0
  74. package/dist/providers/cerebras.d.ts +2 -0
  75. package/dist/providers/cerebras.js +31 -0
  76. package/dist/providers/cerebras.js.map +1 -0
  77. package/dist/providers/local.d.ts +2 -0
  78. package/dist/providers/local.js +44 -0
  79. package/dist/providers/local.js.map +1 -0
  80. package/dist/providers/openai-compat.d.ts +2 -0
  81. package/dist/providers/openai-compat.js +2 -0
  82. package/dist/providers/openai.d.ts +2 -0
  83. package/dist/providers/openai.js +504 -0
  84. package/dist/providers/openai.js.map +1 -0
  85. package/dist/providers/openrouter.d.ts +2 -0
  86. package/dist/providers/openrouter.js +52 -0
  87. package/dist/providers/openrouter.js.map +1 -0
  88. package/dist/providers/xai.d.ts +2 -0
  89. package/dist/providers/xai.js +2 -0
  90. package/dist/providers-DgmdYGKm.js +168 -0
  91. package/dist/providers-DgmdYGKm.js.map +1 -0
  92. package/dist/providers.d.ts +1 -1
  93. package/dist/providers.js +10 -2
  94. package/dist/restate.d.ts +1 -1
  95. package/dist/run-summary-DAnBZnrt.d.ts +249 -0
  96. package/dist/run-summary-DAnBZnrt.d.ts.map +1 -0
  97. package/dist/run-summary-nD3IQ4ZQ.js +253 -0
  98. package/dist/run-summary-nD3IQ4ZQ.js.map +1 -0
  99. package/dist/session/sqlite.d.ts +1 -1
  100. package/dist/{session-CiA1_8SB.js → session-CqrAFAKJ.js} +2 -2
  101. package/dist/{session-CiA1_8SB.js.map → session-CqrAFAKJ.js.map} +1 -1
  102. package/dist/session.d.ts +1 -1
  103. package/dist/session.js +2 -2
  104. package/dist/skills.d.ts +2 -2
  105. package/dist/system-prompt-BlsdjcUi.d.ts +139 -0
  106. package/dist/system-prompt-BlsdjcUi.d.ts.map +1 -0
  107. package/dist/timeout-CpFm0jJ6.d.ts +10 -0
  108. package/dist/timeout-CpFm0jJ6.d.ts.map +1 -0
  109. package/dist/timeout-kGGSXagK.js +84 -0
  110. package/dist/timeout-kGGSXagK.js.map +1 -0
  111. package/dist/{tool-formatters-w5oaoCS4.d.ts → tool-formatters-Ch8oRpYR.d.ts} +2 -2
  112. package/dist/{tool-formatters-w5oaoCS4.d.ts.map → tool-formatters-Ch8oRpYR.d.ts.map} +1 -1
  113. package/dist/tools/fetch-url.d.ts +1 -1
  114. package/dist/tools/web-search.d.ts +1 -1
  115. package/dist/tools-00_Nqazv.js +1689 -0
  116. package/dist/tools-00_Nqazv.js.map +1 -0
  117. package/dist/tools.d.ts +2 -2
  118. package/dist/tools.js +2 -1
  119. package/dist/{transcript-anchors-BUtOd-qa.js → transcript-anchors-BUmvPnis.js} +10 -9
  120. package/dist/{transcript-anchors-BUtOd-qa.js.map → transcript-anchors-BUmvPnis.js.map} +1 -1
  121. package/dist/{transcript-anchors-BJ-ESWnm.d.ts → transcript-anchors-CL9UM_M4.d.ts} +4 -4
  122. package/dist/{transcript-anchors-BJ-ESWnm.d.ts.map → transcript-anchors-CL9UM_M4.d.ts.map} +1 -1
  123. package/dist/tui.d.ts +3 -3
  124. package/dist/tui.js +38 -13
  125. package/dist/tui.js.map +1 -1
  126. package/dist/{turn-operations-BSSQHNq1.d.ts → turn-operations-Cj6v_ypW.d.ts} +3 -3
  127. package/dist/{turn-operations-BSSQHNq1.d.ts.map → turn-operations-Cj6v_ypW.d.ts.map} +1 -1
  128. package/dist/{turn-operations-B_XeuNp-.js → turn-operations-tG0vuBNc.js} +2 -2
  129. package/dist/{turn-operations-B_XeuNp-.js.map → turn-operations-tG0vuBNc.js.map} +1 -1
  130. package/dist/types-CyVGdbia.js.map +1 -1
  131. package/dist/types.d.ts +4 -3
  132. package/dist/xai-tPdbAGkq.js +379 -0
  133. package/dist/xai-tPdbAGkq.js.map +1 -0
  134. package/docs/ARCHITECTURE.md +4 -2
  135. package/docs/CHAT.md +4 -0
  136. package/docs/SKILL.md +31 -5
  137. package/package.json +63 -1
  138. package/dist/agent-DQIL5BFk.d.ts.map +0 -1
  139. package/dist/headless-2SH7dvoW.js +0 -656
  140. package/dist/headless-2SH7dvoW.js.map +0 -1
  141. package/dist/index-YY3vp2F2.d.ts.map +0 -1
  142. package/dist/messages-Iy_vEycX.js.map +0 -1
  143. package/dist/providers-BMw1D_lj.js +0 -2332
  144. package/dist/providers-BMw1D_lj.js.map +0 -1
  145. package/dist/tools-B32lZXLE.js.map +0 -1
@@ -1,2332 +0,0 @@
1
- import { t as lazyAsync } from "./lazy-DLOurOC_.js";
2
- import { i as documentBlockMarker, n as assertResolvedMediaBlock } from "./types-CyVGdbia.js";
3
- import { c as classifyErrorPrelude, d as isRetryableHttpStatus, f as matchesContextExceeded, l as errorMessage, p as matchesToolPairingError } from "./errors-DJUxZg9b.js";
4
- import { L as unsupportedMediaError, N as splitSystemPrompt, S as userMessage, T as fillEstimatedCost, _ as assistantMessage, b as openaiCompat, j as renderSystemForWire, l as fromAnthropic, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, p as toAnthropic, w as sanitizeToolSpecs, x as toolResultsMessage } from "./messages-Iy_vEycX.js";
5
- import { t as writeFileAtomic } from "./atomic-write-Bgtr5JPu.js";
6
- import { resolve } from "node:path";
7
- import { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
- import { Buffer } from "node:buffer";
9
- import { getModel } from "@earendil-works/pi-ai";
10
- import { homedir } from "node:os";
11
- import { createServer } from "node:http";
12
- import { streamOpenAICodexResponses } from "@earendil-works/pi-ai/openai-codex-responses";
13
- //#region src/chat/anthropic-models.ts
14
- /**
15
- * Anthropic "fast mode" — `speed: "fast"` on the Messages API. ~2.5× output
16
- * tokens/sec at a per-model price multiplier. Supported on Opus 4.6 / 4.7 / 4.8
17
- * only; the API errors if sent on any other model, so it's attached just to
18
- * those entries (see {@link FAST_MODE_OPTIONS}).
19
- *
20
- * The multiplier scales ALL rate components (input, output, and both cache
21
- * rates) uniformly: per Anthropic, "prompt caching multipliers apply on top of
22
- * fast mode pricing" across the full context window. Opus 4.8 is 2× ($5/$25 →
23
- * $10/$50); Opus 4.6 / 4.7 are a steeper 6× ($5/$25 → $30/$150).
24
- * See https://platform.claude.com/docs/en/build-with-claude/fast-mode.
25
- */
26
- const FAST_MODE_OPTION = {
27
- id: "fast",
28
- label: "Fast mode",
29
- description: "~2.5× faster output at premium pricing",
30
- costMultiplier: {
31
- input: 2,
32
- output: 2,
33
- cacheRead: 2,
34
- cacheWrite: 2
35
- }
36
- };
37
- const FAST_MODE_OPTION_LEGACY = {
38
- ...FAST_MODE_OPTION,
39
- description: "~2.5× faster output at 6× pricing",
40
- costMultiplier: {
41
- input: 6,
42
- output: 6,
43
- cacheRead: 6,
44
- cacheWrite: 6
45
- }
46
- };
47
- /** Model ids that accept `speed: "fast"`, mapped to their fast-mode option. */
48
- const FAST_MODE_OPTIONS = {
49
- "claude-opus-4-8": FAST_MODE_OPTION,
50
- "claude-opus-4-7": FAST_MODE_OPTION_LEGACY,
51
- "claude-opus-4-6": FAST_MODE_OPTION_LEGACY
52
- };
53
- /**
54
- * The price multiplier to apply to a model's base rates when fast mode is
55
- * active, or `undefined` when the model has no fast-mode pricing (i.e. doesn't
56
- * support it). Mirrors {@link FAST_MODE_OPTIONS} — kept as a separate accessor
57
- * so the cost layer can stay decoupled from the picker-facing option objects.
58
- */
59
- function fastModeCostMultiplier(modelId) {
60
- return FAST_MODE_OPTIONS[modelId]?.costMultiplier;
61
- }
62
- /**
63
- * Claude Fable 5 — GA 2026-06-09. Anthropic's most capable widely released
64
- * model: $10/$50 per MTok, 1M context, 128k output, adaptive thinking
65
- * (always on), vision. No fast-mode support (Opus 4.6/4.7/4.8 only).
66
- *
67
- * Claude Opus 4.8 — shipped 2026-05-28. Same price/context/output as Opus 4.7
68
- * ($5/$25 per MTok, 1M context, 128k output), adaptive thinking, vision.
69
- * See https://www.anthropic.com/news/claude-opus-4-8.
70
- *
71
- * Claude Mythos 5 is deliberately absent: it's invitation-only (Project
72
- * Glasswing), so it would be a dead picker entry for virtually all users.
73
- *
74
- * See https://platform.claude.com/docs/en/about-claude/models/overview.
75
- */
76
- const ANTHROPIC_EXTRA_MODELS = [{
77
- id: "claude-fable-5",
78
- name: "Claude Fable 5",
79
- reasoning: true,
80
- input: ["text", "image"],
81
- cost: {
82
- input: 10,
83
- output: 50,
84
- cacheRead: 1,
85
- cacheWrite: 12.5
86
- },
87
- contextWindow: 1e6,
88
- maxTokens: 128e3,
89
- provider: "anthropic"
90
- }, {
91
- id: "claude-opus-4-8",
92
- name: "Claude Opus 4.8",
93
- reasoning: true,
94
- input: ["text", "image"],
95
- cost: {
96
- input: 5,
97
- output: 25,
98
- cacheRead: .5,
99
- cacheWrite: 6.25
100
- },
101
- contextWindow: 1e6,
102
- maxTokens: 128e3,
103
- provider: "anthropic",
104
- options: [FAST_MODE_OPTION]
105
- }];
106
- //#endregion
107
- //#region src/providers/oauth.ts
108
- const loadGetOAuthApiKey = lazyAsync(async () => (await import("@earendil-works/pi-ai/oauth")).getOAuthApiKey);
109
- /**
110
- * Resolve the creds-file path at call time rather than module-load time, so
111
- * env changes between import and first OAuth use (test suites, multi-project
112
- * CLIs) are honored.
113
- *
114
- * Order:
115
- * 1. `ZIDANE_CREDENTIALS_PATH` — explicit override. The TUI sets this at
116
- * launch to point at `<storageDir>/credentials.json` so the harness
117
- * providers find the same file the TUI manages.
118
- * 2. `~/.credentials.json` — home-anchored default.
119
- *
120
- * The historic `cwd/.credentials.json` fallback was deliberately dropped:
121
- * launching zidane inside an untrusted checkout would let that repo plant a
122
- * `.credentials.json` and become the credential source (and receive rotated
123
- * refresh tokens on the next refresh persist). Same trust model as the TUI's
124
- * legacy-file migration in `chat/credentials.ts`, which only honors the
125
- * home-anchored location. The only "trusted" cwd under that model is `$HOME`
126
- * itself — where the cwd-relative path resolves to `~/.credentials.json`
127
- * anyway, so anchoring at home preserves the legitimate legacy case.
128
- */
129
- function credentialsFilePath() {
130
- return process.env.ZIDANE_CREDENTIALS_PATH ?? resolve(homedir(), ".credentials.json");
131
- }
132
- /**
133
- * POSIX mode for credentials on disk. Read+write for the owner only — refresh
134
- * tokens and API keys leak if the file is world-readable on a shared box.
135
- * Ignored on Windows.
136
- */
137
- const CREDENTIALS_FILE_MODE = 384;
138
- /**
139
- * In-process mutex table keyed by provider id. Concurrent `resolveOAuthApiKey`
140
- * calls for the same provider (typical when a host fans out multiple streams
141
- * at once) coalesce onto a single refresh. Without this, N concurrent calls
142
- * each hit `getOAuthApiKey`, producing N writes to `.credentials.json` with
143
- * last-write-wins semantics — and, in worst-case interleaving, a corrupted
144
- * file.
145
- */
146
- const refreshLocks = /* @__PURE__ */ new Map();
147
- /**
148
- * Pull a complete `{ access, refresh, expires }` triple out of provider params
149
- * when (and only when) all three OAuth fields are present and well-typed.
150
- * Returns `undefined` for the static-api-key path. Shared by the OAuth-capable
151
- * providers so the runtime-credential guard can't drift between them.
152
- */
153
- /**
154
- * Whether a credential string is an OAuth *access token* rather than a real,
155
- * static API key. Anthropic OAuth tokens are prefixed `sk-ant-oat`; OpenAI
156
- * Codex OAuth access tokens are JWTs (`eyJ...`). Used to decide whether an
157
- * env-provided value can be refreshed (OAuth) or must be sent verbatim (API key).
158
- */
159
- function isOAuthAccessToken(value) {
160
- return value.startsWith("sk-ant-oat") || isJwtLikeAccessToken(value);
161
- }
162
- function isJwtLikeAccessToken(value) {
163
- const parts = value.split(".");
164
- if (parts.length !== 3 || !parts[0]?.startsWith("eyJ")) return false;
165
- try {
166
- const header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
167
- return Boolean(header && typeof header === "object" && "alg" in header);
168
- } catch {
169
- return false;
170
- }
171
- }
172
- function extractRuntimeCredentials(params) {
173
- if (typeof params?.access === "string" && typeof params.refresh === "string" && typeof params.expires === "number") return {
174
- access: params.access,
175
- refresh: params.refresh,
176
- expires: params.expires
177
- };
178
- }
179
- /**
180
- * Read the shared credentials file.
181
- *
182
- * Returns `{}` when the file is missing OR when the file exists but is corrupted
183
- * / not valid JSON. Corrupt-file tolerance is important: the refresh-then-persist
184
- * path below writes atomically, but older builds wrote in place and could have
185
- * truncated the file on a crash. Treating corruption as "no stored creds" lets
186
- * users re-auth without manually deleting the file.
187
- *
188
- * Supports two on-disk shapes:
189
- * 1. **Legacy** (untagged): `{ "anthropic": { access, refresh, expires, ... } }`
190
- * — written by older `bun run auth`.
191
- * 2. **New** (kind-tagged): `{ "anthropic": { kind: 'oauth', access, refresh, expires, ... } }`
192
- * — written by the TUI. Entries with `kind: 'apikey'` are skipped — those
193
- * reach providers via env vars set at TUI launch (see `applyApiKeyEnv`).
194
- */
195
- function readOAuthCredentials() {
196
- const path = credentialsFilePath();
197
- if (!existsSync(path)) return {};
198
- try {
199
- const raw = readFileSync(path, "utf-8");
200
- const parsed = JSON.parse(raw);
201
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
202
- const result = {};
203
- for (const [key, value] of Object.entries(parsed)) {
204
- if (!value || typeof value !== "object" || Array.isArray(value)) continue;
205
- const v = value;
206
- if (v.kind === "apikey") continue;
207
- if (typeof v.access === "string" && typeof v.refresh === "string" && typeof v.expires === "number") result[key] = {
208
- ...v,
209
- access: v.access,
210
- refresh: v.refresh,
211
- expires: v.expires
212
- };
213
- }
214
- return result;
215
- } catch {
216
- return {};
217
- }
218
- }
219
- /**
220
- * Cross-process lockfile parameters for the credentials read-merge-write.
221
- * A holder normally completes in well under a millisecond (read + JSON merge
222
- * + atomic write of a tiny file), so the stale threshold is generous and the
223
- * total wait short — on timeout we proceed without the lock rather than fail
224
- * the refresh (degrades to the historic last-write-wins behavior).
225
- */
226
- const CREDENTIALS_LOCK_STALE_MS = 5e3;
227
- const CREDENTIALS_LOCK_RETRY_MS = 20;
228
- const CREDENTIALS_LOCK_MAX_WAIT_MS = 2e3;
229
- /** Synchronous sleep without spinning — works on Bun and Node main threads. */
230
- function sleepSync(ms) {
231
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
232
- }
233
- /**
234
- * Best-effort exclusive lock via `O_EXCL` create of `<file>.lock`. Returns
235
- * `true` when the lock was acquired (caller must release), `false` on
236
- * timeout (caller proceeds unlocked — see threshold rationale above). Locks
237
- * older than {@link CREDENTIALS_LOCK_STALE_MS} are treated as leftovers from
238
- * a crashed process and broken.
239
- */
240
- function acquireCredentialsLock(lockPath) {
241
- const deadline = Date.now() + CREDENTIALS_LOCK_MAX_WAIT_MS;
242
- while (true) try {
243
- writeFileSync(lockPath, String(process.pid), {
244
- flag: "wx",
245
- mode: CREDENTIALS_FILE_MODE
246
- });
247
- return true;
248
- } catch (err) {
249
- if (err.code !== "EEXIST") return false;
250
- try {
251
- if (Date.now() - statSync(lockPath).mtimeMs > CREDENTIALS_LOCK_STALE_MS) {
252
- unlinkSync(lockPath);
253
- continue;
254
- }
255
- } catch {}
256
- if (Date.now() >= deadline) return false;
257
- sleepSync(CREDENTIALS_LOCK_RETRY_MS);
258
- }
259
- }
260
- function releaseCredentialsLock(lockPath) {
261
- try {
262
- unlinkSync(lockPath);
263
- } catch {}
264
- }
265
- /**
266
- * Atomically write `.credentials.json` with mode 0o600.
267
- *
268
- * Writes to a sibling temp file first, then renames. `rename(2)` is atomic on
269
- * the same filesystem, so readers either see the old file or the new one —
270
- * never a half-written one. This matters when a host has multiple processes
271
- * pointed at the same creds file.
272
- *
273
- * Merges the incoming OAuth entries on top of the raw file contents so that
274
- * entries this layer doesn't manage (e.g. `kind: 'apikey'` from the TUI
275
- * wizard) survive an OAuth refresh cycle. Without this merge,
276
- * `readOAuthCredentials` filters those entries out, and a naive overwrite
277
- * would delete them.
278
- *
279
- * The read-merge-write runs under a best-effort cross-process lockfile
280
- * (`<file>.lock`) so two processes refreshing different providers at the
281
- * same moment can't drop each other's rotated refresh tokens. On lock
282
- * timeout the write proceeds unlocked (last-write-wins, the pre-lock
283
- * behavior) rather than failing the refresh.
284
- */
285
- function writeOAuthCredentials(credentials) {
286
- const path = credentialsFilePath();
287
- const lockPath = `${path}.lock`;
288
- const locked = acquireCredentialsLock(lockPath);
289
- try {
290
- let existing = {};
291
- try {
292
- if (existsSync(path)) {
293
- const raw = readFileSync(path, "utf-8");
294
- const parsed = JSON.parse(raw);
295
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed;
296
- }
297
- } catch {}
298
- const merged = {
299
- ...existing,
300
- ...credentials
301
- };
302
- for (const [key, value] of Object.entries(merged)) if (value === void 0) delete merged[key];
303
- writeFileAtomic(path, JSON.stringify(merged, null, 2), { mode: CREDENTIALS_FILE_MODE });
304
- } finally {
305
- if (locked) releaseCredentialsLock(lockPath);
306
- }
307
- }
308
- function credentialsFromParams(params, extraKeys = []) {
309
- if (typeof params?.access !== "string" || typeof params.refresh !== "string" || typeof params.expires !== "number") return void 0;
310
- const extras = Object.fromEntries(extraKeys.map((key) => [key, params[key]]).filter(([, value]) => value !== void 0));
311
- return {
312
- access: params.access,
313
- refresh: params.refresh,
314
- expires: params.expires,
315
- ...extras
316
- };
317
- }
318
- async function resolveOAuthApiKey(options, callbacks) {
319
- if (typeof options.params?.apiKey === "string") return options.params.apiKey;
320
- const paramsCredentials = credentialsFromParams(options.params, options.extraCredentialKeys);
321
- if (paramsCredentials) return await withRefreshLock(`params:${options.providerId}`, () => resolveCredentialSource("params", paramsCredentials));
322
- if (typeof options.params?.access === "string") return options.params.access;
323
- const readCredentials = options.readCredentials ?? readOAuthCredentials;
324
- const writeCredentials = options.writeCredentials ?? writeOAuthCredentials;
325
- const envValue = options.envKey ? process.env[options.envKey] : void 0;
326
- if (envValue) {
327
- if (!isOAuthAccessToken(envValue)) return envValue;
328
- if (!readCredentials()[options.providerId]) return envValue;
329
- return await withRefreshLock(`file:${options.providerId}`, async () => {
330
- const allCredentials = readCredentials();
331
- const stored = allCredentials[options.providerId];
332
- if (!stored) return envValue;
333
- return await resolveCredentialSource("file", stored, allCredentials, writeCredentials);
334
- });
335
- }
336
- return await withRefreshLock(`file:${options.providerId}`, async () => {
337
- const allCredentials = readCredentials();
338
- const storedCredentials = allCredentials[options.providerId];
339
- if (!storedCredentials) throw new Error(options.missingError);
340
- return await resolveCredentialSource("file", storedCredentials, allCredentials, writeCredentials);
341
- });
342
- async function resolveCredentialSource(source, current, allCredentials, persistCredentials) {
343
- let result;
344
- try {
345
- result = await (options.getOAuthApiKey ?? await loadGetOAuthApiKey())(options.providerId, { [options.providerId]: current });
346
- } catch (err) {
347
- throw new Error(options.refreshError(errorMessage(err)));
348
- }
349
- if (!result) throw new Error(options.refreshError(options.missingError));
350
- if (result.newCredentials !== current) {
351
- if (source === "file" && allCredentials && persistCredentials) {
352
- allCredentials[options.providerId] = result.newCredentials;
353
- persistCredentials(allCredentials);
354
- }
355
- await callbacks?.onOAuthRefresh?.({
356
- provider: options.provider,
357
- providerId: options.providerId,
358
- source,
359
- previousCredentials: { ...current },
360
- credentials: { ...result.newCredentials }
361
- });
362
- }
363
- return result.apiKey;
364
- }
365
- }
366
- /**
367
- * Coalesce concurrent refresh calls onto a single in-flight promise per key.
368
- * The key combines the source (`params` vs `file`) and provider id, so a
369
- * per-agent param-only refresh does not starve a separate file-backed one.
370
- */
371
- async function withRefreshLock(key, fn) {
372
- const existing = refreshLocks.get(key);
373
- if (existing) return existing;
374
- const task = (async () => {
375
- try {
376
- return await fn();
377
- } finally {
378
- refreshLocks.delete(key);
379
- }
380
- })();
381
- refreshLocks.set(key, task);
382
- return task;
383
- }
384
- //#endregion
385
- //#region src/providers/anthropic.ts
386
- const loadAnthropicSdk = lazyAsync(async () => {
387
- try {
388
- return (await import("@anthropic-ai/sdk")).default;
389
- } catch (err) {
390
- throw new Error("The `anthropic` provider requires the `@anthropic-ai/sdk` package, which is an optional peer dependency. Install it with your package manager (e.g. `bun add @anthropic-ai/sdk`).", err instanceof Error ? { cause: err } : void 0);
391
- }
392
- });
393
- /** Beta flags sent unconditionally on the OAuth path (Claude Code parity). */
394
- const OAUTH_DEFAULT_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
395
- /**
396
- * Beta flag that enables the `speed: "fast"` request field (fast mode — up to
397
- * 2.5× output tokens/sec on Opus 4.6/4.7/4.8 at premium pricing). Sent as an
398
- * `anthropic-beta` header only on requests that opt into fast mode; absent
399
- * otherwise so standard requests keep a byte-stable header for prompt caching.
400
- *
401
- * See: https://platform.claude.com/docs/en/build-with-claude/fast-mode
402
- */
403
- const FAST_MODE_BETA = "fast-mode-2026-02-01";
404
- const API_KEY_RESERVED_EXTRA_HEADER_NAMES = new Set(["anthropic-beta"]);
405
- const OAUTH_RESERVED_EXTRA_HEADER_NAMES = new Set([
406
- "anthropic-beta",
407
- "anthropic-dangerous-direct-browser-access",
408
- "user-agent",
409
- "x-app"
410
- ]);
411
- function filterReservedExtraHeaders(extraHeaders, reservedNames) {
412
- const out = {};
413
- for (const [name, value] of Object.entries(extraHeaders ?? {})) if (!reservedNames.has(name.toLowerCase())) out[name] = value;
414
- return out;
415
- }
416
- /**
417
- * Build the `anthropic-beta` header value — OAuth defaults plus caller-supplied
418
- * extras, de-duped while preserving order. Returns `undefined` when no betas
419
- * apply (non-OAuth, no extras).
420
- */
421
- function resolveAnthropicBetas(isOAuth, extraBetas) {
422
- const seen = /* @__PURE__ */ new Set();
423
- const out = [];
424
- if (isOAuth) {
425
- for (const b of OAUTH_DEFAULT_BETAS) if (!seen.has(b)) {
426
- seen.add(b);
427
- out.push(b);
428
- }
429
- }
430
- if (extraBetas) {
431
- for (const b of extraBetas) if (typeof b === "string" && b.length > 0 && !seen.has(b)) {
432
- seen.add(b);
433
- out.push(b);
434
- }
435
- }
436
- return out.length > 0 ? out.join(",") : void 0;
437
- }
438
- function getConfiguredApiKey(anthropicParams) {
439
- if (anthropicParams?.apiKey) return anthropicParams.apiKey;
440
- if (anthropicParams?.access) return anthropicParams.access;
441
- if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
442
- const access = readOAuthCredentials().anthropic?.access;
443
- if (typeof access === "string" && access.length > 0) return access;
444
- throw new Error("No API key found. Run `bun run auth` first.");
445
- }
446
- /**
447
- * Resolve {@link AnthropicParams.extraHeaders} to a concrete map. A function
448
- * form is invoked (and awaited) per call so gateway auth tokens can refresh
449
- * each turn; a static map passes through.
450
- */
451
- async function resolveExtraHeaders(value) {
452
- return typeof value === "function" ? value() : value;
453
- }
454
- function createClient(SDK, apiKey, isOAuth, baseURL, extraBetas, extraHeaders) {
455
- const base = baseURL ? { baseURL } : {};
456
- const betaHeader = resolveAnthropicBetas(isOAuth, extraBetas);
457
- if (isOAuth) {
458
- const defaultHeaders = {
459
- ...filterReservedExtraHeaders(extraHeaders, OAUTH_RESERVED_EXTRA_HEADER_NAMES),
460
- "anthropic-dangerous-direct-browser-access": "true",
461
- "user-agent": "zidane/2.0.0",
462
- "x-app": "cli"
463
- };
464
- if (betaHeader) defaultHeaders["anthropic-beta"] = betaHeader;
465
- return new SDK({
466
- apiKey: null,
467
- authToken: apiKey,
468
- dangerouslyAllowBrowser: true,
469
- defaultHeaders,
470
- ...base
471
- });
472
- }
473
- const defaultHeaders = filterReservedExtraHeaders(extraHeaders, API_KEY_RESERVED_EXTRA_HEADER_NAMES);
474
- if (betaHeader) defaultHeaders["anthropic-beta"] = betaHeader;
475
- return new SDK({
476
- apiKey,
477
- ...Object.keys(defaultHeaders).length > 0 ? { defaultHeaders } : {},
478
- ...base
479
- });
480
- }
481
- /**
482
- * Map `ThinkingLevel` budgeted tiers to Anthropic's `output_config.effort`
483
- * enum. `minimal` collapses to `low` (the closest equivalent — Anthropic does
484
- * not have a sub-`low` tier).
485
- */
486
- const EFFORT_FOR_LEVEL = {
487
- minimal: "low",
488
- low: "low",
489
- medium: "medium",
490
- high: "high",
491
- xhigh: "xhigh",
492
- max: "max"
493
- };
494
- /**
495
- * Decide how to translate a `ThinkingLevel` into Anthropic's request shape.
496
- * Pure / synchronous — exported so tests can assert routing without standing
497
- * up the SDK.
498
- *
499
- * Routing rules:
500
- * - `'off'` → `null` (no thinking field, no effort hint).
501
- * - `'adaptive'` → adaptive thinking with no effort hint (model decides
502
- * everything). When `customBudget` is set, it is carried as `maxTokensCap`
503
- * so the request builder caps `max_tokens` accordingly — adaptive has no
504
- * native budget knob, but capping the response envelope soft-bounds the
505
- * thinking that lives inside it.
506
- * - Budgeted levels → adaptive thinking with an `effort` hint, unless
507
- * `customBudget` is provided.
508
- * - Any level + `customBudget` → explicit-budget `enabled` path. The caller
509
- * has opted into precise budget control and accepts the Anthropic
510
- * deprecation warning that comes with it on opus 4.6+. `'adaptive'` is the
511
- * sole exception: it never falls back to enabled, since adaptive is the
512
- * current Anthropic API surface for self-budgeted thinking.
513
- */
514
- function planAnthropicThinking(level, customBudget) {
515
- if (level === "off") return null;
516
- if (level === "adaptive") {
517
- if (typeof customBudget === "number" && customBudget > 0) return {
518
- kind: "adaptive",
519
- maxTokensCap: customBudget
520
- };
521
- return { kind: "adaptive" };
522
- }
523
- if (customBudget !== void 0) return {
524
- kind: "enabled",
525
- budgetTokens: customBudget,
526
- maxTokensBump: customBudget
527
- };
528
- return {
529
- kind: "adaptive",
530
- effort: EFFORT_FOR_LEVEL[level]
531
- };
532
- }
533
- /**
534
- * Map Anthropic's native `stop_reason` to the zidane `TurnFinishReason` union.
535
- *
536
- * `pause_turn` and `model_context_window_exceeded` are 4.6+ stop reasons that
537
- * pre-Z21 collapsed to `'other'` and silently terminated the run. They now
538
- * map to `'pause'` and `'length'` respectively, and the surrounding caller
539
- * adjusts the `done` flag so the loop can recover.
540
- */
541
- function mapStopReason(stopReason) {
542
- if (!stopReason) return void 0;
543
- switch (stopReason) {
544
- case "end_turn":
545
- case "stop_sequence": return "stop";
546
- case "tool_use": return "tool-calls";
547
- case "max_tokens":
548
- case "model_context_window_exceeded": return "length";
549
- case "refusal": return "content-filter";
550
- case "pause_turn": return "pause";
551
- default: return "other";
552
- }
553
- }
554
- const EPHEMERAL = { type: "ephemeral" };
555
- /**
556
- * Mutate an Anthropic request in place to add cache breakpoints on the three
557
- * stable prefixes:
558
- * 1. System prompt — `cache_control` on the static prefix only (see below).
559
- * 2. Tool definitions — last tool.
560
- * 3. Conversation — last content block of the last message.
561
- *
562
- * Each breakpoint tells Anthropic to cache the prefix ending at that block;
563
- * subsequent turns reuse the cached prefix and pay only for the delta. Safe
564
- * no-op when any prefix is empty (no tools, empty system, etc.).
565
- *
566
- * System-prompt boundary handling (`SYSTEM_PROMPT_BOUNDARY`):
567
- *
568
- * - When `originalSystem` is provided AND carries the marker, the system
569
- * block is rewritten as a 2-block array: static (cached) then dynamic
570
- * (uncached). The caller is expected to have already stripped the marker
571
- * from `params.system` via {@link renderSystemForWire} — this helper
572
- * only re-splits to apply `cache_control` cleanly.
573
- * - When `originalSystem` is omitted or marker-free, the existing single-
574
- * block treatment applies: `cache_control` on the whole system string.
575
- * - Array system prompts: existing semantics — `cache_control` lands on the
576
- * last block. Callers building their own multi-block layout own the
577
- * breakpoint placement.
578
- */
579
- function applyAnthropicCacheBreakpoints(params, originalSystem) {
580
- if (typeof params.system === "string") {
581
- if (params.system.length > 0) {
582
- const parts = splitSystemPrompt(originalSystem && originalSystem.length > 0 ? originalSystem : params.system);
583
- if (parts.dynamic.length > 0) {
584
- const blocks = [];
585
- if (parts.static.length > 0) blocks.push({
586
- type: "text",
587
- text: parts.static,
588
- cache_control: EPHEMERAL
589
- });
590
- blocks.push({
591
- type: "text",
592
- text: parts.dynamic
593
- });
594
- params.system = blocks;
595
- } else params.system = [{
596
- type: "text",
597
- text: parts.static,
598
- cache_control: EPHEMERAL
599
- }];
600
- }
601
- } else if (Array.isArray(params.system) && params.system.length > 0) {
602
- const lastIdx = params.system.length - 1;
603
- params.system = params.system.map((block, i) => i === lastIdx ? {
604
- ...block,
605
- cache_control: EPHEMERAL
606
- } : block);
607
- }
608
- if (params.tools && params.tools.length > 0) {
609
- const lastIdx = params.tools.length - 1;
610
- params.tools = params.tools.map((tool, i) => i === lastIdx ? {
611
- ...tool,
612
- cache_control: EPHEMERAL
613
- } : tool);
614
- }
615
- if (params.messages.length === 0) return;
616
- const lastMsgIdx = params.messages.length - 1;
617
- const lastMsg = params.messages[lastMsgIdx];
618
- if (typeof lastMsg.content === "string") {
619
- if (lastMsg.content.length === 0) return;
620
- params.messages[lastMsgIdx] = {
621
- ...lastMsg,
622
- content: [{
623
- type: "text",
624
- text: lastMsg.content,
625
- cache_control: EPHEMERAL
626
- }]
627
- };
628
- return;
629
- }
630
- if (!Array.isArray(lastMsg.content) || lastMsg.content.length === 0) return;
631
- const blocks = lastMsg.content;
632
- let targetIdx = blocks.length - 1;
633
- while (targetIdx >= 0 && isThinkingBlock(blocks[targetIdx])) targetIdx -= 1;
634
- if (targetIdx < 0) return;
635
- const nextBlocks = blocks.slice();
636
- nextBlocks[targetIdx] = {
637
- ...nextBlocks[targetIdx],
638
- cache_control: EPHEMERAL
639
- };
640
- params.messages[lastMsgIdx] = {
641
- ...lastMsg,
642
- content: nextBlocks
643
- };
644
- }
645
- function isThinkingBlock(block) {
646
- return block.type === "thinking" || block.type === "redacted_thinking";
647
- }
648
- /**
649
- * Duck-type check for an Anthropic SDK `APIError` — avoids a runtime dependency
650
- * on `@anthropic-ai/sdk` so `classifyError` stays usable even when the optional
651
- * peer dep isn't loaded (e.g. host code calling it on an unrelated provider's
652
- * error).
653
- *
654
- * Anthropic's APIError shape: `.status: number` + `.error` (parsed body, object
655
- * or null). Plain `Error`s don't have `.status` + `.error`.
656
- */
657
- function looksLikeAnthropicApiError(err) {
658
- if (!err || typeof err !== "object") return false;
659
- const e = err;
660
- return typeof e.status === "number" && "error" in e;
661
- }
662
- /**
663
- * Anthropic SSE `error` event types that warrant a retry. `overloaded_error`
664
- * is the headline case (capacity event mid-stream); `api_error` and
665
- * `timeout_error` are both server-side transient failures. `invalid_request_error`,
666
- * `authentication_error`, `permission_error`, `not_found_error`, `billing_error`
667
- * are terminal — retrying them would just hammer a bad request.
668
- *
669
- * `rate_limit_error` here would be redundant: the SDK's pre-stream retry
670
- * handles 429, and a mid-stream `rate_limit_error` is exceedingly rare. We
671
- * include it anyway for symmetry with the HTTP-level retry policy.
672
- */
673
- const SSE_RETRYABLE_ERROR_TYPES = new Set([
674
- "overloaded_error",
675
- "api_error",
676
- "timeout_error",
677
- "rate_limit_error"
678
- ]);
679
- /**
680
- * Try to parse an Anthropic SSE `error` event payload out of a thrown error's
681
- * message. The SDK's `MessageStream` surfaces mid-stream error events by
682
- * throwing an `AnthropicError` whose `.message` contains the raw JSON shape:
683
- *
684
- * {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"},"request_id":"req_..."}
685
- *
686
- * Returns the inner `{ type, message }` or `null` when the message isn't
687
- * the canonical SSE error shape. Tolerant: silently returns `null` for
688
- * non-string input, non-JSON content, or any shape mismatch.
689
- */
690
- function matchSseErrorEvent(message) {
691
- if (typeof message !== "string" || message.length === 0) return null;
692
- if (!message.includes("\"type\":\"error\"") && !message.includes("\"type\": \"error\"")) return null;
693
- const start = message.indexOf("{");
694
- if (start < 0) return null;
695
- try {
696
- const parsed = JSON.parse(message.slice(start));
697
- if (!parsed || typeof parsed !== "object") return null;
698
- const outer = parsed;
699
- if (outer.type !== "error" || !outer.error || typeof outer.error !== "object") return null;
700
- const inner = outer.error;
701
- if (typeof inner.type !== "string") return null;
702
- return {
703
- type: inner.type,
704
- message: typeof inner.message === "string" ? inner.message : inner.type
705
- };
706
- } catch {
707
- return null;
708
- }
709
- }
710
- /**
711
- * Classify an Anthropic SDK / HTTP error for typed-error wrapping.
712
- *
713
- * - `prompt is too long` (400 invalid_request_error) → `context_exceeded`.
714
- * - `'tool_use' ids were found without 'tool_result' blocks` /
715
- * `'tool_result must be preceded by a tool_call'` (400) →
716
- * `tool_pairing_corruption`. The local repair pass should have caught
717
- * this — the typed error tells consumers their pre-send pipeline is
718
- * shipping orphaned blocks somehow (custom `context:transform` hook,
719
- * bypassed loop, mid-stream session edit) so they can patch the root
720
- * cause instead of chasing 400s.
721
- * - Any other Anthropic `APIError`-shaped value → `provider_error` with the
722
- * native status/type code.
723
- * - Unknown errors → `null` (loop wraps in `AgentProviderError` generically).
724
- *
725
- * Anthropic's wire error shape is `{ type: 'error', error: { type, message } }` — the
726
- * SDK stores the parsed body on `err.error`. We walk both levels so callers see the
727
- * most specific `providerCode` we can find.
728
- */
729
- function classifyAnthropicError(err) {
730
- const prelude = classifyErrorPrelude(err);
731
- if (prelude === "not-object") return null;
732
- if (prelude === "aborted") return { kind: "aborted" };
733
- const anyErr = err;
734
- if (!looksLikeAnthropicApiError(err)) {
735
- const sseError = matchSseErrorEvent(anyErr.message);
736
- if (sseError) return {
737
- kind: "provider_error",
738
- providerCode: sseError.type,
739
- message: sseError.message,
740
- retryable: SSE_RETRYABLE_ERROR_TYPES.has(sseError.type)
741
- };
742
- return null;
743
- }
744
- const innerType = anyErr.error?.error?.type;
745
- const outerType = anyErr.error?.type;
746
- const nativeType = innerType && innerType !== "error" ? innerType : outerType;
747
- const message = anyErr.error?.error?.message ?? anyErr.error?.message ?? anyErr.message ?? "";
748
- if (matchesContextExceeded(message)) return {
749
- kind: "context_exceeded",
750
- providerCode: nativeType ?? "invalid_request_error",
751
- message
752
- };
753
- if (matchesToolPairingError(message)) return {
754
- kind: "tool_pairing_corruption",
755
- providerCode: nativeType ?? "invalid_request_error",
756
- message
757
- };
758
- const status = anyErr.status;
759
- const retryable = typeof status === "number" ? isRetryableHttpStatus(status) : void 0;
760
- return {
761
- kind: "provider_error",
762
- providerCode: nativeType ?? (status ? String(status) : void 0),
763
- message,
764
- ...retryable !== void 0 ? { retryable } : {}
765
- };
766
- }
767
- function shouldFallbackFromFastMode(err) {
768
- const classified = classifyAnthropicError(err);
769
- if (classified?.kind === "provider_error" && classified.retryable === true) return true;
770
- const message = err instanceof Error ? err.message : String(err);
771
- return /\b(?:fast|speed)\b/i.test(message) && /capacity|overload|rate|unavailable|unsupported|not supported|not permitted|extra inputs|invalid/i.test(message);
772
- }
773
- /**
774
- * Build a user `SessionMessage` from multimodal prompt parts.
775
- *
776
- * - Text parts → text blocks.
777
- * - Image parts → base64 image blocks.
778
- * - Document parts → canonical document blocks. `toAnthropic` emits native
779
- * document blocks only for base64 PDFs; text/non-PDF documents fall back to
780
- * attachment-tagged text to avoid invalid Anthropic source media types.
781
- *
782
- * The format mirrors `defaultPromptMessage`'s output on shape — Anthropic-specific
783
- * handling differs only in that `promptMessage` being present tells the agent loop
784
- * to route PromptPart[] through this function rather than throwing on base64 docs.
785
- */
786
- function anthropicPromptMessage(parts) {
787
- const content = [];
788
- for (const part of parts) {
789
- if (part.type === "text") {
790
- if (part.text.length > 0) content.push({
791
- type: "text",
792
- text: part.text
793
- });
794
- continue;
795
- }
796
- if (part.type === "image") {
797
- content.push({
798
- type: "image",
799
- mediaType: part.mediaType,
800
- data: part.data
801
- });
802
- continue;
803
- }
804
- if (part.type === "audio") throw unsupportedMediaError("audio", "anthropic");
805
- if (part.type === "video") throw unsupportedMediaError("video", "anthropic");
806
- content.push({
807
- type: "document",
808
- mediaType: part.mediaType,
809
- data: part.data,
810
- encoding: part.encoding,
811
- ...part.name ? { name: part.name } : {}
812
- });
813
- }
814
- return {
815
- role: "user",
816
- content
817
- };
818
- }
819
- function anthropic(anthropicParams) {
820
- const isOAuth = getConfiguredApiKey(anthropicParams).includes("sk-ant-oat");
821
- const defaultModel = anthropicParams?.defaultModel || "claude-opus-4-8";
822
- let runtimeCredentials = extractRuntimeCredentials(anthropicParams);
823
- return {
824
- name: "anthropic",
825
- meta: {
826
- defaultModel,
827
- isOAuth,
828
- clearsContextServerSide: Boolean(anthropicParams?.contextManagement),
829
- capabilities: {
830
- vision: true,
831
- imageInToolResult: true,
832
- documents: true,
833
- audio: false,
834
- video: false,
835
- nativeWebSearch: { maxUses: 5 }
836
- }
837
- },
838
- formatTools(tools) {
839
- const nativeWebSearch = this.meta.capabilities?.nativeWebSearch;
840
- const hasWebSearch = !!nativeWebSearch && tools.some((t) => t.name === "web_search");
841
- const out = sanitizeToolSpecs(hasWebSearch ? tools.filter((t) => t.name !== "web_search") : tools, { profile: "anthropic" }).map((t) => ({
842
- name: t.name,
843
- description: t.description,
844
- input_schema: t.inputSchema
845
- }));
846
- if (hasWebSearch && nativeWebSearch) {
847
- const block = {
848
- type: "web_search_20250305",
849
- name: "web_search"
850
- };
851
- if (typeof nativeWebSearch.maxUses === "number") block.max_uses = nativeWebSearch.maxUses;
852
- out.push(block);
853
- }
854
- return out;
855
- },
856
- userMessage(content) {
857
- return {
858
- role: "user",
859
- content: [{
860
- type: "text",
861
- text: content
862
- }]
863
- };
864
- },
865
- assistantMessage(content) {
866
- return {
867
- role: "assistant",
868
- content: [{
869
- type: "text",
870
- text: content
871
- }]
872
- };
873
- },
874
- toolResultsMessage(results) {
875
- return {
876
- role: "user",
877
- content: results.map((r) => ({
878
- type: "tool_result",
879
- callId: r.id,
880
- output: r.content,
881
- ...r.isError ? { isError: true } : {}
882
- }))
883
- };
884
- },
885
- promptMessage: anthropicPromptMessage,
886
- classifyError: classifyAnthropicError,
887
- async countTokens(payload, signal) {
888
- try {
889
- const SDK = await loadAnthropicSdk();
890
- const apiKey = await resolveOAuthApiKey({
891
- provider: "anthropic",
892
- providerId: "anthropic",
893
- params: runtimeCredentials ? {
894
- ...anthropicParams,
895
- ...runtimeCredentials
896
- } : anthropicParams,
897
- envKey: "ANTHROPIC_API_KEY",
898
- missingError: "No API key found. Run `bun run auth` first.",
899
- refreshError: (reason) => `Anthropic OAuth token refresh failed. ${reason}`
900
- });
901
- const callIsOAuth = apiKey.includes("sk-ant-oat");
902
- const client = createClient(SDK, apiKey, callIsOAuth, anthropicParams?.baseURL, anthropicParams?.extraBetas, await resolveExtraHeaders(anthropicParams?.extraHeaders));
903
- const wireSystem = renderSystemForWire(payload.system);
904
- const system = callIsOAuth ? `You are Claude Code, Anthropic's official CLI for Claude.` : wireSystem;
905
- const messages = callIsOAuth && payload.system ? [
906
- {
907
- role: "user",
908
- content: [{
909
- type: "text",
910
- text: wireSystem
911
- }]
912
- },
913
- {
914
- role: "assistant",
915
- content: [{
916
- type: "text",
917
- text: "Understood. I will proceed with these instructions above the rest of my system prompt."
918
- }]
919
- },
920
- ...payload.messages
921
- ] : [...payload.messages];
922
- return (await client.messages.countTokens({
923
- model: payload.model,
924
- system,
925
- tools: payload.tools,
926
- messages: messages.map((m) => toAnthropic(m))
927
- }, signal ? { signal } : void 0)).input_tokens;
928
- } catch (err) {
929
- if (process.env.ZIDANE_DEBUG_COUNT) console.error("[anthropic.countTokens] failed:", err);
930
- return null;
931
- }
932
- },
933
- async stream(options, callbacks) {
934
- const SDK = await loadAnthropicSdk();
935
- const apiKey = await resolveOAuthApiKey({
936
- provider: "anthropic",
937
- providerId: "anthropic",
938
- params: runtimeCredentials ? {
939
- ...anthropicParams,
940
- ...runtimeCredentials
941
- } : anthropicParams,
942
- envKey: "ANTHROPIC_API_KEY",
943
- missingError: "No API key found. Run `bun run auth` first.",
944
- refreshError: (reason) => `Anthropic OAuth token refresh failed. Run \`bun run auth --anthropic\` again. ${reason}`
945
- }, {
946
- ...callbacks,
947
- async onOAuthRefresh(ctx) {
948
- if (ctx.source === "params") runtimeCredentials = {
949
- access: ctx.credentials.access,
950
- refresh: ctx.credentials.refresh,
951
- expires: ctx.credentials.expires
952
- };
953
- await callbacks.onOAuthRefresh?.(ctx);
954
- }
955
- });
956
- const requestBetas = options.modelOptions?.fast ? [...anthropicParams?.extraBetas ?? [], FAST_MODE_BETA] : anthropicParams?.extraBetas;
957
- const callIsOAuth = apiKey.includes("sk-ant-oat");
958
- const client = createClient(SDK, apiKey, callIsOAuth, anthropicParams?.baseURL, requestBetas, await resolveExtraHeaders(anthropicParams?.extraHeaders));
959
- const wireSystem = renderSystemForWire(options.system);
960
- const system = callIsOAuth ? `You are Claude Code, Anthropic's official CLI for Claude.` : wireSystem;
961
- const messages = callIsOAuth && options.system ? [
962
- {
963
- role: "user",
964
- content: [{
965
- type: "text",
966
- text: wireSystem
967
- }]
968
- },
969
- {
970
- role: "assistant",
971
- content: [{
972
- type: "text",
973
- text: "Understood. I will proceed with these instructions above the rest of my system prompt."
974
- }]
975
- },
976
- ...options.messages
977
- ] : [...options.messages];
978
- const thinking = options.thinking ?? "off";
979
- const modelId = options.model;
980
- const params = {
981
- ...anthropicParams?.extraBodyParams ?? {},
982
- model: modelId,
983
- max_tokens: options.maxTokens,
984
- system,
985
- tools: options.tools,
986
- messages: messages.map((m) => toAnthropic(m)),
987
- stream: true
988
- };
989
- if (anthropicParams?.contextManagement) params.context_management = anthropicParams.contextManagement;
990
- if (options.cache !== false) applyAnthropicCacheBreakpoints(params, callIsOAuth ? void 0 : options.system);
991
- const plan = planAnthropicThinking(thinking, options.thinkingBudget);
992
- if (plan) {
993
- if (plan.kind === "enabled") {
994
- params.thinking = {
995
- type: "enabled",
996
- budget_tokens: plan.budgetTokens,
997
- display: "summarized"
998
- };
999
- params.max_tokens = plan.maxTokensBump + params.max_tokens;
1000
- } else {
1001
- params.thinking = {
1002
- type: "adaptive",
1003
- display: "summarized"
1004
- };
1005
- if (plan.effort) params.output_config = { effort: plan.effort };
1006
- if (typeof plan.maxTokensCap === "number" && plan.maxTokensCap > 0) params.max_tokens = Math.min(params.max_tokens, plan.maxTokensCap);
1007
- }
1008
- params.temperature = 1;
1009
- }
1010
- if (options.modelOptions?.fast) params.speed = "fast";
1011
- if (options.toolChoice) if (options.toolChoice.type === "tool" && options.toolChoice.name) params.tool_choice = {
1012
- type: "tool",
1013
- name: options.toolChoice.name
1014
- };
1015
- else if (options.toolChoice.type === "required") params.tool_choice = { type: "any" };
1016
- else params.tool_choice = { type: "auto" };
1017
- const runStream = async (requestParams, fastRequested) => {
1018
- const s = client.messages.stream(requestParams, { signal: options.signal });
1019
- let text = "";
1020
- let observedOutput = false;
1021
- s.on("text", (delta) => {
1022
- observedOutput = true;
1023
- text += delta;
1024
- callbacks.onText(delta);
1025
- });
1026
- if (callbacks.onThinking) s.on("thinking", (delta) => {
1027
- observedOutput = true;
1028
- callbacks.onThinking(delta);
1029
- });
1030
- if (callbacks.onToolCallDelta) s.on("inputJson", () => {
1031
- observedOutput = true;
1032
- callbacks.onToolCallDelta();
1033
- });
1034
- if (callbacks.onServerToolUse || callbacks.onServerToolResult) s.on("contentBlock", (block) => {
1035
- observedOutput = true;
1036
- if (block.type === "server_tool_use") callbacks.onServerToolUse?.({
1037
- id: block.id,
1038
- name: block.name,
1039
- input: block.input ?? {}
1040
- });
1041
- else if (block.type === "web_search_tool_result") callbacks.onServerToolResult?.({
1042
- toolUseId: block.tool_use_id,
1043
- toolName: "web_search",
1044
- content: block.content
1045
- });
1046
- });
1047
- let response;
1048
- try {
1049
- response = await s.finalMessage();
1050
- } catch (err) {
1051
- if (fastRequested && !observedOutput && shouldFallbackFromFastMode(err)) {
1052
- const standardParams = { ...requestParams };
1053
- delete standardParams.speed;
1054
- return await runStream(standardParams, false);
1055
- }
1056
- throw err;
1057
- }
1058
- const toolCalls = response.content.filter((b) => b.type === "tool_use").map((b) => ({
1059
- id: b.id,
1060
- name: b.name,
1061
- input: b.input
1062
- }));
1063
- const finishReason = mapStopReason(response.stop_reason);
1064
- const isPause = response.stop_reason === "pause_turn";
1065
- return {
1066
- assistantMessage: fromAnthropic({
1067
- role: "assistant",
1068
- content: response.content
1069
- }),
1070
- text,
1071
- toolCalls,
1072
- done: !isPause && (response.stop_reason === "end_turn" || toolCalls.length === 0),
1073
- usage: fillEstimatedCost({
1074
- input: response.usage.input_tokens,
1075
- output: response.usage.output_tokens,
1076
- cacheCreation: response.usage.cache_creation_input_tokens ?? void 0,
1077
- cacheRead: response.usage.cache_read_input_tokens ?? void 0,
1078
- ...finishReason ? { finishReason } : {},
1079
- modelId: response.model ?? options.model
1080
- }, "anthropic", fastRequested ? fastModeCostMultiplier(response.model ?? options.model) : void 0)
1081
- };
1082
- };
1083
- return await runStream(params, options.modelOptions?.fast === true);
1084
- }
1085
- };
1086
- }
1087
- //#endregion
1088
- //#region src/providers/arcee.ts
1089
- const BASE_URL$3 = "https://api.arcee.ai/api/v1";
1090
- function getApiKey$4(params) {
1091
- if (typeof params?.apiKey === "string" && params.apiKey.length > 0) return params.apiKey;
1092
- if (process.env.ARCEE_API_KEY) return process.env.ARCEE_API_KEY;
1093
- throw new Error("No Arcee API key found. Pass `apiKey` or set ARCEE_API_KEY in your environment.");
1094
- }
1095
- /**
1096
- * Arcee provider (Trinity-Large-Thinking and friends).
1097
- *
1098
- * Thin wrapper around {@link openaiCompat} pinned to the `reasoning_content`
1099
- * round-trip mode: Trinity emits chain-of-thought in `<think>…</think>` blocks
1100
- * that vLLM parses into a plain `reasoning_content` string. We capture it and
1101
- * echo it back on every assistant message so the model resumes its prior
1102
- * reasoning across tool calls — without this, tool calls leak into the
1103
- * reasoning field as raw XML (Arcee's `xml_in_reasoning` pitfall). `content` is
1104
- * also normalized from `null` → `''` on tool-call turns per the same guidance.
1105
- *
1106
- * See https://docs.arcee.ai/capabilities/reasoning-traces.
1107
- */
1108
- function arcee(params) {
1109
- return openaiCompat({
1110
- name: "arcee",
1111
- apiKey: getApiKey$4(params),
1112
- baseURL: params?.baseURL || BASE_URL$3,
1113
- defaultModel: params?.defaultModel || "trinity-large-thinking",
1114
- capabilities: params?.capabilities ?? {
1115
- vision: false,
1116
- imageInToolResult: false
1117
- },
1118
- supportsReasoning: true,
1119
- reasoningMode: "reasoning_content",
1120
- reasoningContentField: params?.reasoningContentField ?? "reasoning",
1121
- normalizeNullContent: true,
1122
- extraHeaders: params?.extraHeaders
1123
- });
1124
- }
1125
- //#endregion
1126
- //#region src/providers/baseten.ts
1127
- const BASE_URL$2 = "https://inference.baseten.co/v1";
1128
- /**
1129
- * Models that take OpenAI-style `reasoning_effort` (low/medium/high) and
1130
- * reason by default. Everything else on Model APIs (Kimi, GLM, Nemotron) is
1131
- * opt-in via `chat_template_args: { enable_thinking: true }`.
1132
- * See https://docs.baseten.co/inference/model-apis/reasoning.
1133
- */
1134
- const EFFORT_MODELS = new Set(["deepseek-ai/DeepSeek-V4-Pro", "openai/gpt-oss-120b"]);
1135
- function getApiKey$3(params) {
1136
- if (typeof params?.apiKey === "string" && params.apiKey.length > 0) return params.apiKey;
1137
- if (process.env.BASETEN_API_KEY) return process.env.BASETEN_API_KEY;
1138
- throw new Error("No Baseten API key found. Pass `apiKey` or set BASETEN_API_KEY in your environment.");
1139
- }
1140
- /**
1141
- * Map zidane's thinking level to Baseten's per-model reasoning request shape:
1142
- *
1143
- * - `reasoning_effort` models (DeepSeek V4 Pro, GPT-OSS 120B) reason by
1144
- * default; the effort string tunes depth. `'minimal'` maps to `'low'`,
1145
- * Anthropic-only upper tiers map to `'high'`, `'adaptive'` sends nothing
1146
- * (server default = medium); `'off'` sends nothing (cannot be disabled).
1147
- * - All other models opt in via `chat_template_args.enable_thinking` — any
1148
- * non-off level enables it (the template arg is boolean, no depth knob).
1149
- *
1150
- * Exported for tests.
1151
- */
1152
- function planBasetenReasoning(ctx) {
1153
- const { model, thinking } = ctx;
1154
- if (!thinking || thinking === "off") return void 0;
1155
- if (EFFORT_MODELS.has(model)) {
1156
- if (thinking === "adaptive") return void 0;
1157
- return { reasoning_effort: thinking === "minimal" ? "low" : thinking === "xhigh" || thinking === "max" ? "high" : thinking };
1158
- }
1159
- return { chat_template_args: { enable_thinking: true } };
1160
- }
1161
- /**
1162
- * Baseten Model APIs provider.
1163
- *
1164
- * Thin wrapper around {@link openaiCompat} pinned to the `reasoning_content`
1165
- * round-trip mode: thinking arrives in the `reasoning_content` delta field and
1166
- * is echoed back on assistant messages under the same name so reasoning
1167
- * models resume their chain-of-thought across tool calls. The thinking level
1168
- * maps to Baseten's request shape via {@link planBasetenReasoning}.
1169
- */
1170
- function baseten(params) {
1171
- return openaiCompat({
1172
- name: "baseten",
1173
- apiKey: getApiKey$3(params),
1174
- baseURL: params?.baseURL || BASE_URL$2,
1175
- defaultModel: params?.defaultModel || "zai-org/GLM-5.1",
1176
- capabilities: params?.capabilities ?? {
1177
- vision: false,
1178
- imageInToolResult: false
1179
- },
1180
- supportsReasoning: true,
1181
- reasoningMode: "reasoning_content",
1182
- reasoningContentField: "reasoning_content",
1183
- planReasoningRequest: planBasetenReasoning,
1184
- extraHeaders: params?.extraHeaders
1185
- });
1186
- }
1187
- //#endregion
1188
- //#region src/providers/cerebras.ts
1189
- const BASE_URL$1 = "https://api.cerebras.ai/v1";
1190
- function getApiKey$2(params) {
1191
- if (typeof params?.apiKey === "string" && params.apiKey.length > 0) return params.apiKey;
1192
- if (process.env.CEREBRAS_API_KEY) return process.env.CEREBRAS_API_KEY;
1193
- throw new Error("No Cerebras API key found. Pass `apiKey` or set CEREBRAS_API_KEY in your environment.");
1194
- }
1195
- /**
1196
- * Cerebras provider.
1197
- *
1198
- * Thin wrapper around {@link openaiCompat} with Cerebras-specific defaults
1199
- * (base URL, default model).
1200
- */
1201
- function cerebras(params) {
1202
- return openaiCompat({
1203
- name: "cerebras",
1204
- apiKey: getApiKey$2(params),
1205
- baseURL: BASE_URL$1,
1206
- defaultModel: params?.defaultModel || "zai-glm-4.7",
1207
- capabilities: params?.capabilities ?? {
1208
- vision: false,
1209
- imageInToolResult: false
1210
- },
1211
- extraHeaders: params?.extraHeaders
1212
- });
1213
- }
1214
- //#endregion
1215
- //#region src/chat/oauth-page/pkce.ts
1216
- /**
1217
- * PKCE helper. Inlined here (rather than imported from pi-ai) because
1218
- * `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want
1219
- * to reach into `node_modules/.../utils/oauth/pkce.js` — that's an
1220
- * implementation-detail path that breaks on minor upstream rearrangements.
1221
- *
1222
- * Web Crypto API only. Works under Bun + Node 22+ without any node:crypto
1223
- * import (matches pi-ai's own behavior). Output is base64url per RFC 7636.
1224
- */
1225
- function base64UrlEncode(bytes) {
1226
- let binary = "";
1227
- for (const byte of bytes) binary += String.fromCharCode(byte);
1228
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1229
- }
1230
- async function generatePkce() {
1231
- const verifierBytes = new Uint8Array(32);
1232
- crypto.getRandomValues(verifierBytes);
1233
- const verifier = base64UrlEncode(verifierBytes);
1234
- const data = new TextEncoder().encode(verifier);
1235
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1236
- return {
1237
- verifier,
1238
- challenge: base64UrlEncode(new Uint8Array(hashBuffer))
1239
- };
1240
- }
1241
- //#endregion
1242
- //#region src/chat/oauth-page/cursor.ts
1243
- /** pi-ai / zidane OAuth provider id. Also the credentials-file key. */
1244
- const CURSOR_OAUTH_PROVIDER_ID = "cursor";
1245
- const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
1246
- const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
1247
- const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
1248
- const POLL_MAX_ATTEMPTS = 150;
1249
- const POLL_BASE_DELAY_MS = 1e3;
1250
- const POLL_MAX_DELAY_MS = 1e4;
1251
- const POLL_BACKOFF_MULTIPLIER = 1.2;
1252
- const POLL_MAX_CONSECUTIVE_ERRORS = 3;
1253
- /** Fallback lifetime when the JWT carries no `exp`. */
1254
- const DEFAULT_TOKEN_TTL_MS = 3600 * 1e3;
1255
- /** Refresh slightly early so requests don't race expiry. */
1256
- const TOKEN_EXPIRY_SKEW_MS = 300 * 1e3;
1257
- /**
1258
- * Derive expiry (epoch ms) from a JWT's `exp` claim, minus a safety skew.
1259
- * Returns `now + 1h` for malformed tokens so a missing claim never wedges
1260
- * refresh into an immediate-expire loop.
1261
- */
1262
- function cursorTokenExpiry(token) {
1263
- try {
1264
- const parts = token.split(".");
1265
- const payload = parts[1];
1266
- if (parts.length !== 3 || !payload) return Date.now() + DEFAULT_TOKEN_TTL_MS;
1267
- const json = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
1268
- if (typeof json.exp === "number") return json.exp * 1e3 - TOKEN_EXPIRY_SKEW_MS;
1269
- } catch {}
1270
- return Date.now() + DEFAULT_TOKEN_TTL_MS;
1271
- }
1272
- function delay(ms) {
1273
- return new Promise((r) => setTimeout(r, ms));
1274
- }
1275
- /**
1276
- * Poll Cursor's auth endpoint until login completes. `404` means the user
1277
- * hasn't finished the browser flow yet; anything else non-2xx is a hard error.
1278
- */
1279
- async function pollForTokens(uuid, verifier, callbacks) {
1280
- let wait = POLL_BASE_DELAY_MS;
1281
- let consecutiveErrors = 0;
1282
- for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
1283
- if (callbacks.signal?.aborted) throw new Error("Cursor login aborted");
1284
- await delay(wait);
1285
- try {
1286
- const url = `${CURSOR_POLL_URL}?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`;
1287
- const response = await fetch(url, { signal: callbacks.signal });
1288
- if (response.status === 404) {
1289
- consecutiveErrors = 0;
1290
- wait = Math.min(wait * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY_MS);
1291
- continue;
1292
- }
1293
- if (response.ok) {
1294
- const data = await response.json();
1295
- if (!data.accessToken || !data.refreshToken) throw new Error(`Cursor poll response missing tokens: ${JSON.stringify(data)}`);
1296
- return {
1297
- accessToken: data.accessToken,
1298
- refreshToken: data.refreshToken
1299
- };
1300
- }
1301
- throw new Error(`Cursor poll failed (${response.status})`);
1302
- } catch (err) {
1303
- if (callbacks.signal?.aborted) throw new Error("Cursor login aborted");
1304
- consecutiveErrors++;
1305
- if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS) throw err instanceof Error ? err : new Error(String(err));
1306
- callbacks.onProgress?.("Waiting for Cursor login to complete…");
1307
- }
1308
- }
1309
- throw new Error("Cursor authentication timed out");
1310
- }
1311
- async function loginCursor(callbacks) {
1312
- const { verifier, challenge } = await generatePkce();
1313
- const uuid = crypto.randomUUID();
1314
- const authUrl = new URL(CURSOR_LOGIN_URL);
1315
- authUrl.searchParams.set("challenge", challenge);
1316
- authUrl.searchParams.set("uuid", uuid);
1317
- authUrl.searchParams.set("mode", "login");
1318
- authUrl.searchParams.set("redirectTarget", "cli");
1319
- callbacks.onAuth({
1320
- url: authUrl.toString(),
1321
- instructions: "A browser window should open. Complete login in Cursor to finish."
1322
- });
1323
- const tokens = await pollForTokens(uuid, verifier, callbacks);
1324
- return {
1325
- access: tokens.accessToken,
1326
- refresh: tokens.refreshToken,
1327
- expires: cursorTokenExpiry(tokens.accessToken)
1328
- };
1329
- }
1330
- async function refreshCursorToken(credentials) {
1331
- const response = await fetch(CURSOR_REFRESH_URL, {
1332
- method: "POST",
1333
- headers: {
1334
- "Authorization": `Bearer ${credentials.refresh}`,
1335
- "Content-Type": "application/json"
1336
- },
1337
- body: "{}"
1338
- });
1339
- if (!response.ok) {
1340
- const text = await response.text().catch(() => "");
1341
- throw new Error(`Cursor token refresh failed (${response.status}): ${text || response.statusText}`);
1342
- }
1343
- const data = await response.json();
1344
- if (!data.accessToken) throw new Error(`Cursor token refresh response missing access token: ${JSON.stringify(data)}`);
1345
- return {
1346
- access: data.accessToken,
1347
- refresh: data.refreshToken || credentials.refresh,
1348
- expires: cursorTokenExpiry(data.accessToken)
1349
- };
1350
- }
1351
- /**
1352
- * Build a Cursor `OAuthProviderInterface`. Shape matches Anthropic / Codex so
1353
- * it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.
1354
- *
1355
- * No `renderPage` parameter: Cursor's poll flow has no local callback server,
1356
- * so there is no post-redirect page to theme.
1357
- */
1358
- function createCursorOAuthProvider() {
1359
- return {
1360
- id: CURSOR_OAUTH_PROVIDER_ID,
1361
- name: "Cursor",
1362
- usesCallbackServer: false,
1363
- login: loginCursor,
1364
- refreshToken: refreshCursorToken,
1365
- getApiKey: (credentials) => credentials.access
1366
- };
1367
- }
1368
- //#endregion
1369
- //#region src/providers/cursor.ts
1370
- const DEFAULT_MODEL$3 = "claude-4.6-sonnet";
1371
- const NOT_IMPLEMENTED_MESSAGE = "Cursor OAuth login works, but inference over Cursor is not implemented yet. Cursor uses a protobuf/HTTP-2 agent protocol (not an OpenAI-compatible API), so the streaming transport still needs to be ported. Use another provider for now.";
1372
- function cursor(params) {
1373
- const defaultModel = params?.defaultModel || DEFAULT_MODEL$3;
1374
- const base = openaiCompat({
1375
- name: "cursor",
1376
- apiKey: params?.apiKey ?? "oauth",
1377
- baseURL: "https://api2.cursor.sh",
1378
- defaultModel,
1379
- capabilities: {
1380
- vision: true,
1381
- imageInToolResult: false
1382
- },
1383
- extraHeaders: params?.extraHeaders
1384
- });
1385
- return {
1386
- ...base,
1387
- meta: {
1388
- ...base.meta,
1389
- defaultModel
1390
- },
1391
- async stream(options, callbacks) {
1392
- await resolveOAuthApiKey({
1393
- provider: "cursor",
1394
- providerId: CURSOR_OAUTH_PROVIDER_ID,
1395
- params,
1396
- missingError: "No Cursor credentials found. Run `bun run auth --cursor` first.",
1397
- refreshError: (reason) => `Cursor OAuth token refresh failed. Run \`bun run auth --cursor\` again. ${reason}`
1398
- }, callbacks);
1399
- throw new Error(NOT_IMPLEMENTED_MESSAGE);
1400
- }
1401
- };
1402
- }
1403
- //#endregion
1404
- //#region src/providers/local.ts
1405
- function getBaseURL(params) {
1406
- if (typeof params?.baseURL === "string" && params.baseURL.length > 0) return params.baseURL;
1407
- if (process.env.LOCAL_LLM_BASE_URL) return process.env.LOCAL_LLM_BASE_URL;
1408
- throw new Error("No local LLM base URL found. Pass `baseURL` or set LOCAL_LLM_BASE_URL (e.g. http://localhost:11434/v1 for Ollama, http://localhost:8000/v1 for vLLM).");
1409
- }
1410
- function getApiKey$1(params) {
1411
- if (typeof params?.apiKey === "string" && params.apiKey.length > 0) return params.apiKey;
1412
- if (process.env.LOCAL_LLM_API_KEY) return process.env.LOCAL_LLM_API_KEY;
1413
- return "no-key";
1414
- }
1415
- function getDefaultModel(params) {
1416
- if (typeof params?.defaultModel === "string" && params.defaultModel.length > 0) return params.defaultModel;
1417
- if (process.env.LOCAL_LLM_DEFAULT_MODEL) return process.env.LOCAL_LLM_DEFAULT_MODEL;
1418
- }
1419
- /**
1420
- * Local OpenAI-compatible LLM provider.
1421
- *
1422
- * Thin wrapper around {@link openaiCompat} for self-hosted runtimes that
1423
- * speak the standard `POST /chat/completions` + SSE dialect: Ollama, vLLM,
1424
- * LM Studio, Lemonade, llama.cpp's server, text-generation-webui, etc.
1425
- *
1426
- * Caching and reasoning are left off — local runtimes typically strict-
1427
- * validate the request schema and would 400 on the extra fields. Override
1428
- * `capabilities` when pointing at a vision-capable deployment.
1429
- */
1430
- function local(params) {
1431
- return openaiCompat({
1432
- name: "local",
1433
- apiKey: getApiKey$1(params),
1434
- baseURL: getBaseURL(params),
1435
- defaultModel: getDefaultModel(params),
1436
- capabilities: params?.capabilities ?? {
1437
- vision: false,
1438
- imageInToolResult: false
1439
- },
1440
- extraHeaders: params?.extraHeaders
1441
- });
1442
- }
1443
- //#endregion
1444
- //#region src/providers/openai.ts
1445
- const PROVIDER_ID = "openai-codex";
1446
- const DEFAULT_MODEL$2 = "gpt-5.5";
1447
- const lookupModel = getModel;
1448
- function resolveModel(modelId) {
1449
- const model = lookupModel(PROVIDER_ID, modelId);
1450
- if (model) return model;
1451
- const fallback = lookupModel(PROVIDER_ID, DEFAULT_MODEL$2);
1452
- if (!fallback) throw new Error(`OpenAI Codex model registry is missing the default model: ${DEFAULT_MODEL$2}`);
1453
- return {
1454
- ...fallback,
1455
- id: modelId,
1456
- name: modelId
1457
- };
1458
- }
1459
- function emptyUsage() {
1460
- return {
1461
- input: 0,
1462
- output: 0,
1463
- cacheRead: 0,
1464
- cacheWrite: 0,
1465
- totalTokens: 0,
1466
- cost: {
1467
- input: 0,
1468
- output: 0,
1469
- cacheRead: 0,
1470
- cacheWrite: 0,
1471
- total: 0
1472
- }
1473
- };
1474
- }
1475
- function formatTools(tools) {
1476
- return sanitizeToolSpecs(tools, { profile: "openai" }).map((t) => ({
1477
- name: t.name,
1478
- description: t.description,
1479
- parameters: t.inputSchema
1480
- }));
1481
- }
1482
- function documentMarker(doc) {
1483
- return documentBlockMarker(doc, "document omitted");
1484
- }
1485
- function piToolResultMessage(result, toolName = "") {
1486
- const content = typeof result.output === "string" ? [{
1487
- type: "text",
1488
- text: result.output
1489
- }] : result.output.map((block) => {
1490
- if (block.type === "image") {
1491
- assertResolvedMediaBlock(block, "OpenAI wire messages");
1492
- return {
1493
- type: "image",
1494
- data: block.data,
1495
- mimeType: block.mediaType
1496
- };
1497
- }
1498
- if (block.type === "audio") throw unsupportedMediaError("audio", "openai");
1499
- if (block.type === "video") throw unsupportedMediaError("video", "openai");
1500
- if (block.type === "document") {
1501
- assertResolvedMediaBlock(block, "OpenAI wire messages");
1502
- return {
1503
- type: "text",
1504
- text: documentMarker(block)
1505
- };
1506
- }
1507
- return {
1508
- type: "text",
1509
- text: block.text
1510
- };
1511
- });
1512
- return {
1513
- role: "toolResult",
1514
- toolCallId: result.callId,
1515
- toolName,
1516
- content,
1517
- isError: result.isError ?? false,
1518
- timestamp: Date.now()
1519
- };
1520
- }
1521
- function piUserMessage(content) {
1522
- for (const b of content) {
1523
- if (b.type === "audio") throw unsupportedMediaError("audio", "openai");
1524
- if (b.type === "video") throw unsupportedMediaError("video", "openai");
1525
- }
1526
- const textBlocks = content.filter((b) => b.type === "text");
1527
- const imageBlocks = content.filter((b) => b.type === "image");
1528
- const documentBlocks = content.filter((b) => b.type === "document");
1529
- if (imageBlocks.length === 0 && textBlocks.length === 0 && documentBlocks.length === 0) return null;
1530
- if (imageBlocks.length === 0 && documentBlocks.length === 0 && textBlocks.length === 1) return {
1531
- role: "user",
1532
- content: textBlocks[0].text,
1533
- timestamp: Date.now()
1534
- };
1535
- return {
1536
- role: "user",
1537
- content: [
1538
- ...imageBlocks.map((img) => {
1539
- assertResolvedMediaBlock(img, "OpenAI wire messages");
1540
- return {
1541
- type: "image",
1542
- data: img.data,
1543
- mimeType: img.mediaType
1544
- };
1545
- }),
1546
- ...documentBlocks.map((block) => {
1547
- assertResolvedMediaBlock(block, "OpenAI wire messages");
1548
- return {
1549
- type: "text",
1550
- text: documentMarker(block)
1551
- };
1552
- }),
1553
- ...textBlocks.map((block) => ({
1554
- type: "text",
1555
- text: block.text
1556
- }))
1557
- ],
1558
- timestamp: Date.now()
1559
- };
1560
- }
1561
- function piAssistantMessage(content, modelId) {
1562
- const piContent = [];
1563
- for (const block of content) if (block.type === "text") piContent.push({
1564
- type: "text",
1565
- text: block.text
1566
- });
1567
- else if (block.type === "thinking") {
1568
- if (block.signatureProducer === "anthropic") continue;
1569
- piContent.push({
1570
- type: "thinking",
1571
- thinking: block.text,
1572
- thinkingSignature: block.signature
1573
- });
1574
- } else if (block.type === "tool_call") piContent.push({
1575
- type: "toolCall",
1576
- id: block.id,
1577
- name: block.name,
1578
- arguments: block.input
1579
- });
1580
- return {
1581
- role: "assistant",
1582
- content: piContent,
1583
- api: "openai-codex-responses",
1584
- provider: PROVIDER_ID,
1585
- model: modelId,
1586
- usage: emptyUsage(),
1587
- stopReason: "stop",
1588
- timestamp: Date.now()
1589
- };
1590
- }
1591
- function toPiMessages(messages, modelId) {
1592
- const out = [];
1593
- for (let i = 0; i < messages.length; i++) {
1594
- const msg = messages[i];
1595
- const toolCalls = msg.content.filter((b) => b.type === "tool_call");
1596
- if (msg.role === "assistant" && toolCalls.length > 0) {
1597
- const next = messages[i + 1];
1598
- const nextToolResults = next?.role === "user" ? next.content.filter((b) => b.type === "tool_result") : [];
1599
- const resultById = new Map(nextToolResults.map((result) => [result.callId, result]));
1600
- let assistantContent = [];
1601
- for (const block of msg.content) {
1602
- assistantContent.push(block);
1603
- if (block.type !== "tool_call") continue;
1604
- out.push(piAssistantMessage(assistantContent, modelId));
1605
- assistantContent = [];
1606
- const result = resultById.get(block.id);
1607
- if (result) out.push(piToolResultMessage(result, block.name));
1608
- else out.push(piToolResultMessage({
1609
- type: "tool_result",
1610
- callId: block.id,
1611
- output: SYNTHETIC_TOOL_RESULT_PLACEHOLDER,
1612
- isError: true
1613
- }, block.name));
1614
- }
1615
- if (assistantContent.length > 0) out.push(piAssistantMessage(assistantContent, modelId));
1616
- if (next?.role === "user") {
1617
- const userMessage = piUserMessage(next.content.filter((block) => {
1618
- if (block.type !== "tool_result") return true;
1619
- return false;
1620
- }));
1621
- if (userMessage) out.push(userMessage);
1622
- i++;
1623
- }
1624
- continue;
1625
- }
1626
- if (msg.content.filter((b) => b.type === "tool_result").length > 0) {
1627
- const userMsg = msg.role === "user" ? piUserMessage(msg.content.filter((b) => b.type !== "tool_result")) : null;
1628
- if (userMsg) out.push(userMsg);
1629
- continue;
1630
- }
1631
- if (msg.role === "user") {
1632
- const userMsg = piUserMessage(msg.content);
1633
- if (userMsg) out.push(userMsg);
1634
- continue;
1635
- }
1636
- out.push(piAssistantMessage(msg.content, modelId));
1637
- }
1638
- return out;
1639
- }
1640
- /**
1641
- * Map pi-ai's `StopReason` to the zidane `TurnFinishReason` union — the same
1642
- * native-stop-reason fidelity anthropic's `mapStopReason` / openai-compat's
1643
- * `mapOAIFinishReason` provide. Notably `length` (max-token truncation) is no
1644
- * longer misreported as a clean `stop`. `hasToolCalls` is the fallback for
1645
- * messages whose stop reason is missing or doesn't carry the tool-call signal.
1646
- */
1647
- function mapCodexStopReason(stopReason, hasToolCalls) {
1648
- switch (stopReason) {
1649
- case "toolUse": return "tool-calls";
1650
- case "length": return "length";
1651
- case "error": return "error";
1652
- case "stop": return hasToolCalls ? "tool-calls" : "stop";
1653
- case "aborted": return "other";
1654
- default: return hasToolCalls ? "tool-calls" : "stop";
1655
- }
1656
- }
1657
- function fromPiAssistantMessage(message) {
1658
- const content = [];
1659
- for (const block of message.content) if (block.type === "text") content.push({
1660
- type: "text",
1661
- text: block.text
1662
- });
1663
- else if (block.type === "thinking") {
1664
- const out = {
1665
- type: "thinking",
1666
- text: block.thinking
1667
- };
1668
- if (typeof block.thinkingSignature === "string") {
1669
- out.signature = block.thinkingSignature;
1670
- out.signatureProducer = "openai";
1671
- }
1672
- content.push(out);
1673
- } else if (block.type === "toolCall") content.push({
1674
- type: "tool_call",
1675
- id: block.id,
1676
- name: block.name,
1677
- input: block.arguments
1678
- });
1679
- return {
1680
- role: "assistant",
1681
- content
1682
- };
1683
- }
1684
- function extractToolCalls(message) {
1685
- return message.content.filter((block) => block.type === "toolCall").map((block) => ({
1686
- id: block.id,
1687
- name: block.name,
1688
- input: block.arguments
1689
- }));
1690
- }
1691
- function extractText(message) {
1692
- return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1693
- }
1694
- function toTurnUsage(usage, finishReason, modelId) {
1695
- return fillEstimatedCost({
1696
- input: usage.input,
1697
- output: usage.output,
1698
- cacheRead: usage.cacheRead || void 0,
1699
- cacheCreation: usage.cacheWrite || void 0,
1700
- cost: usage.cost.total || void 0,
1701
- ...finishReason ? { finishReason } : {},
1702
- modelId
1703
- }, "openai");
1704
- }
1705
- /**
1706
- * Transient pi-ai error patterns worth a loop-level retry. pi-ai already retries
1707
- * 429 / 5xx pre-stream 3× internally (see `openai-codex-responses.js`); by the
1708
- * time the error reaches us, that budget is exhausted. The loop-level retry
1709
- * with its longer backoff is a second line of defense for capacity events
1710
- * that persist past pi-ai's short retry window, plus mid-stream emissions
1711
- * (pi-ai surfaces those as plain `Error`s with no status).
1712
- *
1713
- * Mirrors pi-ai's own `isRetryableError` regex so the two retry layers
1714
- * recognize the same failure modes.
1715
- */
1716
- const TRANSIENT_OPENAI_MESSAGE_RE = /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused|gateway.?time.?out|temporarily.?unavailable/i;
1717
- /** Numeric HTTP status codes pi-ai sometimes attaches when bubbling structured errors. */
1718
- function isRetryableStatusCode(err) {
1719
- const status = err.status;
1720
- if (typeof status !== "number") return false;
1721
- return isRetryableHttpStatus(status);
1722
- }
1723
- /**
1724
- * Classify an OpenAI Codex error. pi-ai surfaces errors either as thrown `Error`s
1725
- * (wrapping `event.error.errorMessage`) or via stream event types.
1726
- *
1727
- * Retryable hint is set when pi-ai's own retry budget is exhausted on a
1728
- * transient failure — pattern-matched against `message` since pi-ai strips
1729
- * structured fields on plain-Error throws. Set `behavior.retry.maxAttempts: 1`
1730
- * to disable the loop-level retry.
1731
- */
1732
- function classifyOpenAIError(err) {
1733
- const prelude = classifyErrorPrelude(err);
1734
- if (prelude === "not-object") return null;
1735
- if (prelude === "aborted") return { kind: "aborted" };
1736
- const anyErr = err;
1737
- const message = anyErr.message ?? "";
1738
- const code = anyErr.code ?? anyErr.type;
1739
- if (code === "context_length_exceeded" || matchesContextExceeded(message)) return {
1740
- kind: "context_exceeded",
1741
- providerCode: code ?? "context_length_exceeded",
1742
- message
1743
- };
1744
- if (matchesToolPairingError(message)) return {
1745
- kind: "tool_pairing_corruption",
1746
- providerCode: code ?? "invalid_request_error",
1747
- message
1748
- };
1749
- if (message.length > 0) return {
1750
- kind: "provider_error",
1751
- providerCode: code,
1752
- message,
1753
- ...isRetryableStatusCode(anyErr) || TRANSIENT_OPENAI_MESSAGE_RE.test(message) ? { retryable: true } : {}
1754
- };
1755
- return null;
1756
- }
1757
- /**
1758
- * A standard OpenAI API key usable against `api.openai.com` (for the exact
1759
- * token-count endpoint), or `null` when only a Codex-OAuth credential is
1760
- * available. Codex OAuth access tokens are JWTs (`eyJ…`) and are NOT valid
1761
- * bearers for the standard API; a real key looks like `sk-…` (but not the
1762
- * Anthropic `sk-ant-…` form). Checks `params.apiKey` then `OPENAI_API_KEY`.
1763
- */
1764
- function pickStandardOpenAIKey(params) {
1765
- const candidates = [params?.apiKey, process.env.OPENAI_API_KEY];
1766
- for (const key of candidates) if (typeof key === "string" && key.startsWith("sk-") && !key.startsWith("sk-ant-")) return key;
1767
- return null;
1768
- }
1769
- /**
1770
- * Convert canonical {@link SessionMessage}s to OpenAI Responses-API `input`
1771
- * items for the token-count endpoint. Only the text-bearing shape is needed
1772
- * here (the count path sends a tiny dummy message); images/tool-results are
1773
- * down-converted to text so the count never throws on an exotic block.
1774
- */
1775
- function toResponsesInput(messages) {
1776
- return messages.map((msg) => {
1777
- const text = msg.content.map((b) => b.type === "text" ? b.text : "").filter(Boolean).join("\n");
1778
- const part = msg.role === "assistant" ? "output_text" : "input_text";
1779
- return {
1780
- role: msg.role === "assistant" ? "assistant" : "user",
1781
- content: [{
1782
- type: part,
1783
- text
1784
- }]
1785
- };
1786
- });
1787
- }
1788
- /** Convert pi-ai `PiTool` specs to Responses-API function tools. */
1789
- function toResponsesTools(tools) {
1790
- return tools.map((t) => {
1791
- const tool = t;
1792
- return {
1793
- type: "function",
1794
- name: tool.name,
1795
- description: tool.description,
1796
- parameters: tool.parameters
1797
- };
1798
- });
1799
- }
1800
- function applyPayloadOverrides(payload, options) {
1801
- const body = payload;
1802
- if (options.toolChoice) if (options.toolChoice.type === "tool" && options.toolChoice.name) body.tool_choice = {
1803
- type: "function",
1804
- name: options.toolChoice.name
1805
- };
1806
- else if (options.toolChoice.type === "required") body.tool_choice = "required";
1807
- else body.tool_choice = "auto";
1808
- return body;
1809
- }
1810
- function openai(params) {
1811
- const defaultModel = params?.defaultModel || DEFAULT_MODEL$2;
1812
- const baseCredentials = extractRuntimeCredentials(params);
1813
- let runtimeCredentials = baseCredentials ? {
1814
- ...baseCredentials,
1815
- ...params?.accountId ? { accountId: params.accountId } : {}
1816
- } : void 0;
1817
- return {
1818
- name: "openai",
1819
- meta: {
1820
- defaultModel,
1821
- isOAuth: true,
1822
- capabilities: {
1823
- vision: true,
1824
- imageInToolResult: true,
1825
- audio: false,
1826
- video: false
1827
- }
1828
- },
1829
- formatTools,
1830
- userMessage,
1831
- assistantMessage,
1832
- toolResultsMessage,
1833
- classifyError: classifyOpenAIError,
1834
- async countTokens(payload, signal) {
1835
- const standardKey = pickStandardOpenAIKey(params);
1836
- if (!standardKey) return null;
1837
- try {
1838
- const res = await fetch("https://api.openai.com/v1/responses/input_tokens", {
1839
- method: "POST",
1840
- headers: {
1841
- ...params?.extraHeaders,
1842
- "authorization": `Bearer ${standardKey}`,
1843
- "content-type": "application/json"
1844
- },
1845
- body: JSON.stringify({
1846
- model: payload.model || defaultModel,
1847
- instructions: renderSystemForWire(payload.system),
1848
- input: toResponsesInput(payload.messages),
1849
- tools: toResponsesTools(payload.tools)
1850
- }),
1851
- ...signal ? { signal } : {}
1852
- });
1853
- if (!res.ok) return null;
1854
- const json = await res.json();
1855
- return typeof json.input_tokens === "number" ? json.input_tokens : null;
1856
- } catch {
1857
- return null;
1858
- }
1859
- },
1860
- async stream(options, callbacks) {
1861
- const modelId = options.model || defaultModel;
1862
- const model = resolveModel(modelId);
1863
- const apiKey = await resolveOAuthApiKey({
1864
- provider: "openai",
1865
- providerId: PROVIDER_ID,
1866
- params: runtimeCredentials ? {
1867
- ...params,
1868
- ...runtimeCredentials
1869
- } : params,
1870
- envKey: "OPENAI_CODEX_API_KEY",
1871
- extraCredentialKeys: ["accountId"],
1872
- missingError: "No OpenAI Codex OAuth token found. Run `bun run auth --openai` first.",
1873
- refreshError: (reason) => `OpenAI Codex OAuth token refresh failed. Run \`bun run auth --openai\` again. ${reason}`
1874
- }, {
1875
- ...callbacks,
1876
- async onOAuthRefresh(ctx) {
1877
- if (ctx.source === "params") runtimeCredentials = {
1878
- access: ctx.credentials.access,
1879
- refresh: ctx.credentials.refresh,
1880
- expires: ctx.credentials.expires,
1881
- ...typeof ctx.credentials.accountId === "string" ? { accountId: ctx.credentials.accountId } : {}
1882
- };
1883
- await callbacks.onOAuthRefresh?.(ctx);
1884
- }
1885
- });
1886
- const context = {
1887
- systemPrompt: renderSystemForWire(options.system),
1888
- messages: toPiMessages(options.messages, modelId),
1889
- tools: options.tools
1890
- };
1891
- const reasoningLevel = options.thinking && options.thinking !== "off" && options.thinking !== "adaptive" ? options.thinking === "max" ? "xhigh" : options.thinking : void 0;
1892
- const stream = streamOpenAICodexResponses(model, context, {
1893
- apiKey,
1894
- maxTokens: options.maxTokens,
1895
- signal: options.signal,
1896
- transport: params?.transport,
1897
- reasoningEffort: reasoningLevel,
1898
- reasoningSummary: reasoningLevel ? "auto" : void 0,
1899
- ...options.modelOptions?.fast ? { serviceTier: "priority" } : {},
1900
- ...params?.extraHeaders ? { headers: params.extraHeaders } : {},
1901
- onPayload: (payload) => applyPayloadOverrides(payload, options)
1902
- });
1903
- let finalMessage;
1904
- let text = "";
1905
- let thinking = "";
1906
- for await (const event of stream) if (event.type === "text_delta") {
1907
- text += event.delta;
1908
- callbacks.onText(event.delta);
1909
- } else if (event.type === "thinking_delta") {
1910
- thinking += event.delta;
1911
- callbacks.onThinking?.(event.delta);
1912
- } else if (event.type === "thinking_end") {
1913
- const delta = event.content.startsWith(thinking) ? event.content.slice(thinking.length) : thinking ? "" : event.content;
1914
- if (delta) {
1915
- thinking += delta;
1916
- callbacks.onThinking?.(delta);
1917
- }
1918
- } else if (event.type === "done") finalMessage = event.message;
1919
- else if (event.type === "error") throw new Error(event.error.errorMessage || "OpenAI Codex API error");
1920
- finalMessage ??= await stream.result();
1921
- text ||= extractText(finalMessage);
1922
- const toolCalls = extractToolCalls(finalMessage);
1923
- const assistantTurn = fromPiAssistantMessage(finalMessage);
1924
- const finishReason = mapCodexStopReason(finalMessage.stopReason, toolCalls.length > 0);
1925
- return {
1926
- assistantMessage: assistantTurn,
1927
- text,
1928
- toolCalls,
1929
- done: toolCalls.length === 0,
1930
- usage: toTurnUsage(finalMessage.usage, finishReason, modelId)
1931
- };
1932
- }
1933
- };
1934
- }
1935
- //#endregion
1936
- //#region src/providers/openrouter.ts
1937
- const BASE_URL = "https://openrouter.ai/api/v1";
1938
- const DEFAULT_MODEL$1 = "anthropic/claude-sonnet-4.6";
1939
- const MODEL_ALIASES = { "anthropic/claude-sonnet-4-6": DEFAULT_MODEL$1 };
1940
- function getApiKey(params) {
1941
- if (typeof params?.apiKey === "string" && params.apiKey.length > 0) return params.apiKey;
1942
- if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
1943
- throw new Error("No OpenRouter API key found. Pass `apiKey` or set OPENROUTER_API_KEY in your environment.");
1944
- }
1945
- function normalizeOpenRouterModelId(modelId) {
1946
- return MODEL_ALIASES[modelId] ?? modelId;
1947
- }
1948
- /**
1949
- * OpenRouter provider.
1950
- *
1951
- * Thin wrapper around {@link openaiCompat} with OpenRouter-specific defaults
1952
- * (base URL, default model) and required attribution headers.
1953
- */
1954
- function openrouter(params) {
1955
- const base = openaiCompat({
1956
- name: "openrouter",
1957
- apiKey: getApiKey(params),
1958
- baseURL: BASE_URL,
1959
- defaultModel: normalizeOpenRouterModelId(params?.defaultModel || DEFAULT_MODEL$1),
1960
- extraHeaders: {
1961
- "HTTP-Referer": "https://github.com/Tahul/zidane",
1962
- "X-Title": "zidane"
1963
- },
1964
- capabilities: params?.capabilities ?? {
1965
- vision: true,
1966
- imageInToolResult: false,
1967
- documents: true
1968
- },
1969
- pdfEngine: params?.pdfEngine ?? "pdf-text",
1970
- cacheBreakpoints: true,
1971
- supportsReasoning: true
1972
- });
1973
- return {
1974
- ...base,
1975
- async stream(options, callbacks) {
1976
- return await base.stream({
1977
- ...options,
1978
- model: normalizeOpenRouterModelId(options.model)
1979
- }, callbacks);
1980
- }
1981
- };
1982
- }
1983
- //#endregion
1984
- //#region src/chat/oauth-page/xai.ts
1985
- /** pi-ai / zidane OAuth provider id. Also the credentials-file key. */
1986
- const XAI_OAUTH_PROVIDER_ID = "xai-oauth";
1987
- const DEFAULT_BASE_URL = "https://api.x.ai/v1";
1988
- const DISCOVERY_URL = `https://auth.x.ai/.well-known/openid-configuration`;
1989
- /** Public client id used by the Grok CLI OAuth surface. */
1990
- const DEFAULT_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
1991
- const DEFAULT_SCOPE = "openid profile email offline_access grok-cli:access api:access";
1992
- /** xAI matches the registered loopback IP exactly — `localhost` is rejected. */
1993
- const DEFAULT_CALLBACK_HOST = "127.0.0.1";
1994
- const DEFAULT_CALLBACK_PORT = 56121;
1995
- const CALLBACK_PATH = "/callback";
1996
- const PROVIDER_NAME = "xAI Grok";
1997
- /** Refresh slightly early so requests don't race expiry. */
1998
- const REFRESH_SKEW_MS = 12e4;
1999
- const CALLBACK_TIMEOUT_MS = 18e4;
2000
- function xaiOAuthClientId() {
2001
- return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;
2002
- }
2003
- function xaiOAuthScope() {
2004
- return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE;
2005
- }
2006
- function xaiOAuthCallbackHost() {
2007
- return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST;
2008
- }
2009
- function xaiOAuthCallbackPort() {
2010
- const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10);
2011
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT;
2012
- }
2013
- /** Resolve the xAI API base URL (env override → default). */
2014
- function xaiBaseUrl() {
2015
- return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
2016
- }
2017
- function base64Url(bytes) {
2018
- let binary = "";
2019
- for (const b of bytes) binary += String.fromCharCode(b);
2020
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2021
- }
2022
- function randomToken(byteLength = 16) {
2023
- return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)));
2024
- }
2025
- /**
2026
- * Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.
2027
- *
2028
- * The discovery doc is cached long-term in `credentials.json`. A single MITM
2029
- * during initial login could otherwise pin a malicious `token_endpoint` that
2030
- * receives every subsequent refresh token. Validating scheme + host keeps the
2031
- * endpoint on `x.ai` / `*.x.ai`.
2032
- */
2033
- function validateEndpoint(value, field) {
2034
- let url;
2035
- try {
2036
- url = new URL(value);
2037
- } catch {
2038
- throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`);
2039
- }
2040
- if (url.protocol !== "https:") throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`);
2041
- const host = url.hostname.toLowerCase();
2042
- if (host !== "x.ai" && !host.endsWith(".x.ai")) throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`);
2043
- return url.toString();
2044
- }
2045
- async function discover() {
2046
- let response;
2047
- try {
2048
- response = await fetch(DISCOVERY_URL, {
2049
- headers: { Accept: "application/json" },
2050
- signal: AbortSignal.timeout(15e3)
2051
- });
2052
- } catch (cause) {
2053
- throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`);
2054
- }
2055
- if (!response.ok) throw new Error(`xAI OIDC discovery returned ${response.status}`);
2056
- const payload = await response.json();
2057
- return {
2058
- authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ""), "authorization_endpoint"),
2059
- token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ""), "token_endpoint")
2060
- };
2061
- }
2062
- /**
2063
- * Start the loopback callback server on `127.0.0.1:56121`. Falls back to an
2064
- * OS-assigned port if 56121 is taken — xAI accepts any loopback port as long
2065
- * as the `redirect_uri` we send on the authorize URL matches the listener.
2066
- */
2067
- async function startXaiCallbackServer(renderPage) {
2068
- const callbackHost = xaiOAuthCallbackHost();
2069
- const callbackPort = xaiOAuthCallbackPort();
2070
- let settle;
2071
- let settled = false;
2072
- const callbackPromise = new Promise((resolve) => {
2073
- settle = (value) => {
2074
- if (settled) return;
2075
- settled = true;
2076
- resolve(value);
2077
- };
2078
- });
2079
- const server = createServer((req, res) => {
2080
- try {
2081
- const origin = req.headers.origin;
2082
- if (origin === "https://accounts.x.ai" || origin === "https://auth.x.ai") {
2083
- res.setHeader("Access-Control-Allow-Origin", origin);
2084
- res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
2085
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
2086
- res.setHeader("Access-Control-Allow-Private-Network", "true");
2087
- res.setHeader("Vary", "Origin");
2088
- }
2089
- if (req.method === "OPTIONS") {
2090
- res.statusCode = 204;
2091
- res.end();
2092
- return;
2093
- }
2094
- const url = new URL(req.url ?? "/", `http://${callbackHost}`);
2095
- if (url.pathname !== CALLBACK_PATH) {
2096
- res.statusCode = 404;
2097
- res.end("Not found");
2098
- return;
2099
- }
2100
- const result = {
2101
- code: url.searchParams.get("code") ?? void 0,
2102
- state: url.searchParams.get("state") ?? void 0,
2103
- error: url.searchParams.get("error") ?? void 0,
2104
- errorDescription: url.searchParams.get("error_description") ?? void 0
2105
- };
2106
- res.statusCode = result.error ? 400 : 200;
2107
- res.setHeader("Content-Type", "text/html; charset=utf-8");
2108
- res.end(renderPage(result.error ? {
2109
- kind: "error",
2110
- provider: PROVIDER_NAME,
2111
- message: `${PROVIDER_NAME} authentication did not complete.`,
2112
- details: result.errorDescription ?? result.error
2113
- } : {
2114
- kind: "success",
2115
- provider: PROVIDER_NAME,
2116
- message: "xAI authentication completed. You can close this window."
2117
- }));
2118
- settle?.(result);
2119
- } catch {
2120
- res.statusCode = 500;
2121
- res.end("Internal error");
2122
- }
2123
- });
2124
- const listen = (port) => new Promise((resolve, reject) => {
2125
- server.once("error", reject);
2126
- server.listen(port, callbackHost, () => {
2127
- server.removeListener("error", reject);
2128
- const addr = server.address();
2129
- resolve(typeof addr === "object" && addr ? addr.port : port);
2130
- });
2131
- });
2132
- let actualPort;
2133
- try {
2134
- actualPort = await listen(callbackPort);
2135
- } catch {
2136
- actualPort = await listen(0);
2137
- }
2138
- return {
2139
- redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,
2140
- waitForCallback: (timeoutMs) => Promise.race([callbackPromise, new Promise((resolve) => setTimeout(resolve, timeoutMs, {
2141
- error: "timeout",
2142
- errorDescription: "Timed out waiting for the xAI OAuth callback."
2143
- }))]),
2144
- close: () => {
2145
- try {
2146
- server.close();
2147
- } catch {}
2148
- }
2149
- };
2150
- }
2151
- function expiryFromPayload(payload) {
2152
- const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : Number(payload.expires_in ?? 3600);
2153
- return Date.now() + expiresIn * 1e3 - REFRESH_SKEW_MS;
2154
- }
2155
- async function exchangeCode(tokenEndpoint, code, redirectUri, verifier, discovery) {
2156
- const clientId = xaiOAuthClientId();
2157
- const response = await fetch(tokenEndpoint, {
2158
- method: "POST",
2159
- headers: {
2160
- "Content-Type": "application/x-www-form-urlencoded",
2161
- "Accept": "application/json"
2162
- },
2163
- body: new URLSearchParams({
2164
- grant_type: "authorization_code",
2165
- client_id: clientId,
2166
- code,
2167
- redirect_uri: redirectUri,
2168
- code_verifier: verifier
2169
- }),
2170
- signal: AbortSignal.timeout(3e4)
2171
- });
2172
- if (!response.ok) throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => "")}`);
2173
- const payload = await response.json();
2174
- const access = String(payload.access_token ?? "");
2175
- const refresh = String(payload.refresh_token ?? "");
2176
- if (!access) throw new Error("xAI token exchange did not return an access_token.");
2177
- if (!refresh) throw new Error("xAI token exchange did not return a refresh_token.");
2178
- return {
2179
- access,
2180
- refresh,
2181
- expires: expiryFromPayload(payload),
2182
- tokenEndpoint,
2183
- discovery
2184
- };
2185
- }
2186
- async function loginXai(renderPage, callbacks) {
2187
- const discovery = await discover();
2188
- const { verifier, challenge } = await generatePkce();
2189
- const state = randomToken();
2190
- const nonce = randomToken();
2191
- const server = await startXaiCallbackServer(renderPage);
2192
- try {
2193
- const authUrl = new URL(discovery.authorization_endpoint);
2194
- authUrl.searchParams.set("response_type", "code");
2195
- authUrl.searchParams.set("client_id", xaiOAuthClientId());
2196
- authUrl.searchParams.set("redirect_uri", server.redirectUri);
2197
- authUrl.searchParams.set("scope", xaiOAuthScope());
2198
- authUrl.searchParams.set("code_challenge", challenge);
2199
- authUrl.searchParams.set("code_challenge_method", "S256");
2200
- authUrl.searchParams.set("state", state);
2201
- authUrl.searchParams.set("nonce", nonce);
2202
- authUrl.searchParams.set("plan", "generic");
2203
- authUrl.searchParams.set("referrer", "zidane");
2204
- callbacks.onAuth({
2205
- url: authUrl.toString(),
2206
- instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`
2207
- });
2208
- const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS);
2209
- if (result.error) throw new Error(result.errorDescription ?? result.error);
2210
- if (result.state !== state) throw new Error("xAI OAuth state mismatch — possible CSRF. Please retry.");
2211
- if (!result.code) throw new Error("xAI OAuth callback did not include an authorization code.");
2212
- callbacks.onProgress?.("Exchanging authorization code for tokens...");
2213
- return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery);
2214
- } finally {
2215
- server.close();
2216
- }
2217
- }
2218
- async function refreshXaiToken(credentials) {
2219
- const xai = credentials;
2220
- const clientId = xaiOAuthClientId();
2221
- if (!credentials.refresh) throw new Error("xAI credentials are missing a refresh token — re-login required.");
2222
- const tokenEndpoint = xai.tokenEndpoint ?? xai.discovery?.token_endpoint ?? (await discover()).token_endpoint;
2223
- validateEndpoint(tokenEndpoint, "token_endpoint");
2224
- const response = await fetch(tokenEndpoint, {
2225
- method: "POST",
2226
- headers: {
2227
- "Content-Type": "application/x-www-form-urlencoded",
2228
- "Accept": "application/json"
2229
- },
2230
- body: new URLSearchParams({
2231
- grant_type: "refresh_token",
2232
- client_id: clientId,
2233
- refresh_token: credentials.refresh
2234
- }),
2235
- signal: AbortSignal.timeout(3e4)
2236
- });
2237
- if (!response.ok) throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => "")}`);
2238
- const payload = await response.json();
2239
- const access = String(payload.access_token ?? "");
2240
- if (!access) throw new Error("xAI token refresh did not return an access_token.");
2241
- return {
2242
- ...xai,
2243
- access,
2244
- refresh: String(payload.refresh_token ?? credentials.refresh),
2245
- expires: expiryFromPayload(payload),
2246
- tokenEndpoint
2247
- };
2248
- }
2249
- /**
2250
- * Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /
2251
- * Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.
2252
- *
2253
- * `usesCallbackServer: true` — the loopback flow has a real callback server,
2254
- * so the TUI doesn't need to prompt for a manual code paste.
2255
- */
2256
- function createXaiOAuthProvider(renderPage) {
2257
- return {
2258
- id: XAI_OAUTH_PROVIDER_ID,
2259
- name: "xAI Grok (SuperGrok / X Premium+)",
2260
- usesCallbackServer: true,
2261
- login: (callbacks) => loginXai(renderPage, callbacks),
2262
- refreshToken: refreshXaiToken,
2263
- getApiKey: (credentials) => credentials.access
2264
- };
2265
- }
2266
- //#endregion
2267
- //#region src/providers/xai.ts
2268
- const DEFAULT_MODEL = "grok-4.3";
2269
- function xai(params) {
2270
- const defaultModel = params?.defaultModel || DEFAULT_MODEL;
2271
- const baseURL = xaiBaseUrl();
2272
- const capabilities = params?.capabilities ?? {
2273
- vision: true,
2274
- imageInToolResult: false
2275
- };
2276
- const baseCredentials = extractRuntimeCredentials(params);
2277
- let runtimeCredentials = baseCredentials;
2278
- const staticKey = params?.apiKey ?? process.env.XAI_API_KEY;
2279
- const isOAuth = baseCredentials !== void 0 || !staticKey || isOAuthAccessToken(staticKey);
2280
- /**
2281
- * Build the openai-compat delegate for a resolved bearer. Cheap — the factory
2282
- * only wires closures — so we re-create it per turn with the freshly resolved
2283
- * (possibly refreshed) OAuth token rather than baking a stale key in once.
2284
- */
2285
- function delegateFor(apiKey) {
2286
- return openaiCompat({
2287
- name: "xai",
2288
- apiKey,
2289
- baseURL,
2290
- defaultModel,
2291
- capabilities,
2292
- extraHeaders: params?.extraHeaders
2293
- });
2294
- }
2295
- const skeleton = delegateFor("placeholder-resolved-at-stream");
2296
- return {
2297
- ...skeleton,
2298
- name: "xai",
2299
- meta: {
2300
- ...skeleton.meta,
2301
- defaultModel,
2302
- isOAuth
2303
- },
2304
- async stream(options, callbacks) {
2305
- return delegateFor(await resolveOAuthApiKey({
2306
- provider: "xai",
2307
- providerId: XAI_OAUTH_PROVIDER_ID,
2308
- params: runtimeCredentials ? {
2309
- ...params,
2310
- ...runtimeCredentials
2311
- } : params,
2312
- envKey: "XAI_API_KEY",
2313
- missingError: "No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.",
2314
- refreshError: (reason) => `xAI OAuth token refresh failed. Run \`bun run auth --xai\` again. ${reason}`
2315
- }, {
2316
- ...callbacks,
2317
- async onOAuthRefresh(ctx) {
2318
- if (ctx.source === "params") runtimeCredentials = {
2319
- access: ctx.credentials.access,
2320
- refresh: ctx.credentials.refresh,
2321
- expires: ctx.credentials.expires
2322
- };
2323
- await callbacks.onOAuthRefresh?.(ctx);
2324
- }
2325
- })).stream(options, callbacks);
2326
- }
2327
- };
2328
- }
2329
- //#endregion
2330
- export { local as a, generatePkce as c, planBasetenReasoning as d, arcee as f, FAST_MODE_OPTIONS as g, ANTHROPIC_EXTRA_MODELS as h, openai as i, cerebras as l, applyAnthropicCacheBreakpoints as m, createXaiOAuthProvider as n, cursor as o, anthropic as p, openrouter as r, createCursorOAuthProvider as s, xai as t, baseten as u };
2331
-
2332
- //# sourceMappingURL=providers-BMw1D_lj.js.map