@jacobbd/relay-ai 0.6.2 → 0.7.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.
@@ -0,0 +1,1854 @@
1
+ // src/config.ts
2
+ import { dirname, join as join2 } from "path";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
4
+
5
+ // src/paths.ts
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ var APP_DIR_NAME = "relay-ai";
9
+ var LEGACY_APP_DIR_NAME = "opencode-starter";
10
+ function userHome(env = process.env) {
11
+ return env.HOME ?? env.USERPROFILE ?? homedir();
12
+ }
13
+ function resolveAppHomeOverride(env = process.env) {
14
+ const override = env.RELAY_AI_HOME ?? env.OPENCODE_STARTER_HOME;
15
+ return override?.trim() || void 0;
16
+ }
17
+ function getAppHome(env = process.env) {
18
+ const override = resolveAppHomeOverride(env);
19
+ if (override) return override;
20
+ return join(userHome(env), `.${APP_DIR_NAME}`);
21
+ }
22
+ function getLegacyAppHome(env = process.env) {
23
+ return join(userHome(env), `.${LEGACY_APP_DIR_NAME}`);
24
+ }
25
+ function getConfigPath(env = process.env) {
26
+ return join(getAppHome(env), "config.json");
27
+ }
28
+ function getProvidersPath(env = process.env) {
29
+ return join(getAppHome(env), "providers.json");
30
+ }
31
+ function getSecretsPath(env = process.env) {
32
+ return join(getAppHome(env), "secrets.json");
33
+ }
34
+ function getLegacyConfPath(env = process.env, platform = process.platform) {
35
+ const home = userHome(env);
36
+ const appName = `${LEGACY_APP_DIR_NAME}-nodejs`;
37
+ if (platform === "darwin") {
38
+ return join(home, "Library", "Preferences", appName, "config.json");
39
+ }
40
+ if (platform === "win32") {
41
+ return join(env.APPDATA ?? join(home, "AppData", "Roaming"), appName, "Config", "config.json");
42
+ }
43
+ return join(env.XDG_CONFIG_HOME ?? join(home, ".config"), appName, "config.json");
44
+ }
45
+
46
+ // src/config.ts
47
+ function readJsonFile(path) {
48
+ try {
49
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
50
+ return parsed && typeof parsed === "object" ? parsed : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function ensureAppHomeMigrated() {
56
+ const configPath = getConfigPath();
57
+ if (existsSync(configPath)) return;
58
+ const legacyConfig = join2(getLegacyAppHome(), "config.json");
59
+ if (!existsSync(legacyConfig)) return;
60
+ mkdirSync(getAppHome(), { recursive: true, mode: 448 });
61
+ copyFileSync(legacyConfig, configPath);
62
+ const legacyVertex = join2(getLegacyAppHome(), "vertex-models.json");
63
+ const vertexPath = join2(getAppHome(), "vertex-models.json");
64
+ if (existsSync(legacyVertex) && !existsSync(vertexPath)) {
65
+ copyFileSync(legacyVertex, vertexPath);
66
+ }
67
+ }
68
+ function ensureConfigMigrated() {
69
+ ensureAppHomeMigrated();
70
+ const configPath = getConfigPath();
71
+ if (existsSync(configPath)) return;
72
+ const legacyPath = getLegacyConfPath();
73
+ if (!existsSync(legacyPath)) return;
74
+ const legacy = readJsonFile(legacyPath);
75
+ if (!legacy) return;
76
+ mkdirSync(dirname(configPath), { recursive: true, mode: 448 });
77
+ writeFileSync(configPath, `${JSON.stringify(legacy, null, 2)}
78
+ `, { encoding: "utf8", mode: 384 });
79
+ try {
80
+ renameSync(legacyPath, `${legacyPath}.migrated`);
81
+ } catch {
82
+ }
83
+ }
84
+ function readConfig() {
85
+ ensureConfigMigrated();
86
+ return readJsonFile(getConfigPath()) ?? {};
87
+ }
88
+ function loadPreferences() {
89
+ const config = readConfig();
90
+ const lastProvider = config.lastProvider === "opencode" ? "zen" : config.lastProvider;
91
+ return {
92
+ lastBackend: config.lastBackend,
93
+ lastModel: config.lastModel,
94
+ lastProvider,
95
+ lastCodexProvider: config.lastCodexProvider,
96
+ lastCodexModel: config.lastCodexModel,
97
+ lastGeminiProvider: config.lastGeminiProvider,
98
+ lastGeminiModel: config.lastGeminiModel,
99
+ lastAntigravityProvider: config.lastAntigravityProvider,
100
+ lastAntigravityModel: config.lastAntigravityModel,
101
+ lastClaudeTransparentMode: config.lastClaudeTransparentMode,
102
+ recentModelsByProvider: config.recentModelsByProvider,
103
+ favoriteModels: config.favoriteModels,
104
+ antigravityCliFavoriteModels: config.antigravityCliFavoriteModels,
105
+ antigravityCliFavoritesHintShown: config.antigravityCliFavoritesHintShown,
106
+ appPathOverrides: config.appPathOverrides,
107
+ recentLaunchFolders: config.recentLaunchFolders,
108
+ server: config.server
109
+ };
110
+ }
111
+
112
+ // src/provider-factory.ts
113
+ import { wrapLanguageModel, extractReasoningMiddleware } from "ai";
114
+
115
+ // src/constants.ts
116
+ import { homedir as homedir2 } from "os";
117
+ import { join as join3 } from "path";
118
+
119
+ // package.json
120
+ var package_default = {
121
+ name: "@jacobbd/relay-ai",
122
+ version: "0.7.0",
123
+ publishConfig: {
124
+ access: "public"
125
+ },
126
+ description: "Relay any model into any coding agent \u2014 launch Claude Code, Codex, and more with multi-provider gateways",
127
+ author: "jacob-bd",
128
+ license: "MIT",
129
+ repository: {
130
+ type: "git",
131
+ url: "git+https://github.com/jacob-bd/relay-ai.git"
132
+ },
133
+ homepage: "https://github.com/jacob-bd/relay-ai#readme",
134
+ keywords: [
135
+ "claude",
136
+ "claude-code",
137
+ "codex",
138
+ "ai",
139
+ "llm",
140
+ "cli",
141
+ "gateway",
142
+ "relay",
143
+ "vertex"
144
+ ],
145
+ type: "module",
146
+ bin: {
147
+ "relay-ai": "dist/cli.js"
148
+ },
149
+ files: [
150
+ "dist",
151
+ "README.md"
152
+ ],
153
+ engines: {
154
+ node: ">=18"
155
+ },
156
+ scripts: {
157
+ build: "tsup && tsup --config tsup.core.config.ts && node scripts/copy-ui-assets.mjs",
158
+ dev: "tsup --watch",
159
+ test: "vitest run",
160
+ "test:watch": "vitest",
161
+ typecheck: "tsc --noEmit",
162
+ "refresh:models-dev": "node scripts/refresh-models-dev-cache.mjs",
163
+ prepublishOnly: `node -e "if (require('./package.json').version !== require('./package-lock.json').version) { console.error('Error: package.json and package-lock.json versions are out of sync! Run npm install to sync.'); process.exit(1); }" && npm run build`
164
+ },
165
+ dependencies: {
166
+ "@ai-sdk/alibaba": "^1.0.26",
167
+ "@ai-sdk/amazon-bedrock": "^4.0.113",
168
+ "@ai-sdk/azure": "^3.0.70",
169
+ "@ai-sdk/cerebras": "^2.0.54",
170
+ "@ai-sdk/cohere": "^3.0.36",
171
+ "@ai-sdk/deepinfra": "^2.0.52",
172
+ "@ai-sdk/gateway": "^3.0.125",
173
+ "@ai-sdk/google": "^3.0.80",
174
+ "@ai-sdk/google-vertex": "^4.0.142",
175
+ "@ai-sdk/groq": "^3.0.39",
176
+ "@ai-sdk/mistral": "^3.0.37",
177
+ "@ai-sdk/openai": "^3.0.68",
178
+ "@ai-sdk/openai-compatible": "^2.0.48",
179
+ "@ai-sdk/perplexity": "^3.0.33",
180
+ "@ai-sdk/togetherai": "^2.0.53",
181
+ "@ai-sdk/vercel": "^2.0.50",
182
+ "@ai-sdk/xai": "^3.0.93",
183
+ "@clack/prompts": "^0.9.1",
184
+ "@openrouter/ai-sdk-provider": "^2.9.0",
185
+ ai: "^6.0.197",
186
+ "gitlab-ai-provider": "^6.8.0",
187
+ graphql: "^16.14.2",
188
+ "ipaddr.js": "^2.4.0",
189
+ "node-forge": "^1.4.0",
190
+ open: "^11.0.0",
191
+ picocolors: "^1.1.1",
192
+ "smol-toml": "^1.6.1",
193
+ "venice-ai-sdk-provider": "^2.0.2",
194
+ ws: "^8.21.0",
195
+ zod: "^3.25.76"
196
+ },
197
+ devDependencies: {
198
+ "@types/node": "^22.0.0",
199
+ "@types/node-forge": "^1.3.14",
200
+ "@types/ws": "^8.18.1",
201
+ "@vitest/coverage-v8": "^2.1.9",
202
+ tsup: "^8.0.0",
203
+ typescript: "^5.5.0",
204
+ vitest: "^2.0.0"
205
+ },
206
+ optionalDependencies: {
207
+ "@napi-rs/keyring": "^1.3.0"
208
+ },
209
+ overrides: {
210
+ ws: "^8.21.0"
211
+ },
212
+ exports: {
213
+ "./core": {
214
+ types: "./dist/core/index.d.ts",
215
+ import: "./dist/core/index.js",
216
+ default: "./dist/core/index.js"
217
+ },
218
+ "./package.json": "./package.json"
219
+ }
220
+ };
221
+
222
+ // src/constants.ts
223
+ var CODEX_RESPONSES_LITE_WS_URL = "wss://chatgpt.com/backend-api/codex/responses";
224
+ var CODEX_RESPONSES_LITE_VERSION = "0.144.1";
225
+ var CODEX_RESPONSES_WEBSOCKETS_BETA = "responses_websockets=2026-02-06";
226
+ var OPENCODE_CACHE_PATH = join3(homedir2(), ".cache", "opencode", "models.json");
227
+ var VERTEX_ANTHROPIC_NPM = "@ai-sdk/google-vertex/anthropic";
228
+ var VERSION = package_default.version;
229
+
230
+ // src/oauth/refresh-http.ts
231
+ async function postOAuthRefresh(url, body, options) {
232
+ const isJson = options.contentType === "json";
233
+ const response = await fetch(url, {
234
+ method: "POST",
235
+ headers: {
236
+ "Content-Type": isJson ? "application/json" : "application/x-www-form-urlencoded",
237
+ Accept: "application/json",
238
+ ...options.headers
239
+ },
240
+ body: isJson ? JSON.stringify(body) : body.toString()
241
+ });
242
+ if (!response.ok) {
243
+ const detail = options.includeBody ? await response.text().catch(() => "") : "";
244
+ const status = options.includeStatus ? ` (${response.status})` : "";
245
+ throw new Error(`${options.errorPrefix}${status}${detail ? `: ${detail}` : ""}`);
246
+ }
247
+ return response.json();
248
+ }
249
+
250
+ // src/oauth/openai.ts
251
+ var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
252
+ var ISSUER = "https://auth.openai.com";
253
+ var DEVICE_CODE_DEFAULT_EXPIRES_MS = 5 * 60 * 1e3;
254
+ function extractOpenAiAccountId(tokens) {
255
+ const token = tokens.id_token ?? tokens.access_token;
256
+ if (!token) return void 0;
257
+ const parts = token.split(".");
258
+ if (parts.length !== 3) return void 0;
259
+ try {
260
+ const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString());
261
+ return claims.chatgpt_account_id ?? claims["https://api.openai.com/auth"]?.chatgpt_account_id ?? claims.organizations?.[0]?.id;
262
+ } catch {
263
+ return void 0;
264
+ }
265
+ }
266
+ async function refreshOpenAiAccessToken(refreshToken) {
267
+ return postOAuthRefresh(
268
+ `${ISSUER}/oauth/token`,
269
+ new URLSearchParams({
270
+ grant_type: "refresh_token",
271
+ refresh_token: refreshToken,
272
+ client_id: CLIENT_ID
273
+ }),
274
+ {
275
+ contentType: "form",
276
+ errorPrefix: "OpenAI token refresh failed",
277
+ includeStatus: true
278
+ }
279
+ );
280
+ }
281
+
282
+ // src/oauth/responses-websocket.ts
283
+ var RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
284
+ var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set(["response.completed", "response.failed", "response.incomplete"]);
285
+ function toHeaderRecord(headers) {
286
+ const out = {};
287
+ if (!headers) return out;
288
+ if (headers instanceof Headers) {
289
+ headers.forEach((value, key) => {
290
+ out[key] = value;
291
+ });
292
+ } else if (Array.isArray(headers)) {
293
+ for (const [key, value] of headers) out[key] = value;
294
+ } else {
295
+ for (const [key, value] of Object.entries(headers)) out[key] = String(value);
296
+ }
297
+ return out;
298
+ }
299
+ function hasResponsesLiteHeader(headers) {
300
+ return Object.entries(headers).some(
301
+ ([k, v]) => k.toLowerCase() === RESPONSES_LITE_HEADER && v.toLowerCase() === "true"
302
+ );
303
+ }
304
+ function bodyToString(body) {
305
+ if (body == null) return "";
306
+ if (typeof body === "string") return body;
307
+ if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
308
+ if (body instanceof ArrayBuffer) return Buffer.from(new Uint8Array(body)).toString("utf8");
309
+ return String(body);
310
+ }
311
+ function applyResponsesLiteShape(payload) {
312
+ const reasoning = payload.reasoning && typeof payload.reasoning === "object" ? { ...payload.reasoning } : {};
313
+ reasoning.context = "all_turns";
314
+ return {
315
+ ...payload,
316
+ reasoning,
317
+ parallel_tool_calls: false,
318
+ store: false
319
+ };
320
+ }
321
+ function createResponsesWebSocketFetch(wsUrl, log) {
322
+ const debug = (msg) => {
323
+ try {
324
+ log?.(`ws: ${msg}`);
325
+ } catch {
326
+ }
327
+ };
328
+ return async (_input, init) => {
329
+ const { WebSocket } = await import("ws");
330
+ const headers = toHeaderRecord(init?.headers);
331
+ headers["OpenAI-Beta"] = CODEX_RESPONSES_WEBSOCKETS_BETA;
332
+ debug(`connecting ${wsUrl} headers=[${Object.keys(headers).join(", ")}]`);
333
+ let payload = {};
334
+ try {
335
+ payload = JSON.parse(bodyToString(init?.body));
336
+ } catch {
337
+ payload = {};
338
+ }
339
+ if (hasResponsesLiteHeader(headers)) {
340
+ payload = applyResponsesLiteShape(payload);
341
+ }
342
+ const outgoing = JSON.stringify({ type: "response.create", ...payload });
343
+ const encoder = new TextEncoder();
344
+ let socket;
345
+ let frameCount = 0;
346
+ const stream = new ReadableStream({
347
+ start(controller) {
348
+ let closed = false;
349
+ const close = () => {
350
+ if (closed) return;
351
+ closed = true;
352
+ try {
353
+ controller.close();
354
+ } catch {
355
+ }
356
+ try {
357
+ socket.close();
358
+ } catch {
359
+ }
360
+ };
361
+ const fail = (message) => {
362
+ if (closed) return;
363
+ debug(`fail: ${message}`);
364
+ try {
365
+ controller.enqueue(encoder.encode(
366
+ `data: ${JSON.stringify({ type: "error", error: { message } })}
367
+
368
+ `
369
+ ));
370
+ } catch {
371
+ }
372
+ close();
373
+ };
374
+ socket = new WebSocket(wsUrl, { headers });
375
+ socket.on("open", () => {
376
+ debug(`open \u2014 sending ${outgoing.length}B payload`);
377
+ socket.send(outgoing);
378
+ });
379
+ socket.on("unexpected-response", (_req, res) => {
380
+ debug(`unexpected-response status=${res.statusCode}`);
381
+ });
382
+ socket.on("message", (data) => {
383
+ const text = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
384
+ frameCount += 1;
385
+ if (frameCount <= 3) debug(`frame#${frameCount}: ${text.slice(0, 200)}`);
386
+ let event;
387
+ try {
388
+ event = JSON.parse(text);
389
+ } catch {
390
+ controller.enqueue(encoder.encode(`data: ${text.replace(/\r?\n/g, " ")}
391
+
392
+ `));
393
+ return;
394
+ }
395
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}
396
+
397
+ `));
398
+ const type = event.type;
399
+ if (typeof type === "string" && TERMINAL_EVENT_TYPES.has(type)) {
400
+ debug(`terminal event: ${type} (after ${frameCount} frames)`);
401
+ close();
402
+ }
403
+ });
404
+ socket.on("error", (err) => fail(err.message));
405
+ socket.on("close", (code, reason) => {
406
+ debug(`close code=${code} frames=${frameCount}${reason?.length ? ` reason=${reason.toString("utf8").slice(0, 200)}` : ""}`);
407
+ if (closed) return;
408
+ if (code === 1e3 || code === 1005) {
409
+ close();
410
+ return;
411
+ }
412
+ fail(`WebSocket closed (${code})${reason?.length ? `: ${reason.toString("utf8")}` : ""}`);
413
+ });
414
+ const signal = init?.signal;
415
+ if (signal) {
416
+ if (signal.aborted) {
417
+ close();
418
+ return;
419
+ }
420
+ signal.addEventListener("abort", close, { once: true });
421
+ }
422
+ },
423
+ cancel() {
424
+ try {
425
+ socket?.close();
426
+ } catch {
427
+ }
428
+ }
429
+ });
430
+ return new Response(stream, {
431
+ status: 200,
432
+ headers: { "content-type": "text/event-stream; charset=utf-8" }
433
+ });
434
+ };
435
+ }
436
+
437
+ // src/oauth/claude-identity.ts
438
+ import { createHash, randomUUID } from "crypto";
439
+ var CLAUDE_CODE_CLI_VERSION = "2.1.195";
440
+ var CLAUDE_CODE_USER_AGENT = `claude-cli/${CLAUDE_CODE_CLI_VERSION} (external, cli)`;
441
+ var CLAUDE_CODE_ENTRYPOINT = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
442
+ var sessionCache = /* @__PURE__ */ new Map();
443
+ function getOrCreateSessionId(seed) {
444
+ let id = sessionCache.get(seed);
445
+ if (!id) {
446
+ id = randomUUID();
447
+ sessionCache.set(seed, id);
448
+ }
449
+ return id;
450
+ }
451
+ function uuidFromHash(input) {
452
+ const h = createHash("sha256").update(input).digest("hex");
453
+ return [
454
+ h.slice(0, 8),
455
+ h.slice(8, 12),
456
+ "4" + h.slice(13, 16),
457
+ (parseInt(h[16], 16) & 3 | 8).toString(16) + h.slice(17, 20),
458
+ h.slice(20, 32)
459
+ ].join("-");
460
+ }
461
+ var HEX64_RE = /^[a-f0-9]{64}$/i;
462
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
463
+ function resolveCliUserID(providerData, seed) {
464
+ const v = providerData?.cliUserID;
465
+ if (typeof v === "string" && HEX64_RE.test(v)) return v;
466
+ return createHash("sha256").update(`cliUserID:${seed}`).digest("hex");
467
+ }
468
+ function resolveAccountUUID(providerData, seed) {
469
+ const v = providerData?.accountUUID;
470
+ if (typeof v === "string" && UUID_RE.test(v)) return v;
471
+ return uuidFromHash(`account:${seed}`);
472
+ }
473
+ function buildUserIdJson(deviceId, accountUUID, sessionId) {
474
+ return JSON.stringify({ device_id: deviceId, account_uuid: accountUUID, session_id: sessionId });
475
+ }
476
+ function injectClaudeIdentity(body, providerData, seed) {
477
+ const deviceId = resolveCliUserID(providerData, seed);
478
+ const accountUUID = resolveAccountUUID(providerData, seed);
479
+ const sessionId = getOrCreateSessionId(seed);
480
+ const userId = buildUserIdJson(deviceId, accountUUID, sessionId);
481
+ const existing = body.metadata;
482
+ body.metadata = { ...existing ?? {}, user_id: userId };
483
+ return { sessionId, userId };
484
+ }
485
+
486
+ // src/provider-factory.ts
487
+ var RESPONSES_ONLY_PREFIXES = [
488
+ "gpt-5-codex",
489
+ "gpt-5-pro",
490
+ "gpt-5.2-pro",
491
+ "o3",
492
+ "o4"
493
+ ];
494
+ var factoryCache = /* @__PURE__ */ new Map();
495
+ function modelPrefersResponsesApi(modelId) {
496
+ const lower = modelId.toLowerCase();
497
+ if (RESPONSES_ONLY_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(`${prefix}-`))) {
498
+ return true;
499
+ }
500
+ const gpt5Minor = lower.match(/^gpt-5\.(\d+)(?:-|$)/);
501
+ if (gpt5Minor && Number(gpt5Minor[1]) >= 4) return true;
502
+ if (lower.startsWith("gpt-") && lower.includes("-codex")) return true;
503
+ if (lower.startsWith("grok-") && (lower.includes("multi-agent") || lower.includes("multiagent"))) return true;
504
+ return false;
505
+ }
506
+ var OPENAI_CHAT_COMPLETIONS_ONLY = [
507
+ "davinci-002",
508
+ "babbage-002",
509
+ "gpt-3.5-turbo-instruct"
510
+ ];
511
+ function shouldUseOpenAiResponsesEndpoint(modelId) {
512
+ return !OPENAI_CHAT_COMPLETIONS_ONLY.includes(modelId.toLowerCase());
513
+ }
514
+ function findCreateFactory(mod) {
515
+ for (const value of Object.values(mod)) {
516
+ if (typeof value === "function" && value.name.startsWith("create")) {
517
+ return value;
518
+ }
519
+ }
520
+ throw new Error("No create* factory export found in provider package");
521
+ }
522
+ async function loadSdkProviderFactory(npm) {
523
+ let cached = factoryCache.get(npm);
524
+ if (!cached) {
525
+ cached = (async () => {
526
+ try {
527
+ const mod = await import(npm);
528
+ return findCreateFactory(mod);
529
+ } catch (err) {
530
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
531
+ if (code === "ERR_MODULE_NOT_FOUND") {
532
+ throw new Error(`SDK provider package not installed: ${npm}. Run: npm install ${npm}`);
533
+ }
534
+ throw err;
535
+ }
536
+ })();
537
+ factoryCache.set(npm, cached);
538
+ cached.catch(() => factoryCache.delete(npm));
539
+ }
540
+ return cached;
541
+ }
542
+ async function createLanguageModel(spec) {
543
+ const { npm, modelId, apiKey, baseURL } = spec;
544
+ if (npm === VERTEX_ANTHROPIC_NPM) {
545
+ if (!spec.vertex?.project) {
546
+ throw new Error("Vertex project is required for @ai-sdk/google-vertex/anthropic");
547
+ }
548
+ const { createVertexAnthropic } = await import("@ai-sdk/google-vertex/anthropic");
549
+ const vertex = createVertexAnthropic({
550
+ project: spec.vertex.project,
551
+ location: spec.vertex.location
552
+ });
553
+ return vertex(modelId);
554
+ }
555
+ if (npm === "@ai-sdk/openai") {
556
+ const { createOpenAI } = await import("@ai-sdk/openai");
557
+ const accountId = spec.authType === "oauth" ? spec.oauthAccountId ?? extractOpenAiAccountId({ access_token: apiKey }) : void 0;
558
+ const oauthOptions = spec.authType === "oauth" ? {
559
+ apiKey,
560
+ baseURL: "https://chatgpt.com/backend-api/codex",
561
+ headers: {
562
+ ...accountId ? { "ChatGPT-Account-Id": accountId } : {},
563
+ originator: "relay-ai",
564
+ // Responses-Lite models (backend prefer_websockets/use_responses_lite,
565
+ // e.g. gpt-5.6-luna) require these on the request.
566
+ ...spec.useResponsesLite ? { version: CODEX_RESPONSES_LITE_VERSION, "x-openai-internal-codex-responses-lite": "true" } : {}
567
+ },
568
+ // Models the backend flags with prefer_websockets are only served over
569
+ // the WebSocket Responses transport, not HTTP.
570
+ ...spec.preferWebSockets ? { fetch: createResponsesWebSocketFetch(CODEX_RESPONSES_LITE_WS_URL, spec.onDebug) } : {}
571
+ } : { apiKey };
572
+ const openai = createOpenAI(oauthOptions);
573
+ return shouldUseOpenAiResponsesEndpoint(modelId) ? openai.responses(modelId) : openai.chat(modelId);
574
+ }
575
+ if (npm === "@ai-sdk/xai") {
576
+ const { createXai } = await import("@ai-sdk/xai");
577
+ const xai = createXai({ apiKey });
578
+ return modelPrefersResponsesApi(modelId) ? xai.responses(modelId) : xai(modelId);
579
+ }
580
+ if (npm === "@ai-sdk/google") {
581
+ const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
582
+ const google = createGoogleGenerativeAI({ apiKey });
583
+ return google(modelId);
584
+ }
585
+ if (npm === "@ai-sdk/anthropic") {
586
+ const { createAnthropic } = await import("@ai-sdk/anthropic");
587
+ const root = baseURL?.replace(/\/v1\/?$/, "").replace(/\/$/, "");
588
+ const anthropicOptions = spec.authType === "oauth" ? {
589
+ authToken: apiKey,
590
+ ...spec.providerId === "claude-code" ? {
591
+ headers: {
592
+ "User-Agent": CLAUDE_CODE_USER_AGENT,
593
+ "x-app": "cli",
594
+ "X-Claude-Code-Session-Id": injectClaudeIdentity(
595
+ {},
596
+ spec.providerData,
597
+ spec.oauthAccountId ?? apiKey
598
+ ).sessionId
599
+ }
600
+ } : {}
601
+ } : { apiKey };
602
+ if (spec.headers) {
603
+ anthropicOptions.headers = { ...anthropicOptions.headers, ...spec.headers };
604
+ }
605
+ if (!root || root === "https://api.anthropic.com") {
606
+ return createAnthropic(anthropicOptions)(modelId);
607
+ }
608
+ const sdkBase = baseURL.endsWith("/v1") ? baseURL : `${root}/v1`;
609
+ return createAnthropic({ ...anthropicOptions, baseURL: sdkBase })(modelId);
610
+ }
611
+ let model;
612
+ if (npm === "@ai-sdk/openai-compatible") {
613
+ const { createOpenAICompatible } = await import("@ai-sdk/openai-compatible");
614
+ const options = {
615
+ name: spec.providerId ?? "openai-compatible",
616
+ baseURL: baseURL ?? "",
617
+ ...apiKey.trim() ? { apiKey } : {},
618
+ ...spec.headers ? { headers: spec.headers } : {}
619
+ };
620
+ model = createOpenAICompatible({
621
+ ...options
622
+ })(modelId);
623
+ } else if (npm === "@openrouter/ai-sdk-provider") {
624
+ const { createOpenRouter } = await import("@openrouter/ai-sdk-provider");
625
+ model = createOpenRouter({ apiKey, baseURL, ...spec.headers ? { headers: spec.headers } : {} })(modelId);
626
+ } else {
627
+ const create = await loadSdkProviderFactory(npm);
628
+ const provider = create({
629
+ apiKey,
630
+ ...baseURL ? { baseURL } : {},
631
+ ...spec.headers ? { headers: spec.headers } : {}
632
+ });
633
+ model = provider(modelId);
634
+ }
635
+ const isReasoning = modelId.toLowerCase().match(/deepseek-r1|think|reasoning|qwq/);
636
+ if (isReasoning) {
637
+ return wrapLanguageModel({
638
+ model,
639
+ middleware: [extractReasoningMiddleware({ tagName: "think" })]
640
+ });
641
+ }
642
+ return model;
643
+ }
644
+ var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
645
+ var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
646
+ var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
647
+ var MISTRAL_EFFORT_LEVELS = ["high", "off"];
648
+ var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
649
+ var OPENROUTER_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
650
+ var DEEPSEEK_EFFORT_LEVELS = ["high", "max", "off"];
651
+ var GLM_52_EFFORT_LEVELS = ["high", "xhigh"];
652
+ var EMPTY_REASONING = {
653
+ levels: [],
654
+ defaultLevel: "",
655
+ supportsSummaries: false,
656
+ mode: "none",
657
+ source: "none",
658
+ confidence: "inferred"
659
+ };
660
+ function isClaudeReasoningModel(modelId) {
661
+ const lower = modelId.toLowerCase();
662
+ if (!lower.startsWith("claude-")) return false;
663
+ if (lower.includes("fable") || lower.includes("mythos")) return true;
664
+ const m = lower.match(/claude-(?:opus|sonnet|haiku)-(\d+)-(\d+)/);
665
+ if (!m) return false;
666
+ const major = Number(m[1]);
667
+ const minor = Number(m[2]);
668
+ return major > 4 || major === 4 && minor >= 6;
669
+ }
670
+ function isGeminiReasoningModel(modelId) {
671
+ const lower = modelId.toLowerCase();
672
+ return lower.startsWith("gemini-2.5-") || lower.startsWith("gemini-3") || lower.startsWith("gemini-3.");
673
+ }
674
+ function isMistralReasoningModel(modelId) {
675
+ const lower = modelId.toLowerCase();
676
+ return lower.startsWith("mistral-") || lower.startsWith("magistral-") || lower.startsWith("ministral-") || lower.includes("reasoning");
677
+ }
678
+ function isXaiReasoningEffortModel(modelId) {
679
+ const lower = modelId.toLowerCase();
680
+ if (lower.includes("non-reasoning")) return false;
681
+ if (lower.startsWith("grok-build")) return false;
682
+ if (lower.startsWith("grok-imagine")) return false;
683
+ if (modelPrefersResponsesApi(modelId)) return true;
684
+ if (lower === "grok-4.3" || lower.startsWith("grok-4.3-")) return true;
685
+ if (lower === "grok-4.5" || lower.startsWith("grok-4.5-")) return true;
686
+ if (lower.includes("-reasoning")) return true;
687
+ return false;
688
+ }
689
+ function xaiDefaultReasoningEffort(modelId) {
690
+ const lower = modelId.toLowerCase();
691
+ if (lower === "grok-4.5" || lower.startsWith("grok-4.5-")) return "high";
692
+ return "low";
693
+ }
694
+ function isDeepSeekReasoningModel(modelId) {
695
+ const lower = modelId.toLowerCase();
696
+ return lower === "deepseek-v4-flash" || lower === "deepseek-v4-pro" || lower.startsWith("deepseek-v4-flash-") || lower.startsWith("deepseek-v4-pro-") || lower === "deepseek-reasoner" || lower === "deepseek-chat";
697
+ }
698
+ function isKimiReasoningModel(modelId) {
699
+ const lower = modelId.toLowerCase();
700
+ return lower.startsWith("kimi-");
701
+ }
702
+ function isGlm52ReasoningModel(modelId) {
703
+ const lower = modelId.toLowerCase();
704
+ return lower === "glm-5.2" || lower === "z-ai/glm-5.2" || lower === "zai/glm-5.2" || lower === "zai-org/glm-5.2" || lower === "zai-org/glm5.2" || lower === "glm5.2";
705
+ }
706
+ function hasSupportedParameter(metadata, param) {
707
+ return (metadata?.supportedParameters ?? []).some((p) => p === param);
708
+ }
709
+ function isOpenRouterRoute(npm, metadata) {
710
+ return npm === "@openrouter/ai-sdk-provider" || metadata?.providerId === "openrouter" || metadata?.apiBaseUrl?.includes("openrouter.ai") === true;
711
+ }
712
+ function openRouterReasoningCapabilities(metadata) {
713
+ if (metadata?.supportedParameters && !hasSupportedParameter(metadata, "reasoning")) {
714
+ return {
715
+ ...EMPTY_REASONING,
716
+ source: "provider-metadata",
717
+ confidence: "documented"
718
+ };
719
+ }
720
+ if (hasSupportedParameter(metadata, "reasoning")) {
721
+ return {
722
+ levels: [...OPENROUTER_EFFORT_LEVELS],
723
+ defaultLevel: "medium",
724
+ supportsSummaries: false,
725
+ mode: "controllable",
726
+ source: "provider-metadata",
727
+ confidence: "documented",
728
+ wireFormat: { kind: "openrouter-reasoning" }
729
+ };
730
+ }
731
+ if (metadata?.reasoning) {
732
+ return {
733
+ ...EMPTY_REASONING,
734
+ mode: "internal-only",
735
+ source: "model-metadata",
736
+ confidence: "inferred"
737
+ };
738
+ }
739
+ return EMPTY_REASONING;
740
+ }
741
+ function getReasoningCapabilities(npm, modelId, metadata) {
742
+ const id = modelId.toLowerCase();
743
+ if (isOpenRouterRoute(npm, metadata)) {
744
+ return openRouterReasoningCapabilities(metadata);
745
+ }
746
+ if (npm === "@ai-sdk/anthropic" || id.startsWith("claude-")) {
747
+ const isClaude = isClaudeReasoningModel(modelId);
748
+ if (isClaude || metadata?.reasoning) {
749
+ return {
750
+ levels: [...ANTHROPIC_EFFORT_LEVELS],
751
+ defaultLevel: "high",
752
+ supportsSummaries: true,
753
+ mode: "controllable",
754
+ source: isClaude ? "provider-rule" : "model-metadata",
755
+ confidence: isClaude ? "documented" : "inferred",
756
+ wireFormat: { kind: "anthropic-thinking" }
757
+ };
758
+ }
759
+ return EMPTY_REASONING;
760
+ }
761
+ if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
762
+ const prefersResponses = modelPrefersResponsesApi(modelId);
763
+ if (prefersResponses || metadata?.reasoning) {
764
+ return {
765
+ levels: [...OPENAI_EFFORT_LEVELS],
766
+ defaultLevel: "medium",
767
+ supportsSummaries: true,
768
+ mode: "controllable",
769
+ source: prefersResponses ? "provider-rule" : "model-metadata",
770
+ confidence: prefersResponses ? "documented" : "inferred",
771
+ wireFormat: { kind: "openai-reasoning-effort" }
772
+ };
773
+ }
774
+ return EMPTY_REASONING;
775
+ }
776
+ if (npm === "@ai-sdk/google" || id.startsWith("gemini-")) {
777
+ if (isGeminiReasoningModel(modelId)) {
778
+ return {
779
+ levels: [...GEMINI_EFFORT_LEVELS],
780
+ defaultLevel: "medium",
781
+ supportsSummaries: true,
782
+ mode: "controllable",
783
+ source: "provider-rule",
784
+ confidence: "documented",
785
+ wireFormat: { kind: "google-thinking-config" }
786
+ };
787
+ }
788
+ return EMPTY_REASONING;
789
+ }
790
+ if (npm === "@ai-sdk/mistral") {
791
+ if (isMistralReasoningModel(modelId)) {
792
+ return {
793
+ levels: [...MISTRAL_EFFORT_LEVELS],
794
+ defaultLevel: "high",
795
+ supportsSummaries: false,
796
+ mode: "controllable",
797
+ source: "provider-rule",
798
+ confidence: "documented",
799
+ wireFormat: { kind: "mistral-reasoning-effort" }
800
+ };
801
+ }
802
+ return EMPTY_REASONING;
803
+ }
804
+ if (npm === "@ai-sdk/xai") {
805
+ if (isXaiReasoningEffortModel(modelId)) {
806
+ const levels = modelPrefersResponsesApi(modelId) ? ["low", "medium", "high", "xhigh"] : [...XAI_EFFORT_LEVELS];
807
+ return {
808
+ levels,
809
+ defaultLevel: xaiDefaultReasoningEffort(modelId),
810
+ supportsSummaries: true,
811
+ mode: "controllable",
812
+ source: "provider-rule",
813
+ confidence: "documented",
814
+ wireFormat: { kind: "openai-reasoning-effort" }
815
+ };
816
+ }
817
+ return EMPTY_REASONING;
818
+ }
819
+ if (isDeepSeekReasoningModel(modelId)) {
820
+ return {
821
+ levels: [...DEEPSEEK_EFFORT_LEVELS],
822
+ defaultLevel: "high",
823
+ supportsSummaries: true,
824
+ mode: "controllable",
825
+ source: "provider-rule",
826
+ confidence: "documented",
827
+ wireFormat: { kind: "deepseek-thinking" }
828
+ };
829
+ }
830
+ if (isKimiReasoningModel(modelId)) {
831
+ return {
832
+ levels: [...OPENAI_EFFORT_LEVELS],
833
+ defaultLevel: "high",
834
+ supportsSummaries: false,
835
+ mode: "controllable",
836
+ source: "provider-rule",
837
+ confidence: "documented",
838
+ wireFormat: { kind: "openai-reasoning-effort" }
839
+ };
840
+ }
841
+ if (isGlm52ReasoningModel(modelId)) {
842
+ return {
843
+ levels: [...GLM_52_EFFORT_LEVELS],
844
+ defaultLevel: "high",
845
+ supportsSummaries: false,
846
+ mode: "controllable",
847
+ source: "provider-rule",
848
+ confidence: "documented",
849
+ wireFormat: { kind: "openai-reasoning-effort" }
850
+ };
851
+ }
852
+ if (hasSupportedParameter(metadata, "reasoning_effort")) {
853
+ return {
854
+ levels: ["low", "medium", "high", "xhigh"],
855
+ defaultLevel: "medium",
856
+ supportsSummaries: false,
857
+ mode: "controllable",
858
+ source: "provider-metadata",
859
+ confidence: "documented",
860
+ wireFormat: { kind: "openai-reasoning-effort" }
861
+ };
862
+ }
863
+ if (hasSupportedParameter(metadata, "reasoning")) {
864
+ return {
865
+ levels: [...OPENROUTER_EFFORT_LEVELS],
866
+ defaultLevel: "medium",
867
+ supportsSummaries: false,
868
+ mode: "controllable",
869
+ source: "provider-metadata",
870
+ confidence: "documented",
871
+ wireFormat: { kind: "openrouter-reasoning" }
872
+ };
873
+ }
874
+ if (metadata?.reasoning) {
875
+ return {
876
+ levels: ["low", "medium", "high"],
877
+ defaultLevel: "medium",
878
+ supportsSummaries: false,
879
+ mode: "controllable",
880
+ source: "model-metadata",
881
+ confidence: "inferred",
882
+ wireFormat: { kind: "openai-reasoning-effort" }
883
+ };
884
+ }
885
+ return EMPTY_REASONING;
886
+ }
887
+
888
+ // src/registry/io.ts
889
+ import {
890
+ chmodSync,
891
+ copyFileSync as copyFileSync2,
892
+ existsSync as existsSync2,
893
+ mkdirSync as mkdirSync2,
894
+ openSync,
895
+ readFileSync as readFileSync2,
896
+ renameSync as renameSync2,
897
+ writeSync,
898
+ closeSync
899
+ } from "fs";
900
+ import { dirname as dirname2 } from "path";
901
+
902
+ // src/registry/types.ts
903
+ var REGISTRY_SCHEMA_VERSION = 1;
904
+
905
+ // src/registry/migrate.ts
906
+ var LEGACY_CLOUD_PROVIDER_IDS = [
907
+ { legacyId: "opencode", id: "zen", name: "OpenCode Zen" },
908
+ { legacyId: "opencode-go", id: "go", name: "OpenCode Go" }
909
+ ];
910
+ function migrateLegacyCloudProviders(registry) {
911
+ let changed = false;
912
+ for (const { legacyId, id, name } of LEGACY_CLOUD_PROVIDER_IDS) {
913
+ const legacyIdx = registry.providers.findIndex((provider) => provider.id === legacyId);
914
+ if (legacyIdx < 0) continue;
915
+ if (registry.providers.some((provider) => provider.id === id)) {
916
+ registry.providers.splice(legacyIdx, 1);
917
+ } else {
918
+ registry.providers[legacyIdx] = {
919
+ ...registry.providers[legacyIdx],
920
+ id,
921
+ templateId: id,
922
+ name,
923
+ api: {}
924
+ };
925
+ }
926
+ changed = true;
927
+ }
928
+ return changed;
929
+ }
930
+ function migrateOAuthOpenAiProvider(registry) {
931
+ if (registry.providers.some((p) => p.id === "openai-oauth")) return false;
932
+ const idx = registry.providers.findIndex(
933
+ (p) => p.id === "openai" && p.authType === "oauth"
934
+ );
935
+ if (idx < 0) return false;
936
+ const existing = registry.providers[idx];
937
+ registry.providers[idx] = {
938
+ ...existing,
939
+ id: "openai-oauth",
940
+ templateId: existing.templateId || "openai",
941
+ name: existing.name === "OpenAI" ? "OpenAI (ChatGPT)" : existing.name
942
+ };
943
+ return true;
944
+ }
945
+ function migrateOAuthXaiProvider(registry) {
946
+ if (registry.providers.some((p) => p.id === "xai-oauth")) return false;
947
+ const idx = registry.providers.findIndex(
948
+ (p) => p.id === "xai" && p.authType === "oauth"
949
+ );
950
+ if (idx < 0) return false;
951
+ const existing = registry.providers[idx];
952
+ registry.providers[idx] = {
953
+ ...existing,
954
+ id: "xai-oauth",
955
+ templateId: existing.templateId || "xai",
956
+ name: existing.name === "xAI" ? "xAI Grok (SuperGrok)" : existing.name
957
+ };
958
+ return true;
959
+ }
960
+ function migrateAlibabaDashScopeChinaLabel(registry) {
961
+ const provider = registry.providers.find(
962
+ (p) => p.id === "alibaba" && p.templateId === "alibaba" && p.name === "Alibaba DashScope" && p.api.url === "https://dashscope.aliyuncs.com/compatible-mode/v1"
963
+ );
964
+ if (!provider) return false;
965
+ provider.name = "Alibaba DashScope (China)";
966
+ return true;
967
+ }
968
+
969
+ // src/registry/validate.ts
970
+ var PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
971
+ function isValidProviderId(id) {
972
+ return PROVIDER_ID_PATTERN.test(id);
973
+ }
974
+
975
+ // src/registry/io.ts
976
+ var DIR_MODE = 448;
977
+ var FILE_MODE = 384;
978
+ function ensureSecureAppHome() {
979
+ const home = getAppHome();
980
+ mkdirSync2(home, { recursive: true, mode: DIR_MODE });
981
+ try {
982
+ chmodSync(home, DIR_MODE);
983
+ } catch {
984
+ }
985
+ }
986
+ function writeSecureFile(path, content) {
987
+ ensureSecureAppHome();
988
+ mkdirSync2(dirname2(path), { recursive: true, mode: DIR_MODE });
989
+ const fd = openSync(path, "w", FILE_MODE);
990
+ try {
991
+ writeSync(fd, content);
992
+ } finally {
993
+ closeSync(fd);
994
+ }
995
+ try {
996
+ chmodSync(path, FILE_MODE);
997
+ } catch {
998
+ }
999
+ }
1000
+ function parseProvider(raw) {
1001
+ if (!raw || typeof raw !== "object") return null;
1002
+ const p = raw;
1003
+ if (typeof p.id !== "string" || !isValidProviderId(p.id)) return null;
1004
+ if (typeof p.templateId !== "string" || !p.templateId) return null;
1005
+ if (typeof p.name !== "string" || !p.name) return null;
1006
+ if (typeof p.enabled !== "boolean") return null;
1007
+ if (typeof p.authRef !== "string" || !p.authRef) return null;
1008
+ if (typeof p.addedAt !== "string" || !p.addedAt) return null;
1009
+ const api = p.api;
1010
+ if (!api || typeof api !== "object") return null;
1011
+ const provider = {
1012
+ id: p.id,
1013
+ templateId: p.templateId,
1014
+ name: p.name,
1015
+ enabled: p.enabled,
1016
+ authRef: p.authRef,
1017
+ api,
1018
+ addedAt: p.addedAt
1019
+ };
1020
+ if (p.subscriptionFilter === "free" || p.subscriptionFilter === "zen" || p.subscriptionFilter === "go") {
1021
+ provider.subscriptionFilter = p.subscriptionFilter;
1022
+ }
1023
+ if (p.authType === "api" || p.authType === "oauth" || p.authType === "none") {
1024
+ provider.authType = p.authType;
1025
+ }
1026
+ if (typeof p.refreshedAt === "string") provider.refreshedAt = p.refreshedAt;
1027
+ if (p.modelsCache && typeof p.modelsCache === "object") {
1028
+ const cache = p.modelsCache;
1029
+ if (typeof cache.fetchedAt === "string" && Array.isArray(cache.models)) {
1030
+ provider.modelsCache = {
1031
+ fetchedAt: cache.fetchedAt,
1032
+ models: cache.models.filter((m) => m && typeof m === "object")
1033
+ };
1034
+ }
1035
+ }
1036
+ return provider;
1037
+ }
1038
+ function parseRegistry(raw) {
1039
+ const empty = { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1040
+ if (!raw || typeof raw !== "object") return empty;
1041
+ const data = raw;
1042
+ const providers = [];
1043
+ if (Array.isArray(data.providers)) {
1044
+ for (const entry of data.providers) {
1045
+ const parsed = parseProvider(entry);
1046
+ if (parsed) providers.push(parsed);
1047
+ }
1048
+ }
1049
+ const registry = {
1050
+ schemaVersion: typeof data.schemaVersion === "number" ? data.schemaVersion : REGISTRY_SCHEMA_VERSION,
1051
+ providers
1052
+ };
1053
+ if (typeof data.importedAt === "string") registry.importedAt = data.importedAt;
1054
+ if (typeof data.pricingCacheAt === "string") registry.pricingCacheAt = data.pricingCacheAt;
1055
+ return registry;
1056
+ }
1057
+ function loadRegistry(path = getProvidersPath(), { persist = true } = {}) {
1058
+ if (!existsSync2(path)) {
1059
+ return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1060
+ }
1061
+ try {
1062
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
1063
+ const registry = parseRegistry(raw);
1064
+ let migrated = migrateLegacyCloudProviders(registry);
1065
+ if (migrateOAuthOpenAiProvider(registry)) migrated = true;
1066
+ if (migrateOAuthXaiProvider(registry)) migrated = true;
1067
+ if (migrateAlibabaDashScopeChinaLabel(registry)) migrated = true;
1068
+ if (migrated && persist) {
1069
+ try {
1070
+ saveRegistry(registry, path);
1071
+ } catch {
1072
+ }
1073
+ }
1074
+ return registry;
1075
+ } catch {
1076
+ return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1077
+ }
1078
+ }
1079
+ function saveRegistry(registry, path = getProvidersPath()) {
1080
+ const payload = `${JSON.stringify(registry, null, 2)}
1081
+ `;
1082
+ const backup = `${path}.bak`;
1083
+ if (existsSync2(path)) {
1084
+ try {
1085
+ copyFileSync2(path, backup);
1086
+ } catch {
1087
+ }
1088
+ }
1089
+ const tmp = `${path}.tmp`;
1090
+ writeSecureFile(tmp, payload);
1091
+ renameSync2(tmp, path);
1092
+ }
1093
+
1094
+ // src/core/errors.ts
1095
+ var DEFAULT_RETRYABLE = {
1096
+ INVALID_ROUTE_ID: false,
1097
+ ROUTE_NOT_FOUND: false,
1098
+ PROVIDER_DISABLED: false,
1099
+ CREDENTIAL_UNAVAILABLE: false,
1100
+ OAUTH_REFRESH_FAILED: true,
1101
+ UNSUPPORTED_MODEL: false,
1102
+ UNSUPPORTED_REGISTRY_VERSION: false,
1103
+ PROVIDER_LOAD_FAILED: true
1104
+ };
1105
+ var RelayCoreError = class extends Error {
1106
+ code;
1107
+ retryable;
1108
+ providerId;
1109
+ routeId;
1110
+ constructor(code, message, options = {}) {
1111
+ super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
1112
+ this.name = "RelayCoreError";
1113
+ this.code = code;
1114
+ this.retryable = options.retryable ?? DEFAULT_RETRYABLE[code];
1115
+ if (options.providerId !== void 0) this.providerId = options.providerId;
1116
+ if (options.routeId !== void 0) this.routeId = options.routeId;
1117
+ }
1118
+ toJSON() {
1119
+ return {
1120
+ name: this.name,
1121
+ code: this.code,
1122
+ message: this.message,
1123
+ retryable: this.retryable,
1124
+ ...this.providerId !== void 0 ? { providerId: this.providerId } : {},
1125
+ ...this.routeId !== void 0 ? { routeId: this.routeId } : {}
1126
+ };
1127
+ }
1128
+ };
1129
+ function isRelayCoreError(err) {
1130
+ return err instanceof RelayCoreError;
1131
+ }
1132
+
1133
+ // src/core/route-id.ts
1134
+ var SEPARATOR = "::";
1135
+ function toRelayRouteId(providerId, modelId) {
1136
+ if (!isValidProviderId(providerId)) {
1137
+ throw new RelayCoreError("INVALID_ROUTE_ID", `Invalid provider id for route id: ${JSON.stringify(providerId)}`, {});
1138
+ }
1139
+ if (!modelId) {
1140
+ throw new RelayCoreError("INVALID_ROUTE_ID", "Model id must be non-empty for a route id", { providerId });
1141
+ }
1142
+ return `${providerId}${SEPARATOR}${modelId}`;
1143
+ }
1144
+ function parseRelayRouteId(routeId) {
1145
+ const idx = typeof routeId === "string" ? routeId.indexOf(SEPARATOR) : -1;
1146
+ if (idx <= 0 || idx === routeId.length - SEPARATOR.length) {
1147
+ throw new RelayCoreError("INVALID_ROUTE_ID", `Route id must be "provider::model", got: ${JSON.stringify(routeId)}`);
1148
+ }
1149
+ const providerId = routeId.slice(0, idx);
1150
+ const modelId = routeId.slice(idx + SEPARATOR.length);
1151
+ if (!isValidProviderId(providerId)) {
1152
+ throw new RelayCoreError("INVALID_ROUTE_ID", `Invalid provider id in route id: ${JSON.stringify(routeId)}`);
1153
+ }
1154
+ return { providerId, modelId };
1155
+ }
1156
+
1157
+ // src/core/catalog.ts
1158
+ function loadCoreRegistry(path) {
1159
+ const registry = loadRegistry(path, { persist: false });
1160
+ if (registry.schemaVersion > REGISTRY_SCHEMA_VERSION) {
1161
+ throw new RelayCoreError(
1162
+ "UNSUPPORTED_REGISTRY_VERSION",
1163
+ `Registry schema v${registry.schemaVersion} is newer than supported v${REGISTRY_SCHEMA_VERSION} \u2014 upgrade relay-ai.`
1164
+ );
1165
+ }
1166
+ return registry;
1167
+ }
1168
+ function mapReasoning(provider, model) {
1169
+ const base = { tools: "unknown", vision: "unknown" };
1170
+ const npm = model.npm ?? provider.api.npm ?? "";
1171
+ const upstreamModelId = model.upstreamModelId ?? model.id;
1172
+ try {
1173
+ const caps = getReasoningCapabilities(npm, upstreamModelId, {
1174
+ providerId: provider.id,
1175
+ apiBaseUrl: model.apiUrl ?? provider.api.url,
1176
+ supportedParameters: model.supportedParameters,
1177
+ reasoning: model.reasoning,
1178
+ interleavedReasoningField: model.interleavedReasoningField,
1179
+ upstreamModelId
1180
+ });
1181
+ switch (caps.mode) {
1182
+ case "none":
1183
+ return { ...base, reasoning: "none" };
1184
+ case "internal-only":
1185
+ return { ...base, reasoning: "fixed" };
1186
+ case "controllable":
1187
+ return {
1188
+ ...base,
1189
+ reasoning: "adjustable",
1190
+ reasoningLevels: [...caps.levels],
1191
+ defaultReasoningLevel: caps.defaultLevel
1192
+ };
1193
+ default:
1194
+ return { ...base, reasoning: "unknown" };
1195
+ }
1196
+ } catch {
1197
+ return { ...base, reasoning: "unknown" };
1198
+ }
1199
+ }
1200
+ function favoriteKey(providerId, modelId) {
1201
+ return `${providerId}::${modelId}`;
1202
+ }
1203
+ function toDescriptor(provider, model, favorites) {
1204
+ const upstreamModelId = model.upstreamModelId ?? model.id;
1205
+ return {
1206
+ routeId: toRelayRouteId(provider.id, model.id),
1207
+ providerId: provider.id,
1208
+ providerName: provider.name,
1209
+ modelId: model.id,
1210
+ upstreamModelId,
1211
+ displayName: model.name,
1212
+ authType: provider.authType ?? "api",
1213
+ favorite: favorites.has(favoriteKey(provider.id, model.id)),
1214
+ ...model.contextWindow !== void 0 ? { contextWindow: model.contextWindow } : {},
1215
+ ...model.cost ? {
1216
+ pricing: {
1217
+ input: model.cost.input,
1218
+ output: model.cost.output,
1219
+ ...model.cost.cache_read !== void 0 ? { cacheRead: model.cost.cache_read } : {},
1220
+ ...model.cost.cache_write !== void 0 ? { cacheWrite: model.cost.cache_write } : {}
1221
+ }
1222
+ } : {},
1223
+ capabilities: mapReasoning(provider, model)
1224
+ };
1225
+ }
1226
+ function listRelayModels(registryPath) {
1227
+ const registry = loadCoreRegistry(registryPath);
1228
+ const favorites = new Set(
1229
+ (loadPreferences().favoriteModels ?? []).map((f) => favoriteKey(f.providerId, f.modelId))
1230
+ );
1231
+ const descriptors = [];
1232
+ for (const provider of registry.providers) {
1233
+ if (!provider.enabled) continue;
1234
+ for (const model of provider.modelsCache?.models ?? []) {
1235
+ descriptors.push(toDescriptor(provider, model, favorites));
1236
+ }
1237
+ }
1238
+ return descriptors.sort(
1239
+ (a, b) => Number(b.favorite) - Number(a.favorite) || a.providerName.localeCompare(b.providerName) || a.displayName.localeCompare(b.displayName)
1240
+ );
1241
+ }
1242
+
1243
+ // src/context-window.ts
1244
+ import { readFileSync as readFileSync3 } from "fs";
1245
+
1246
+ // src/registry/opencode-auth.ts
1247
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "fs";
1248
+ import { homedir as homedir3 } from "os";
1249
+ import { join as join4 } from "path";
1250
+ function oauthCredentialToKeychainJson(cred) {
1251
+ return JSON.stringify(cred);
1252
+ }
1253
+
1254
+ // src/oauth/types.ts
1255
+ function tokensToStoredCredential(tokens, existingRefresh, accountId, providerData) {
1256
+ const mergedProviderData = providerData || tokens.providerData ? { ...providerData, ...tokens.providerData } : void 0;
1257
+ return {
1258
+ type: "oauth",
1259
+ access: tokens.access_token,
1260
+ refresh: tokens.refresh_token ?? existingRefresh ?? "",
1261
+ expires: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
1262
+ ...accountId ? { accountId } : {},
1263
+ ...mergedProviderData ? { providerData: mergedProviderData } : {}
1264
+ };
1265
+ }
1266
+ function parseStoredOAuthCredential(raw) {
1267
+ if (!raw?.trim().startsWith("{")) return null;
1268
+ try {
1269
+ const parsed = JSON.parse(raw);
1270
+ if (parsed.type === "oauth" && typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number") {
1271
+ return parsed;
1272
+ }
1273
+ } catch {
1274
+ }
1275
+ return null;
1276
+ }
1277
+ var OAUTH_REFRESH_SKEW_MS = 12e4;
1278
+ function oauthCredentialNeedsRefresh(cred, skewMs = OAUTH_REFRESH_SKEW_MS) {
1279
+ return cred.expires <= Date.now() + Math.max(0, skewMs);
1280
+ }
1281
+ function accessTokenIsExpiring(token, skewMs = OAUTH_REFRESH_SKEW_MS) {
1282
+ if (!token) return false;
1283
+ const parts = token.split(".");
1284
+ if (parts.length < 2) return false;
1285
+ try {
1286
+ let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1287
+ while (payload.length % 4 !== 0) payload += "=";
1288
+ const claims = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
1289
+ if (typeof claims.exp !== "number") return false;
1290
+ return claims.exp * 1e3 <= Date.now() + Math.max(0, skewMs);
1291
+ } catch {
1292
+ return false;
1293
+ }
1294
+ }
1295
+ var NATIVE_OAUTH_PROVIDER_IDS = ["xai", "xai-oauth", "openai", "openai-oauth", "github-copilot", "claude-code", "antigravity"];
1296
+
1297
+ // src/oauth/github.ts
1298
+ var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
1299
+ var COPILOT_USER_URL = "https://api.github.com/copilot_internal/user";
1300
+ var DEVICE_CODE_DEFAULT_EXPIRES_MS2 = 15 * 60 * 1e3;
1301
+ var FREE_COPILOT_SKUS = /* @__PURE__ */ new Set([
1302
+ "free_limited_copilot",
1303
+ "free_educational_quota",
1304
+ "no_auth_limited_copilot"
1305
+ ]);
1306
+ function classifyCopilotAccount(user) {
1307
+ const login = typeof user["login"] === "string" && user["login"].trim() ? user["login"].trim() : void 0;
1308
+ const sku = typeof user["access_type_sku"] === "string" && user["access_type_sku"].trim() ? user["access_type_sku"].trim() : void 0;
1309
+ const plan = typeof user["copilot_plan"] === "string" && user["copilot_plan"].trim() ? user["copilot_plan"].trim() : void 0;
1310
+ if (!sku && !plan) {
1311
+ return {
1312
+ ...login ? { login } : {},
1313
+ lookup_status: "unknown"
1314
+ };
1315
+ }
1316
+ const isFree = FREE_COPILOT_SKUS.has(sku?.toLowerCase() ?? "") || plan?.toLowerCase() === "free";
1317
+ return {
1318
+ ...login ? { login } : {},
1319
+ ...sku ? { access_type_sku: sku } : {},
1320
+ ...plan ? { copilot_plan: plan } : {},
1321
+ is_free_plan: isFree,
1322
+ lookup_status: "known"
1323
+ };
1324
+ }
1325
+ async function fetchCopilotAccount(ghuToken) {
1326
+ const response = await fetch(COPILOT_USER_URL, {
1327
+ method: "GET",
1328
+ headers: {
1329
+ Authorization: `Bearer ${ghuToken}`,
1330
+ Accept: "application/json",
1331
+ "User-Agent": `relay-ai/${VERSION}`,
1332
+ "Editor-Version": "vscode/1.85.1",
1333
+ "X-GitHub-Api-Version": "2025-04-01"
1334
+ }
1335
+ });
1336
+ if (!response.ok) {
1337
+ const detail = await response.text().catch(() => "");
1338
+ throw new Error(`GitHub Copilot account lookup failed (${response.status})${detail ? `: ${detail}` : ""}`);
1339
+ }
1340
+ const json = await response.json();
1341
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
1342
+ throw new Error("GitHub Copilot account lookup returned invalid JSON");
1343
+ }
1344
+ return classifyCopilotAccount(json);
1345
+ }
1346
+ async function exchangeForCopilotToken(ghuToken) {
1347
+ const response = await fetch(COPILOT_TOKEN_URL, {
1348
+ method: "GET",
1349
+ headers: {
1350
+ Authorization: `Bearer ${ghuToken}`,
1351
+ "User-Agent": `relay-ai/${VERSION}`,
1352
+ Accept: "application/json"
1353
+ }
1354
+ });
1355
+ if (!response.ok) {
1356
+ const msg = await response.text().catch(() => "");
1357
+ throw new Error(`GitHub Copilot token exchange failed (${response.status})${msg ? `: ${msg}` : ""}`);
1358
+ }
1359
+ const json = await response.json();
1360
+ if (!json.token) {
1361
+ throw new Error("GitHub Copilot token exchange response missing token field \u2014 is Copilot subscription active?");
1362
+ }
1363
+ let expiresIn = 1800;
1364
+ if (json.expires_at) {
1365
+ const expiresMs = new Date(json.expires_at).getTime() - Date.now();
1366
+ if (expiresMs > 0) expiresIn = Math.floor(expiresMs / 1e3);
1367
+ }
1368
+ let account = { lookup_status: "unknown" };
1369
+ try {
1370
+ account = await fetchCopilotAccount(ghuToken);
1371
+ } catch {
1372
+ }
1373
+ return {
1374
+ access_token: json.token,
1375
+ expires_in: expiresIn,
1376
+ providerData: { copilot: account }
1377
+ };
1378
+ }
1379
+ async function refreshGithubCopilotToken(ghuToken) {
1380
+ const copilot = await exchangeForCopilotToken(ghuToken);
1381
+ return {
1382
+ ...copilot,
1383
+ refresh_token: ghuToken
1384
+ // keep the same ghu_ token as refresh
1385
+ };
1386
+ }
1387
+
1388
+ // src/oauth/xai.ts
1389
+ var CLIENT_ID2 = "b1a00492-073a-47ea-816f-4c329264a828";
1390
+ var TOKEN_URL = "https://auth.x.ai/oauth2/token";
1391
+ var DEVICE_CODE_DEFAULT_EXPIRES_MS3 = 5 * 60 * 1e3;
1392
+ function authHeaders() {
1393
+ return {
1394
+ "Content-Type": "application/x-www-form-urlencoded",
1395
+ Accept: "application/json",
1396
+ "User-Agent": `relay-ai/${VERSION}`
1397
+ };
1398
+ }
1399
+ async function refreshXaiAccessToken(refreshToken) {
1400
+ return postOAuthRefresh(
1401
+ TOKEN_URL,
1402
+ new URLSearchParams({
1403
+ grant_type: "refresh_token",
1404
+ refresh_token: refreshToken,
1405
+ client_id: CLIENT_ID2
1406
+ }),
1407
+ {
1408
+ contentType: "form",
1409
+ errorPrefix: "xAI token refresh failed",
1410
+ includeStatus: true,
1411
+ includeBody: true,
1412
+ headers: authHeaders()
1413
+ }
1414
+ );
1415
+ }
1416
+
1417
+ // src/oauth/claude-code.ts
1418
+ import { randomBytes } from "crypto";
1419
+ import open from "open";
1420
+ var CLAUDE_CODE_CLIENT_ID = process.env.CLAUDE_OAUTH_CLIENT_ID ?? "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1421
+ var TOKEN_URL2 = "https://api.anthropic.com/v1/oauth/token";
1422
+ var REDIRECT_URI = process.env.CLAUDE_CODE_REDIRECT_URI ?? "https://platform.claude.com/oauth/code/callback";
1423
+ async function refreshClaudeCodeToken(refreshToken) {
1424
+ return postOAuthRefresh(
1425
+ TOKEN_URL2,
1426
+ {
1427
+ grant_type: "refresh_token",
1428
+ client_id: CLAUDE_CODE_CLIENT_ID,
1429
+ refresh_token: refreshToken
1430
+ },
1431
+ {
1432
+ contentType: "json",
1433
+ errorPrefix: "Claude Code token refresh failed",
1434
+ includeBody: true
1435
+ }
1436
+ );
1437
+ }
1438
+
1439
+ // src/oauth/antigravity-oauth.ts
1440
+ import open2 from "open";
1441
+ import { readFileSync as readFileSync5 } from "fs";
1442
+ import { homedir as homedir4 } from "os";
1443
+ import { join as pathJoin } from "path";
1444
+
1445
+ // src/oauth/callback-server.ts
1446
+ import http from "http";
1447
+
1448
+ // src/oauth/antigravity-oauth.ts
1449
+ var DEFAULT_ANTIGRAVITY_CLIENT_ID = ["107100606059", "1-tmhssin2h2", "1lcre235vtol", "ojh4g403ep.a", "pps.googleus", "ercontent.co", "m"].join("");
1450
+ var DEFAULT_ANTIGRAVITY_CLIENT_SECRET = ["GOCS", "PX-K", "58FW", "R486", "LdLJ", "1mLB", "8sXC", "4z6q", "DAf"].join("");
1451
+ var ANTIGRAVITY_CLIENT_ID = process.env.ANTIGRAVITY_OAUTH_CLIENT_ID ?? DEFAULT_ANTIGRAVITY_CLIENT_ID;
1452
+ var ANTIGRAVITY_CLIENT_SECRET = process.env.ANTIGRAVITY_OAUTH_CLIENT_SECRET ?? DEFAULT_ANTIGRAVITY_CLIENT_SECRET;
1453
+ var TOKEN_URL3 = "https://oauth2.googleapis.com/token";
1454
+ var SCOPES = [
1455
+ "openid",
1456
+ "https://www.googleapis.com/auth/cloud-platform",
1457
+ "https://www.googleapis.com/auth/userinfo.email",
1458
+ "https://www.googleapis.com/auth/userinfo.profile",
1459
+ "https://www.googleapis.com/auth/cclog",
1460
+ "https://www.googleapis.com/auth/experimentsandconfigs"
1461
+ ].join(" ");
1462
+ var ANTIGRAVITY_VERSION = "4.2.0";
1463
+ var ANTIGRAVITY_USER_AGENT = `vscode/1.X.X (Antigravity/${ANTIGRAVITY_VERSION})`;
1464
+ async function refreshAntigravityToken(refreshToken) {
1465
+ return postOAuthRefresh(
1466
+ TOKEN_URL3,
1467
+ new URLSearchParams({
1468
+ grant_type: "refresh_token",
1469
+ client_id: ANTIGRAVITY_CLIENT_ID,
1470
+ client_secret: ANTIGRAVITY_CLIENT_SECRET,
1471
+ refresh_token: refreshToken
1472
+ }),
1473
+ {
1474
+ contentType: "form",
1475
+ errorPrefix: "Antigravity token refresh failed",
1476
+ includeBody: true
1477
+ }
1478
+ );
1479
+ }
1480
+
1481
+ // src/oauth/refresh.ts
1482
+ function oauthCredentialShouldRefresh(cred, providerId) {
1483
+ if (oauthCredentialNeedsRefresh(cred)) return true;
1484
+ if (NATIVE_OAUTH_PROVIDER_IDS.includes(providerId) && accessTokenIsExpiring(cred.access)) return true;
1485
+ return false;
1486
+ }
1487
+ async function refreshStoredOAuthCredential(providerId, cred) {
1488
+ if (!cred.refresh) {
1489
+ throw new Error(`${providerId}: OAuth refresh token missing \u2014 run relay-ai providers auth ${providerId}`);
1490
+ }
1491
+ let tokens;
1492
+ if (providerId === "openai" || providerId === "openai-oauth") {
1493
+ tokens = await refreshOpenAiAccessToken(cred.refresh);
1494
+ } else if (providerId === "xai" || providerId === "xai-oauth") {
1495
+ tokens = await refreshXaiAccessToken(cred.refresh);
1496
+ } else if (providerId === "github-copilot") {
1497
+ tokens = await refreshGithubCopilotToken(cred.refresh);
1498
+ } else if (providerId === "claude-code") {
1499
+ tokens = await refreshClaudeCodeToken(cred.refresh);
1500
+ } else if (providerId === "antigravity") {
1501
+ tokens = await refreshAntigravityToken(cred.refresh);
1502
+ } else {
1503
+ throw new Error(`OAuth refresh not implemented for provider "${providerId}"`);
1504
+ }
1505
+ return tokensToStoredCredential(tokens, cred.refresh, cred.accountId, cred.providerData);
1506
+ }
1507
+
1508
+ // src/secrets-file.ts
1509
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1510
+ var DIR_MODE2 = 448;
1511
+ var FILE_MODE2 = 384;
1512
+ function emptySecrets() {
1513
+ return { version: 1, accounts: {} };
1514
+ }
1515
+ function readSecretsFile(env = process.env) {
1516
+ const path = getSecretsPath(env);
1517
+ if (!existsSync4(path)) return emptySecrets();
1518
+ try {
1519
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
1520
+ if (raw?.version !== 1 || !raw.accounts || typeof raw.accounts !== "object") {
1521
+ return emptySecrets();
1522
+ }
1523
+ const accounts = {};
1524
+ for (const [k, v] of Object.entries(raw.accounts)) {
1525
+ if (typeof v === "string" && v.length > 0) accounts[k] = v;
1526
+ }
1527
+ return { version: 1, accounts };
1528
+ } catch {
1529
+ return emptySecrets();
1530
+ }
1531
+ }
1532
+ function writeSecretsFile(data, env = process.env) {
1533
+ const home = getAppHome(env);
1534
+ mkdirSync3(home, { recursive: true, mode: DIR_MODE2 });
1535
+ try {
1536
+ chmodSync2(home, DIR_MODE2);
1537
+ } catch {
1538
+ }
1539
+ const path = getSecretsPath(env);
1540
+ writeFileSync2(path, `${JSON.stringify(data, null, 2)}
1541
+ `, { encoding: "utf8", mode: FILE_MODE2 });
1542
+ try {
1543
+ chmodSync2(path, FILE_MODE2);
1544
+ } catch {
1545
+ }
1546
+ }
1547
+ function readFileAccount(account, env = process.env) {
1548
+ const value = readSecretsFile(env).accounts[account];
1549
+ return value?.length ? value : null;
1550
+ }
1551
+ function writeFileAccount(account, value, env = process.env) {
1552
+ if (!account || !value) return false;
1553
+ try {
1554
+ const data = readSecretsFile(env);
1555
+ data.accounts[account] = value;
1556
+ writeSecretsFile(data, env);
1557
+ return true;
1558
+ } catch {
1559
+ return false;
1560
+ }
1561
+ }
1562
+ function deleteFileAccount(account, env = process.env) {
1563
+ try {
1564
+ const data = readSecretsFile(env);
1565
+ if (!(account in data.accounts)) return true;
1566
+ delete data.accounts[account];
1567
+ writeSecretsFile(data, env);
1568
+ return true;
1569
+ } catch {
1570
+ return false;
1571
+ }
1572
+ }
1573
+
1574
+ // src/env.ts
1575
+ function resolveApiKey() {
1576
+ const key = process.env["OPENCODE_API_KEY"];
1577
+ if (!key?.trim()) return null;
1578
+ return key.trim().split(/\r?\n/)[0]?.trim() || null;
1579
+ }
1580
+ function classifyKeyringError(err) {
1581
+ const msg = err instanceof Error ? err.message : String(err);
1582
+ const lower = msg.toLowerCase();
1583
+ if (lower.includes("cannot find module") || lower.includes("module not found") || lower.includes("failed to load")) {
1584
+ return "native keyring module not available on this system";
1585
+ }
1586
+ if (lower.includes("secret service") || lower.includes("dbus") || lower.includes("daemon")) {
1587
+ return "Secret Service daemon is not running (start GNOME Keyring or KWallet)";
1588
+ }
1589
+ if (lower.includes("denied") || lower.includes("locked") || lower.includes("cancelled") || lower.includes("user refused")) {
1590
+ return "keychain access was denied or the keychain is locked";
1591
+ }
1592
+ return `keyring error: ${msg}`;
1593
+ }
1594
+ var KEYRING_SERVICE = "relay-ai";
1595
+ var KEYRING_ACCOUNT = "relay-ai";
1596
+ var KEYRING_CHUNK_PREFIX = "__relay_chunked__:";
1597
+ var KEYRING_CHUNK_SIZE = 1200;
1598
+ var LEGACY_KEYRING_SERVICE = "opencode-starter";
1599
+ var LEGACY_KEYRING_ACCOUNT = "opencode-starter";
1600
+ var GLOBAL_OPENCODE_KEYRING_ACCOUNT = "global:opencode";
1601
+ function oauthProviderIdFromAccount(account) {
1602
+ const prefix = "oauth:provider:";
1603
+ return account.startsWith(prefix) ? account.slice(prefix.length) : null;
1604
+ }
1605
+ var oauthRefreshInflight = /* @__PURE__ */ new Map();
1606
+ function parseAuthRef(authRef) {
1607
+ if (authRef.startsWith("keyring:")) {
1608
+ const account = authRef.slice("keyring:".length);
1609
+ return account ? { kind: "keyring", account } : null;
1610
+ }
1611
+ if (authRef.startsWith("env:")) {
1612
+ const varName = authRef.slice("env:".length);
1613
+ return varName ? { kind: "env", varName } : null;
1614
+ }
1615
+ return null;
1616
+ }
1617
+ function relayAiKeyEnvVar(providerId) {
1618
+ return `RELAY_AI_KEY_${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
1619
+ }
1620
+ function readEnvCredential(varName) {
1621
+ const raw = process.env[varName];
1622
+ if (!raw?.trim()) return null;
1623
+ return raw.trim().split(/\r?\n/)[0]?.trim() || null;
1624
+ }
1625
+ async function readOsKeyringAccount(account, diag) {
1626
+ try {
1627
+ const { Entry } = await import("@napi-rs/keyring");
1628
+ const value = new Entry(KEYRING_SERVICE, account).getPassword() ?? null;
1629
+ if (!value?.startsWith(KEYRING_CHUNK_PREFIX)) return value;
1630
+ const chunkCount = Number(value.slice(KEYRING_CHUNK_PREFIX.length));
1631
+ let combined = "";
1632
+ for (let i = 0; i < chunkCount; i++) {
1633
+ combined += new Entry(KEYRING_SERVICE, `${account}::chunk::${i}`).getPassword() ?? "";
1634
+ }
1635
+ return combined;
1636
+ } catch (err) {
1637
+ diag?.(classifyKeyringError(err));
1638
+ return null;
1639
+ }
1640
+ }
1641
+ async function writeOsKeyringAccount(account, key, diag) {
1642
+ try {
1643
+ const { Entry } = await import("@napi-rs/keyring");
1644
+ if (key.length <= KEYRING_CHUNK_SIZE) {
1645
+ new Entry(KEYRING_SERVICE, account).setPassword(key);
1646
+ return true;
1647
+ }
1648
+ const chunkCount = Math.ceil(key.length / KEYRING_CHUNK_SIZE);
1649
+ for (let i = 0; i < chunkCount; i++) {
1650
+ const chunk = key.slice(i * KEYRING_CHUNK_SIZE, (i + 1) * KEYRING_CHUNK_SIZE);
1651
+ new Entry(KEYRING_SERVICE, `${account}::chunk::${i}`).setPassword(chunk);
1652
+ }
1653
+ new Entry(KEYRING_SERVICE, account).setPassword(`${KEYRING_CHUNK_PREFIX}${chunkCount}`);
1654
+ return true;
1655
+ } catch (err) {
1656
+ diag?.(classifyKeyringError(err));
1657
+ return false;
1658
+ }
1659
+ }
1660
+ async function readKeyringAccount(account, diag) {
1661
+ const fromOs = await readOsKeyringAccount(account, diag);
1662
+ if (fromOs) return fromOs;
1663
+ return readFileAccount(account);
1664
+ }
1665
+ async function writeKeyringAccount(account, key, diag) {
1666
+ if (await writeOsKeyringAccount(account, key, diag)) {
1667
+ deleteFileAccount(account);
1668
+ return true;
1669
+ }
1670
+ if (writeFileAccount(account, key)) {
1671
+ diag?.("OS keyring unavailable \u2014 saved to secrets.json under RELAY_AI_HOME");
1672
+ return true;
1673
+ }
1674
+ return false;
1675
+ }
1676
+ async function readGlobalOpencodeCredential(diag) {
1677
+ const fromEnv = resolveApiKey();
1678
+ if (fromEnv) return fromEnv;
1679
+ const global = await readKeyringAccount(GLOBAL_OPENCODE_KEYRING_ACCOUNT, diag);
1680
+ if (global) return global;
1681
+ const current = await readKeyringAccount(KEYRING_ACCOUNT, diag);
1682
+ if (current) return current;
1683
+ try {
1684
+ const { Entry } = await import("@napi-rs/keyring");
1685
+ return new Entry(LEGACY_KEYRING_SERVICE, LEGACY_KEYRING_ACCOUNT).getPassword() ?? null;
1686
+ } catch (err) {
1687
+ diag?.(classifyKeyringError(err));
1688
+ return null;
1689
+ }
1690
+ }
1691
+ async function resolveProviderCredential(providerId, authRef, diag) {
1692
+ const namespaced = readEnvCredential(relayAiKeyEnvVar(providerId));
1693
+ if (namespaced) return namespaced;
1694
+ const parsed = parseAuthRef(authRef);
1695
+ if (!parsed) return null;
1696
+ if (parsed.kind === "env") {
1697
+ return readEnvCredential(parsed.varName);
1698
+ }
1699
+ if (parsed.account === GLOBAL_OPENCODE_KEYRING_ACCOUNT) {
1700
+ return readGlobalOpencodeCredential(diag);
1701
+ }
1702
+ return readProviderSecret(parsed.account, diag);
1703
+ }
1704
+ async function resolveProviderOAuthAccountId(authRef, diag) {
1705
+ const parsed = parseAuthRef(authRef);
1706
+ if (!parsed || parsed.kind !== "keyring" || !oauthProviderIdFromAccount(parsed.account)) return void 0;
1707
+ const raw = await readKeyringAccount(parsed.account, diag);
1708
+ return parseStoredOAuthCredential(raw)?.accountId;
1709
+ }
1710
+ async function resolveProviderOAuthProviderData(authRef, diag) {
1711
+ const parsed = parseAuthRef(authRef);
1712
+ if (!parsed || parsed.kind !== "keyring" || !oauthProviderIdFromAccount(parsed.account)) return void 0;
1713
+ const raw = await readKeyringAccount(parsed.account, diag);
1714
+ return parseStoredOAuthCredential(raw)?.providerData;
1715
+ }
1716
+ function decodeProviderSecret(raw) {
1717
+ if (!raw) return null;
1718
+ const trimmed = raw.trim();
1719
+ if (!trimmed.startsWith("{")) return trimmed;
1720
+ const oauth = parseStoredOAuthCredential(trimmed);
1721
+ if (oauth) return oauth.access;
1722
+ try {
1723
+ const parsed = JSON.parse(trimmed);
1724
+ if (parsed.type === "oauth" && typeof parsed.access === "string") return parsed.access;
1725
+ if (parsed.type === "wellknown" && typeof parsed.token === "string") return parsed.token;
1726
+ } catch {
1727
+ }
1728
+ return trimmed;
1729
+ }
1730
+ async function refreshOAuthKeyringAccount(account, providerId, raw, diag) {
1731
+ const existing = oauthRefreshInflight.get(account);
1732
+ if (existing) return existing;
1733
+ const work = (async () => {
1734
+ const cred = parseStoredOAuthCredential(raw);
1735
+ if (!cred || !oauthCredentialShouldRefresh(cred, providerId)) {
1736
+ return decodeProviderSecret(raw);
1737
+ }
1738
+ try {
1739
+ const refreshed = await refreshStoredOAuthCredential(providerId, cred);
1740
+ const json = oauthCredentialToKeychainJson(refreshed);
1741
+ await writeKeyringAccount(account, json, diag);
1742
+ return refreshed.access;
1743
+ } catch (err) {
1744
+ diag?.(err instanceof Error ? err.message : String(err));
1745
+ if (cred.access && cred.expires > Date.now()) return cred.access;
1746
+ throw err;
1747
+ }
1748
+ })();
1749
+ oauthRefreshInflight.set(account, work);
1750
+ try {
1751
+ return await work;
1752
+ } finally {
1753
+ oauthRefreshInflight.delete(account);
1754
+ }
1755
+ }
1756
+ async function readProviderSecret(account, diag) {
1757
+ const raw = await readKeyringAccount(account, diag);
1758
+ if (!raw) return null;
1759
+ const oauthProviderId = oauthProviderIdFromAccount(account);
1760
+ if (oauthProviderId && raw.trim().startsWith("{")) {
1761
+ return refreshOAuthKeyringAccount(account, oauthProviderId, raw, diag);
1762
+ }
1763
+ return decodeProviderSecret(raw);
1764
+ }
1765
+
1766
+ // src/core/model.ts
1767
+ function findRoute(registry, providerId, modelId, routeId) {
1768
+ const provider = registry.providers.find((p) => p.id === providerId);
1769
+ if (!provider) {
1770
+ throw new RelayCoreError("ROUTE_NOT_FOUND", `No provider registered with id "${providerId}".`, { providerId, routeId });
1771
+ }
1772
+ if (!provider.enabled) {
1773
+ throw new RelayCoreError("PROVIDER_DISABLED", `Provider "${provider.name}" is disabled \u2014 enable it in relay-ai ui.`, { providerId, routeId });
1774
+ }
1775
+ const model = provider.modelsCache?.models.find((m) => m.id === modelId);
1776
+ if (!model) {
1777
+ throw new RelayCoreError("UNSUPPORTED_MODEL", `Provider "${provider.name}" has no cached model "${modelId}" \u2014 refresh its models in relay-ai ui.`, { providerId, routeId });
1778
+ }
1779
+ return { provider, model };
1780
+ }
1781
+ async function resolveCredential(provider, routeId) {
1782
+ try {
1783
+ const credential = await resolveProviderCredential(provider.id, provider.authRef);
1784
+ if (credential) return credential;
1785
+ if (provider.authType === "none") return "";
1786
+ throw new RelayCoreError(
1787
+ "CREDENTIAL_UNAVAILABLE",
1788
+ `No credential available for provider "${provider.name}" \u2014 re-authenticate in relay-ai ui.`,
1789
+ { providerId: provider.id, routeId }
1790
+ );
1791
+ } catch (err) {
1792
+ if (isRelayCoreError(err)) throw err;
1793
+ if (provider.authType === "oauth") {
1794
+ throw new RelayCoreError(
1795
+ "OAUTH_REFRESH_FAILED",
1796
+ `OAuth token refresh failed for provider "${provider.name}" \u2014 re-authenticate in relay-ai ui.`,
1797
+ { providerId: provider.id, routeId, cause: err }
1798
+ );
1799
+ }
1800
+ throw new RelayCoreError(
1801
+ "PROVIDER_LOAD_FAILED",
1802
+ `Failed to resolve the credential for provider "${provider.name}".`,
1803
+ { providerId: provider.id, routeId, cause: err }
1804
+ );
1805
+ }
1806
+ }
1807
+ async function createRelayModel(routeId) {
1808
+ const { providerId, modelId } = parseRelayRouteId(routeId);
1809
+ const registry = loadCoreRegistry();
1810
+ const { provider, model } = findRoute(registry, providerId, modelId, routeId);
1811
+ const npm = model.npm ?? provider.api.npm;
1812
+ if (!npm) {
1813
+ throw new RelayCoreError("UNSUPPORTED_MODEL", `Model "${modelId}" has no SDK provider package \u2014 refresh the provider's models in relay-ai ui.`, { providerId, routeId });
1814
+ }
1815
+ const apiKey = await resolveCredential(provider, routeId);
1816
+ let oauthAccountId;
1817
+ let providerData;
1818
+ if (provider.authType === "oauth") {
1819
+ oauthAccountId = await resolveProviderOAuthAccountId(provider.authRef);
1820
+ providerData = await resolveProviderOAuthProviderData(provider.authRef);
1821
+ }
1822
+ const spec = {
1823
+ npm,
1824
+ modelId: model.upstreamModelId ?? model.id,
1825
+ apiKey,
1826
+ baseURL: model.apiUrl ?? provider.api.url,
1827
+ providerId: provider.id,
1828
+ authType: provider.authType,
1829
+ oauthAccountId,
1830
+ providerData,
1831
+ headers: provider.api.headers,
1832
+ useResponsesLite: model.useResponsesLite,
1833
+ preferWebSockets: model.preferWebSockets
1834
+ };
1835
+ try {
1836
+ return await createLanguageModel(spec);
1837
+ } catch (err) {
1838
+ if (isRelayCoreError(err)) throw err;
1839
+ throw new RelayCoreError(
1840
+ "PROVIDER_LOAD_FAILED",
1841
+ `Failed to construct model "${modelId}" for provider "${provider.name}".`,
1842
+ { providerId, routeId, cause: err }
1843
+ );
1844
+ }
1845
+ }
1846
+ export {
1847
+ RelayCoreError,
1848
+ createRelayModel,
1849
+ isRelayCoreError,
1850
+ listRelayModels,
1851
+ parseRelayRouteId,
1852
+ toRelayRouteId
1853
+ };
1854
+ //# sourceMappingURL=index.js.map