@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/README.md +22 -1
- package/dist/{chunk-YM6G7BHE.js → chunk-XQCHSU6L.js} +122 -9
- package/dist/chunk-XQCHSU6L.js.map +1 -0
- package/dist/cli.js +853 -88
- package/dist/cli.js.map +1 -1
- package/dist/ui/public/app.js +34 -0
- package/dist/ui/public/style.css +89 -0
- package/dist/{ui-command-2HNQX7U3.js → ui-command-W2W6P6VJ.js} +40 -6
- package/dist/ui-command-W2W6P6VJ.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-YM6G7BHE.js.map +0 -1
- package/dist/ui-command-2HNQX7U3.js.map +0 -1
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-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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) =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
8984
|
+
resolve2(code ?? 1);
|
|
9022
8985
|
});
|
|
9023
8986
|
child.on("error", (err) => {
|
|
9024
8987
|
console.error(`Failed to launch Antigravity IDE: ${err.message}`);
|
|
9025
|
-
|
|
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((
|
|
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
|
-
|
|
9189
|
+
resolve2("sigint");
|
|
9227
9190
|
};
|
|
9228
9191
|
const onSigterm = () => {
|
|
9229
9192
|
cleanup();
|
|
9230
|
-
|
|
9193
|
+
resolve2("sigterm");
|
|
9231
9194
|
};
|
|
9232
9195
|
const onSighup = () => {
|
|
9233
9196
|
cleanup();
|
|
9234
|
-
|
|
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((
|
|
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
|
-
|
|
9823
|
+
resolve2("sigint");
|
|
9861
9824
|
};
|
|
9862
9825
|
const onSigterm = () => {
|
|
9863
9826
|
cleanup();
|
|
9864
|
-
|
|
9827
|
+
resolve2("sigterm");
|
|
9865
9828
|
};
|
|
9866
9829
|
const onSighup = () => {
|
|
9867
9830
|
cleanup();
|
|
9868
|
-
|
|
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((
|
|
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
|
-
|
|
10540
|
+
resolve2("sigint");
|
|
10578
10541
|
};
|
|
10579
10542
|
const onSigterm = () => {
|
|
10580
10543
|
cleanup();
|
|
10581
|
-
|
|
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
|
|
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-
|
|
13464
|
+
const { runUiCommand } = await import("./ui-command-W2W6P6VJ.js");
|
|
12700
13465
|
return runUiCommand({ trace: parsed.trace });
|
|
12701
13466
|
}
|
|
12702
13467
|
if (parsed.command === "models") {
|