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