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.
- package/dist/{acp-CjAoILwj.js → acp-DgKEJH3P.js} +7 -7
- package/dist/{acp-CjAoILwj.js.map → acp-DgKEJH3P.js.map} +1 -1
- package/dist/acp-cli.js +11 -6
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +1 -1
- package/dist/{tools-B32lZXLE.js → agent-B-cKanSC.js} +8035 -9459
- package/dist/agent-B-cKanSC.js.map +1 -0
- package/dist/{agent-DQIL5BFk.d.ts → agent-BAgOgS37.d.ts} +367 -208
- package/dist/agent-BAgOgS37.d.ts.map +1 -0
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +5 -0
- package/dist/anthropic-RGqsJlC8.js +807 -0
- package/dist/anthropic-RGqsJlC8.js.map +1 -0
- package/dist/{atomic-write-Bgtr5JPu.js → atomic-write-B7zr1ddF.js} +2 -2
- package/dist/{atomic-write-Bgtr5JPu.js.map → atomic-write-B7zr1ddF.js.map} +1 -1
- package/dist/{auth-DPrSP1YV.js → auth-C-ms5N2x.js} +33 -5
- package/dist/{auth-DPrSP1YV.js.map → auth-C-ms5N2x.js.map} +1 -1
- package/dist/cache-telemetry-BKAwsNxm.js +169 -0
- package/dist/cache-telemetry-BKAwsNxm.js.map +1 -0
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat/pure.js +1 -1
- package/dist/chat.d.ts +21 -7
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +30 -5
- package/dist/chat.js.map +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/docker.js +0 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/errors-CkTvg97v.d.ts +203 -0
- package/dist/errors-CkTvg97v.d.ts.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +3 -0
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +2 -2
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +406 -1
- package/dist/headless.js.map +1 -0
- package/dist/{index-YY3vp2F2.d.ts → index-CLuLFFLa.d.ts} +88 -418
- package/dist/index-CLuLFFLa.d.ts.map +1 -0
- package/dist/{index-C5qw3pj7.d.ts → index-CgBIl2mX.d.ts} +2 -2
- package/dist/{index-C5qw3pj7.d.ts.map → index-CgBIl2mX.d.ts.map} +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +113 -167
- package/dist/index.js.map +1 -1
- package/dist/{logger-C2ngE8CY.d.ts → logger-CJ-lpKPy.d.ts} +2 -2
- package/dist/{logger-C2ngE8CY.d.ts.map → logger-CJ-lpKPy.d.ts.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/messages-YZQLKaz2.js +1157 -0
- package/dist/messages-YZQLKaz2.js.map +1 -0
- package/dist/oauth-8LqbeI_Q.js +292 -0
- package/dist/oauth-8LqbeI_Q.js.map +1 -0
- package/dist/{messages-Iy_vEycX.js → openai-compat-0Y7jcncn.js} +27 -1379
- package/dist/openai-compat-0Y7jcncn.js.map +1 -0
- package/dist/output/stream-json.d.ts +2 -2
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +2 -2
- package/dist/{presets-B9nDwmaz.js → presets-Pr02ZNcy.js} +3 -2
- package/dist/{presets-B9nDwmaz.js.map → presets-Pr02ZNcy.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +210 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -0
- package/dist/providers/anthropic.js +2 -0
- package/dist/providers/arcee.d.ts +2 -0
- package/dist/providers/arcee.js +42 -0
- package/dist/providers/arcee.js.map +1 -0
- package/dist/providers/baseten.d.ts +2 -0
- package/dist/providers/baseten.js +66 -0
- package/dist/providers/baseten.js.map +1 -0
- package/dist/providers/cerebras.d.ts +2 -0
- package/dist/providers/cerebras.js +31 -0
- package/dist/providers/cerebras.js.map +1 -0
- package/dist/providers/local.d.ts +2 -0
- package/dist/providers/local.js +44 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/openai-compat.d.ts +2 -0
- package/dist/providers/openai-compat.js +2 -0
- package/dist/providers/openai.d.ts +2 -0
- package/dist/providers/openai.js +504 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +2 -0
- package/dist/providers/openrouter.js +52 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/xai.d.ts +2 -0
- package/dist/providers/xai.js +2 -0
- package/dist/providers-DgmdYGKm.js +168 -0
- package/dist/providers-DgmdYGKm.js.map +1 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +10 -2
- package/dist/restate.d.ts +1 -1
- package/dist/run-summary-DAnBZnrt.d.ts +249 -0
- package/dist/run-summary-DAnBZnrt.d.ts.map +1 -0
- package/dist/run-summary-nD3IQ4ZQ.js +253 -0
- package/dist/run-summary-nD3IQ4ZQ.js.map +1 -0
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-CiA1_8SB.js → session-CqrAFAKJ.js} +2 -2
- package/dist/{session-CiA1_8SB.js.map → session-CqrAFAKJ.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/system-prompt-BlsdjcUi.d.ts +139 -0
- package/dist/system-prompt-BlsdjcUi.d.ts.map +1 -0
- package/dist/timeout-CpFm0jJ6.d.ts +10 -0
- package/dist/timeout-CpFm0jJ6.d.ts.map +1 -0
- package/dist/timeout-kGGSXagK.js +84 -0
- package/dist/timeout-kGGSXagK.js.map +1 -0
- package/dist/{tool-formatters-w5oaoCS4.d.ts → tool-formatters-Ch8oRpYR.d.ts} +2 -2
- package/dist/{tool-formatters-w5oaoCS4.d.ts.map → tool-formatters-Ch8oRpYR.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools-00_Nqazv.js +1689 -0
- package/dist/tools-00_Nqazv.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +2 -1
- package/dist/{transcript-anchors-BUtOd-qa.js → transcript-anchors-BUmvPnis.js} +10 -9
- package/dist/{transcript-anchors-BUtOd-qa.js.map → transcript-anchors-BUmvPnis.js.map} +1 -1
- package/dist/{transcript-anchors-BJ-ESWnm.d.ts → transcript-anchors-CL9UM_M4.d.ts} +4 -4
- package/dist/{transcript-anchors-BJ-ESWnm.d.ts.map → transcript-anchors-CL9UM_M4.d.ts.map} +1 -1
- package/dist/tui.d.ts +3 -3
- package/dist/tui.js +38 -13
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-BSSQHNq1.d.ts → turn-operations-Cj6v_ypW.d.ts} +3 -3
- package/dist/{turn-operations-BSSQHNq1.d.ts.map → turn-operations-Cj6v_ypW.d.ts.map} +1 -1
- package/dist/{turn-operations-B_XeuNp-.js → turn-operations-tG0vuBNc.js} +2 -2
- package/dist/{turn-operations-B_XeuNp-.js.map → turn-operations-tG0vuBNc.js.map} +1 -1
- package/dist/types-CyVGdbia.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/xai-tPdbAGkq.js +379 -0
- package/dist/xai-tPdbAGkq.js.map +1 -0
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CHAT.md +4 -0
- package/docs/SKILL.md +31 -5
- package/package.json +63 -1
- package/dist/agent-DQIL5BFk.d.ts.map +0 -1
- package/dist/headless-2SH7dvoW.js +0 -656
- package/dist/headless-2SH7dvoW.js.map +0 -1
- package/dist/index-YY3vp2F2.d.ts.map +0 -1
- package/dist/messages-Iy_vEycX.js.map +0 -1
- package/dist/providers-BMw1D_lj.js +0 -2332
- package/dist/providers-BMw1D_lj.js.map +0 -1
- 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
|