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