@jacobbd/relay-ai 0.7.1 → 0.7.3

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
@@ -14,6 +14,8 @@ import {
14
14
  addOpencodeCloudFromApiKey,
15
15
  addProviderFromTemplate,
16
16
  aliasModelId,
17
+ anthropicMessagesEndpoint,
18
+ anthropicModelsEndpoint,
17
19
  appendCodexBodyDump,
18
20
  authenticateProvider,
19
21
  buildAntigravityChildEnv,
@@ -45,6 +47,7 @@ import {
45
47
  effectiveProviderBaseUrl,
46
48
  effortProviderOptions,
47
49
  encodeToolUseId,
50
+ estimateAnthropicInputTokens,
48
51
  evaluateAgySwitchCompatibility,
49
52
  extractApiKey,
50
53
  favoriteProviderDisplayName,
@@ -60,6 +63,8 @@ import {
60
63
  fmtProvider,
61
64
  fmtProviderBracket,
62
65
  fmtUrl,
66
+ formatAnthropicModelEntry,
67
+ formatAnthropicModelList,
63
68
  formatCodexModelLabel,
64
69
  formatRegistryAuthLabel,
65
70
  formatUpdateNotification,
@@ -172,7 +177,7 @@ import {
172
177
  validateCustomEndpointUrl,
173
178
  writeSecureLogLine,
174
179
  zenRegistryStub
175
- } from "./chunk-6CBNKM55.js";
180
+ } from "./chunk-5LPUIWNJ.js";
176
181
  import {
177
182
  filterTemplates,
178
183
  init_provider_templates,
@@ -215,12 +220,12 @@ function findOpencodeBinary() {
215
220
  stdio: ["pipe", "pipe", "pipe"]
216
221
  });
217
222
  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;
223
+ const path3 = (isWindows ? lines.find((l) => l.toLowerCase().endsWith(".cmd")) : null) ?? lines[0];
224
+ if (path3) return path3;
220
225
  } catch {
221
226
  }
222
- for (const path2 of OPENCODE_FALLBACK_PATHS) {
223
- if (existsSync(path2)) return path2;
227
+ for (const path3 of OPENCODE_FALLBACK_PATHS) {
228
+ if (existsSync(path3)) return path3;
224
229
  }
225
230
  return null;
226
231
  }
@@ -596,7 +601,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
596
601
  if (isLinux && !simulate) {
597
602
  secretServiceAvailable = await isSecretServiceAvailable();
598
603
  }
599
- const { display, path: path2 } = detectShellProfile();
604
+ const { display, path: path3 } = detectShellProfile();
600
605
  const saveOptions = (() => {
601
606
  if (isMac) {
602
607
  return [
@@ -655,9 +660,9 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
655
660
  if (await saveToCredentialStore(trimmedKey)) {
656
661
  try {
657
662
  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") : "";
663
+ const existing = existsSync2(path3) ? readFileSync(path3, "utf8") : "";
659
664
  if (!existing.includes(autoLoadLine)) {
660
- appendFileSync(path2, `
665
+ appendFileSync(path3, `
661
666
  # relay-ai: load API key from macOS Keychain
662
667
  ${autoLoadLine}
663
668
  `);
@@ -692,9 +697,9 @@ ${autoLoadLine}
692
697
  }
693
698
  } else if (saveChoice === "profile") {
694
699
  try {
695
- if (!existsSync2(path2)) appendFileSync(path2, "");
700
+ if (!existsSync2(path3)) appendFileSync(path3, "");
696
701
  const escapedKey = trimmedKey.replace(/'/g, "'\\''");
697
- appendFileSync(path2, `
702
+ appendFileSync(path3, `
698
703
  export OPENCODE_API_KEY='${escapedKey}'
699
704
  `);
700
705
  p.log.success(`Key saved to ${display} \u2014 active now and in all future terminals.`);
@@ -3812,11 +3817,11 @@ function ownedOverlayPaths(env = process.env) {
3812
3817
  }
3813
3818
  return paths;
3814
3819
  }
3815
- function atomicWriteFile(path2, content) {
3816
- mkdirSync(dirname(path2), { recursive: true });
3817
- const tmp = `${path2}.tmp.${process.pid}`;
3820
+ function atomicWriteFile(path3, content) {
3821
+ mkdirSync(dirname(path3), { recursive: true });
3822
+ const tmp = `${path3}.tmp.${process.pid}`;
3818
3823
  writeFileSync(tmp, content, "utf8");
3819
- renameSync(tmp, path2);
3824
+ renameSync(tmp, path3);
3820
3825
  }
3821
3826
  function rotateBackups(filePath, env = process.env) {
3822
3827
  if (!existsSync3(filePath)) return;
@@ -3834,24 +3839,24 @@ function rotateBackups(filePath, env = process.env) {
3834
3839
  }
3835
3840
  }
3836
3841
  }
3837
- function writeOverlayFile(path2, content, env = process.env) {
3838
- rotateBackups(path2, env);
3839
- atomicWriteFile(path2, content);
3842
+ function writeOverlayFile(path3, content, env = process.env) {
3843
+ rotateBackups(path3, env);
3844
+ atomicWriteFile(path3, content);
3840
3845
  }
3841
3846
  function readSessionLock(env = process.env) {
3842
- const path2 = getSessionLockPath(env);
3843
- if (!existsSync3(path2)) return null;
3847
+ const path3 = getSessionLockPath(env);
3848
+ if (!existsSync3(path3)) return null;
3844
3849
  try {
3845
- const parsed = JSON.parse(readFileSync2(path2, "utf8"));
3850
+ const parsed = JSON.parse(readFileSync2(path3, "utf8"));
3846
3851
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
3847
3852
  } catch {
3848
3853
  }
3849
3854
  return null;
3850
3855
  }
3851
3856
  function writeSessionLock(lock, env = process.env) {
3852
- const path2 = getSessionLockPath(env);
3857
+ const path3 = getSessionLockPath(env);
3853
3858
  mkdirSync(getRelayAiCodexDir(env), { recursive: true });
3854
- atomicWriteFile(path2, `${JSON.stringify(lock, null, 2)}
3859
+ atomicWriteFile(path3, `${JSON.stringify(lock, null, 2)}
3855
3860
  `);
3856
3861
  }
3857
3862
  function isProcessAlive(pid) {
@@ -3868,11 +3873,11 @@ function isConcurrentSession(lock) {
3868
3873
  }
3869
3874
  function restoreCodexOverlay(env = process.env) {
3870
3875
  const removed = [];
3871
- for (const path2 of ownedOverlayPaths(env)) {
3872
- if (!existsSync3(path2)) continue;
3876
+ for (const path3 of ownedOverlayPaths(env)) {
3877
+ if (!existsSync3(path3)) continue;
3873
3878
  try {
3874
- rmSync(path2, { force: true });
3875
- removed.push(path2);
3879
+ rmSync(path3, { force: true });
3880
+ removed.push(path3);
3876
3881
  } catch {
3877
3882
  }
3878
3883
  }
@@ -4019,16 +4024,16 @@ function findCodexBinary() {
4019
4024
  }
4020
4025
  function selectCodexBinary(candidates, exists, canRun) {
4021
4026
  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;
4027
+ for (const path3 of candidates) {
4028
+ if (!path3 || seen.has(path3)) continue;
4029
+ seen.add(path3);
4030
+ if (exists(path3) && canRun(path3)) return path3;
4026
4031
  }
4027
4032
  return null;
4028
4033
  }
4029
- function canRunCodexBinary(path2) {
4034
+ function canRunCodexBinary(path3) {
4030
4035
  try {
4031
- execFileSync(path2, ["--version"], {
4036
+ execFileSync(path3, ["--version"], {
4032
4037
  encoding: "utf8",
4033
4038
  stdio: ["ignore", "pipe", "pipe"],
4034
4039
  timeout: 5e3,
@@ -8248,12 +8253,12 @@ function findAntigravityCliBinary() {
8248
8253
  encoding: "utf8",
8249
8254
  stdio: ["pipe", "pipe", "pipe"]
8250
8255
  });
8251
- const path2 = result.trim().split("\n")[0]?.trim();
8252
- if (path2) return path2;
8256
+ const path3 = result.trim().split("\n")[0]?.trim();
8257
+ if (path3) return path3;
8253
8258
  } catch {
8254
8259
  }
8255
- for (const path2 of FALLBACK_PATHS) {
8256
- if (existsSync6(path2)) return path2;
8260
+ for (const path3 of FALLBACK_PATHS) {
8261
+ if (existsSync6(path3)) return path3;
8257
8262
  }
8258
8263
  return null;
8259
8264
  }
@@ -9111,9 +9116,9 @@ function applyRestoreNumber(config, key, had, value) {
9111
9116
  delete config[key];
9112
9117
  }
9113
9118
  }
9114
- function readCodexConfigText(path2 = getCodexConfigPath()) {
9115
- if (!existsSync8(path2)) return "";
9116
- return readFileSync3(path2, "utf8");
9119
+ function readCodexConfigText(path3 = getCodexConfigPath()) {
9120
+ if (!existsSync8(path3)) return "";
9121
+ return readFileSync3(path3, "utf8");
9117
9122
  }
9118
9123
  function parseCodexConfig(text4) {
9119
9124
  if (!text4.trim()) return {};
@@ -9321,10 +9326,10 @@ function getAppCatalogPath(providerId, env = process.env) {
9321
9326
  return join10(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
9322
9327
  }
9323
9328
  function readAppSessionLock(env = process.env) {
9324
- const path2 = getAppSessionLockPath(env);
9325
- if (!existsSync9(path2)) return null;
9329
+ const path3 = getAppSessionLockPath(env);
9330
+ if (!existsSync9(path3)) return null;
9326
9331
  try {
9327
- const parsed = JSON.parse(readFileSync4(path2, "utf8"));
9332
+ const parsed = JSON.parse(readFileSync4(path3, "utf8"));
9328
9333
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
9329
9334
  } catch {
9330
9335
  }
@@ -9335,14 +9340,14 @@ function writeAppSessionLock(lock, env = process.env) {
9335
9340
  `);
9336
9341
  }
9337
9342
  function clearAppSessionLock(env = process.env) {
9338
- const path2 = getAppSessionLockPath(env);
9339
- if (existsSync9(path2)) rmSync4(path2, { force: true });
9343
+ const path3 = getAppSessionLockPath(env);
9344
+ if (existsSync9(path3)) rmSync4(path3, { force: true });
9340
9345
  }
9341
9346
  function readAppRestoreState(env = process.env) {
9342
- const path2 = getAppRestoreStatePath(env);
9343
- if (!existsSync9(path2)) return null;
9347
+ const path3 = getAppRestoreStatePath(env);
9348
+ if (!existsSync9(path3)) return null;
9344
9349
  try {
9345
- return JSON.parse(readFileSync4(path2, "utf8"));
9350
+ return JSON.parse(readFileSync4(path3, "utf8"));
9346
9351
  } catch {
9347
9352
  return null;
9348
9353
  }
@@ -9353,8 +9358,8 @@ function writeAppRestoreState(state, env = process.env) {
9353
9358
  `);
9354
9359
  }
9355
9360
  function clearAppRestoreState(env = process.env) {
9356
- const path2 = getAppRestoreStatePath(env);
9357
- if (existsSync9(path2)) rmSync4(path2, { force: true });
9361
+ const path3 = getAppRestoreStatePath(env);
9362
+ if (existsSync9(path3)) rmSync4(path3, { force: true });
9358
9363
  }
9359
9364
  function backupConfigToml(env = process.env) {
9360
9365
  const configPath = getCodexConfigPath();
@@ -9384,10 +9389,10 @@ function ownedAppCatalogPaths(env = process.env) {
9384
9389
  }
9385
9390
  function removeAppCatalogs(env = process.env) {
9386
9391
  const removed = [];
9387
- for (const path2 of ownedAppCatalogPaths(env)) {
9392
+ for (const path3 of ownedAppCatalogPaths(env)) {
9388
9393
  try {
9389
- rmSync4(path2, { force: true });
9390
- removed.push(path2);
9394
+ rmSync4(path3, { force: true });
9395
+ removed.push(path3);
9391
9396
  } catch {
9392
9397
  }
9393
9398
  }
@@ -10235,10 +10240,10 @@ function getSessionLockPath2() {
10235
10240
  return join12(getClaudeDesktopHome(), ".relay-ai.lock");
10236
10241
  }
10237
10242
  function inspectSessionLock() {
10238
- const path2 = getSessionLockPath2();
10239
- if (!existsSync11(path2)) return { status: "missing" };
10243
+ const path3 = getSessionLockPath2();
10244
+ if (!existsSync11(path3)) return { status: "missing" };
10240
10245
  try {
10241
- const parsed = JSON.parse(readFileSync6(path2, "utf8"));
10246
+ const parsed = JSON.parse(readFileSync6(path3, "utf8"));
10242
10247
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string" && typeof parsed.uuid === "string" && typeof parsed.proxyPort === "number") {
10243
10248
  return { status: "valid", lock: parsed };
10244
10249
  }
@@ -10247,13 +10252,13 @@ function inspectSessionLock() {
10247
10252
  return { status: "unreadable" };
10248
10253
  }
10249
10254
  function writeSessionLock2(lock) {
10250
- const path2 = getSessionLockPath2();
10251
- const tempPath = `${path2}.tmp.${process.pid}`;
10252
- mkdirSync6(dirname4(path2), { recursive: true });
10255
+ const path3 = getSessionLockPath2();
10256
+ const tempPath = `${path3}.tmp.${process.pid}`;
10257
+ mkdirSync6(dirname4(path3), { recursive: true });
10253
10258
  try {
10254
10259
  writeFileSync5(tempPath, `${JSON.stringify(lock, null, 2)}
10255
10260
  `, "utf8");
10256
- renameSync2(tempPath, path2);
10261
+ renameSync2(tempPath, path3);
10257
10262
  } finally {
10258
10263
  try {
10259
10264
  rmSync5(tempPath, { force: true });
@@ -11153,26 +11158,61 @@ function printAiInstallResult(result) {
11153
11158
  console.error(`relay-ai agent skill target version: v${result.version}`);
11154
11159
  if (result.installed.length > 0) {
11155
11160
  console.error(`Installed ${result.installed.length} new skill(s):`);
11156
- for (const path2 of result.installed) console.error(` \u2713 ${path2}`);
11161
+ for (const path3 of result.installed) console.error(` \u2713 ${path3}`);
11157
11162
  }
11158
11163
  if (result.updated.length > 0) {
11159
11164
  console.error(`Updated ${result.updated.length} skill(s):`);
11160
- for (const { path: path2, fromVersion } of result.updated) {
11165
+ for (const { path: path3, fromVersion } of result.updated) {
11161
11166
  const from = fromVersion ? `v${fromVersion}` : "unknown";
11162
- console.error(` \u2713 ${path2} (${from} \u2192 v${result.version})`);
11167
+ console.error(` \u2713 ${path3} (${from} \u2192 v${result.version})`);
11163
11168
  }
11164
11169
  }
11165
11170
  if (result.skipped.length > 0) {
11166
11171
  console.error(`Skipped ${result.skipped.length} (already v${result.version}):`);
11167
- for (const path2 of result.skipped) console.error(` \xB7 ${path2}`);
11172
+ for (const path3 of result.skipped) console.error(` \xB7 ${path3}`);
11168
11173
  }
11169
11174
  if (result.failed.length > 0) {
11170
11175
  console.error(`Failed ${result.failed.length}:`);
11171
- for (const path2 of result.failed) console.error(` \u2717 ${path2}`);
11176
+ for (const path3 of result.failed) console.error(` \u2717 ${path3}`);
11172
11177
  }
11173
11178
  return result.failed.length > 0 ? 1 : 0;
11174
11179
  }
11175
11180
 
11181
+ // src/http-proxy/discovery-cache.ts
11182
+ import * as fs2 from "fs";
11183
+ import * as path2 from "path";
11184
+ import * as os from "os";
11185
+
11186
+ // src/http-proxy/anthropic-host.ts
11187
+ var ANTHROPIC_UPSTREAM_HOST = "api.anthropic.com";
11188
+ var RELAY_SENTINEL_HOST = "api.anthropic.com.relay.invalid";
11189
+ var RELAY_BASE_URL = `https://${RELAY_SENTINEL_HOST}`;
11190
+
11191
+ // src/http-proxy/discovery-cache.ts
11192
+ var GATEWAY_DISCOVERY_BASE_URL = RELAY_BASE_URL;
11193
+ var CACHE_FILE = "gateway-models.json";
11194
+ function cachePath(baseEnv) {
11195
+ const claudeDir = baseEnv["CLAUDE_CONFIG_DIR"]?.trim() || path2.join(os.homedir(), ".claude");
11196
+ return path2.join(claudeDir, "cache", CACHE_FILE);
11197
+ }
11198
+ function writeGatewayDiscoveryCache(baseEnv, routes) {
11199
+ try {
11200
+ const models = routes.filter((route) => Boolean(route.gatewayAliasId)).map((route) => formatAnthropicModelEntry(
11201
+ route.gatewayAliasId,
11202
+ route.displayName,
11203
+ route.contextWindow
11204
+ ));
11205
+ const file = cachePath(baseEnv);
11206
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
11207
+ fs2.writeFileSync(
11208
+ file,
11209
+ JSON.stringify({ baseUrl: GATEWAY_DISCOVERY_BASE_URL, fetchedAt: Date.now(), models }),
11210
+ { encoding: "utf8", mode: 384 }
11211
+ );
11212
+ } catch {
11213
+ }
11214
+ }
11215
+
11176
11216
  // src/http-proxy/env.ts
11177
11217
  var PROXY_ENV_NAMES = [
11178
11218
  "HTTPS_PROXY",
@@ -11254,6 +11294,8 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
11254
11294
  if (name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN") continue;
11255
11295
  delete env[name];
11256
11296
  }
11297
+ env["ANTHROPIC_BASE_URL"] = RELAY_BASE_URL;
11298
+ env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1";
11257
11299
  return env;
11258
11300
  }
11259
11301
 
@@ -11262,14 +11304,14 @@ import { randomBytes, randomUUID as randomUUID3 } from "crypto";
11262
11304
  import {
11263
11305
  chmodSync,
11264
11306
  existsSync as existsSync13,
11265
- mkdirSync as mkdirSync8,
11307
+ mkdirSync as mkdirSync9,
11266
11308
  readFileSync as readFileSync8,
11267
11309
  readdirSync as readdirSync3,
11268
11310
  rmSync as rmSync6,
11269
11311
  statSync as statSync2,
11270
- writeFileSync as writeFileSync7
11312
+ writeFileSync as writeFileSync8
11271
11313
  } from "fs";
11272
- import { dirname as dirname5, join as join14, resolve } from "path";
11314
+ import { dirname as dirname6, join as join15, resolve } from "path";
11273
11315
  import forge from "node-forge";
11274
11316
  var SESSION_ROOT = "http-proxy-sessions";
11275
11317
  var OWNER_FILE = "owner.pid";
@@ -11289,15 +11331,15 @@ function processIsRunning(pid) {
11289
11331
  }
11290
11332
  }
11291
11333
  function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
11292
- const root = join14(appHome, SESSION_ROOT);
11334
+ const root = join15(appHome, SESSION_ROOT);
11293
11335
  if (!existsSync13(root)) return;
11294
11336
  const now = Date.now();
11295
11337
  for (const name of readdirSync3(root)) {
11296
- const sessionDir = join14(root, name);
11338
+ const sessionDir = join15(root, name);
11297
11339
  try {
11298
11340
  const stat = statSync2(sessionDir);
11299
11341
  if (!stat.isDirectory()) continue;
11300
- const ownerPath = join14(sessionDir, OWNER_FILE);
11342
+ const ownerPath = join15(sessionDir, OWNER_FILE);
11301
11343
  if (!existsSync13(ownerPath)) {
11302
11344
  if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
11303
11345
  rmSync6(sessionDir, { recursive: true, force: true });
@@ -11320,13 +11362,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
11320
11362
  }
11321
11363
  function createHttpProxyCertificates(appHome = getAppHome()) {
11322
11364
  cleanupStaleHttpProxySessions(appHome);
11323
- const root = join14(appHome, SESSION_ROOT);
11324
- mkdirSync8(root, { recursive: true, mode: 448 });
11365
+ const root = join15(appHome, SESSION_ROOT);
11366
+ mkdirSync9(root, { recursive: true, mode: 448 });
11325
11367
  chmodSync(root, 448);
11326
- const sessionDir = join14(root, randomUUID3());
11327
- mkdirSync8(sessionDir, { mode: 448 });
11368
+ const sessionDir = join15(root, randomUUID3());
11369
+ mkdirSync9(sessionDir, { mode: 448 });
11328
11370
  chmodSync(sessionDir, 448);
11329
- writeFileSync7(join14(sessionDir, OWNER_FILE), `${process.pid}
11371
+ writeFileSync8(join15(sessionDir, OWNER_FILE), `${process.pid}
11330
11372
  `, { mode: 384 });
11331
11373
  try {
11332
11374
  const caKeys = forge.pki.rsa.generateKeyPair(2048);
@@ -11350,19 +11392,19 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
11350
11392
  server.serialNumber = serialNumber();
11351
11393
  server.validity.notBefore = new Date(Date.now() - 6e4);
11352
11394
  server.validity.notAfter = new Date(Date.now() + 48 * 60 * 60 * 1e3);
11353
- server.setSubject([{ name: "commonName", value: "api.anthropic.com" }]);
11395
+ server.setSubject([{ name: "commonName", value: RELAY_SENTINEL_HOST }]);
11354
11396
  server.setIssuer(ca.subject.attributes);
11355
11397
  server.setExtensions([
11356
11398
  { name: "basicConstraints", cA: false, critical: true },
11357
11399
  { name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
11358
11400
  { name: "extKeyUsage", serverAuth: true },
11359
- { name: "subjectAltName", altNames: [{ type: 2, value: "api.anthropic.com" }] },
11401
+ { name: "subjectAltName", altNames: [{ type: 2, value: RELAY_SENTINEL_HOST }] },
11360
11402
  { name: "subjectKeyIdentifier" }
11361
11403
  ]);
11362
11404
  server.sign(caKeys.privateKey, forge.md.sha256.create());
11363
11405
  const caCert = forge.pki.certificateToPem(ca);
11364
- const caCertPath = join14(sessionDir, "relay-ai-ca.pem");
11365
- writeFileSync7(caCertPath, caCert, { encoding: "utf8", mode: 384 });
11406
+ const caCertPath = join15(sessionDir, "relay-ai-ca.pem");
11407
+ writeFileSync8(caCertPath, caCert, { encoding: "utf8", mode: 384 });
11366
11408
  chmodSync(caCertPath, 384);
11367
11409
  let cleaned = false;
11368
11410
  const cleanupOnExit = () => {
@@ -11407,8 +11449,8 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
11407
11449
  const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
11408
11450
  const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
11409
11451
  if (!additionalCa) return relayCaCertPath;
11410
- const combinedPath = join14(dirname5(relayCaCertPath), "combined-ca.pem");
11411
- writeFileSync7(
11452
+ const combinedPath = join15(dirname6(relayCaCertPath), "combined-ca.pem");
11453
+ writeFileSync8(
11412
11454
  combinedPath,
11413
11455
  `${relayCa}
11414
11456
  ${additionalCa}
@@ -11425,41 +11467,7 @@ import * as https from "https";
11425
11467
  import * as net from "net";
11426
11468
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
11427
11469
  import { URL as URL2 } from "url";
11428
-
11429
- // src/anthropic-endpoints.ts
11430
- var MESSAGE_PATH = "/v1/messages";
11431
- var COUNT_TOKENS_PATH = "/v1/messages/count_tokens";
11432
- function anthropicMessagesEndpoint(url) {
11433
- if (!url) return null;
11434
- try {
11435
- const pathname = new URL(url, "http://relay.local").pathname;
11436
- if (pathname === MESSAGE_PATH) return "messages";
11437
- if (pathname === COUNT_TOKENS_PATH) return "count_tokens";
11438
- } catch {
11439
- }
11440
- return null;
11441
- }
11442
- var NON_CONTEXT_FIELDS = /* @__PURE__ */ new Set([
11443
- "model",
11444
- "stream",
11445
- "max_tokens",
11446
- "temperature",
11447
- "top_p",
11448
- "top_k",
11449
- "stop_sequences",
11450
- "metadata"
11451
- ]);
11452
- function estimateAnthropicInputTokens(body) {
11453
- const contextBody = Object.fromEntries(
11454
- Object.entries(body).filter(([key]) => !NON_CONTEXT_FIELDS.has(key))
11455
- );
11456
- const serialized = JSON.stringify(contextBody);
11457
- if (!serialized || serialized === "{}") return 0;
11458
- return Math.max(1, Math.ceil(Buffer.byteLength(serialized, "utf8") / 4));
11459
- }
11460
-
11461
- // src/http-proxy/server.ts
11462
- var ANTHROPIC_HOST = "api.anthropic.com";
11470
+ var ANTHROPIC_HOST = RELAY_SENTINEL_HOST;
11463
11471
  var MAX_BODY_BYTES = 50 * 1024 * 1024;
11464
11472
  var PROXY_USERNAME = "relay-ai";
11465
11473
  function authorityParts(authority) {
@@ -11533,15 +11541,29 @@ function readRawBody(req) {
11533
11541
  req.once("error", fail);
11534
11542
  });
11535
11543
  }
11536
- function requestHeadersWithoutProxyHeaders(req) {
11544
+ function requestHeadersWithoutProxyHeaders(req, hostOverride) {
11537
11545
  const headers = [];
11538
11546
  for (let index = 0; index < req.rawHeaders.length; index += 2) {
11539
11547
  const name = req.rawHeaders[index];
11540
11548
  if (/^proxy-(authorization|connection)$/i.test(name)) continue;
11549
+ if (hostOverride && /^host$/i.test(name)) {
11550
+ headers.push(name, hostOverride);
11551
+ continue;
11552
+ }
11541
11553
  headers.push(name, req.rawHeaders[index + 1] ?? "");
11542
11554
  }
11543
11555
  return headers;
11544
11556
  }
11557
+ function restoreBillingCch(headers) {
11558
+ for (let index = 0; index < headers.length; index += 2) {
11559
+ if (!/^x-anthropic-billing-header$/i.test(headers[index])) continue;
11560
+ const value = headers[index + 1] ?? "";
11561
+ if (/(?:^|;)\s*cch=/.test(value)) break;
11562
+ headers[index + 1] = `${value.replace(/;?\s*$/, "")}; cch=00000;`;
11563
+ break;
11564
+ }
11565
+ return headers;
11566
+ }
11545
11567
  function copyResponse(upstream, res) {
11546
11568
  res.writeHead(upstream.statusCode ?? 502, upstream.statusMessage, upstream.rawHeaders);
11547
11569
  upstream.once("error", (error) => res.destroy(error));
@@ -11562,7 +11584,7 @@ function forwardRawRequest(req, res, rawBody, origin, rejectUnauthorized) {
11562
11584
  port: origin.port || void 0,
11563
11585
  method: req.method,
11564
11586
  path: req.url,
11565
- headers: requestHeadersWithoutProxyHeaders(req),
11587
+ headers: restoreBillingCch(requestHeadersWithoutProxyHeaders(req, ANTHROPIC_UPSTREAM_HOST)),
11566
11588
  ...origin.protocol === "https:" ? { rejectUnauthorized } : {}
11567
11589
  }, (upstreamRes) => {
11568
11590
  copyResponse(upstreamRes, res);
@@ -11618,6 +11640,40 @@ function forwardToAdapter(req, res, rawBody, adapter) {
11618
11640
  upstream.end(rawBody);
11619
11641
  });
11620
11642
  }
11643
+ function fetchUpstreamModelsList(req, origin, rejectUnauthorized) {
11644
+ return new Promise((resolve2) => {
11645
+ const transport = origin.protocol === "https:" ? https : http2;
11646
+ const rawHeaders = requestHeadersWithoutProxyHeaders(req, ANTHROPIC_UPSTREAM_HOST);
11647
+ const headers = ["accept-encoding", "identity"];
11648
+ for (let i = 0; i < rawHeaders.length; i += 2) {
11649
+ const name = rawHeaders[i];
11650
+ if (/^accept-encoding$/i.test(name)) continue;
11651
+ headers.push(name, rawHeaders[i + 1] ?? "");
11652
+ }
11653
+ const upstream = transport.request({
11654
+ protocol: origin.protocol,
11655
+ hostname: origin.hostname,
11656
+ port: origin.port || void 0,
11657
+ method: "GET",
11658
+ path: req.url,
11659
+ headers,
11660
+ ...origin.protocol === "https:" ? { rejectUnauthorized } : {}
11661
+ }, (upstreamRes) => {
11662
+ const chunks = [];
11663
+ upstreamRes.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
11664
+ upstreamRes.once("end", () => {
11665
+ resolve2({
11666
+ statusCode: upstreamRes.statusCode ?? 500,
11667
+ headers: upstreamRes.headers,
11668
+ body: Buffer.concat(chunks)
11669
+ });
11670
+ });
11671
+ upstreamRes.once("error", () => resolve2(null));
11672
+ });
11673
+ upstream.once("error", () => resolve2(null));
11674
+ upstream.end();
11675
+ });
11676
+ }
11621
11677
  function forwardPlainHttp(req, res) {
11622
11678
  let target;
11623
11679
  try {
@@ -11656,6 +11712,9 @@ async function startHttpProxy(options) {
11656
11712
  const routesById = /* @__PURE__ */ new Map();
11657
11713
  for (const route of options.routes) {
11658
11714
  for (const id of routeLookupIds(route.aliasId)) routesById.set(id, route);
11715
+ if (route.gatewayAliasId) {
11716
+ for (const id of routeLookupIds(route.gatewayAliasId)) routesById.set(id, route);
11717
+ }
11659
11718
  }
11660
11719
  const anthropicOrigin = new URL2(options.anthropicOrigin ?? "https://api.anthropic.com");
11661
11720
  if (anthropicOrigin.protocol !== "http:" && anthropicOrigin.protocol !== "https:") {
@@ -11690,6 +11749,75 @@ async function startHttpProxy(options) {
11690
11749
  }
11691
11750
  return;
11692
11751
  }
11752
+ const modelsEndpoint = anthropicModelsEndpoint(req.url);
11753
+ if (req.method === "GET" && modelsEndpoint && options.routes.length > 0) {
11754
+ if (modelsEndpoint === "list") {
11755
+ const upstreamResult = await fetchUpstreamModelsList(
11756
+ req,
11757
+ anthropicOrigin,
11758
+ options.anthropicRejectUnauthorized ?? true
11759
+ );
11760
+ const relayEntries = options.routes.filter((r) => Boolean(r.gatewayAliasId)).map((r) => formatAnthropicModelEntry(
11761
+ r.gatewayAliasId,
11762
+ r.displayName,
11763
+ r.contextWindow
11764
+ ));
11765
+ if (upstreamResult && upstreamResult.statusCode === 200) {
11766
+ try {
11767
+ const parsed = JSON.parse(upstreamResult.body.toString("utf8"));
11768
+ if (Array.isArray(parsed.data)) {
11769
+ const existingIds = new Set(parsed.data.map((m) => String(m.id ?? "")));
11770
+ for (const entry of relayEntries) {
11771
+ if (!existingIds.has(entry.id)) {
11772
+ parsed.data.push(entry);
11773
+ }
11774
+ }
11775
+ const responseBuffer = Buffer.from(JSON.stringify(parsed));
11776
+ const headers = { ...upstreamResult.headers };
11777
+ delete headers["content-length"];
11778
+ delete headers["content-encoding"];
11779
+ headers["content-type"] = "application/json";
11780
+ headers["content-length"] = String(responseBuffer.length);
11781
+ res.writeHead(200, headers);
11782
+ res.end(responseBuffer);
11783
+ return;
11784
+ }
11785
+ } catch {
11786
+ }
11787
+ }
11788
+ const fallbackList = formatAnthropicModelList(
11789
+ options.routes.map((r) => ({
11790
+ id: r.gatewayAliasId ?? r.aliasId,
11791
+ name: r.displayName,
11792
+ contextWindow: r.contextWindow
11793
+ }))
11794
+ );
11795
+ const buffer = Buffer.from(JSON.stringify(fallbackList));
11796
+ res.writeHead(200, {
11797
+ "content-type": "application/json",
11798
+ "content-length": String(buffer.length)
11799
+ });
11800
+ res.end(buffer);
11801
+ return;
11802
+ }
11803
+ if (typeof modelsEndpoint === "object" && modelsEndpoint.id) {
11804
+ const matched = routesById.get(modelsEndpoint.id);
11805
+ if (matched) {
11806
+ const entry = formatAnthropicModelEntry(
11807
+ matched.gatewayAliasId ?? matched.aliasId,
11808
+ matched.displayName,
11809
+ matched.contextWindow
11810
+ );
11811
+ const buffer = Buffer.from(JSON.stringify(entry));
11812
+ res.writeHead(200, {
11813
+ "content-type": "application/json",
11814
+ "content-length": String(buffer.length)
11815
+ });
11816
+ res.end(buffer);
11817
+ return;
11818
+ }
11819
+ }
11820
+ }
11693
11821
  const endpoint = anthropicMessagesEndpoint(req.url);
11694
11822
  if (req.method === "POST" && endpoint) {
11695
11823
  let parsed = null;
@@ -11891,6 +12019,7 @@ async function launchClaudeWithHttpProxy(options, dependencies = defaultDependen
11891
12019
  proxy.handle.proxyUrl,
11892
12020
  proxy.handle.caCertPath
11893
12021
  );
12022
+ writeGatewayDiscoveryCache(options.baseEnv, proxy.loaded.routes);
11894
12023
  const exitCode = await dependencies.launch(
11895
12024
  childEnv,
11896
12025
  proxy.startingModel,
@@ -13264,7 +13393,7 @@ Options:
13264
13393
  --trace Write debug logs under ~/.relay-ai/logs/`);
13265
13394
  return 0;
13266
13395
  }
13267
- const { runUiCommand } = await import("./ui-command-S3TQKB3D.js");
13396
+ const { runUiCommand } = await import("./ui-command-DMPYSK7W.js");
13268
13397
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
13269
13398
  }
13270
13399
  if (parsed.command === "models") {