@jacobbd/relay-ai 0.12.5 → 0.12.6

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/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  addManualModel,
4
4
  removeManualModel
5
- } from "./chunk-SJEJAHKL.js";
5
+ } from "./chunk-6ULON7FU.js";
6
6
  import {
7
7
  CODEX_APP_AUTO_COMPACT_RATIO,
8
8
  CODEX_APP_PROVIDER_ID,
@@ -148,7 +148,7 @@ import {
148
148
  waitForCodexAppQuit,
149
149
  writeSecureLogLine,
150
150
  zenRegistryStub
151
- } from "./chunk-S7HYE2FI.js";
151
+ } from "./chunk-5MEDBKPX.js";
152
152
  import {
153
153
  filterTemplates,
154
154
  getTemplateById,
@@ -222,7 +222,7 @@ import {
222
222
  thinkingProviderOptions,
223
223
  upstreamHttpStatus,
224
224
  validateCustomEndpointUrl
225
- } from "./chunk-2LUDFIZX.js";
225
+ } from "./chunk-VPDX24ZR.js";
226
226
  import "./chunk-JIDIH7DS.js";
227
227
 
228
228
  // src/cli.ts
@@ -237,6 +237,7 @@ import * as p2 from "@clack/prompts";
237
237
 
238
238
  // src/opencode-serve.ts
239
239
  import { execSync, spawn } from "child_process";
240
+ import { randomBytes } from "crypto";
240
241
  import { existsSync } from "fs";
241
242
  import { homedir } from "os";
242
243
  import { join } from "path";
@@ -268,13 +269,99 @@ function findOpencodeBinary() {
268
269
  }
269
270
  return null;
270
271
  }
272
+ function parseOpencodeServerBanner(output) {
273
+ const v1 = /opencode server listening on (http:\/\/127\.0\.0\.1:\d+)/.exec(output);
274
+ if (v1?.[1]) return { generation: "v1", url: v1[1] };
275
+ const v2 = /(?:^|\n)\s*server listening on (http:\/\/127\.0\.0\.1:\d+)/.exec(output);
276
+ if (v2?.[1]) return { generation: "v2", url: v2[1] };
277
+ return null;
278
+ }
279
+ function v2SdkPackage(packageName, baseUrl) {
280
+ if (packageName.startsWith("aisdk:")) return packageName.slice("aisdk:".length);
281
+ if (!packageName.startsWith("@opencode/ai/providers/")) return packageName;
282
+ const provider = packageName.slice("@opencode/ai/providers/".length);
283
+ const packages = {
284
+ anthropic: "@ai-sdk/anthropic",
285
+ "anthropic-compatible": "@ai-sdk/anthropic",
286
+ openai: "@ai-sdk/openai",
287
+ "openai-compatible": "@ai-sdk/openai-compatible",
288
+ google: "@ai-sdk/google",
289
+ "google-vertex": "@ai-sdk/google-vertex",
290
+ xai: "@ai-sdk/xai",
291
+ azure: "@ai-sdk/azure",
292
+ "amazon-bedrock": "@ai-sdk/amazon-bedrock",
293
+ openrouter: "@openrouter/ai-sdk-provider"
294
+ };
295
+ return packages[provider] ?? (baseUrl ? "@ai-sdk/openai-compatible" : "");
296
+ }
297
+ function normalizeV2Providers(models, integrations, providers, env = process.env) {
298
+ const providerById = /* @__PURE__ */ new Map();
299
+ for (const value of providers) {
300
+ if (!value || typeof value !== "object") continue;
301
+ const provider = value;
302
+ if (provider.id) providerById.set(provider.id, provider);
303
+ }
304
+ const v2Models = models.filter((value) => !!value && typeof value === "object");
305
+ const result = [];
306
+ for (const value of integrations) {
307
+ if (!value || typeof value !== "object") continue;
308
+ const integration = value;
309
+ if (!integration.id || !integration.connections?.length) continue;
310
+ const provider = providerById.get(integration.id);
311
+ const rawModels = {};
312
+ for (const model of v2Models) {
313
+ if (model.providerID !== integration.id || !model.id) continue;
314
+ if (model.enabled === false || model.status === "deprecated") continue;
315
+ const settings = { ...provider?.settings ?? {}, ...model.settings ?? {} };
316
+ const baseUrl = typeof settings["baseURL"] === "string" ? settings["baseURL"] : void 0;
317
+ const packageName = v2SdkPackage(model.package ?? provider?.package ?? "", baseUrl);
318
+ if (!packageName) continue;
319
+ const cost = model.cost?.[0];
320
+ rawModels[model.id] = {
321
+ id: model.id,
322
+ name: model.name,
323
+ family: model.family,
324
+ api: {
325
+ id: model.modelID ?? model.id,
326
+ npm: packageName,
327
+ url: baseUrl
328
+ },
329
+ cost: cost && typeof cost.input === "number" && typeof cost.output === "number" ? {
330
+ input: cost.input,
331
+ output: cost.output,
332
+ cache_read: cost.cache?.read,
333
+ cache_write: cost.cache?.write
334
+ } : void 0,
335
+ limit: model.limit
336
+ };
337
+ }
338
+ if (Object.keys(rawModels).length === 0) continue;
339
+ const envConnection = integration.connections.find((connection) => connection.type === "env" && connection.name);
340
+ result.push({
341
+ id: integration.id,
342
+ name: integration.name ?? integration.id,
343
+ key: envConnection?.name ? env[envConnection.name] : void 0,
344
+ configured: true,
345
+ models: rawModels
346
+ });
347
+ }
348
+ return result;
349
+ }
350
+ async function fetchV2Data(url, authHeader) {
351
+ const response = await fetch(url, { headers: { authorization: authHeader } });
352
+ if (!response.ok) return null;
353
+ const body = await response.json();
354
+ return body.data ?? null;
355
+ }
271
356
  async function fetchRawOpencodeProviders() {
272
357
  const binary = findOpencodeBinary();
273
358
  if (!binary) return null;
274
359
  return new Promise((resolve2) => {
275
360
  let child = null;
276
361
  let settled = false;
277
- const TIMEOUT_MS = 1e4;
362
+ const TIMEOUT_MS = 15e3;
363
+ const password3 = randomBytes(24).toString("hex");
364
+ const authHeader = `Basic ${Buffer.from(`opencode:${password3}`).toString("base64")}`;
278
365
  const finish = (value) => {
279
366
  if (settled) return;
280
367
  settled = true;
@@ -289,31 +376,49 @@ async function fetchRawOpencodeProviders() {
289
376
  finish(null);
290
377
  }, TIMEOUT_MS);
291
378
  try {
292
- child = isWindows ? spawn("cmd.exe", ["/c", binary, "serve", "--port", "0"], { stdio: ["pipe", "pipe", "pipe"] }) : spawn(binary, ["serve", "--port", "0"], { stdio: ["pipe", "pipe", "pipe"] });
379
+ child = isWindows ? spawn("cmd.exe", ["/c", binary, "serve", "--port", "0"], {
380
+ stdio: ["pipe", "pipe", "pipe"],
381
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: password3 }
382
+ }) : spawn(binary, ["serve", "--port", "0"], {
383
+ stdio: ["pipe", "pipe", "pipe"],
384
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: password3 }
385
+ });
293
386
  } catch {
294
387
  finish(null);
295
388
  return;
296
389
  }
297
- const portRegex = /opencode server listening on http:\/\/127\.0\.0\.1:(\d+)/;
298
390
  let portFound = false;
299
391
  let stdoutBuf = "";
300
392
  const onData = (chunk) => {
301
393
  if (portFound) return;
302
394
  stdoutBuf += chunk.toString();
303
- const match = portRegex.exec(stdoutBuf);
304
- if (!match) return;
395
+ const banner = parseOpencodeServerBanner(stdoutBuf);
396
+ if (!banner) return;
305
397
  portFound = true;
306
- const port = match[1];
307
- fetch(`http://127.0.0.1:${port}/config/providers`).then((res) => res.json()).then((data) => {
308
- const raw = data.providers;
309
- if (!Array.isArray(raw)) {
398
+ if (banner.generation === "v1") {
399
+ fetch(`${banner.url}/config/providers`, { headers: { authorization: authHeader } }).then((res) => res.json()).then((data) => {
400
+ const raw = data.providers;
401
+ finish(Array.isArray(raw) ? raw : null);
402
+ }).catch(() => finish(null));
403
+ return;
404
+ }
405
+ void (async () => {
406
+ try {
407
+ const integrations = await fetchV2Data(`${banner.url}/api/integration`, authHeader);
408
+ const models = await fetchV2Data(`${banner.url}/api/model`, authHeader);
409
+ if (!Array.isArray(integrations) || !Array.isArray(models)) {
410
+ finish(null);
411
+ return;
412
+ }
413
+ const connectedIds = integrations.filter((value) => !!value && typeof value === "object").filter((integration) => !!integration.id && !!integration.connections?.length).map((integration) => integration.id);
414
+ const providerValues = await Promise.all(connectedIds.map(
415
+ (id) => fetchV2Data(`${banner.url}/api/provider/${encodeURIComponent(id)}`, authHeader)
416
+ ));
417
+ finish(normalizeV2Providers(models, integrations, providerValues.filter((value) => value !== null)));
418
+ } catch {
310
419
  finish(null);
311
- return;
312
420
  }
313
- finish(raw);
314
- }).catch(() => {
315
- finish(null);
316
- });
421
+ })();
317
422
  };
318
423
  child.stdout?.on("data", onData);
319
424
  child.stderr?.on("data", onData);
@@ -1415,7 +1520,7 @@ async function runProvidersImport() {
1415
1520
  );
1416
1521
  if (result.skipped.length > 0) {
1417
1522
  for (const s of result.skipped) {
1418
- const reason = s.reason === "user-skipped" ? "skipped by you" : s.reason === "conflict-kept" ? "kept your existing config" : s.reason === "oauth-no-token" ? "OAuth provider in OpenCode but not signed in \u2014 run relay-ai providers auth" : s.reason === "no-api-key" ? "no API key in OpenCode \u2014 add key there or use relay-ai providers add" : s.reason === "manual-only" ? "uses gcloud/AWS credentials \u2014 not importable via API key" : s.reason === "placeholder-key" ? "placeholder API key \u2014 provider not imported" : s.reason === "invalid-key" ? "API key failed verification \u2014 provider not imported" : s.reason === "credential-save-failed" ? "could not save credential \u2014 provider not imported" : s.reason;
1523
+ const reason = s.reason === "user-skipped" ? "skipped by you" : s.reason === "conflict-kept" ? "kept your existing config" : s.reason === "oauth-no-token" ? "OAuth provider in OpenCode but not signed in \u2014 run relay-ai providers auth" : s.reason === "no-api-key" ? "API key is not available to import \u2014 use relay-ai providers add" : s.reason === "manual-only" ? "uses gcloud/AWS credentials \u2014 not importable via API key" : s.reason === "placeholder-key" ? "placeholder API key \u2014 provider not imported" : s.reason === "invalid-key" ? "API key failed verification \u2014 provider not imported" : s.reason === "credential-save-failed" ? "could not save credential \u2014 provider not imported" : s.reason;
1419
1524
  p6.log.warn(`Skipped ${s.name} (${s.id}): ${reason}`);
1420
1525
  }
1421
1526
  }
@@ -3435,7 +3540,7 @@ function responsesRateLimitBody(modelId, message) {
3435
3540
  }
3436
3541
 
3437
3542
  // src/codex/routing.ts
3438
- import { randomBytes } from "crypto";
3543
+ import { randomBytes as randomBytes2 } from "crypto";
3439
3544
  function classifyCodexDispatch(modelId, relayRoutes, nativeModelIds) {
3440
3545
  if (nativeModelIds.has(modelId)) return { kind: "native", modelId };
3441
3546
  const route = relayRoutes.find((candidate) => candidate.modelId === modelId);
@@ -3451,7 +3556,7 @@ function classifyCodexMixedDispatch(input) {
3451
3556
  return dispatch;
3452
3557
  }
3453
3558
  function createMixedProxyCapability() {
3454
- return randomBytes(32).toString("base64url");
3559
+ return randomBytes2(32).toString("base64url");
3455
3560
  }
3456
3561
  function mixedProxyBaseUrl(port, capability) {
3457
3562
  return `http://127.0.0.1:${port}/_relay-codex/${capability}`;
@@ -13877,7 +13982,7 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
13877
13982
  }
13878
13983
 
13879
13984
  // src/http-proxy/ca.ts
13880
- import { randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
13985
+ import { randomBytes as randomBytes3, randomUUID as randomUUID4 } from "crypto";
13881
13986
  import {
13882
13987
  chmodSync as chmodSync3,
13883
13988
  existsSync as existsSync13,
@@ -13894,7 +13999,7 @@ var SESSION_ROOT = "http-proxy-sessions";
13894
13999
  var OWNER_FILE = "owner.pid";
13895
14000
  var MID_CREATION_GRACE_MS = 3e4;
13896
14001
  function serialNumber() {
13897
- const bytes = randomBytes2(16);
14002
+ const bytes = randomBytes3(16);
13898
14003
  bytes[0] &= 127;
13899
14004
  return bytes.toString("hex");
13900
14005
  }
@@ -14042,7 +14147,7 @@ ${additionalCa}
14042
14147
  import * as http2 from "http";
14043
14148
  import * as https from "https";
14044
14149
  import * as net from "net";
14045
- import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
14150
+ import { randomBytes as randomBytes4, timingSafeEqual } from "crypto";
14046
14151
  import { URL as URL2 } from "url";
14047
14152
  var ANTHROPIC_HOST = RELAY_SENTINEL_HOST;
14048
14153
  var MAX_BODY_BYTES = 50 * 1024 * 1024;
@@ -14430,7 +14535,7 @@ async function startHttpProxy(options) {
14430
14535
  });
14431
14536
  mitmServer.on("tlsClientError", () => {
14432
14537
  });
14433
- const password3 = randomBytes3(32).toString("base64url");
14538
+ const password3 = randomBytes4(32).toString("base64url");
14434
14539
  const expectedAuthorization = `Basic ${Buffer.from(`${PROXY_USERNAME}:${password3}`).toString("base64")}`;
14435
14540
  const sockets = /* @__PURE__ */ new Set();
14436
14541
  const proxyServer = http2.createServer((req, res) => {
@@ -16051,7 +16156,7 @@ Options:
16051
16156
  --trace Write debug logs under ~/.relay-ai/logs/`);
16052
16157
  return 0;
16053
16158
  }
16054
- const { runUiCommand } = await import("./ui-command-7AZ7SLTB.js");
16159
+ const { runUiCommand } = await import("./ui-command-U3L4LI2K.js");
16055
16160
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
16056
16161
  }
16057
16162
  if (parsed.command === "models") {