@jacobbd/relay-ai 0.7.1 → 0.7.2

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
@@ -60,6 +60,8 @@ import {
60
60
  fmtProvider,
61
61
  fmtProviderBracket,
62
62
  fmtUrl,
63
+ formatAnthropicModelEntry,
64
+ formatAnthropicModelList,
63
65
  formatCodexModelLabel,
64
66
  formatRegistryAuthLabel,
65
67
  formatUpdateNotification,
@@ -172,7 +174,7 @@ import {
172
174
  validateCustomEndpointUrl,
173
175
  writeSecureLogLine,
174
176
  zenRegistryStub
175
- } from "./chunk-6CBNKM55.js";
177
+ } from "./chunk-LNQ46VW7.js";
176
178
  import {
177
179
  filterTemplates,
178
180
  init_provider_templates,
@@ -215,12 +217,12 @@ function findOpencodeBinary() {
215
217
  stdio: ["pipe", "pipe", "pipe"]
216
218
  });
217
219
  const lines = result.trim().split("\n").map((l) => l.trim()).filter(Boolean);
218
- const path2 = (isWindows ? lines.find((l) => l.toLowerCase().endsWith(".cmd")) : null) ?? lines[0];
219
- if (path2) return path2;
220
+ const path3 = (isWindows ? lines.find((l) => l.toLowerCase().endsWith(".cmd")) : null) ?? lines[0];
221
+ if (path3) return path3;
220
222
  } catch {
221
223
  }
222
- for (const path2 of OPENCODE_FALLBACK_PATHS) {
223
- if (existsSync(path2)) return path2;
224
+ for (const path3 of OPENCODE_FALLBACK_PATHS) {
225
+ if (existsSync(path3)) return path3;
224
226
  }
225
227
  return null;
226
228
  }
@@ -596,7 +598,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
596
598
  if (isLinux && !simulate) {
597
599
  secretServiceAvailable = await isSecretServiceAvailable();
598
600
  }
599
- const { display, path: path2 } = detectShellProfile();
601
+ const { display, path: path3 } = detectShellProfile();
600
602
  const saveOptions = (() => {
601
603
  if (isMac) {
602
604
  return [
@@ -655,9 +657,9 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
655
657
  if (await saveToCredentialStore(trimmedKey)) {
656
658
  try {
657
659
  const autoLoadLine = `export OPENCODE_API_KEY="$(security find-generic-password -s relay-ai -a ${GLOBAL_OPENCODE_KEYRING_ACCOUNT} -w 2>/dev/null)"`;
658
- const existing = existsSync2(path2) ? readFileSync(path2, "utf8") : "";
660
+ const existing = existsSync2(path3) ? readFileSync(path3, "utf8") : "";
659
661
  if (!existing.includes(autoLoadLine)) {
660
- appendFileSync(path2, `
662
+ appendFileSync(path3, `
661
663
  # relay-ai: load API key from macOS Keychain
662
664
  ${autoLoadLine}
663
665
  `);
@@ -692,9 +694,9 @@ ${autoLoadLine}
692
694
  }
693
695
  } else if (saveChoice === "profile") {
694
696
  try {
695
- if (!existsSync2(path2)) appendFileSync(path2, "");
697
+ if (!existsSync2(path3)) appendFileSync(path3, "");
696
698
  const escapedKey = trimmedKey.replace(/'/g, "'\\''");
697
- appendFileSync(path2, `
699
+ appendFileSync(path3, `
698
700
  export OPENCODE_API_KEY='${escapedKey}'
699
701
  `);
700
702
  p.log.success(`Key saved to ${display} \u2014 active now and in all future terminals.`);
@@ -3812,11 +3814,11 @@ function ownedOverlayPaths(env = process.env) {
3812
3814
  }
3813
3815
  return paths;
3814
3816
  }
3815
- function atomicWriteFile(path2, content) {
3816
- mkdirSync(dirname(path2), { recursive: true });
3817
- const tmp = `${path2}.tmp.${process.pid}`;
3817
+ function atomicWriteFile(path3, content) {
3818
+ mkdirSync(dirname(path3), { recursive: true });
3819
+ const tmp = `${path3}.tmp.${process.pid}`;
3818
3820
  writeFileSync(tmp, content, "utf8");
3819
- renameSync(tmp, path2);
3821
+ renameSync(tmp, path3);
3820
3822
  }
3821
3823
  function rotateBackups(filePath, env = process.env) {
3822
3824
  if (!existsSync3(filePath)) return;
@@ -3834,24 +3836,24 @@ function rotateBackups(filePath, env = process.env) {
3834
3836
  }
3835
3837
  }
3836
3838
  }
3837
- function writeOverlayFile(path2, content, env = process.env) {
3838
- rotateBackups(path2, env);
3839
- atomicWriteFile(path2, content);
3839
+ function writeOverlayFile(path3, content, env = process.env) {
3840
+ rotateBackups(path3, env);
3841
+ atomicWriteFile(path3, content);
3840
3842
  }
3841
3843
  function readSessionLock(env = process.env) {
3842
- const path2 = getSessionLockPath(env);
3843
- if (!existsSync3(path2)) return null;
3844
+ const path3 = getSessionLockPath(env);
3845
+ if (!existsSync3(path3)) return null;
3844
3846
  try {
3845
- const parsed = JSON.parse(readFileSync2(path2, "utf8"));
3847
+ const parsed = JSON.parse(readFileSync2(path3, "utf8"));
3846
3848
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
3847
3849
  } catch {
3848
3850
  }
3849
3851
  return null;
3850
3852
  }
3851
3853
  function writeSessionLock(lock, env = process.env) {
3852
- const path2 = getSessionLockPath(env);
3854
+ const path3 = getSessionLockPath(env);
3853
3855
  mkdirSync(getRelayAiCodexDir(env), { recursive: true });
3854
- atomicWriteFile(path2, `${JSON.stringify(lock, null, 2)}
3856
+ atomicWriteFile(path3, `${JSON.stringify(lock, null, 2)}
3855
3857
  `);
3856
3858
  }
3857
3859
  function isProcessAlive(pid) {
@@ -3868,11 +3870,11 @@ function isConcurrentSession(lock) {
3868
3870
  }
3869
3871
  function restoreCodexOverlay(env = process.env) {
3870
3872
  const removed = [];
3871
- for (const path2 of ownedOverlayPaths(env)) {
3872
- if (!existsSync3(path2)) continue;
3873
+ for (const path3 of ownedOverlayPaths(env)) {
3874
+ if (!existsSync3(path3)) continue;
3873
3875
  try {
3874
- rmSync(path2, { force: true });
3875
- removed.push(path2);
3876
+ rmSync(path3, { force: true });
3877
+ removed.push(path3);
3876
3878
  } catch {
3877
3879
  }
3878
3880
  }
@@ -4019,16 +4021,16 @@ function findCodexBinary() {
4019
4021
  }
4020
4022
  function selectCodexBinary(candidates, exists, canRun) {
4021
4023
  const seen = /* @__PURE__ */ new Set();
4022
- for (const path2 of candidates) {
4023
- if (!path2 || seen.has(path2)) continue;
4024
- seen.add(path2);
4025
- if (exists(path2) && canRun(path2)) return path2;
4024
+ for (const path3 of candidates) {
4025
+ if (!path3 || seen.has(path3)) continue;
4026
+ seen.add(path3);
4027
+ if (exists(path3) && canRun(path3)) return path3;
4026
4028
  }
4027
4029
  return null;
4028
4030
  }
4029
- function canRunCodexBinary(path2) {
4031
+ function canRunCodexBinary(path3) {
4030
4032
  try {
4031
- execFileSync(path2, ["--version"], {
4033
+ execFileSync(path3, ["--version"], {
4032
4034
  encoding: "utf8",
4033
4035
  stdio: ["ignore", "pipe", "pipe"],
4034
4036
  timeout: 5e3,
@@ -8248,12 +8250,12 @@ function findAntigravityCliBinary() {
8248
8250
  encoding: "utf8",
8249
8251
  stdio: ["pipe", "pipe", "pipe"]
8250
8252
  });
8251
- const path2 = result.trim().split("\n")[0]?.trim();
8252
- if (path2) return path2;
8253
+ const path3 = result.trim().split("\n")[0]?.trim();
8254
+ if (path3) return path3;
8253
8255
  } catch {
8254
8256
  }
8255
- for (const path2 of FALLBACK_PATHS) {
8256
- if (existsSync6(path2)) return path2;
8257
+ for (const path3 of FALLBACK_PATHS) {
8258
+ if (existsSync6(path3)) return path3;
8257
8259
  }
8258
8260
  return null;
8259
8261
  }
@@ -9111,9 +9113,9 @@ function applyRestoreNumber(config, key, had, value) {
9111
9113
  delete config[key];
9112
9114
  }
9113
9115
  }
9114
- function readCodexConfigText(path2 = getCodexConfigPath()) {
9115
- if (!existsSync8(path2)) return "";
9116
- return readFileSync3(path2, "utf8");
9116
+ function readCodexConfigText(path3 = getCodexConfigPath()) {
9117
+ if (!existsSync8(path3)) return "";
9118
+ return readFileSync3(path3, "utf8");
9117
9119
  }
9118
9120
  function parseCodexConfig(text4) {
9119
9121
  if (!text4.trim()) return {};
@@ -9321,10 +9323,10 @@ function getAppCatalogPath(providerId, env = process.env) {
9321
9323
  return join10(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
9322
9324
  }
9323
9325
  function readAppSessionLock(env = process.env) {
9324
- const path2 = getAppSessionLockPath(env);
9325
- if (!existsSync9(path2)) return null;
9326
+ const path3 = getAppSessionLockPath(env);
9327
+ if (!existsSync9(path3)) return null;
9326
9328
  try {
9327
- const parsed = JSON.parse(readFileSync4(path2, "utf8"));
9329
+ const parsed = JSON.parse(readFileSync4(path3, "utf8"));
9328
9330
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
9329
9331
  } catch {
9330
9332
  }
@@ -9335,14 +9337,14 @@ function writeAppSessionLock(lock, env = process.env) {
9335
9337
  `);
9336
9338
  }
9337
9339
  function clearAppSessionLock(env = process.env) {
9338
- const path2 = getAppSessionLockPath(env);
9339
- if (existsSync9(path2)) rmSync4(path2, { force: true });
9340
+ const path3 = getAppSessionLockPath(env);
9341
+ if (existsSync9(path3)) rmSync4(path3, { force: true });
9340
9342
  }
9341
9343
  function readAppRestoreState(env = process.env) {
9342
- const path2 = getAppRestoreStatePath(env);
9343
- if (!existsSync9(path2)) return null;
9344
+ const path3 = getAppRestoreStatePath(env);
9345
+ if (!existsSync9(path3)) return null;
9344
9346
  try {
9345
- return JSON.parse(readFileSync4(path2, "utf8"));
9347
+ return JSON.parse(readFileSync4(path3, "utf8"));
9346
9348
  } catch {
9347
9349
  return null;
9348
9350
  }
@@ -9353,8 +9355,8 @@ function writeAppRestoreState(state, env = process.env) {
9353
9355
  `);
9354
9356
  }
9355
9357
  function clearAppRestoreState(env = process.env) {
9356
- const path2 = getAppRestoreStatePath(env);
9357
- if (existsSync9(path2)) rmSync4(path2, { force: true });
9358
+ const path3 = getAppRestoreStatePath(env);
9359
+ if (existsSync9(path3)) rmSync4(path3, { force: true });
9358
9360
  }
9359
9361
  function backupConfigToml(env = process.env) {
9360
9362
  const configPath = getCodexConfigPath();
@@ -9384,10 +9386,10 @@ function ownedAppCatalogPaths(env = process.env) {
9384
9386
  }
9385
9387
  function removeAppCatalogs(env = process.env) {
9386
9388
  const removed = [];
9387
- for (const path2 of ownedAppCatalogPaths(env)) {
9389
+ for (const path3 of ownedAppCatalogPaths(env)) {
9388
9390
  try {
9389
- rmSync4(path2, { force: true });
9390
- removed.push(path2);
9391
+ rmSync4(path3, { force: true });
9392
+ removed.push(path3);
9391
9393
  } catch {
9392
9394
  }
9393
9395
  }
@@ -10235,10 +10237,10 @@ function getSessionLockPath2() {
10235
10237
  return join12(getClaudeDesktopHome(), ".relay-ai.lock");
10236
10238
  }
10237
10239
  function inspectSessionLock() {
10238
- const path2 = getSessionLockPath2();
10239
- if (!existsSync11(path2)) return { status: "missing" };
10240
+ const path3 = getSessionLockPath2();
10241
+ if (!existsSync11(path3)) return { status: "missing" };
10240
10242
  try {
10241
- const parsed = JSON.parse(readFileSync6(path2, "utf8"));
10243
+ const parsed = JSON.parse(readFileSync6(path3, "utf8"));
10242
10244
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string" && typeof parsed.uuid === "string" && typeof parsed.proxyPort === "number") {
10243
10245
  return { status: "valid", lock: parsed };
10244
10246
  }
@@ -10247,13 +10249,13 @@ function inspectSessionLock() {
10247
10249
  return { status: "unreadable" };
10248
10250
  }
10249
10251
  function writeSessionLock2(lock) {
10250
- const path2 = getSessionLockPath2();
10251
- const tempPath = `${path2}.tmp.${process.pid}`;
10252
- mkdirSync6(dirname4(path2), { recursive: true });
10252
+ const path3 = getSessionLockPath2();
10253
+ const tempPath = `${path3}.tmp.${process.pid}`;
10254
+ mkdirSync6(dirname4(path3), { recursive: true });
10253
10255
  try {
10254
10256
  writeFileSync5(tempPath, `${JSON.stringify(lock, null, 2)}
10255
10257
  `, "utf8");
10256
- renameSync2(tempPath, path2);
10258
+ renameSync2(tempPath, path3);
10257
10259
  } finally {
10258
10260
  try {
10259
10261
  rmSync5(tempPath, { force: true });
@@ -11153,26 +11155,61 @@ function printAiInstallResult(result) {
11153
11155
  console.error(`relay-ai agent skill target version: v${result.version}`);
11154
11156
  if (result.installed.length > 0) {
11155
11157
  console.error(`Installed ${result.installed.length} new skill(s):`);
11156
- for (const path2 of result.installed) console.error(` \u2713 ${path2}`);
11158
+ for (const path3 of result.installed) console.error(` \u2713 ${path3}`);
11157
11159
  }
11158
11160
  if (result.updated.length > 0) {
11159
11161
  console.error(`Updated ${result.updated.length} skill(s):`);
11160
- for (const { path: path2, fromVersion } of result.updated) {
11162
+ for (const { path: path3, fromVersion } of result.updated) {
11161
11163
  const from = fromVersion ? `v${fromVersion}` : "unknown";
11162
- console.error(` \u2713 ${path2} (${from} \u2192 v${result.version})`);
11164
+ console.error(` \u2713 ${path3} (${from} \u2192 v${result.version})`);
11163
11165
  }
11164
11166
  }
11165
11167
  if (result.skipped.length > 0) {
11166
11168
  console.error(`Skipped ${result.skipped.length} (already v${result.version}):`);
11167
- for (const path2 of result.skipped) console.error(` \xB7 ${path2}`);
11169
+ for (const path3 of result.skipped) console.error(` \xB7 ${path3}`);
11168
11170
  }
11169
11171
  if (result.failed.length > 0) {
11170
11172
  console.error(`Failed ${result.failed.length}:`);
11171
- for (const path2 of result.failed) console.error(` \u2717 ${path2}`);
11173
+ for (const path3 of result.failed) console.error(` \u2717 ${path3}`);
11172
11174
  }
11173
11175
  return result.failed.length > 0 ? 1 : 0;
11174
11176
  }
11175
11177
 
11178
+ // src/http-proxy/discovery-cache.ts
11179
+ import * as fs2 from "fs";
11180
+ import * as path2 from "path";
11181
+ import * as os from "os";
11182
+
11183
+ // src/http-proxy/anthropic-host.ts
11184
+ var ANTHROPIC_UPSTREAM_HOST = "api.anthropic.com";
11185
+ var RELAY_SENTINEL_HOST = "api.anthropic.com.relay.invalid";
11186
+ var RELAY_BASE_URL = `https://${RELAY_SENTINEL_HOST}`;
11187
+
11188
+ // src/http-proxy/discovery-cache.ts
11189
+ var GATEWAY_DISCOVERY_BASE_URL = RELAY_BASE_URL;
11190
+ var CACHE_FILE = "gateway-models.json";
11191
+ function cachePath(baseEnv) {
11192
+ const claudeDir = baseEnv["CLAUDE_CONFIG_DIR"]?.trim() || path2.join(os.homedir(), ".claude");
11193
+ return path2.join(claudeDir, "cache", CACHE_FILE);
11194
+ }
11195
+ function writeGatewayDiscoveryCache(baseEnv, routes) {
11196
+ try {
11197
+ const models = routes.filter((route) => Boolean(route.gatewayAliasId)).map((route) => formatAnthropicModelEntry(
11198
+ route.gatewayAliasId,
11199
+ route.displayName,
11200
+ route.contextWindow
11201
+ ));
11202
+ const file = cachePath(baseEnv);
11203
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
11204
+ fs2.writeFileSync(
11205
+ file,
11206
+ JSON.stringify({ baseUrl: GATEWAY_DISCOVERY_BASE_URL, fetchedAt: Date.now(), models }),
11207
+ { encoding: "utf8", mode: 384 }
11208
+ );
11209
+ } catch {
11210
+ }
11211
+ }
11212
+
11176
11213
  // src/http-proxy/env.ts
11177
11214
  var PROXY_ENV_NAMES = [
11178
11215
  "HTTPS_PROXY",
@@ -11254,6 +11291,8 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
11254
11291
  if (name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN") continue;
11255
11292
  delete env[name];
11256
11293
  }
11294
+ env["ANTHROPIC_BASE_URL"] = RELAY_BASE_URL;
11295
+ env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1";
11257
11296
  return env;
11258
11297
  }
11259
11298
 
@@ -11262,14 +11301,14 @@ import { randomBytes, randomUUID as randomUUID3 } from "crypto";
11262
11301
  import {
11263
11302
  chmodSync,
11264
11303
  existsSync as existsSync13,
11265
- mkdirSync as mkdirSync8,
11304
+ mkdirSync as mkdirSync9,
11266
11305
  readFileSync as readFileSync8,
11267
11306
  readdirSync as readdirSync3,
11268
11307
  rmSync as rmSync6,
11269
11308
  statSync as statSync2,
11270
- writeFileSync as writeFileSync7
11309
+ writeFileSync as writeFileSync8
11271
11310
  } from "fs";
11272
- import { dirname as dirname5, join as join14, resolve } from "path";
11311
+ import { dirname as dirname6, join as join15, resolve } from "path";
11273
11312
  import forge from "node-forge";
11274
11313
  var SESSION_ROOT = "http-proxy-sessions";
11275
11314
  var OWNER_FILE = "owner.pid";
@@ -11289,15 +11328,15 @@ function processIsRunning(pid) {
11289
11328
  }
11290
11329
  }
11291
11330
  function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
11292
- const root = join14(appHome, SESSION_ROOT);
11331
+ const root = join15(appHome, SESSION_ROOT);
11293
11332
  if (!existsSync13(root)) return;
11294
11333
  const now = Date.now();
11295
11334
  for (const name of readdirSync3(root)) {
11296
- const sessionDir = join14(root, name);
11335
+ const sessionDir = join15(root, name);
11297
11336
  try {
11298
11337
  const stat = statSync2(sessionDir);
11299
11338
  if (!stat.isDirectory()) continue;
11300
- const ownerPath = join14(sessionDir, OWNER_FILE);
11339
+ const ownerPath = join15(sessionDir, OWNER_FILE);
11301
11340
  if (!existsSync13(ownerPath)) {
11302
11341
  if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
11303
11342
  rmSync6(sessionDir, { recursive: true, force: true });
@@ -11320,13 +11359,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
11320
11359
  }
11321
11360
  function createHttpProxyCertificates(appHome = getAppHome()) {
11322
11361
  cleanupStaleHttpProxySessions(appHome);
11323
- const root = join14(appHome, SESSION_ROOT);
11324
- mkdirSync8(root, { recursive: true, mode: 448 });
11362
+ const root = join15(appHome, SESSION_ROOT);
11363
+ mkdirSync9(root, { recursive: true, mode: 448 });
11325
11364
  chmodSync(root, 448);
11326
- const sessionDir = join14(root, randomUUID3());
11327
- mkdirSync8(sessionDir, { mode: 448 });
11365
+ const sessionDir = join15(root, randomUUID3());
11366
+ mkdirSync9(sessionDir, { mode: 448 });
11328
11367
  chmodSync(sessionDir, 448);
11329
- writeFileSync7(join14(sessionDir, OWNER_FILE), `${process.pid}
11368
+ writeFileSync8(join15(sessionDir, OWNER_FILE), `${process.pid}
11330
11369
  `, { mode: 384 });
11331
11370
  try {
11332
11371
  const caKeys = forge.pki.rsa.generateKeyPair(2048);
@@ -11350,19 +11389,19 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
11350
11389
  server.serialNumber = serialNumber();
11351
11390
  server.validity.notBefore = new Date(Date.now() - 6e4);
11352
11391
  server.validity.notAfter = new Date(Date.now() + 48 * 60 * 60 * 1e3);
11353
- server.setSubject([{ name: "commonName", value: "api.anthropic.com" }]);
11392
+ server.setSubject([{ name: "commonName", value: RELAY_SENTINEL_HOST }]);
11354
11393
  server.setIssuer(ca.subject.attributes);
11355
11394
  server.setExtensions([
11356
11395
  { name: "basicConstraints", cA: false, critical: true },
11357
11396
  { name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
11358
11397
  { name: "extKeyUsage", serverAuth: true },
11359
- { name: "subjectAltName", altNames: [{ type: 2, value: "api.anthropic.com" }] },
11398
+ { name: "subjectAltName", altNames: [{ type: 2, value: RELAY_SENTINEL_HOST }] },
11360
11399
  { name: "subjectKeyIdentifier" }
11361
11400
  ]);
11362
11401
  server.sign(caKeys.privateKey, forge.md.sha256.create());
11363
11402
  const caCert = forge.pki.certificateToPem(ca);
11364
- const caCertPath = join14(sessionDir, "relay-ai-ca.pem");
11365
- writeFileSync7(caCertPath, caCert, { encoding: "utf8", mode: 384 });
11403
+ const caCertPath = join15(sessionDir, "relay-ai-ca.pem");
11404
+ writeFileSync8(caCertPath, caCert, { encoding: "utf8", mode: 384 });
11366
11405
  chmodSync(caCertPath, 384);
11367
11406
  let cleaned = false;
11368
11407
  const cleanupOnExit = () => {
@@ -11407,8 +11446,8 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
11407
11446
  const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
11408
11447
  const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
11409
11448
  if (!additionalCa) return relayCaCertPath;
11410
- const combinedPath = join14(dirname5(relayCaCertPath), "combined-ca.pem");
11411
- writeFileSync7(
11449
+ const combinedPath = join15(dirname6(relayCaCertPath), "combined-ca.pem");
11450
+ writeFileSync8(
11412
11451
  combinedPath,
11413
11452
  `${relayCa}
11414
11453
  ${additionalCa}
@@ -11429,6 +11468,20 @@ import { URL as URL2 } from "url";
11429
11468
  // src/anthropic-endpoints.ts
11430
11469
  var MESSAGE_PATH = "/v1/messages";
11431
11470
  var COUNT_TOKENS_PATH = "/v1/messages/count_tokens";
11471
+ var MODELS_PATH = "/v1/models";
11472
+ function anthropicModelsEndpoint(url) {
11473
+ if (!url) return null;
11474
+ try {
11475
+ const pathname = new URL(url, "http://relay.local").pathname;
11476
+ if (pathname === MODELS_PATH || pathname === `${MODELS_PATH}/`) return "list";
11477
+ if (pathname.startsWith(`${MODELS_PATH}/`)) {
11478
+ const id = decodeURIComponent(pathname.slice(MODELS_PATH.length + 1));
11479
+ if (id) return { id };
11480
+ }
11481
+ } catch {
11482
+ }
11483
+ return null;
11484
+ }
11432
11485
  function anthropicMessagesEndpoint(url) {
11433
11486
  if (!url) return null;
11434
11487
  try {
@@ -11459,7 +11512,7 @@ function estimateAnthropicInputTokens(body) {
11459
11512
  }
11460
11513
 
11461
11514
  // src/http-proxy/server.ts
11462
- var ANTHROPIC_HOST = "api.anthropic.com";
11515
+ var ANTHROPIC_HOST = RELAY_SENTINEL_HOST;
11463
11516
  var MAX_BODY_BYTES = 50 * 1024 * 1024;
11464
11517
  var PROXY_USERNAME = "relay-ai";
11465
11518
  function authorityParts(authority) {
@@ -11533,15 +11586,29 @@ function readRawBody(req) {
11533
11586
  req.once("error", fail);
11534
11587
  });
11535
11588
  }
11536
- function requestHeadersWithoutProxyHeaders(req) {
11589
+ function requestHeadersWithoutProxyHeaders(req, hostOverride) {
11537
11590
  const headers = [];
11538
11591
  for (let index = 0; index < req.rawHeaders.length; index += 2) {
11539
11592
  const name = req.rawHeaders[index];
11540
11593
  if (/^proxy-(authorization|connection)$/i.test(name)) continue;
11594
+ if (hostOverride && /^host$/i.test(name)) {
11595
+ headers.push(name, hostOverride);
11596
+ continue;
11597
+ }
11541
11598
  headers.push(name, req.rawHeaders[index + 1] ?? "");
11542
11599
  }
11543
11600
  return headers;
11544
11601
  }
11602
+ function restoreBillingCch(headers) {
11603
+ for (let index = 0; index < headers.length; index += 2) {
11604
+ if (!/^x-anthropic-billing-header$/i.test(headers[index])) continue;
11605
+ const value = headers[index + 1] ?? "";
11606
+ if (/(?:^|;)\s*cch=/.test(value)) break;
11607
+ headers[index + 1] = `${value.replace(/;?\s*$/, "")}; cch=00000;`;
11608
+ break;
11609
+ }
11610
+ return headers;
11611
+ }
11545
11612
  function copyResponse(upstream, res) {
11546
11613
  res.writeHead(upstream.statusCode ?? 502, upstream.statusMessage, upstream.rawHeaders);
11547
11614
  upstream.once("error", (error) => res.destroy(error));
@@ -11562,7 +11629,7 @@ function forwardRawRequest(req, res, rawBody, origin, rejectUnauthorized) {
11562
11629
  port: origin.port || void 0,
11563
11630
  method: req.method,
11564
11631
  path: req.url,
11565
- headers: requestHeadersWithoutProxyHeaders(req),
11632
+ headers: restoreBillingCch(requestHeadersWithoutProxyHeaders(req, ANTHROPIC_UPSTREAM_HOST)),
11566
11633
  ...origin.protocol === "https:" ? { rejectUnauthorized } : {}
11567
11634
  }, (upstreamRes) => {
11568
11635
  copyResponse(upstreamRes, res);
@@ -11618,6 +11685,40 @@ function forwardToAdapter(req, res, rawBody, adapter) {
11618
11685
  upstream.end(rawBody);
11619
11686
  });
11620
11687
  }
11688
+ function fetchUpstreamModelsList(req, origin, rejectUnauthorized) {
11689
+ return new Promise((resolve2) => {
11690
+ const transport = origin.protocol === "https:" ? https : http2;
11691
+ const rawHeaders = requestHeadersWithoutProxyHeaders(req, ANTHROPIC_UPSTREAM_HOST);
11692
+ const headers = ["accept-encoding", "identity"];
11693
+ for (let i = 0; i < rawHeaders.length; i += 2) {
11694
+ const name = rawHeaders[i];
11695
+ if (/^accept-encoding$/i.test(name)) continue;
11696
+ headers.push(name, rawHeaders[i + 1] ?? "");
11697
+ }
11698
+ const upstream = transport.request({
11699
+ protocol: origin.protocol,
11700
+ hostname: origin.hostname,
11701
+ port: origin.port || void 0,
11702
+ method: "GET",
11703
+ path: req.url,
11704
+ headers,
11705
+ ...origin.protocol === "https:" ? { rejectUnauthorized } : {}
11706
+ }, (upstreamRes) => {
11707
+ const chunks = [];
11708
+ upstreamRes.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
11709
+ upstreamRes.once("end", () => {
11710
+ resolve2({
11711
+ statusCode: upstreamRes.statusCode ?? 500,
11712
+ headers: upstreamRes.headers,
11713
+ body: Buffer.concat(chunks)
11714
+ });
11715
+ });
11716
+ upstreamRes.once("error", () => resolve2(null));
11717
+ });
11718
+ upstream.once("error", () => resolve2(null));
11719
+ upstream.end();
11720
+ });
11721
+ }
11621
11722
  function forwardPlainHttp(req, res) {
11622
11723
  let target;
11623
11724
  try {
@@ -11656,6 +11757,9 @@ async function startHttpProxy(options) {
11656
11757
  const routesById = /* @__PURE__ */ new Map();
11657
11758
  for (const route of options.routes) {
11658
11759
  for (const id of routeLookupIds(route.aliasId)) routesById.set(id, route);
11760
+ if (route.gatewayAliasId) {
11761
+ for (const id of routeLookupIds(route.gatewayAliasId)) routesById.set(id, route);
11762
+ }
11659
11763
  }
11660
11764
  const anthropicOrigin = new URL2(options.anthropicOrigin ?? "https://api.anthropic.com");
11661
11765
  if (anthropicOrigin.protocol !== "http:" && anthropicOrigin.protocol !== "https:") {
@@ -11690,6 +11794,75 @@ async function startHttpProxy(options) {
11690
11794
  }
11691
11795
  return;
11692
11796
  }
11797
+ const modelsEndpoint = anthropicModelsEndpoint(req.url);
11798
+ if (req.method === "GET" && modelsEndpoint && options.routes.length > 0) {
11799
+ if (modelsEndpoint === "list") {
11800
+ const upstreamResult = await fetchUpstreamModelsList(
11801
+ req,
11802
+ anthropicOrigin,
11803
+ options.anthropicRejectUnauthorized ?? true
11804
+ );
11805
+ const relayEntries = options.routes.filter((r) => Boolean(r.gatewayAliasId)).map((r) => formatAnthropicModelEntry(
11806
+ r.gatewayAliasId,
11807
+ r.displayName,
11808
+ r.contextWindow
11809
+ ));
11810
+ if (upstreamResult && upstreamResult.statusCode === 200) {
11811
+ try {
11812
+ const parsed = JSON.parse(upstreamResult.body.toString("utf8"));
11813
+ if (Array.isArray(parsed.data)) {
11814
+ const existingIds = new Set(parsed.data.map((m) => String(m.id ?? "")));
11815
+ for (const entry of relayEntries) {
11816
+ if (!existingIds.has(entry.id)) {
11817
+ parsed.data.push(entry);
11818
+ }
11819
+ }
11820
+ const responseBuffer = Buffer.from(JSON.stringify(parsed));
11821
+ const headers = { ...upstreamResult.headers };
11822
+ delete headers["content-length"];
11823
+ delete headers["content-encoding"];
11824
+ headers["content-type"] = "application/json";
11825
+ headers["content-length"] = String(responseBuffer.length);
11826
+ res.writeHead(200, headers);
11827
+ res.end(responseBuffer);
11828
+ return;
11829
+ }
11830
+ } catch {
11831
+ }
11832
+ }
11833
+ const fallbackList = formatAnthropicModelList(
11834
+ options.routes.map((r) => ({
11835
+ id: r.gatewayAliasId ?? r.aliasId,
11836
+ name: r.displayName,
11837
+ contextWindow: r.contextWindow
11838
+ }))
11839
+ );
11840
+ const buffer = Buffer.from(JSON.stringify(fallbackList));
11841
+ res.writeHead(200, {
11842
+ "content-type": "application/json",
11843
+ "content-length": String(buffer.length)
11844
+ });
11845
+ res.end(buffer);
11846
+ return;
11847
+ }
11848
+ if (typeof modelsEndpoint === "object" && modelsEndpoint.id) {
11849
+ const matched = routesById.get(modelsEndpoint.id);
11850
+ if (matched) {
11851
+ const entry = formatAnthropicModelEntry(
11852
+ matched.gatewayAliasId ?? matched.aliasId,
11853
+ matched.displayName,
11854
+ matched.contextWindow
11855
+ );
11856
+ const buffer = Buffer.from(JSON.stringify(entry));
11857
+ res.writeHead(200, {
11858
+ "content-type": "application/json",
11859
+ "content-length": String(buffer.length)
11860
+ });
11861
+ res.end(buffer);
11862
+ return;
11863
+ }
11864
+ }
11865
+ }
11693
11866
  const endpoint = anthropicMessagesEndpoint(req.url);
11694
11867
  if (req.method === "POST" && endpoint) {
11695
11868
  let parsed = null;
@@ -11891,6 +12064,7 @@ async function launchClaudeWithHttpProxy(options, dependencies = defaultDependen
11891
12064
  proxy.handle.proxyUrl,
11892
12065
  proxy.handle.caCertPath
11893
12066
  );
12067
+ writeGatewayDiscoveryCache(options.baseEnv, proxy.loaded.routes);
11894
12068
  const exitCode = await dependencies.launch(
11895
12069
  childEnv,
11896
12070
  proxy.startingModel,
@@ -13264,7 +13438,7 @@ Options:
13264
13438
  --trace Write debug logs under ~/.relay-ai/logs/`);
13265
13439
  return 0;
13266
13440
  }
13267
- const { runUiCommand } = await import("./ui-command-S3TQKB3D.js");
13441
+ const { runUiCommand } = await import("./ui-command-5VEFZ5GM.js");
13268
13442
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
13269
13443
  }
13270
13444
  if (parsed.command === "models") {