@jacobbd/relay-ai 0.4.6 → 0.4.7

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
@@ -4,6 +4,7 @@ import {
4
4
  BACKENDS,
5
5
  CODEX_APP_AUTO_COMPACT_RATIO,
6
6
  CODEX_APP_PROVIDER_ID,
7
+ CONFLICTING_ENV_VARS,
7
8
  GLOBAL_OPENCODE_KEYRING_ACCOUNT,
8
9
  MAX_MODEL_CATALOG,
9
10
  PREVIEW_PROXY_PORT,
@@ -19,9 +20,11 @@ import {
19
20
  buildAntigravityChildEnv,
20
21
  buildAppCatalogFile,
21
22
  buildCatalogFile,
23
+ buildCatalogRoutes,
22
24
  buildChildEnv,
23
25
  buildClaudeCodeBillingSystemLine,
24
26
  buildCodexAppRootConfig,
27
+ buildHttpProxyRoutes,
25
28
  buildImportProviderList,
26
29
  buildVertexRuntimeConfig,
27
30
  cachedModelToLocal,
@@ -72,13 +75,13 @@ import {
72
75
  getReasoningCapabilities,
73
76
  grabRoundTripSignature,
74
77
  hasApplicationDefaultCredentials,
78
+ httpProxyModelId,
75
79
  injectClaudeIdentity,
76
80
  isClaudeAppRunning,
77
81
  isCodexAppRunning,
78
82
  isFreeStatus,
79
83
  isLikelyPlaceholderKey,
80
84
  isOAuthImportProvider,
81
- isSdkMigratedNpm,
82
85
  isSecretServiceAvailable,
83
86
  isValidProviderId,
84
87
  launchClaude,
@@ -92,6 +95,7 @@ import {
92
95
  logActiveModel,
93
96
  logConnected,
94
97
  logProxy,
98
+ makeRouteResolver,
95
99
  makeTraceLogger,
96
100
  maxToolsForNpm,
97
101
  migrateGlobalOpencodeCredential,
@@ -157,6 +161,7 @@ import {
157
161
  startProxy,
158
162
  startProxyCatalog,
159
163
  startServer,
164
+ supportsClaudeTransparentMode,
160
165
  supportsNativeOAuth,
161
166
  syntheticTemplate,
162
167
  thinkingProviderOptions,
@@ -166,7 +171,7 @@ import {
166
171
  validateCustomEndpointUrl,
167
172
  writeSecureLogLine,
168
173
  zenRegistryStub
169
- } from "./chunk-YM6G7BHE.js";
174
+ } from "./chunk-XQCHSU6L.js";
170
175
  import {
171
176
  filterTemplates,
172
177
  init_provider_templates,
@@ -651,54 +656,6 @@ async function runFirstRunWizard(trace = false) {
651
656
  return "continue";
652
657
  }
653
658
 
654
- // src/catalog.ts
655
- function localModelToRoute(lp, model) {
656
- if (model.modelFormat === "anthropic" && !model.baseUrl) return null;
657
- if (model.modelFormat === "openai" && !isSdkMigratedNpm(model.npm) && !model.completionsUrl) return null;
658
- const upstreamUrl = model.modelFormat === "cloud-code" ? model.baseUrl ?? ANTIGRAVITY_BASE_URLS[0] : model.modelFormat === "anthropic" ? model.baseUrl : model.completionsUrl;
659
- return {
660
- aliasId: claudeCodeClientModelId(aliasModelId(model.id, lp.id), model.contextWindow),
661
- realModelId: model.upstreamModelId,
662
- displayName: `${model.name || model.id} (${lp.name})`,
663
- upstreamUrl: upstreamUrl ?? "",
664
- apiKey: lp.apiKey,
665
- modelFormat: model.modelFormat,
666
- contextWindow: model.contextWindow,
667
- npm: model.npm,
668
- baseURL: model.apiBaseUrl,
669
- providerId: lp.id,
670
- authType: lp.authType,
671
- oauthAccountId: lp.oauthAccountId,
672
- providerData: lp.providerData,
673
- headers: lp.headers,
674
- supportedParameters: model.supportedParameters,
675
- reasoning: model.reasoning,
676
- interleavedReasoningField: model.interleavedReasoningField,
677
- useResponsesLite: model.useResponsesLite,
678
- preferWebSockets: model.preferWebSockets
679
- };
680
- }
681
- function makeRouteResolver(localProviders) {
682
- return (providerId, modelId) => {
683
- const provider = localProviders?.find((lp) => lp.id === providerId);
684
- const model = provider?.models.find((m) => m.id === modelId);
685
- return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
686
- };
687
- }
688
- function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
689
- const droppedFavorites = [];
690
- const tail = favorites.map((fav) => {
691
- const route = resolveRoute(fav.providerId, fav.modelId);
692
- if (!route) droppedFavorites.push(fav);
693
- return route;
694
- }).filter((route) => route !== void 0);
695
- const routes = [
696
- startingRoute,
697
- ...tail.filter((route) => route.aliasId !== startingRoute.aliasId)
698
- ].slice(0, max);
699
- return { routes, droppedFavorites };
700
- }
701
-
702
659
  // src/prompts.ts
703
660
  import * as p3 from "@clack/prompts";
704
661
  import pc2 from "picocolors";
@@ -736,6 +693,12 @@ function scoreModelSearch(query, fields) {
736
693
  }
737
694
 
738
695
  // src/prompts.ts
696
+ function claudeTransparentModeOptions(modelLabel) {
697
+ return [
698
+ { value: true, label: `Yes \u2014 Use ${modelLabel} alongside Anthropic models` },
699
+ { value: false, label: `No \u2014 Use only ${modelLabel} through Relay AI` }
700
+ ];
701
+ }
739
702
  var BROWSE_ALL = "__browse_all__";
740
703
  var MAX_RECENT = 3;
741
704
  var MODEL_SEARCH_THRESHOLD = 25;
@@ -3085,7 +3048,7 @@ async function startCodexProxy(routes, options = {}) {
3085
3048
  headers: route.headers
3086
3049
  }));
3087
3050
  }
3088
- return new Promise((resolve, reject2) => {
3051
+ return new Promise((resolve2, reject2) => {
3089
3052
  const log14 = debug ? makeTraceLogger(getCodexProxyDebugLogPath()) : () => {
3090
3053
  };
3091
3054
  if (debug) resetCodexBodyDumpLog();
@@ -3583,7 +3546,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
3583
3546
  reject2(new Error("Failed to bind codex proxy"));
3584
3547
  return;
3585
3548
  }
3586
- resolve({
3549
+ resolve2({
3587
3550
  port: addr.port,
3588
3551
  close: () => {
3589
3552
  process.off("unhandledRejection", onRejection);
@@ -3967,7 +3930,7 @@ function buildCodexChildEnv(route, proxyPort) {
3967
3930
  return env;
3968
3931
  }
3969
3932
  function launchCodex(modelId, env, extraArgs) {
3970
- return new Promise((resolve) => {
3933
+ return new Promise((resolve2) => {
3971
3934
  const codexPath = findCodexBinary();
3972
3935
  const args = ["--profile", profileName(), "-m", modelId, ...ensureCodexSandboxArgs(extraArgs)];
3973
3936
  const child = spawn(codexPath, args, {
@@ -3980,7 +3943,7 @@ function launchCodex(modelId, env, extraArgs) {
3980
3943
  };
3981
3944
  process.once("SIGINT", () => forward("SIGINT"));
3982
3945
  process.once("SIGTERM", () => forward("SIGTERM"));
3983
- child.on("exit", (code) => resolve(code ?? 0));
3946
+ child.on("exit", (code) => resolve2(code ?? 0));
3984
3947
  });
3985
3948
  }
3986
3949
 
@@ -5234,7 +5197,7 @@ function prepareGeminiChildEnv(proxyPort, proxyToken) {
5234
5197
  };
5235
5198
  }
5236
5199
  function launchGemini(geminiPath, modelId, env, extraArgs) {
5237
- return new Promise((resolve) => {
5200
+ return new Promise((resolve2) => {
5238
5201
  const args = ["-m", modelId, ...extraArgs];
5239
5202
  const child = spawn2(geminiPath, args, {
5240
5203
  stdio: "inherit",
@@ -5248,7 +5211,7 @@ function launchGemini(geminiPath, modelId, env, extraArgs) {
5248
5211
  const done = (code) => {
5249
5212
  process.off("SIGINT", onSigInt);
5250
5213
  process.off("SIGTERM", onSigTerm);
5251
- resolve(code);
5214
+ resolve2(code);
5252
5215
  };
5253
5216
  child.on("error", () => done(1));
5254
5217
  child.on("exit", (code) => done(code ?? 0));
@@ -5859,7 +5822,7 @@ ${JSON.stringify(response, null, 2)}`);
5859
5822
  process.off("unhandledRejection", onRejection);
5860
5823
  process.off("uncaughtException", onException);
5861
5824
  };
5862
- return new Promise((resolve, reject2) => {
5825
+ return new Promise((resolve2, reject2) => {
5863
5826
  server.on("error", (err) => {
5864
5827
  cleanup();
5865
5828
  reject2(err);
@@ -5871,7 +5834,7 @@ ${JSON.stringify(response, null, 2)}`);
5871
5834
  reject2(new Error("Failed to bind gemini proxy"));
5872
5835
  return;
5873
5836
  }
5874
- resolve({
5837
+ resolve2({
5875
5838
  port: addr.port,
5876
5839
  token: proxyToken,
5877
5840
  close: () => {
@@ -8237,13 +8200,13 @@ async function startCloudCodeGateway(routes, opts = {}) {
8237
8200
  respondJson(res, 400, { error: { code: 400, message: `Failed to read request: ${err instanceof Error ? err.message : String(err)}` } });
8238
8201
  });
8239
8202
  });
8240
- return new Promise((resolve, reject2) => {
8203
+ return new Promise((resolve2, reject2) => {
8241
8204
  server.on("error", reject2);
8242
8205
  server.listen(0, "127.0.0.1", () => {
8243
8206
  const addr = server.address();
8244
8207
  if (addr && typeof addr === "object") {
8245
8208
  const port = addr.port;
8246
- resolve({
8209
+ resolve2({
8247
8210
  port,
8248
8211
  url: `http://127.0.0.1:${port}`,
8249
8212
  close: () => new Promise((res, rej) => {
@@ -8741,11 +8704,11 @@ function readAntigravityCliVersion(binaryPath = findAntigravityCliBinary() ?? vo
8741
8704
  }
8742
8705
  }
8743
8706
  function launchAntigravityCli(env, extraArgs) {
8744
- return new Promise((resolve) => {
8707
+ return new Promise((resolve2) => {
8745
8708
  const binaryPath = findAntigravityCliBinary();
8746
8709
  if (!binaryPath) {
8747
8710
  console.error('Antigravity CLI binary "agy" not found.');
8748
- resolve(127);
8711
+ resolve2(127);
8749
8712
  return;
8750
8713
  }
8751
8714
  const child = spawn3(binaryPath, extraArgs, {
@@ -8766,12 +8729,12 @@ function launchAntigravityCli(env, extraArgs) {
8766
8729
  process.once("SIGTERM", handleSIGTERM);
8767
8730
  child.on("exit", (code) => {
8768
8731
  cleanup();
8769
- resolve(code ?? 1);
8732
+ resolve2(code ?? 1);
8770
8733
  });
8771
8734
  child.on("error", (err) => {
8772
8735
  cleanup();
8773
8736
  console.error(`Failed to launch Antigravity CLI: ${err.message}`);
8774
- resolve(1);
8737
+ resolve2(1);
8775
8738
  });
8776
8739
  });
8777
8740
  }
@@ -8815,7 +8778,7 @@ function prepareIdeProfile(profileDir, gatewayUrl) {
8815
8778
 
8816
8779
  // src/antigravity/launch-ide.ts
8817
8780
  function sleep(ms) {
8818
- return new Promise((resolve) => setTimeout(resolve, ms));
8781
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
8819
8782
  }
8820
8783
  function runPowerShell(script) {
8821
8784
  return execSync3(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`, {
@@ -8962,12 +8925,12 @@ function findAntigravityIdeBinary() {
8962
8925
  return null;
8963
8926
  }
8964
8927
  function launchAntigravityApp(env, profileDir, gatewayUrl, extraArgs) {
8965
- return new Promise((resolve) => {
8928
+ return new Promise((resolve2) => {
8966
8929
  let settled = false;
8967
8930
  const settle = (code) => {
8968
8931
  if (settled) return;
8969
8932
  settled = true;
8970
- resolve(code);
8933
+ resolve2(code);
8971
8934
  };
8972
8935
  const binaryPath = findAntigravityAppBinary();
8973
8936
  if (!binaryPath) {
@@ -8998,12 +8961,12 @@ function launchAntigravityApp(env, profileDir, gatewayUrl, extraArgs) {
8998
8961
  });
8999
8962
  }
9000
8963
  function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
9001
- return new Promise((resolve) => {
8964
+ return new Promise((resolve2) => {
9002
8965
  const binaryPath = findAntigravityIdeBinary();
9003
8966
  if (!binaryPath) {
9004
8967
  console.error('Antigravity IDE app bundle not found at "/Applications/Antigravity IDE.app".');
9005
8968
  console.error("Please make sure Antigravity IDE is installed on your Mac.");
9006
- resolve(127);
8969
+ resolve2(127);
9007
8970
  return;
9008
8971
  }
9009
8972
  prepareIdeProfile(profileDir, gatewayUrl);
@@ -9018,11 +8981,11 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
9018
8981
  env
9019
8982
  });
9020
8983
  child.on("exit", (code) => {
9021
- resolve(code ?? 1);
8984
+ resolve2(code ?? 1);
9022
8985
  });
9023
8986
  child.on("error", (err) => {
9024
8987
  console.error(`Failed to launch Antigravity IDE: ${err.message}`);
9025
- resolve(1);
8988
+ resolve2(1);
9026
8989
  });
9027
8990
  });
9028
8991
  }
@@ -9215,7 +9178,7 @@ async function resolveAndBuildRoutes(provider, model, allProviders, prefs, opts)
9215
9178
  return { routes: result.routes, apiKey: result.apiKey };
9216
9179
  }
9217
9180
  function waitForShutdown() {
9218
- return new Promise((resolve) => {
9181
+ return new Promise((resolve2) => {
9219
9182
  const cleanup = () => {
9220
9183
  process.removeListener("SIGINT", onSigint);
9221
9184
  process.removeListener("SIGTERM", onSigterm);
@@ -9223,15 +9186,15 @@ function waitForShutdown() {
9223
9186
  };
9224
9187
  const onSigint = () => {
9225
9188
  cleanup();
9226
- resolve("sigint");
9189
+ resolve2("sigint");
9227
9190
  };
9228
9191
  const onSigterm = () => {
9229
9192
  cleanup();
9230
- resolve("sigterm");
9193
+ resolve2("sigterm");
9231
9194
  };
9232
9195
  const onSighup = () => {
9233
9196
  cleanup();
9234
- resolve("sighup");
9197
+ resolve2("sighup");
9235
9198
  };
9236
9199
  process.once("SIGINT", onSigint);
9237
9200
  process.once("SIGTERM", onSigterm);
@@ -9849,7 +9812,7 @@ function checkAppSessionLock(isTty, env = process.env) {
9849
9812
  return { ok: true };
9850
9813
  }
9851
9814
  function waitForShutdown2() {
9852
- return new Promise((resolve) => {
9815
+ return new Promise((resolve2) => {
9853
9816
  const cleanup = () => {
9854
9817
  process.removeListener("SIGINT", onSigint);
9855
9818
  process.removeListener("SIGTERM", onSigterm);
@@ -9857,15 +9820,15 @@ function waitForShutdown2() {
9857
9820
  };
9858
9821
  const onSigint = () => {
9859
9822
  cleanup();
9860
- resolve("sigint");
9823
+ resolve2("sigint");
9861
9824
  };
9862
9825
  const onSigterm = () => {
9863
9826
  cleanup();
9864
- resolve("sigterm");
9827
+ resolve2("sigterm");
9865
9828
  };
9866
9829
  const onSighup = () => {
9867
9830
  cleanup();
9868
- resolve("sighup");
9831
+ resolve2("sighup");
9869
9832
  };
9870
9833
  process.once("SIGINT", onSigint);
9871
9834
  process.once("SIGTERM", onSigterm);
@@ -10567,18 +10530,18 @@ function recoverSession() {
10567
10530
  }
10568
10531
  }
10569
10532
  function waitForShutdown3() {
10570
- return new Promise((resolve) => {
10533
+ return new Promise((resolve2) => {
10571
10534
  const cleanup = () => {
10572
10535
  process.removeListener("SIGINT", onSigint);
10573
10536
  process.removeListener("SIGTERM", onSigterm);
10574
10537
  };
10575
10538
  const onSigint = () => {
10576
10539
  cleanup();
10577
- resolve("sigint");
10540
+ resolve2("sigint");
10578
10541
  };
10579
10542
  const onSigterm = () => {
10580
10543
  cleanup();
10581
- resolve("sigterm");
10544
+ resolve2("sigterm");
10582
10545
  };
10583
10546
  process.once("SIGINT", onSigint);
10584
10547
  process.once("SIGTERM", onSigterm);
@@ -11131,6 +11094,8 @@ FAVORITES / MID-SESSION SWITCHING:
11131
11094
  When favorites exist, interactive claude/codex/gemini launches expose /model switching.
11132
11095
  Boot flags (--provider + --model) or print/exec/-p mode use SINGLE-MODEL launch
11133
11096
  (favorites catalog is skipped \u2014 better for agent one-shots).
11097
+ Exception: Claude --http-proxy combines the selected compatible model with
11098
+ compatible saved favorites while keeping native Anthropic models available.
11134
11099
 
11135
11100
  ================================================================================
11136
11101
  COMMANDS
@@ -11149,6 +11114,7 @@ CLAUDE CODE
11149
11114
  Relay options:
11150
11115
  --provider <id> Boot provider (skip wizard with --model)
11151
11116
  --model <id> Boot model id or provider__model slug
11117
+ --http-proxy Keep native Anthropic auth/models and add selected/favorite Relay models
11152
11118
  --dry-run Preview launch, do not start Claude
11153
11119
  --trace Debug logs in ~/.relay-ai/logs/
11154
11120
  --setup Hint to use relay-ai providers
@@ -11162,6 +11128,7 @@ CLAUDE CODE
11162
11128
  Examples:
11163
11129
  relay-ai claude
11164
11130
  relay-ai claude --provider anthropic --model claude-sonnet-4-6 -p "review file.ts"
11131
+ relay-ai claude --http-proxy --provider moonshot --model kimi-k3 -p "review file.ts"
11165
11132
  relay-ai claude --dry-run --provider groq --model llama-3.3-70b-versatile
11166
11133
 
11167
11134
  GOOGLE GEMINI CLI
@@ -11446,8 +11413,720 @@ function printAiInstallResult(result) {
11446
11413
  return result.failed.length > 0 ? 1 : 0;
11447
11414
  }
11448
11415
 
11416
+ // src/http-proxy/env.ts
11417
+ var PROXY_ENV_NAMES = [
11418
+ "HTTPS_PROXY",
11419
+ "https_proxy",
11420
+ "HTTP_PROXY",
11421
+ "http_proxy",
11422
+ "ALL_PROXY",
11423
+ "all_proxy"
11424
+ ];
11425
+ var ANTHROPIC_PROXY_HOST = "api.anthropic.com";
11426
+ function unsupportedInheritedProxyError(proxy) {
11427
+ return new Error(
11428
+ `An existing ${proxy.name} network proxy was detected. Chaining Relay AI through an existing network proxy is not yet supported.`
11429
+ );
11430
+ }
11431
+ function noProxyEntryBypassesAnthropic(rawEntry) {
11432
+ let entry = rawEntry.trim().toLowerCase();
11433
+ if (!entry) return false;
11434
+ if (entry === "*") return true;
11435
+ if (entry.includes("://")) {
11436
+ try {
11437
+ entry = new URL(entry).hostname;
11438
+ } catch {
11439
+ return false;
11440
+ }
11441
+ } else if (entry.startsWith("[")) {
11442
+ entry = entry.slice(1, entry.indexOf("]") === -1 ? void 0 : entry.indexOf("]"));
11443
+ } else {
11444
+ entry = entry.replace(/:\d+$/, "");
11445
+ }
11446
+ if (entry.startsWith("*")) return ANTHROPIC_PROXY_HOST.endsWith(entry.slice(1));
11447
+ if (entry.startsWith(".")) return ANTHROPIC_PROXY_HOST.endsWith(entry);
11448
+ return entry === ANTHROPIC_PROXY_HOST || ANTHROPIC_PROXY_HOST.endsWith(`.${entry}`);
11449
+ }
11450
+ function sanitizeNoProxy(value) {
11451
+ if (value === void 0) return void 0;
11452
+ const safeEntries = value.split(",").map((entry) => entry.trim()).filter((entry) => entry && !noProxyEntryBypassesAnthropic(entry));
11453
+ return safeEntries.length > 0 ? safeEntries.join(",") : void 0;
11454
+ }
11455
+ function isLoopbackHost(hostname) {
11456
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
11457
+ return normalized === "localhost" || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
11458
+ }
11459
+ function findUnsupportedInheritedProxy(env) {
11460
+ for (const name of PROXY_ENV_NAMES) {
11461
+ const value = env[name]?.trim();
11462
+ if (!value) continue;
11463
+ try {
11464
+ const parsed = new URL(value);
11465
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || !isLoopbackHost(parsed.hostname)) {
11466
+ return { name, value };
11467
+ }
11468
+ } catch {
11469
+ return { name, value };
11470
+ }
11471
+ }
11472
+ return void 0;
11473
+ }
11474
+ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
11475
+ const unsupported = findUnsupportedInheritedProxy(baseEnv);
11476
+ if (unsupported) {
11477
+ throw unsupportedInheritedProxyError(unsupported);
11478
+ }
11479
+ const env = { ...baseEnv };
11480
+ env["HTTPS_PROXY"] = proxyUrl;
11481
+ env["HTTP_PROXY"] = proxyUrl;
11482
+ env["https_proxy"] = proxyUrl;
11483
+ env["http_proxy"] = proxyUrl;
11484
+ delete env["ALL_PROXY"];
11485
+ delete env["all_proxy"];
11486
+ const noProxy = sanitizeNoProxy(env["NO_PROXY"]);
11487
+ const lowerNoProxy = sanitizeNoProxy(env["no_proxy"]);
11488
+ if (noProxy) env["NO_PROXY"] = noProxy;
11489
+ else delete env["NO_PROXY"];
11490
+ if (lowerNoProxy) env["no_proxy"] = lowerNoProxy;
11491
+ else delete env["no_proxy"];
11492
+ env["NODE_EXTRA_CA_CERTS"] = caCertPath;
11493
+ for (const name of CONFLICTING_ENV_VARS) {
11494
+ if (name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN") continue;
11495
+ delete env[name];
11496
+ }
11497
+ return env;
11498
+ }
11499
+
11500
+ // src/http-proxy/ca.ts
11501
+ import { randomBytes, randomUUID as randomUUID3 } from "crypto";
11502
+ import {
11503
+ chmodSync,
11504
+ existsSync as existsSync12,
11505
+ mkdirSync as mkdirSync7,
11506
+ readFileSync as readFileSync8,
11507
+ readdirSync as readdirSync3,
11508
+ rmSync as rmSync6,
11509
+ statSync as statSync2,
11510
+ writeFileSync as writeFileSync7
11511
+ } from "fs";
11512
+ import { dirname as dirname4, join as join13, resolve } from "path";
11513
+ import forge from "node-forge";
11514
+ var SESSION_ROOT = "http-proxy-sessions";
11515
+ var OWNER_FILE = "owner.pid";
11516
+ function serialNumber() {
11517
+ const bytes = randomBytes(16);
11518
+ bytes[0] &= 127;
11519
+ return bytes.toString("hex");
11520
+ }
11521
+ function processIsRunning(pid) {
11522
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
11523
+ try {
11524
+ process.kill(pid, 0);
11525
+ return true;
11526
+ } catch (error) {
11527
+ return error.code === "EPERM";
11528
+ }
11529
+ }
11530
+ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
11531
+ const root = join13(appHome, SESSION_ROOT);
11532
+ if (!existsSync12(root)) return;
11533
+ for (const name of readdirSync3(root)) {
11534
+ const sessionDir = join13(root, name);
11535
+ try {
11536
+ if (!statSync2(sessionDir).isDirectory()) continue;
11537
+ const pid = Number(readFileSync8(join13(sessionDir, OWNER_FILE), "utf8").trim());
11538
+ if (!processIsRunning(pid)) rmSync6(sessionDir, { recursive: true, force: true });
11539
+ } catch {
11540
+ rmSync6(sessionDir, { recursive: true, force: true });
11541
+ }
11542
+ }
11543
+ }
11544
+ function createHttpProxyCertificates(appHome = getAppHome()) {
11545
+ cleanupStaleHttpProxySessions(appHome);
11546
+ const root = join13(appHome, SESSION_ROOT);
11547
+ mkdirSync7(root, { recursive: true, mode: 448 });
11548
+ chmodSync(root, 448);
11549
+ const sessionDir = join13(root, randomUUID3());
11550
+ mkdirSync7(sessionDir, { mode: 448 });
11551
+ chmodSync(sessionDir, 448);
11552
+ writeFileSync7(join13(sessionDir, OWNER_FILE), `${process.pid}
11553
+ `, { mode: 384 });
11554
+ try {
11555
+ const caKeys = forge.pki.rsa.generateKeyPair(2048);
11556
+ const ca = forge.pki.createCertificate();
11557
+ ca.publicKey = caKeys.publicKey;
11558
+ ca.serialNumber = serialNumber();
11559
+ ca.validity.notBefore = new Date(Date.now() - 6e4);
11560
+ ca.validity.notAfter = new Date(Date.now() + 48 * 60 * 60 * 1e3);
11561
+ const issuer = [{ name: "commonName", value: `Relay AI session ${process.pid}` }];
11562
+ ca.setSubject(issuer);
11563
+ ca.setIssuer(issuer);
11564
+ ca.setExtensions([
11565
+ { name: "basicConstraints", cA: true, critical: true },
11566
+ { name: "keyUsage", keyCertSign: true, cRLSign: true, digitalSignature: true, critical: true },
11567
+ { name: "subjectKeyIdentifier" }
11568
+ ]);
11569
+ ca.sign(caKeys.privateKey, forge.md.sha256.create());
11570
+ const serverKeys = forge.pki.rsa.generateKeyPair(2048);
11571
+ const server = forge.pki.createCertificate();
11572
+ server.publicKey = serverKeys.publicKey;
11573
+ server.serialNumber = serialNumber();
11574
+ server.validity.notBefore = new Date(Date.now() - 6e4);
11575
+ server.validity.notAfter = new Date(Date.now() + 48 * 60 * 60 * 1e3);
11576
+ server.setSubject([{ name: "commonName", value: "api.anthropic.com" }]);
11577
+ server.setIssuer(ca.subject.attributes);
11578
+ server.setExtensions([
11579
+ { name: "basicConstraints", cA: false, critical: true },
11580
+ { name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
11581
+ { name: "extKeyUsage", serverAuth: true },
11582
+ { name: "subjectAltName", altNames: [{ type: 2, value: "api.anthropic.com" }] },
11583
+ { name: "subjectKeyIdentifier" }
11584
+ ]);
11585
+ server.sign(caKeys.privateKey, forge.md.sha256.create());
11586
+ const caCert = forge.pki.certificateToPem(ca);
11587
+ const caCertPath = join13(sessionDir, "relay-ai-ca.pem");
11588
+ writeFileSync7(caCertPath, caCert, { encoding: "utf8", mode: 384 });
11589
+ chmodSync(caCertPath, 384);
11590
+ let cleaned = false;
11591
+ const cleanupOnExit = () => {
11592
+ if (cleaned) return;
11593
+ cleaned = true;
11594
+ rmSync6(sessionDir, { recursive: true, force: true });
11595
+ };
11596
+ const cleanupOnSighup = () => {
11597
+ cleanupOnExit();
11598
+ process.off("SIGHUP", cleanupOnSighup);
11599
+ try {
11600
+ process.kill(process.pid, "SIGHUP");
11601
+ } catch {
11602
+ process.exit(129);
11603
+ }
11604
+ };
11605
+ const cleanup = () => {
11606
+ process.off("exit", cleanupOnExit);
11607
+ process.off("SIGHUP", cleanupOnSighup);
11608
+ cleanupOnExit();
11609
+ };
11610
+ process.once("exit", cleanupOnExit);
11611
+ process.once("SIGHUP", cleanupOnSighup);
11612
+ return {
11613
+ sessionDir,
11614
+ caCertPath,
11615
+ caCert,
11616
+ serverCert: forge.pki.certificateToPem(server),
11617
+ serverKey: forge.pki.privateKeyToPem(serverKeys.privateKey),
11618
+ cleanup
11619
+ };
11620
+ } catch (error) {
11621
+ rmSync6(sessionDir, { recursive: true, force: true });
11622
+ throw error;
11623
+ }
11624
+ }
11625
+ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
11626
+ if (!additionalCaCertPath?.trim()) return relayCaCertPath;
11627
+ if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) {
11628
+ return relayCaCertPath;
11629
+ }
11630
+ const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
11631
+ const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
11632
+ if (!additionalCa) return relayCaCertPath;
11633
+ const combinedPath = join13(dirname4(relayCaCertPath), "combined-ca.pem");
11634
+ writeFileSync7(
11635
+ combinedPath,
11636
+ `${relayCa}
11637
+ ${additionalCa}
11638
+ `,
11639
+ { encoding: "utf8", mode: 384 }
11640
+ );
11641
+ chmodSync(combinedPath, 384);
11642
+ return combinedPath;
11643
+ }
11644
+
11645
+ // src/http-proxy/server.ts
11646
+ import * as http2 from "http";
11647
+ import * as https from "https";
11648
+ import * as net from "net";
11649
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
11650
+ import { URL as URL2 } from "url";
11651
+
11652
+ // src/anthropic-endpoints.ts
11653
+ var MESSAGE_PATH = "/v1/messages";
11654
+ var COUNT_TOKENS_PATH = "/v1/messages/count_tokens";
11655
+ function anthropicMessagesEndpoint(url) {
11656
+ if (!url) return null;
11657
+ try {
11658
+ const pathname = new URL(url, "http://relay.local").pathname;
11659
+ if (pathname === MESSAGE_PATH) return "messages";
11660
+ if (pathname === COUNT_TOKENS_PATH) return "count_tokens";
11661
+ } catch {
11662
+ }
11663
+ return null;
11664
+ }
11665
+ var NON_CONTEXT_FIELDS = /* @__PURE__ */ new Set([
11666
+ "model",
11667
+ "stream",
11668
+ "max_tokens",
11669
+ "temperature",
11670
+ "top_p",
11671
+ "top_k",
11672
+ "stop_sequences",
11673
+ "metadata"
11674
+ ]);
11675
+ function estimateAnthropicInputTokens(body) {
11676
+ const contextBody = Object.fromEntries(
11677
+ Object.entries(body).filter(([key]) => !NON_CONTEXT_FIELDS.has(key))
11678
+ );
11679
+ const serialized = JSON.stringify(contextBody);
11680
+ if (!serialized || serialized === "{}") return 0;
11681
+ return Math.max(1, Math.ceil(Buffer.byteLength(serialized, "utf8") / 4));
11682
+ }
11683
+
11684
+ // src/http-proxy/server.ts
11685
+ var ANTHROPIC_HOST = "api.anthropic.com";
11686
+ var MAX_BODY_BYTES = 50 * 1024 * 1024;
11687
+ var PROXY_USERNAME = "relay-ai";
11688
+ function authorityParts(authority) {
11689
+ const match = authority.match(/^(\[[^\]]+\]|[^:]+)(?::(\d+))?$/);
11690
+ if (!match) return null;
11691
+ const host = match[1].replace(/^\[|\]$/g, "");
11692
+ const port = Number(match[2] ?? 443);
11693
+ if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return null;
11694
+ return { host, port };
11695
+ }
11696
+ function shouldInterceptConnect(authority) {
11697
+ const target = authorityParts(authority);
11698
+ return Boolean(
11699
+ target && target.port === 443 && target.host.replace(/\.$/, "").toLowerCase() === ANTHROPIC_HOST
11700
+ );
11701
+ }
11702
+ function secureEqual(left, right) {
11703
+ const leftBuffer = Buffer.from(left);
11704
+ const rightBuffer = Buffer.from(right);
11705
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
11706
+ }
11707
+ function isAuthorized(req, expected) {
11708
+ const header = req.headers["proxy-authorization"];
11709
+ const value = Array.isArray(header) ? header[0] : header;
11710
+ return typeof value === "string" && secureEqual(value, expected);
11711
+ }
11712
+ function rejectProxyRequest(res) {
11713
+ res.writeHead(407, {
11714
+ "Content-Type": "text/plain",
11715
+ "Proxy-Authenticate": 'Basic realm="Relay AI"',
11716
+ Connection: "close"
11717
+ });
11718
+ res.end("Proxy authentication required");
11719
+ }
11720
+ function rejectProxyConnect(socket) {
11721
+ socket.end([
11722
+ "HTTP/1.1 407 Proxy Authentication Required",
11723
+ 'Proxy-Authenticate: Basic realm="Relay AI"',
11724
+ "Connection: close",
11725
+ "Content-Length: 0",
11726
+ "",
11727
+ ""
11728
+ ].join("\r\n"));
11729
+ }
11730
+ function readRawBody(req) {
11731
+ return new Promise((resolve2, reject2) => {
11732
+ const chunks = [];
11733
+ let size = 0;
11734
+ let settled = false;
11735
+ const fail = (error) => {
11736
+ if (settled) return;
11737
+ settled = true;
11738
+ reject2(error);
11739
+ };
11740
+ req.on("data", (chunk) => {
11741
+ if (settled) return;
11742
+ size += chunk.length;
11743
+ if (size > MAX_BODY_BYTES) {
11744
+ fail(new Error("Request body too large"));
11745
+ req.destroy();
11746
+ return;
11747
+ }
11748
+ chunks.push(Buffer.from(chunk));
11749
+ });
11750
+ req.once("end", () => {
11751
+ if (settled) return;
11752
+ settled = true;
11753
+ resolve2(Buffer.concat(chunks));
11754
+ });
11755
+ req.once("aborted", () => fail(new Error("Client disconnected")));
11756
+ req.once("error", fail);
11757
+ });
11758
+ }
11759
+ function requestHeadersWithoutProxyHeaders(req) {
11760
+ const headers = [];
11761
+ for (let index = 0; index < req.rawHeaders.length; index += 2) {
11762
+ const name = req.rawHeaders[index];
11763
+ if (/^proxy-(authorization|connection)$/i.test(name)) continue;
11764
+ headers.push(name, req.rawHeaders[index + 1] ?? "");
11765
+ }
11766
+ return headers;
11767
+ }
11768
+ function copyResponse(upstream, res) {
11769
+ res.writeHead(upstream.statusCode ?? 502, upstream.statusMessage, upstream.rawHeaders);
11770
+ upstream.once("error", (error) => res.destroy(error));
11771
+ upstream.pipe(res);
11772
+ }
11773
+ function forwardRawRequest(req, res, rawBody, origin, rejectUnauthorized) {
11774
+ return new Promise((resolve2) => {
11775
+ let settled = false;
11776
+ const done = () => {
11777
+ if (settled) return;
11778
+ settled = true;
11779
+ resolve2();
11780
+ };
11781
+ const transport = origin.protocol === "https:" ? https : http2;
11782
+ const upstream = transport.request({
11783
+ protocol: origin.protocol,
11784
+ hostname: origin.hostname,
11785
+ port: origin.port || void 0,
11786
+ method: req.method,
11787
+ path: req.url,
11788
+ headers: requestHeadersWithoutProxyHeaders(req),
11789
+ ...origin.protocol === "https:" ? { rejectUnauthorized } : {}
11790
+ }, (upstreamRes) => {
11791
+ copyResponse(upstreamRes, res);
11792
+ upstreamRes.once("end", done);
11793
+ upstreamRes.once("error", done);
11794
+ });
11795
+ res.once("close", () => {
11796
+ if (!res.writableFinished) upstream.destroy(new Error("Client disconnected"));
11797
+ done();
11798
+ });
11799
+ upstream.once("error", (error) => {
11800
+ if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
11801
+ if (!res.writableEnded) res.end(`Anthropic upstream unreachable: ${error.message}`);
11802
+ done();
11803
+ });
11804
+ upstream.end(rawBody);
11805
+ });
11806
+ }
11807
+ function forwardToAdapter(req, res, rawBody, adapter) {
11808
+ return new Promise((resolve2) => {
11809
+ let settled = false;
11810
+ const done = () => {
11811
+ if (settled) return;
11812
+ settled = true;
11813
+ resolve2();
11814
+ };
11815
+ const sessionId = req.headers["x-claude-code-session-id"];
11816
+ const upstream = http2.request({
11817
+ hostname: "127.0.0.1",
11818
+ port: adapter.port,
11819
+ method: "POST",
11820
+ path: req.url,
11821
+ headers: {
11822
+ "Content-Type": "application/json",
11823
+ "Content-Length": String(rawBody.length),
11824
+ "x-api-key": adapter.token,
11825
+ ...typeof sessionId === "string" ? { "x-claude-code-session-id": sessionId } : {}
11826
+ }
11827
+ }, (upstreamRes) => {
11828
+ copyResponse(upstreamRes, res);
11829
+ upstreamRes.once("end", done);
11830
+ upstreamRes.once("error", done);
11831
+ });
11832
+ res.once("close", () => {
11833
+ if (!res.writableFinished) upstream.destroy(new Error("Client disconnected"));
11834
+ done();
11835
+ });
11836
+ upstream.once("error", (error) => {
11837
+ if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
11838
+ if (!res.writableEnded) res.end(`Relay adapter unreachable: ${error.message}`);
11839
+ done();
11840
+ });
11841
+ upstream.end(rawBody);
11842
+ });
11843
+ }
11844
+ function forwardPlainHttp(req, res) {
11845
+ let target;
11846
+ try {
11847
+ target = new URL2(req.url ?? "");
11848
+ } catch {
11849
+ res.writeHead(400, { "Content-Type": "text/plain" });
11850
+ res.end("HTTP proxy requests must use an absolute HTTP or HTTPS URL");
11851
+ return;
11852
+ }
11853
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
11854
+ res.writeHead(400, { "Content-Type": "text/plain" });
11855
+ res.end("HTTP proxy requests must use an HTTP or HTTPS URL");
11856
+ return;
11857
+ }
11858
+ const transport = target.protocol === "https:" ? https : http2;
11859
+ const upstream = transport.request({
11860
+ protocol: target.protocol,
11861
+ hostname: target.hostname,
11862
+ port: target.port || void 0,
11863
+ method: req.method,
11864
+ path: `${target.pathname}${target.search}`,
11865
+ headers: requestHeadersWithoutProxyHeaders(req)
11866
+ }, (upstreamRes) => copyResponse(upstreamRes, res));
11867
+ res.once("close", () => {
11868
+ if (!res.writableFinished) upstream.destroy(new Error("Client disconnected"));
11869
+ });
11870
+ upstream.once("error", (error) => {
11871
+ if (!res.headersSent) res.writeHead(502, { "Content-Type": "text/plain" });
11872
+ if (!res.writableEnded) res.end(`Proxy upstream unreachable: ${error.message}`);
11873
+ });
11874
+ req.pipe(upstream);
11875
+ }
11876
+ async function startHttpProxy(options) {
11877
+ const host = "127.0.0.1";
11878
+ const certificates = createHttpProxyCertificates();
11879
+ const routesById = /* @__PURE__ */ new Map();
11880
+ for (const route of options.routes) {
11881
+ for (const id of routeLookupIds(route.aliasId)) routesById.set(id, route);
11882
+ }
11883
+ const anthropicOrigin = new URL2(options.anthropicOrigin ?? "https://api.anthropic.com");
11884
+ if (anthropicOrigin.protocol !== "http:" && anthropicOrigin.protocol !== "https:") {
11885
+ certificates.cleanup();
11886
+ throw new Error("Anthropic origin must use HTTP or HTTPS");
11887
+ }
11888
+ let adapter = options.adapterHandle ?? null;
11889
+ try {
11890
+ if (options.routes.length > 0 && !adapter) {
11891
+ adapter = await startProxyCatalog(
11892
+ options.routes,
11893
+ options.routes[0].aliasId,
11894
+ options.debug
11895
+ );
11896
+ }
11897
+ } catch (error) {
11898
+ certificates.cleanup();
11899
+ throw error;
11900
+ }
11901
+ const mitmServer = https.createServer({
11902
+ key: certificates.serverKey,
11903
+ cert: certificates.serverCert,
11904
+ minVersion: "TLSv1.2"
11905
+ }, async (req, res) => {
11906
+ let rawBody;
11907
+ try {
11908
+ rawBody = await readRawBody(req);
11909
+ } catch (error) {
11910
+ if (!res.headersSent && !res.writableEnded) {
11911
+ res.writeHead(413, { "Content-Type": "text/plain" });
11912
+ res.end(error instanceof Error ? error.message : String(error));
11913
+ }
11914
+ return;
11915
+ }
11916
+ const endpoint = anthropicMessagesEndpoint(req.url);
11917
+ if (req.method === "POST" && endpoint) {
11918
+ let parsed = null;
11919
+ let route;
11920
+ try {
11921
+ parsed = JSON.parse(rawBody.toString("utf8"));
11922
+ if (typeof parsed.model === "string") route = routesById.get(parsed.model);
11923
+ } catch {
11924
+ }
11925
+ if (route && adapter && parsed) {
11926
+ if (endpoint === "count_tokens") {
11927
+ res.writeHead(200, {
11928
+ "Content-Type": "application/json",
11929
+ "x-relay-token-count-source": "local-estimate"
11930
+ });
11931
+ res.end(JSON.stringify({ input_tokens: estimateAnthropicInputTokens(parsed) }));
11932
+ return;
11933
+ }
11934
+ const adapterBody = parsed.model === route.aliasId ? rawBody : Buffer.from(JSON.stringify({ ...parsed, model: route.aliasId }));
11935
+ await forwardToAdapter(req, res, adapterBody, adapter);
11936
+ return;
11937
+ }
11938
+ }
11939
+ await forwardRawRequest(
11940
+ req,
11941
+ res,
11942
+ rawBody,
11943
+ anthropicOrigin,
11944
+ options.anthropicRejectUnauthorized ?? true
11945
+ );
11946
+ });
11947
+ mitmServer.on("tlsClientError", () => {
11948
+ });
11949
+ const password3 = randomBytes2(32).toString("base64url");
11950
+ const expectedAuthorization = `Basic ${Buffer.from(`${PROXY_USERNAME}:${password3}`).toString("base64")}`;
11951
+ const sockets = /* @__PURE__ */ new Set();
11952
+ const proxyServer = http2.createServer((req, res) => {
11953
+ if (!isAuthorized(req, expectedAuthorization)) {
11954
+ rejectProxyRequest(res);
11955
+ return;
11956
+ }
11957
+ forwardPlainHttp(req, res);
11958
+ });
11959
+ proxyServer.on("connection", (socket) => {
11960
+ sockets.add(socket);
11961
+ socket.once("close", () => sockets.delete(socket));
11962
+ });
11963
+ proxyServer.on("connect", (req, clientSocket, head) => {
11964
+ clientSocket.on("error", () => clientSocket.destroy());
11965
+ if (!isAuthorized(req, expectedAuthorization)) {
11966
+ rejectProxyConnect(clientSocket);
11967
+ return;
11968
+ }
11969
+ if (shouldInterceptConnect(req.url ?? "")) {
11970
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
11971
+ if (head.length > 0) clientSocket.unshift(head);
11972
+ mitmServer.emit("connection", clientSocket);
11973
+ return;
11974
+ }
11975
+ const target = authorityParts(req.url ?? "");
11976
+ if (!target) {
11977
+ clientSocket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
11978
+ return;
11979
+ }
11980
+ const upstream = net.connect(target.port, target.host);
11981
+ let established = false;
11982
+ sockets.add(upstream);
11983
+ clientSocket.once("close", () => {
11984
+ if (!upstream.destroyed) upstream.destroy();
11985
+ });
11986
+ upstream.once("close", () => {
11987
+ sockets.delete(upstream);
11988
+ if (established && !clientSocket.destroyed) clientSocket.destroy();
11989
+ });
11990
+ upstream.once("connect", () => {
11991
+ established = true;
11992
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
11993
+ if (head.length > 0) upstream.write(head);
11994
+ clientSocket.pipe(upstream);
11995
+ upstream.pipe(clientSocket);
11996
+ });
11997
+ upstream.once("error", () => {
11998
+ if (clientSocket.destroyed) return;
11999
+ if (established) clientSocket.destroy();
12000
+ else clientSocket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
12001
+ });
12002
+ });
12003
+ try {
12004
+ await new Promise((resolve2, reject2) => {
12005
+ proxyServer.once("error", reject2);
12006
+ proxyServer.listen(0, host, () => {
12007
+ proxyServer.off("error", reject2);
12008
+ resolve2();
12009
+ });
12010
+ });
12011
+ } catch (error) {
12012
+ adapter?.close();
12013
+ certificates.cleanup();
12014
+ throw error;
12015
+ }
12016
+ const address = proxyServer.address();
12017
+ if (!address || typeof address === "string") {
12018
+ adapter?.close();
12019
+ certificates.cleanup();
12020
+ throw new Error("HTTP proxy did not bind to a TCP port");
12021
+ }
12022
+ let closed = false;
12023
+ return {
12024
+ host,
12025
+ port: address.port,
12026
+ proxyUrl: `http://${PROXY_USERNAME}:${encodeURIComponent(password3)}@${host}:${address.port}`,
12027
+ caCertPath: certificates.caCertPath,
12028
+ modelIds: options.routes.map((route) => route.aliasId),
12029
+ close: async () => {
12030
+ if (closed) return;
12031
+ closed = true;
12032
+ for (const socket of sockets) socket.destroy();
12033
+ await new Promise((resolve2) => proxyServer.close(() => resolve2()));
12034
+ try {
12035
+ mitmServer.close();
12036
+ } catch {
12037
+ }
12038
+ adapter?.close();
12039
+ certificates.cleanup();
12040
+ }
12041
+ };
12042
+ }
12043
+
12044
+ // src/http-proxy/index.ts
12045
+ async function resolveHttpProxyRoutes(providers, favorites, selected, resolveCredential = resolveLocalProviderApiKey) {
12046
+ const requestedProviderIds = /* @__PURE__ */ new Set();
12047
+ if (selected) requestedProviderIds.add(selected.providerId);
12048
+ for (const favorite of favorites) requestedProviderIds.add(favorite.providerId);
12049
+ const allowedProviders = [];
12050
+ for (const providerId of requestedProviderIds) {
12051
+ const provider = providers.find((candidate) => candidate.id === providerId);
12052
+ if (!provider) continue;
12053
+ allowedProviders.push({
12054
+ ...provider,
12055
+ apiKey: await resolveCredential(provider) ?? ""
12056
+ });
12057
+ }
12058
+ return buildHttpProxyRoutes(allowedProviders, favorites, selected);
12059
+ }
12060
+ async function startConfiguredHttpProxy(options) {
12061
+ const loaded = await resolveHttpProxyRoutes(
12062
+ options.providers,
12063
+ options.favorites,
12064
+ options.selected
12065
+ );
12066
+ const handle = await startHttpProxy({ routes: loaded.routes, debug: options.debug });
12067
+ try {
12068
+ handle.caCertPath = createHttpProxyCaBundle(
12069
+ handle.caCertPath,
12070
+ options.additionalCaCertPath
12071
+ );
12072
+ } catch (error) {
12073
+ await handle.close();
12074
+ throw error;
12075
+ }
12076
+ let startingModel;
12077
+ if (options.selected) {
12078
+ const selectedModel = options.providers.find((provider) => provider.id === options.selected.providerId)?.models.find((model) => model.id === options.selected.modelId);
12079
+ const expectedId = selectedModel ? claudeCodeClientModelId(
12080
+ httpProxyModelId(options.selected.providerId, options.selected.modelId),
12081
+ selectedModel.contextWindow
12082
+ ) : void 0;
12083
+ startingModel = loaded.routes.find((route) => route.aliasId === expectedId)?.aliasId;
12084
+ }
12085
+ return { handle, loaded, startingModel };
12086
+ }
12087
+
12088
+ // src/http-proxy/launch.ts
12089
+ var defaultDependencies = {
12090
+ start: startConfiguredHttpProxy,
12091
+ launch: launchClaude
12092
+ };
12093
+ async function launchClaudeWithHttpProxy(options, dependencies = defaultDependencies) {
12094
+ const inheritedProxy = findUnsupportedInheritedProxy(options.baseEnv);
12095
+ if (inheritedProxy) {
12096
+ throw unsupportedInheritedProxyError(inheritedProxy);
12097
+ }
12098
+ const proxy = await dependencies.start({
12099
+ providers: options.providers,
12100
+ favorites: options.favorites,
12101
+ selected: options.selected,
12102
+ debug: options.debug,
12103
+ additionalCaCertPath: options.baseEnv["NODE_EXTRA_CA_CERTS"]
12104
+ });
12105
+ try {
12106
+ if (options.selected && !proxy.startingModel) {
12107
+ throw new Error(
12108
+ "The selected Relay model is unavailable, unsupported, or missing its provider credential."
12109
+ );
12110
+ }
12111
+ options.onProxyReady?.(proxy);
12112
+ const childEnv = buildHttpProxyChildEnv(
12113
+ options.baseEnv,
12114
+ proxy.handle.proxyUrl,
12115
+ proxy.handle.caCertPath
12116
+ );
12117
+ const exitCode = await dependencies.launch(
12118
+ childEnv,
12119
+ proxy.startingModel,
12120
+ options.claudeArgs
12121
+ );
12122
+ return { exitCode, proxy };
12123
+ } finally {
12124
+ await proxy.handle.close();
12125
+ }
12126
+ }
12127
+
11449
12128
  // src/cli.ts
11450
- var STARTER_CLAUDE_FLAGS = /* @__PURE__ */ new Set(["--dry-run", "--setup", "--trace", "--help", "-h", "--version", "-v"]);
12129
+ var STARTER_CLAUDE_FLAGS = /* @__PURE__ */ new Set(["--dry-run", "--setup", "--trace", "--http-proxy", "--help", "-h", "--version", "-v"]);
11451
12130
  var RELAY_LAUNCH_FLAGS = /* @__PURE__ */ new Set(["--provider", "--model"]);
11452
12131
  function parseRelayLaunchFlag(arg, rest, index, parsed) {
11453
12132
  if (arg === "--provider" || arg === "--model") {
@@ -11644,6 +12323,10 @@ function parseArgs(args) {
11644
12323
  parsed2.showVersion = true;
11645
12324
  continue;
11646
12325
  }
12326
+ if (arg === "--http-proxy") {
12327
+ parsed2.error = "--http-proxy is available only for relay-ai claude";
12328
+ return parsed2;
12329
+ }
11647
12330
  const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
11648
12331
  if (consumed !== null) {
11649
12332
  if ("error" in consumed) return parsed2;
@@ -11796,6 +12479,7 @@ function parseArgs(args) {
11796
12479
  if (arg === "--dry-run") parsed.dryRun = true;
11797
12480
  if (arg === "--setup") parsed.setup = true;
11798
12481
  if (arg === "--trace") parsed.trace = true;
12482
+ if (arg === "--http-proxy") parsed.httpProxy = true;
11799
12483
  if (arg === "--help" || arg === "-h") parsed.showHelp = true;
11800
12484
  if (arg === "--version" || arg === "-v") parsed.showVersion = true;
11801
12485
  }
@@ -11888,6 +12572,7 @@ ${pc12.bold("Options:")}
11888
12572
  --trace Write debug logs to ~/.relay-ai/logs/ and show errors on exit
11889
12573
  --provider Boot provider id (skip wizard when paired with --model or in print mode)
11890
12574
  --model Boot model id (skip wizard when paired with --provider or in print mode)
12575
+ --http-proxy Keep your normal Anthropic login and add selected/favorite Relay models
11891
12576
  --help Show this command help
11892
12577
  --version Show version
11893
12578
 
@@ -11902,6 +12587,11 @@ ${pc12.bold("Model switching:")}
11902
12587
  lists your starting model plus favorites for live switching.
11903
12588
  With no favorites, launch uses a single model as before.
11904
12589
 
12590
+ ${pc12.bold("Anthropic + Relay mode:")}
12591
+ --http-proxy keeps your normal Anthropic login and models available, then adds
12592
+ only the selected model and compatible favorites. The terminal prints the exact
12593
+ /model commands for switching; Relay models are not added to Claude's built-in picker.
12594
+
11905
12595
  ${pc12.bold("Note:")}
11906
12596
  Claude Code may save the launched model to ~/.claude/settings.json.
11907
12597
  Bare claude later can still show that model \u2014 reset with claude --model sonnet.
@@ -11915,6 +12605,7 @@ ${pc12.bold("Examples:")}
11915
12605
  relay-ai claude --setup
11916
12606
  relay-ai claude --trace --resume abc-123
11917
12607
  relay-ai claude --provider groq --model llama-3.3-70b-versatile
12608
+ relay-ai claude --http-proxy --provider moonshot --model kimi-k3
11918
12609
  relay-ai claude --provider groq --model llama-3.3-70b-versatile -p "review this file"
11919
12610
  relay-ai claude -- --print "hello"
11920
12611
  relay-ai claude -- --dangerously-skip-permissions`;
@@ -12306,7 +12997,7 @@ async function runModelsCommand(opts = {}) {
12306
12997
  return 0;
12307
12998
  }
12308
12999
  async function runClaudeCommand(parsed) {
12309
- const { dryRun, setup, trace, launchProvider, launchModel } = parsed;
13000
+ const { dryRun, setup, trace, httpProxy, launchProvider, launchModel } = parsed;
12310
13001
  const claudeArgs = normalizeClaudeAgentArgs(parsed.claudeArgs);
12311
13002
  const agentStdout = wantsCleanAgentStdout("claude", claudeArgs);
12312
13003
  setAgentStdoutMode(agentStdout);
@@ -12320,7 +13011,8 @@ async function runClaudeCommand(parsed) {
12320
13011
  const prefs = dryRun ? {} : loadPreferences();
12321
13012
  const conflicts = detectConflicts();
12322
13013
  const favorites = dryRun ? [] : prefs.favoriteModels ?? [];
12323
- const launchPlan = planLaunchWizard({
13014
+ const httpProxyOnly = Boolean(httpProxy && !launchProvider && !launchModel);
13015
+ const launchPlan = httpProxyOnly ? { skip: true, target: null } : planLaunchWizard({
12324
13016
  explicit: { providerId: launchProvider, modelId: launchModel },
12325
13017
  childArgs: claudeArgs,
12326
13018
  agent: "claude",
@@ -12337,7 +13029,7 @@ Error: ${launchPlan.error}
12337
13029
  if (setup && !dryRun && !agentStdout) {
12338
13030
  p14.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
12339
13031
  }
12340
- if (!dryRun && await needsFirstRunSetup()) {
13032
+ if (!httpProxyOnly && !dryRun && await needsFirstRunSetup()) {
12341
13033
  const firstRun = await runFirstRunWizard(trace);
12342
13034
  if (firstRun === "cancel") return 0;
12343
13035
  }
@@ -12362,11 +13054,64 @@ Error: ${launchPlan.error}
12362
13054
  catalogSpinner.stop("");
12363
13055
  }
12364
13056
  const allProviders = providersForTarget(providersForPicker(catalog), "claude");
12365
- if (allProviders.length === 0) {
13057
+ if (allProviders.length === 0 && !httpProxyOnly) {
12366
13058
  p14.log.warn("No providers available.");
12367
13059
  p14.log.info(pc12.dim("Run relay-ai providers add or import to get started."));
12368
13060
  return 0;
12369
13061
  }
13062
+ const runTransparentProxy = async (selected) => {
13063
+ if (dryRun) {
13064
+ console.log("");
13065
+ console.log(pc12.bold(pc12.cyan(" DRY RUN \u2014 would keep Anthropic and add Relay models:")));
13066
+ console.log("");
13067
+ console.log(` ${pc12.bold("Anthropic:")} normal Claude Code login and models`);
13068
+ console.log(` ${pc12.bold("Selected:")} ${selected ? `${selected.providerId} / ${selected.modelId}` : "(none)"}`);
13069
+ console.log(` ${pc12.bold("Favorites:")} ${favorites.length} saved`);
13070
+ console.log("");
13071
+ console.log(pc12.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
13072
+ console.log("");
13073
+ return 0;
13074
+ }
13075
+ const debugLogPath2 = prepareClaudeTraceLog();
13076
+ const traceArgs2 = trace ? ["--debug-file", debugLogPath2] : [];
13077
+ if (trace && !agentStdout) p14.log.info(`Debug log: ${debugLogPath2}`);
13078
+ try {
13079
+ const result = await launchClaudeWithHttpProxy({
13080
+ providers: catalog,
13081
+ favorites,
13082
+ selected,
13083
+ baseEnv: process.env,
13084
+ claudeArgs: [...traceArgs2, ...claudeArgs],
13085
+ debug: trace,
13086
+ onProxyReady: (proxy) => {
13087
+ if (agentStdout) return;
13088
+ const count = proxy.handle.modelIds.length;
13089
+ p14.log.info(
13090
+ count === 0 ? "Secure Anthropic passthrough ready; no compatible Relay models were added." : `Secure Anthropic passthrough ready with ${count} Relay model${count === 1 ? "" : "s"}.`
13091
+ );
13092
+ for (const modelId of proxy.handle.modelIds) p14.log.message(pc12.dim(` /model ${modelId}`));
13093
+ }
13094
+ });
13095
+ if (!agentStdout && result.proxy.loaded.unavailable.length > 0) {
13096
+ p14.log.warn(
13097
+ `${result.proxy.loaded.unavailable.length} favorite${result.proxy.loaded.unavailable.length === 1 ? "" : "s"} unavailable or missing credentials.`
13098
+ );
13099
+ }
13100
+ if (!agentStdout && result.proxy.loaded.unsupported.length > 0) {
13101
+ p14.log.warn(
13102
+ `${result.proxy.loaded.unsupported.length} incompatible favorite${result.proxy.loaded.unsupported.length === 1 ? "" : "s"} skipped.`
13103
+ );
13104
+ }
13105
+ if (trace) printTraceLog(debugLogPath2);
13106
+ return result.exitCode;
13107
+ } catch (error) {
13108
+ p14.log.error(
13109
+ `Could not start secure Anthropic + Relay mode: ${error instanceof Error ? error.message : String(error)}`
13110
+ );
13111
+ return 1;
13112
+ }
13113
+ };
13114
+ if (httpProxyOnly) return runTransparentProxy();
12370
13115
  const providerOptions = allProviders.map((lp) => providerSelectOption(lp));
12371
13116
  if (switchMenuActive) {
12372
13117
  providerOptions.unshift({
@@ -12449,6 +13194,26 @@ Error: ${launchPlan.error}
12449
13194
  }
12450
13195
  }
12451
13196
  }
13197
+ let useHttpProxy = Boolean(httpProxy);
13198
+ if (!httpProxy && !launchPlan.skip && supportsClaudeTransparentMode(selectedModel)) {
13199
+ const transparentChoice = await p14.select({
13200
+ message: "Keep your normal Claude models available too?",
13201
+ options: claudeTransparentModeOptions(selectedModel.name || selectedModel.id),
13202
+ initialValue: prefs.lastClaudeTransparentMode ?? true
13203
+ });
13204
+ if (p14.isCancel(transparentChoice)) {
13205
+ p14.cancel("Cancelled.");
13206
+ return 0;
13207
+ }
13208
+ useHttpProxy = transparentChoice;
13209
+ if (!dryRun) savePreferences({ lastClaudeTransparentMode: useHttpProxy });
13210
+ }
13211
+ if (useHttpProxy) {
13212
+ return runTransparentProxy({
13213
+ providerId: activeProvider.id,
13214
+ modelId: selectedModel.id
13215
+ });
13216
+ }
12452
13217
  const localProviders = catalog.length > 0 ? catalog : null;
12453
13218
  if (switchMenuActive) {
12454
13219
  const resolveRoute = makeRouteResolver(
@@ -12696,7 +13461,7 @@ Error: ${parsed.error}
12696
13461
  console.log("Usage: relay-ai ui [--trace]\n\nOpen the settings UI in your browser.");
12697
13462
  return 0;
12698
13463
  }
12699
- const { runUiCommand } = await import("./ui-command-2HNQX7U3.js");
13464
+ const { runUiCommand } = await import("./ui-command-W2W6P6VJ.js");
12700
13465
  return runUiCommand({ trace: parsed.trace });
12701
13466
  }
12702
13467
  if (parsed.command === "models") {