@jacobbd/relay-ai 0.12.5 → 0.12.8

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.
@@ -10,7 +10,7 @@ import {
10
10
  supportsManualModels,
11
11
  translateRequest,
12
12
  validateCustomEndpointUrl
13
- } from "./chunk-2LUDFIZX.js";
13
+ } from "./chunk-HLPRPOIV.js";
14
14
 
15
15
  // src/registry/validate-manual-model.ts
16
16
  import { randomUUID } from "crypto";
@@ -149,4 +149,4 @@ export {
149
149
  addManualModel,
150
150
  removeManualModel
151
151
  };
152
- //# sourceMappingURL=chunk-SJEJAHKL.js.map
152
+ //# sourceMappingURL=chunk-VUEGVJMH.js.map
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-VUEGVJMH.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-BK34AFR4.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-HLPRPOIV.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
  }
@@ -2770,6 +2875,16 @@ function resolveOutputKind(flatName, ctx) {
2770
2875
  if (ns) return { kind: "namespace", namespace: ns.namespace, name: ns.name };
2771
2876
  return { kind: "plain" };
2772
2877
  }
2878
+ function toolItemId(kind, upstreamId) {
2879
+ switch (kind.kind) {
2880
+ case "tool_search":
2881
+ return upstreamId.startsWith("tsc_") ? upstreamId : newItemId("tsc");
2882
+ case "custom":
2883
+ return upstreamId.startsWith("ctc_") ? upstreamId : newItemId("ctc");
2884
+ default:
2885
+ return functionCallItemId(upstreamId);
2886
+ }
2887
+ }
2773
2888
  function buildFinalToolItem(kind, flatName, callId, itemId, argsStr) {
2774
2889
  argsStr = normalizeCodexSubagentArguments(flatName, argsStr);
2775
2890
  switch (kind.kind) {
@@ -2857,14 +2972,16 @@ async function writeResponsesStream(fullStream, modelId, write, onDone, onProgre
2857
2972
  return state;
2858
2973
  };
2859
2974
  const createToolState = (rawId, name, signature) => {
2975
+ const kind = resolveOutputKind(name ?? "", options?.toolContext);
2860
2976
  const upstreamId = rawId ?? newItemId("call");
2861
- const itemId = functionCallItemId(upstreamId);
2977
+ const itemId = toolItemId(kind, upstreamId);
2862
2978
  const state = rememberToolState({
2863
2979
  itemId,
2864
2980
  callId: encodeToolUseId(upstreamId, signature, false),
2865
2981
  name: name ?? "unknown",
2866
2982
  outputIndex: outputIndex++,
2867
- args: ""
2983
+ args: "",
2984
+ kind
2868
2985
  });
2869
2986
  emit("response.output_item.added", {
2870
2987
  type: "response.output_item.added",
@@ -3151,7 +3268,7 @@ async function writeResponsesStream(fullStream, modelId, write, onDone, onProgre
3151
3268
  output_index: tool4.outputIndex,
3152
3269
  arguments: normalizedArgs
3153
3270
  });
3154
- const fcItem = buildFinalToolItem(resolveOutputKind(tool4.name, options?.toolContext), tool4.name, tool4.callId, tool4.itemId, normalizedArgs);
3271
+ const fcItem = buildFinalToolItem(tool4.kind, tool4.name, tool4.callId, tool4.itemId, normalizedArgs);
3155
3272
  emit("response.output_item.done", {
3156
3273
  type: "response.output_item.done",
3157
3274
  output_index: tool4.outputIndex,
@@ -3247,11 +3364,12 @@ async function generateResponsesResponse(model, params, modelId) {
3247
3364
  for (const tc of r.toolCalls) {
3248
3365
  const encodedId = encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc), false);
3249
3366
  const argsStr = JSON.stringify(tc.input ?? {});
3367
+ const kind = resolveOutputKind(tc.toolName, toolContext);
3250
3368
  output.push(buildFinalToolItem(
3251
- resolveOutputKind(tc.toolName, toolContext),
3369
+ kind,
3252
3370
  tc.toolName,
3253
3371
  encodedId,
3254
- functionCallItemId(tc.toolCallId),
3372
+ toolItemId(kind, tc.toolCallId),
3255
3373
  argsStr
3256
3374
  ));
3257
3375
  }
@@ -3435,7 +3553,7 @@ function responsesRateLimitBody(modelId, message) {
3435
3553
  }
3436
3554
 
3437
3555
  // src/codex/routing.ts
3438
- import { randomBytes } from "crypto";
3556
+ import { randomBytes as randomBytes2 } from "crypto";
3439
3557
  function classifyCodexDispatch(modelId, relayRoutes, nativeModelIds) {
3440
3558
  if (nativeModelIds.has(modelId)) return { kind: "native", modelId };
3441
3559
  const route = relayRoutes.find((candidate) => candidate.modelId === modelId);
@@ -3451,7 +3569,7 @@ function classifyCodexMixedDispatch(input) {
3451
3569
  return dispatch;
3452
3570
  }
3453
3571
  function createMixedProxyCapability() {
3454
- return randomBytes(32).toString("base64url");
3572
+ return randomBytes2(32).toString("base64url");
3455
3573
  }
3456
3574
  function mixedProxyBaseUrl(port, capability) {
3457
3575
  return `http://127.0.0.1:${port}/_relay-codex/${capability}`;
@@ -3954,6 +4072,11 @@ var EXTERNAL_TOOL_OUTPUT_TYPES = /* @__PURE__ */ new Set([
3954
4072
  "custom_tool_call_output",
3955
4073
  "tool_search_output"
3956
4074
  ]);
4075
+ var EXTERNAL_TOOL_CALL_TYPES = /* @__PURE__ */ new Set([
4076
+ "function_call",
4077
+ "custom_tool_call",
4078
+ "tool_search_call"
4079
+ ]);
3957
4080
  function responsesInputItems(input) {
3958
4081
  if (Array.isArray(input)) return input;
3959
4082
  if (typeof input === "string") {
@@ -3966,8 +4089,13 @@ function isExternalToolOutputItem(item) {
3966
4089
  const type = item.type;
3967
4090
  return typeof type === "string" && EXTERNAL_TOOL_OUTPUT_TYPES.has(type);
3968
4091
  }
4092
+ function isExternalToolCallItem(item) {
4093
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
4094
+ const type = item.type;
4095
+ return typeof type === "string" && EXTERNAL_TOOL_CALL_TYPES.has(type);
4096
+ }
3969
4097
  function isExternalToolContinuation(input) {
3970
- return Array.isArray(input) && input.length > 0 && input.every(isExternalToolOutputItem);
4098
+ return Array.isArray(input) && input.length > 0 && input.some(isExternalToolOutputItem) && !input.some(isExternalToolCallItem);
3971
4099
  }
3972
4100
  function estimateCodexRequestChars(params) {
3973
4101
  let chars = (params.instructions ?? "").length;
@@ -5939,6 +6067,12 @@ function buildCodexProxyRoutesFromResolved(resolved, providersById) {
5939
6067
  oauthAccountId: route.oauthAccountId,
5940
6068
  providerData: route.providerData,
5941
6069
  contextWindow: route.contextWindow,
6070
+ // Reasoning metadata decides whether a picked effort can be translated
6071
+ // at all (e.g. OpenRouter's supportedParameters gate). Dropping these
6072
+ // silently turned every effort selection into a no-op on the wire.
6073
+ supportedParameters: route.supportedParameters,
6074
+ reasoning: route.reasoning,
6075
+ interleavedReasoningField: route.interleavedReasoningField,
5942
6076
  headers: route.headers
5943
6077
  };
5944
6078
  }).filter((r) => r !== void 0);
@@ -6222,7 +6356,10 @@ async function prepareCodexMixedRelayRoutes(models, trace = false) {
6222
6356
  authType: "oauth",
6223
6357
  oauthAccountId: original.oauthAccountId,
6224
6358
  providerData: original.providerData,
6225
- contextWindow: proxyRoute.contextWindow
6359
+ contextWindow: proxyRoute.contextWindow,
6360
+ supportedParameters: original.model.supportedParameters,
6361
+ reasoning: original.model.reasoning,
6362
+ interleavedReasoningField: original.model.interleavedReasoningField
6226
6363
  }),
6227
6364
  trace
6228
6365
  );
@@ -13877,7 +14014,7 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
13877
14014
  }
13878
14015
 
13879
14016
  // src/http-proxy/ca.ts
13880
- import { randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
14017
+ import { randomBytes as randomBytes3, randomUUID as randomUUID4 } from "crypto";
13881
14018
  import {
13882
14019
  chmodSync as chmodSync3,
13883
14020
  existsSync as existsSync13,
@@ -13894,7 +14031,7 @@ var SESSION_ROOT = "http-proxy-sessions";
13894
14031
  var OWNER_FILE = "owner.pid";
13895
14032
  var MID_CREATION_GRACE_MS = 3e4;
13896
14033
  function serialNumber() {
13897
- const bytes = randomBytes2(16);
14034
+ const bytes = randomBytes3(16);
13898
14035
  bytes[0] &= 127;
13899
14036
  return bytes.toString("hex");
13900
14037
  }
@@ -14042,7 +14179,7 @@ ${additionalCa}
14042
14179
  import * as http2 from "http";
14043
14180
  import * as https from "https";
14044
14181
  import * as net from "net";
14045
- import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
14182
+ import { randomBytes as randomBytes4, timingSafeEqual } from "crypto";
14046
14183
  import { URL as URL2 } from "url";
14047
14184
  var ANTHROPIC_HOST = RELAY_SENTINEL_HOST;
14048
14185
  var MAX_BODY_BYTES = 50 * 1024 * 1024;
@@ -14430,7 +14567,7 @@ async function startHttpProxy(options) {
14430
14567
  });
14431
14568
  mitmServer.on("tlsClientError", () => {
14432
14569
  });
14433
- const password3 = randomBytes3(32).toString("base64url");
14570
+ const password3 = randomBytes4(32).toString("base64url");
14434
14571
  const expectedAuthorization = `Basic ${Buffer.from(`${PROXY_USERNAME}:${password3}`).toString("base64")}`;
14435
14572
  const sockets = /* @__PURE__ */ new Set();
14436
14573
  const proxyServer = http2.createServer((req, res) => {
@@ -16051,7 +16188,7 @@ Options:
16051
16188
  --trace Write debug logs under ~/.relay-ai/logs/`);
16052
16189
  return 0;
16053
16190
  }
16054
- const { runUiCommand } = await import("./ui-command-7AZ7SLTB.js");
16191
+ const { runUiCommand } = await import("./ui-command-QUT3R4HW.js");
16055
16192
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
16056
16193
  }
16057
16194
  if (parsed.command === "models") {