@bman654/clodex 1.2.2 → 1.3.0
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 +1 -1
- package/dist/{chunk-3XM6UZWP.js → chunk-ZN5X7YFE.js} +5 -1
- package/dist/chunk-ZN5X7YFE.js.map +1 -0
- package/dist/claude-wrapper.js +1 -1
- package/dist/cli.js +1400 -390
- package/dist/cli.js.map +1 -1
- package/docs/credential-helpers.md +36 -2
- package/package.json +1 -1
- package/dist/chunk-3XM6UZWP.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ensureLegacyAppHomeMigrated,
|
|
4
4
|
findClaudeBinary,
|
|
5
5
|
getAppHome,
|
|
6
|
+
getCredentialCleanupPath,
|
|
6
7
|
getInstalledClaudeVersion,
|
|
7
8
|
getLogsPath,
|
|
8
9
|
getProvidersPath,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
setServerListenMode,
|
|
25
26
|
setServerMaskGatewayIds,
|
|
26
27
|
unregisterServerRuntimeState
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-ZN5X7YFE.js";
|
|
28
29
|
|
|
29
30
|
// src/cli.ts
|
|
30
31
|
import pc13 from "picocolors";
|
|
@@ -202,7 +203,7 @@ import { join } from "path";
|
|
|
202
203
|
// package.json
|
|
203
204
|
var package_default = {
|
|
204
205
|
name: "@bman654/clodex",
|
|
205
|
-
version: "1.
|
|
206
|
+
version: "1.3.0",
|
|
206
207
|
publishConfig: {
|
|
207
208
|
access: "public"
|
|
208
209
|
},
|
|
@@ -315,7 +316,7 @@ var VERTEX_ANTHROPIC_NPM = "@ai-sdk/google-vertex/anthropic";
|
|
|
315
316
|
var VERSION = package_default.version;
|
|
316
317
|
|
|
317
318
|
// src/env.ts
|
|
318
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
319
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
319
320
|
|
|
320
321
|
// src/credential-helper.ts
|
|
321
322
|
import { spawn } from "child_process";
|
|
@@ -563,11 +564,23 @@ function oauthCredentialToKeychainJson(cred) {
|
|
|
563
564
|
return JSON.stringify(cred);
|
|
564
565
|
}
|
|
565
566
|
function tokensToStoredCredential(tokens, existingRefresh, accountId, providerData) {
|
|
567
|
+
const access = typeof tokens.access_token === "string" ? tokens.access_token.trim() : "";
|
|
568
|
+
if (!access) {
|
|
569
|
+
throw new Error("OAuth token response is missing a valid access token");
|
|
570
|
+
}
|
|
571
|
+
if (tokens.expires_in !== void 0 && (typeof tokens.expires_in !== "number" || !Number.isFinite(tokens.expires_in) || tokens.expires_in < 0)) {
|
|
572
|
+
throw new Error("OAuth token response has an invalid expiration");
|
|
573
|
+
}
|
|
574
|
+
const returnedRefresh = typeof tokens.refresh_token === "string" ? tokens.refresh_token.trim() : "";
|
|
575
|
+
const expires = Date.now() + (tokens.expires_in ?? 3600) * 1e3;
|
|
576
|
+
if (!Number.isFinite(expires)) {
|
|
577
|
+
throw new Error("OAuth token response has an invalid expiration");
|
|
578
|
+
}
|
|
566
579
|
return {
|
|
567
580
|
type: "oauth",
|
|
568
|
-
access
|
|
569
|
-
refresh:
|
|
570
|
-
expires
|
|
581
|
+
access,
|
|
582
|
+
refresh: returnedRefresh || existingRefresh || "",
|
|
583
|
+
expires,
|
|
571
584
|
...accountId ? { accountId } : {},
|
|
572
585
|
...providerData ? { providerData } : {}
|
|
573
586
|
};
|
|
@@ -576,7 +589,7 @@ function parseStoredOAuthCredential(raw) {
|
|
|
576
589
|
if (!raw?.trim().startsWith("{")) return null;
|
|
577
590
|
try {
|
|
578
591
|
const parsed = JSON.parse(raw);
|
|
579
|
-
if (parsed.type === "oauth" && typeof parsed.access === "string" && typeof parsed.refresh === "string" && typeof parsed.expires === "number") {
|
|
592
|
+
if (parsed.type === "oauth" && typeof parsed.access === "string" && parsed.access.trim().length > 0 && typeof parsed.refresh === "string" && typeof parsed.expires === "number" && Number.isFinite(parsed.expires) && (parsed.accessRejected === void 0 || parsed.accessRejected === true)) {
|
|
580
593
|
return parsed;
|
|
581
594
|
}
|
|
582
595
|
} catch {
|
|
@@ -616,23 +629,45 @@ async function sleepMs(ms) {
|
|
|
616
629
|
}
|
|
617
630
|
|
|
618
631
|
// src/oauth/refresh-http.ts
|
|
632
|
+
var OAUTH_REFRESH_TIMEOUT_MS = 3e4;
|
|
619
633
|
async function postOAuthRefresh(url, body, options) {
|
|
620
634
|
const isJson = options.contentType === "json";
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
635
|
+
const abortController = new AbortController();
|
|
636
|
+
const timeout = setTimeout(() => {
|
|
637
|
+
abortController.abort(new DOMException(
|
|
638
|
+
"The operation was aborted due to timeout",
|
|
639
|
+
"TimeoutError"
|
|
640
|
+
));
|
|
641
|
+
}, OAUTH_REFRESH_TIMEOUT_MS);
|
|
642
|
+
timeout.unref();
|
|
643
|
+
try {
|
|
644
|
+
const response = await fetch(url, {
|
|
645
|
+
method: "POST",
|
|
646
|
+
signal: abortController.signal,
|
|
647
|
+
headers: {
|
|
648
|
+
"Content-Type": isJson ? "application/json" : "application/x-www-form-urlencoded",
|
|
649
|
+
Accept: "application/json",
|
|
650
|
+
...options.headers
|
|
651
|
+
},
|
|
652
|
+
body: isJson ? JSON.stringify(body) : body.toString()
|
|
653
|
+
});
|
|
654
|
+
if (!response.ok) {
|
|
655
|
+
let detail = "";
|
|
656
|
+
if (options.includeBody) {
|
|
657
|
+
detail = await response.text().catch(() => "");
|
|
658
|
+
} else {
|
|
659
|
+
try {
|
|
660
|
+
await response.body?.cancel();
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const status = options.includeStatus ? ` (${response.status})` : "";
|
|
665
|
+
throw new Error(`${options.errorPrefix}${status}${detail ? `: ${detail}` : ""}`);
|
|
666
|
+
}
|
|
667
|
+
return await response.json();
|
|
668
|
+
} finally {
|
|
669
|
+
clearTimeout(timeout);
|
|
634
670
|
}
|
|
635
|
-
return response.json();
|
|
636
671
|
}
|
|
637
672
|
|
|
638
673
|
// src/oauth/openai.ts
|
|
@@ -769,6 +804,7 @@ import {
|
|
|
769
804
|
} from "fs";
|
|
770
805
|
import { dirname } from "path";
|
|
771
806
|
var DEFAULT_WAIT_MS = 3e4;
|
|
807
|
+
var DEFAULT_CREDENTIAL_MUTATION_WAIT_MS = 15e4;
|
|
772
808
|
var DEFAULT_RETRY_MS = 25;
|
|
773
809
|
var registryLockContext = new AsyncLocalStorage();
|
|
774
810
|
var RegistryLockLostError = class extends Error {
|
|
@@ -1057,9 +1093,11 @@ function getCredentialMutationLockPath(authRef) {
|
|
|
1057
1093
|
const digest = createHash2("sha256").update("clodex-credential-mutation\0").update(authRef).digest("hex");
|
|
1058
1094
|
return `${getProvidersPath()}.credential-${digest}.lock`;
|
|
1059
1095
|
}
|
|
1060
|
-
function withCredentialMutationLock(authRef, operation) {
|
|
1096
|
+
function withCredentialMutationLock(authRef, operation, options = {}) {
|
|
1061
1097
|
return withRegistryWriteLock(operation, {
|
|
1062
|
-
|
|
1098
|
+
...options,
|
|
1099
|
+
lockPath: getCredentialMutationLockPath(authRef),
|
|
1100
|
+
waitMs: options.waitMs ?? DEFAULT_CREDENTIAL_MUTATION_WAIT_MS
|
|
1063
1101
|
});
|
|
1064
1102
|
}
|
|
1065
1103
|
|
|
@@ -1142,6 +1180,11 @@ function oauthProviderIdFromAccount(account) {
|
|
|
1142
1180
|
return account.startsWith(prefix) ? account.slice(prefix.length) : null;
|
|
1143
1181
|
}
|
|
1144
1182
|
var oauthRefreshInflight = /* @__PURE__ */ new Map();
|
|
1183
|
+
var OAUTH_CREDENTIAL_CACHE_MAX_AGE_MS = 3e4;
|
|
1184
|
+
var oauthCredentialCache = /* @__PURE__ */ new Map();
|
|
1185
|
+
var rejectedEnvCredentialFingerprints = /* @__PURE__ */ new Map();
|
|
1186
|
+
var OAUTH_REFRESH_LOCK_WAIT_MS = 15e4;
|
|
1187
|
+
var OAUTH_STATE_KEY_SEPARATOR = "\0";
|
|
1145
1188
|
function parseAuthRef(authRef) {
|
|
1146
1189
|
if (authRef === "none:anonymous") return { kind: "none" };
|
|
1147
1190
|
if (authRef.startsWith("keyring:")) {
|
|
@@ -1164,6 +1207,26 @@ function readEnvCredential(varName) {
|
|
|
1164
1207
|
if (!raw?.trim()) return null;
|
|
1165
1208
|
return raw.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
1166
1209
|
}
|
|
1210
|
+
function credentialFingerprint(value) {
|
|
1211
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
1212
|
+
}
|
|
1213
|
+
function usableEnvCredential(source, value, rejectedAccessToken) {
|
|
1214
|
+
if (!value) {
|
|
1215
|
+
rejectedEnvCredentialFingerprints.delete(source);
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
const fingerprint = credentialFingerprint(value);
|
|
1219
|
+
if (rejectedAccessToken !== void 0 && fingerprint === credentialFingerprint(rejectedAccessToken)) {
|
|
1220
|
+
rejectedEnvCredentialFingerprints.set(source, fingerprint);
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
const rejectedFingerprint = rejectedEnvCredentialFingerprints.get(source);
|
|
1224
|
+
if (rejectedFingerprint === fingerprint) return null;
|
|
1225
|
+
if (rejectedFingerprint !== void 0) {
|
|
1226
|
+
rejectedEnvCredentialFingerprints.delete(source);
|
|
1227
|
+
}
|
|
1228
|
+
return value;
|
|
1229
|
+
}
|
|
1167
1230
|
function readKeyringAccountFromService(Entry, service, account) {
|
|
1168
1231
|
const value = new Entry(service, account).getPassword() ?? null;
|
|
1169
1232
|
if (!value?.startsWith(KEYRING_CHUNK_PREFIX)) return value;
|
|
@@ -1263,16 +1326,25 @@ async function deleteStoredCredential(ref, diag) {
|
|
|
1263
1326
|
return false;
|
|
1264
1327
|
}
|
|
1265
1328
|
}
|
|
1266
|
-
async function resolveProviderCredential(providerId, authRef, diag) {
|
|
1329
|
+
async function resolveProviderCredential(providerId, authRef, diag, options = {}) {
|
|
1267
1330
|
const parsed = parseAuthRef(authRef);
|
|
1268
1331
|
if (parsed?.kind === "none") return null;
|
|
1269
|
-
const
|
|
1332
|
+
const namespacedVar = clodexKeyEnvVar(providerId);
|
|
1333
|
+
const namespaced = usableEnvCredential(
|
|
1334
|
+
`provider:${providerId}`,
|
|
1335
|
+
readEnvCredential(namespacedVar),
|
|
1336
|
+
options.rejectedAccessToken
|
|
1337
|
+
);
|
|
1270
1338
|
if (namespaced) return namespaced;
|
|
1271
1339
|
if (!parsed) return null;
|
|
1272
1340
|
if (parsed.kind === "env") {
|
|
1273
|
-
return
|
|
1341
|
+
return usableEnvCredential(
|
|
1342
|
+
`provider:${providerId}:env:${parsed.varName}`,
|
|
1343
|
+
readEnvCredential(parsed.varName),
|
|
1344
|
+
options.rejectedAccessToken
|
|
1345
|
+
);
|
|
1274
1346
|
}
|
|
1275
|
-
return readProviderSecret(parsed, diag);
|
|
1347
|
+
return readProviderSecret(parsed, diag, options.rejectedAccessToken);
|
|
1276
1348
|
}
|
|
1277
1349
|
async function resolveProviderOAuthAccountId(authRef, diag) {
|
|
1278
1350
|
const parsed = parseAuthRef(authRef);
|
|
@@ -1286,7 +1358,7 @@ async function resolveProviderOAuthProviderData(authRef, diag) {
|
|
|
1286
1358
|
const raw = await readStoredCredential(parsed, diag);
|
|
1287
1359
|
return parseStoredOAuthCredential(raw)?.providerData;
|
|
1288
1360
|
}
|
|
1289
|
-
function decodeProviderSecret(raw) {
|
|
1361
|
+
function decodeProviderSecret(raw, allowOpaqueJson = false) {
|
|
1290
1362
|
if (!raw) return null;
|
|
1291
1363
|
const trimmed = raw.trim();
|
|
1292
1364
|
if (!trimmed.startsWith("{")) return trimmed;
|
|
@@ -1294,59 +1366,138 @@ function decodeProviderSecret(raw) {
|
|
|
1294
1366
|
if (oauth) return oauth.access;
|
|
1295
1367
|
try {
|
|
1296
1368
|
const parsed = JSON.parse(trimmed);
|
|
1297
|
-
if (parsed.type === "
|
|
1298
|
-
|
|
1369
|
+
if (parsed.type === "wellknown") {
|
|
1370
|
+
return typeof parsed.token === "string" && parsed.token.trim() ? parsed.token.trim() : null;
|
|
1371
|
+
}
|
|
1372
|
+
if (allowOpaqueJson && parsed.type === "oauth") {
|
|
1373
|
+
return typeof parsed.access === "string" && parsed.access.trim() ? parsed.access.trim() : null;
|
|
1374
|
+
}
|
|
1375
|
+
return allowOpaqueJson ? raw : null;
|
|
1299
1376
|
} catch {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function oauthCredentialStateKey(providerId, authRef) {
|
|
1381
|
+
return `${providerId}${OAUTH_STATE_KEY_SEPARATOR}${authRef}`;
|
|
1382
|
+
}
|
|
1383
|
+
function clearOAuthCredentialCache(authRef) {
|
|
1384
|
+
const suffix = `${OAUTH_STATE_KEY_SEPARATOR}${authRef}`;
|
|
1385
|
+
for (const key of oauthCredentialCache.keys()) {
|
|
1386
|
+
if (key.endsWith(suffix)) oauthCredentialCache.delete(key);
|
|
1300
1387
|
}
|
|
1301
|
-
return trimmed;
|
|
1302
1388
|
}
|
|
1303
|
-
|
|
1389
|
+
function cacheOAuthCredential(stateKey, credential) {
|
|
1390
|
+
oauthCredentialCache.set(stateKey, {
|
|
1391
|
+
access: credential.access,
|
|
1392
|
+
expires: credential.expires,
|
|
1393
|
+
...credential.accessRejected === true ? { accessRejected: true } : {},
|
|
1394
|
+
checkedAt: Date.now()
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
function cachedOAuthCredentialIsUsable(credential, providerId, rejectedAccessToken) {
|
|
1398
|
+
if (!credential) return false;
|
|
1399
|
+
const age = Date.now() - credential.checkedAt;
|
|
1400
|
+
return age >= 0 && age < OAUTH_CREDENTIAL_CACHE_MAX_AGE_MS && credential.access !== rejectedAccessToken && credential.accessRejected !== true && !oauthCredentialShouldRefresh(credential, providerId);
|
|
1401
|
+
}
|
|
1402
|
+
async function readOAuthProviderSecret(ref, providerId, diag, rejectedAccessToken) {
|
|
1304
1403
|
const authRef = storedCredentialAuthRef(ref);
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1404
|
+
const stateKey = oauthCredentialStateKey(providerId, authRef);
|
|
1405
|
+
const existing = oauthRefreshInflight.get(stateKey);
|
|
1406
|
+
if (existing) {
|
|
1407
|
+
const resolved = await existing;
|
|
1408
|
+
if (resolved !== rejectedAccessToken) return resolved;
|
|
1409
|
+
return readOAuthProviderSecret(ref, providerId, diag, rejectedAccessToken);
|
|
1410
|
+
}
|
|
1411
|
+
const cached = oauthCredentialCache.get(stateKey);
|
|
1412
|
+
if (cached && cachedOAuthCredentialIsUsable(cached, providerId, rejectedAccessToken)) {
|
|
1413
|
+
return cached.access;
|
|
1414
|
+
}
|
|
1415
|
+
if (cached?.access === rejectedAccessToken) oauthCredentialCache.delete(stateKey);
|
|
1307
1416
|
const work = withCredentialMutationLock(authRef, async () => {
|
|
1308
|
-
const
|
|
1309
|
-
if (
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1417
|
+
const latestCached = oauthCredentialCache.get(stateKey);
|
|
1418
|
+
if (latestCached && cachedOAuthCredentialIsUsable(latestCached, providerId, rejectedAccessToken)) {
|
|
1419
|
+
return latestCached.access;
|
|
1420
|
+
}
|
|
1421
|
+
for (let generation = 0; generation < 3; generation += 1) {
|
|
1422
|
+
const raw = await readStoredCredential(ref, diag);
|
|
1423
|
+
if (!raw) return null;
|
|
1424
|
+
const cred = parseStoredOAuthCredential(raw);
|
|
1425
|
+
if (!cred) {
|
|
1426
|
+
const decoded = decodeProviderSecret(raw);
|
|
1427
|
+
return decoded === rejectedAccessToken ? null : decoded;
|
|
1428
|
+
}
|
|
1429
|
+
cacheOAuthCredential(stateKey, cred);
|
|
1430
|
+
const forceRefresh = cred.access === rejectedAccessToken || cred.accessRejected === true;
|
|
1431
|
+
if (!forceRefresh && !oauthCredentialShouldRefresh(cred, providerId)) {
|
|
1432
|
+
return cred.access;
|
|
1433
|
+
}
|
|
1434
|
+
let refreshed;
|
|
1435
|
+
try {
|
|
1436
|
+
refreshed = await refreshStoredOAuthCredential(providerId, cred);
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
diag?.(err instanceof Error ? err.message : String(err));
|
|
1439
|
+
if (!forceRefresh && cred.access && cred.expires > Date.now()) return cred.access;
|
|
1440
|
+
oauthCredentialCache.delete(stateKey);
|
|
1441
|
+
throw err;
|
|
1442
|
+
}
|
|
1443
|
+
const accessStillRejected = rejectedAccessToken !== void 0 && refreshed.access === rejectedAccessToken || cred.accessRejected === true && refreshed.access === cred.access;
|
|
1444
|
+
const currentRaw = await readStoredCredential(ref, diag);
|
|
1445
|
+
if (currentRaw !== raw) {
|
|
1446
|
+
oauthCredentialCache.delete(stateKey);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const credentialToSave = accessStillRejected ? { ...refreshed, accessRejected: true } : refreshed;
|
|
1450
|
+
const json = oauthCredentialToKeychainJson(credentialToSave);
|
|
1317
1451
|
const saved = await saveProviderCredential(authRef, json, diag);
|
|
1318
|
-
if (!saved)
|
|
1452
|
+
if (!saved) {
|
|
1453
|
+
oauthCredentialCache.delete(stateKey);
|
|
1454
|
+
throw new Error("Could not persist refreshed OAuth credential");
|
|
1455
|
+
}
|
|
1456
|
+
if (accessStillRejected) {
|
|
1457
|
+
oauthCredentialCache.delete(stateKey);
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1319
1460
|
return refreshed.access;
|
|
1320
|
-
} catch (err) {
|
|
1321
|
-
diag?.(err instanceof Error ? err.message : String(err));
|
|
1322
|
-
if (cred.access && cred.expires > Date.now()) return cred.access;
|
|
1323
|
-
throw err;
|
|
1324
1461
|
}
|
|
1462
|
+
throw new Error("OAuth credential changed repeatedly while refresh was in progress");
|
|
1463
|
+
}, {
|
|
1464
|
+
waitMs: OAUTH_REFRESH_LOCK_WAIT_MS
|
|
1325
1465
|
});
|
|
1326
|
-
oauthRefreshInflight.set(
|
|
1466
|
+
oauthRefreshInflight.set(stateKey, work);
|
|
1327
1467
|
try {
|
|
1328
1468
|
return await work;
|
|
1329
1469
|
} finally {
|
|
1330
|
-
oauthRefreshInflight.
|
|
1470
|
+
if (oauthRefreshInflight.get(stateKey) === work) {
|
|
1471
|
+
oauthRefreshInflight.delete(stateKey);
|
|
1472
|
+
}
|
|
1331
1473
|
}
|
|
1332
1474
|
}
|
|
1333
|
-
async function readProviderSecret(ref, diag) {
|
|
1334
|
-
const raw = await readStoredCredential(ref, diag);
|
|
1335
|
-
if (!raw) return null;
|
|
1475
|
+
async function readProviderSecret(ref, diag, rejectedAccessToken) {
|
|
1336
1476
|
const oauthProviderId = oauthProviderIdFromAccount(ref.account);
|
|
1337
|
-
if (oauthProviderId
|
|
1338
|
-
return
|
|
1477
|
+
if (oauthProviderId) {
|
|
1478
|
+
return readOAuthProviderSecret(ref, oauthProviderId, diag, rejectedAccessToken);
|
|
1339
1479
|
}
|
|
1340
|
-
|
|
1480
|
+
const raw = await readStoredCredential(ref, diag);
|
|
1481
|
+
const decoded = decodeProviderSecret(raw, true);
|
|
1482
|
+
return decoded === rejectedAccessToken ? null : decoded;
|
|
1341
1483
|
}
|
|
1342
1484
|
async function saveProviderCredential(authRef, key, diag) {
|
|
1343
1485
|
const parsed = parseAuthRef(authRef);
|
|
1344
1486
|
if (!parsed || parsed.kind === "env" || parsed.kind === "none") return false;
|
|
1345
1487
|
return withCredentialMutationLock(authRef, async () => {
|
|
1488
|
+
const cacheKey = storedCredentialAuthRef(parsed);
|
|
1489
|
+
clearOAuthCredentialCache(cacheKey);
|
|
1346
1490
|
const written = await writeStoredCredential(parsed, key, diag);
|
|
1347
1491
|
if (!written) return false;
|
|
1348
1492
|
const readBack = await readStoredCredential(parsed, diag);
|
|
1349
|
-
if (readBack === key)
|
|
1493
|
+
if (readBack === key) {
|
|
1494
|
+
const oauth = parseStoredOAuthCredential(key);
|
|
1495
|
+
const oauthProviderId = oauthProviderIdFromAccount(parsed.account);
|
|
1496
|
+
if (oauth && oauthProviderId) {
|
|
1497
|
+
cacheOAuthCredential(oauthCredentialStateKey(oauthProviderId, cacheKey), oauth);
|
|
1498
|
+
}
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1350
1501
|
diag?.("credential store read-back verification failed");
|
|
1351
1502
|
return false;
|
|
1352
1503
|
});
|
|
@@ -1379,7 +1530,10 @@ async function probeProviderCredentialStore(authRef, diag) {
|
|
|
1379
1530
|
async function deleteProviderCredential(authRef, diag) {
|
|
1380
1531
|
const parsed = parseAuthRef(authRef);
|
|
1381
1532
|
if (!parsed || parsed.kind === "env" || parsed.kind === "none") return false;
|
|
1382
|
-
return withCredentialMutationLock(authRef, () =>
|
|
1533
|
+
return withCredentialMutationLock(authRef, () => {
|
|
1534
|
+
clearOAuthCredentialCache(storedCredentialAuthRef(parsed));
|
|
1535
|
+
return deleteStoredCredential(parsed, diag);
|
|
1536
|
+
});
|
|
1383
1537
|
}
|
|
1384
1538
|
|
|
1385
1539
|
// src/first-run.ts
|
|
@@ -1393,6 +1547,7 @@ import {
|
|
|
1393
1547
|
closeSync as closeSync2,
|
|
1394
1548
|
copyFileSync,
|
|
1395
1549
|
existsSync,
|
|
1550
|
+
fsyncSync as fsyncSync2,
|
|
1396
1551
|
mkdirSync as mkdirSync2,
|
|
1397
1552
|
openSync as openSync2,
|
|
1398
1553
|
readFileSync as readFileSync3,
|
|
@@ -1442,9 +1597,18 @@ function ensureSecureAppHome() {
|
|
|
1442
1597
|
function writeSecureFile(path, content) {
|
|
1443
1598
|
ensureSecureAppHome();
|
|
1444
1599
|
mkdirSync2(dirname2(path), { recursive: true, mode: DIR_MODE });
|
|
1445
|
-
const fd = openSync2(path, "
|
|
1600
|
+
const fd = openSync2(path, "wx", FILE_MODE);
|
|
1446
1601
|
try {
|
|
1447
|
-
|
|
1602
|
+
const payload = Buffer.from(content);
|
|
1603
|
+
let offset = 0;
|
|
1604
|
+
while (offset < payload.length) {
|
|
1605
|
+
const written = writeSync(fd, payload, offset, payload.length - offset);
|
|
1606
|
+
if (written <= 0) {
|
|
1607
|
+
throw new Error(`Could not complete secure file write: ${path}`);
|
|
1608
|
+
}
|
|
1609
|
+
offset += written;
|
|
1610
|
+
}
|
|
1611
|
+
fsyncSync2(fd);
|
|
1448
1612
|
} finally {
|
|
1449
1613
|
closeSync2(fd);
|
|
1450
1614
|
}
|
|
@@ -1453,6 +1617,18 @@ function writeSecureFile(path, content) {
|
|
|
1453
1617
|
} catch {
|
|
1454
1618
|
}
|
|
1455
1619
|
}
|
|
1620
|
+
function syncParentDirectory(path) {
|
|
1621
|
+
let fd;
|
|
1622
|
+
try {
|
|
1623
|
+
fd = openSync2(dirname2(path), "r");
|
|
1624
|
+
fsyncSync2(fd);
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
const code = error.code;
|
|
1627
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EPERM") throw error;
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (fd !== void 0) closeSync2(fd);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1456
1632
|
function parseProvider(raw) {
|
|
1457
1633
|
if (!raw || typeof raw !== "object") return null;
|
|
1458
1634
|
const p13 = raw;
|
|
@@ -1491,6 +1667,34 @@ function parseProvider(raw) {
|
|
|
1491
1667
|
}
|
|
1492
1668
|
return provider;
|
|
1493
1669
|
}
|
|
1670
|
+
function hasOwn(record, key) {
|
|
1671
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
1672
|
+
}
|
|
1673
|
+
function hasValidStrictProviderFields(raw) {
|
|
1674
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
1675
|
+
const provider = raw;
|
|
1676
|
+
if (hasOwn(provider, "subscriptionFilter") && provider.subscriptionFilter !== "free") {
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
if (hasOwn(provider, "authType") && provider.authType !== "api" && provider.authType !== "oauth" && provider.authType !== "none") {
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1682
|
+
if (hasOwn(provider, "refreshedAt") && typeof provider.refreshedAt !== "string") {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
if (hasOwn(provider, "modelsCache")) {
|
|
1686
|
+
const cache = provider.modelsCache;
|
|
1687
|
+
if (!cache || typeof cache !== "object" || Array.isArray(cache)) return false;
|
|
1688
|
+
const fields = cache;
|
|
1689
|
+
if (typeof fields.fetchedAt !== "string" || !Array.isArray(fields.models)) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
if (fields.models.some((model) => !model || typeof model !== "object" || Array.isArray(model))) {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1494
1698
|
function parseRegistry(raw) {
|
|
1495
1699
|
const empty = { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
1496
1700
|
if (!raw || typeof raw !== "object") return empty;
|
|
@@ -1510,6 +1714,27 @@ function parseRegistry(raw) {
|
|
|
1510
1714
|
if (typeof data.pricingCacheAt === "string") registry.pricingCacheAt = data.pricingCacheAt;
|
|
1511
1715
|
return registry;
|
|
1512
1716
|
}
|
|
1717
|
+
function parseRegistryStrict(raw) {
|
|
1718
|
+
if (!raw || typeof raw !== "object") {
|
|
1719
|
+
throw new Error("Provider registry must be a JSON object.");
|
|
1720
|
+
}
|
|
1721
|
+
const data = raw;
|
|
1722
|
+
if (data.schemaVersion !== REGISTRY_SCHEMA_VERSION) {
|
|
1723
|
+
throw new Error("Provider registry has an unsupported schema version.");
|
|
1724
|
+
}
|
|
1725
|
+
if (!Array.isArray(data.providers)) {
|
|
1726
|
+
throw new Error("Provider registry is missing its providers list.");
|
|
1727
|
+
}
|
|
1728
|
+
for (const entry of data.providers) {
|
|
1729
|
+
if (!parseProvider(entry) || !hasValidStrictProviderFields(entry)) {
|
|
1730
|
+
throw new Error("Provider registry contains an invalid provider entry.");
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return parseRegistry(raw);
|
|
1734
|
+
}
|
|
1735
|
+
function readRegistryStrict(path) {
|
|
1736
|
+
return parseRegistryStrict(JSON.parse(readFileSync3(path, "utf8")));
|
|
1737
|
+
}
|
|
1513
1738
|
function loadRegistry(path = getProvidersPath()) {
|
|
1514
1739
|
ensureLegacyAppHomeMigrated();
|
|
1515
1740
|
if (!existsSync(path)) {
|
|
@@ -1523,7 +1748,7 @@ function loadRegistry(path = getProvidersPath()) {
|
|
|
1523
1748
|
try {
|
|
1524
1749
|
withRegistryWriteLockSync(() => {
|
|
1525
1750
|
if (!existsSync(path)) return;
|
|
1526
|
-
const current =
|
|
1751
|
+
const current = readRegistryStrict(path);
|
|
1527
1752
|
if (migrateOAuthOpenAiProvider(current)) saveRegistry(current, path);
|
|
1528
1753
|
}, { lockPath: `${path}.lock` });
|
|
1529
1754
|
} catch {
|
|
@@ -1534,6 +1759,15 @@ function loadRegistry(path = getProvidersPath()) {
|
|
|
1534
1759
|
return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
1535
1760
|
}
|
|
1536
1761
|
}
|
|
1762
|
+
function loadRegistryStrict(path = getProvidersPath()) {
|
|
1763
|
+
ensureLegacyAppHomeMigrated();
|
|
1764
|
+
if (!existsSync(path)) {
|
|
1765
|
+
return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
|
|
1766
|
+
}
|
|
1767
|
+
const registry = readRegistryStrict(path);
|
|
1768
|
+
migrateOAuthOpenAiProvider(registry);
|
|
1769
|
+
return registry;
|
|
1770
|
+
}
|
|
1537
1771
|
function saveRegistry(registry, path = getProvidersPath()) {
|
|
1538
1772
|
assertRegistryWriteOwnership(path);
|
|
1539
1773
|
const payload = `${JSON.stringify(registry, null, 2)}
|
|
@@ -1550,6 +1784,7 @@ function saveRegistry(registry, path = getProvidersPath()) {
|
|
|
1550
1784
|
writeSecureFile(tmp, payload);
|
|
1551
1785
|
assertRegistryWriteOwnership(path);
|
|
1552
1786
|
renameSync(tmp, path);
|
|
1787
|
+
syncParentDirectory(path);
|
|
1553
1788
|
} finally {
|
|
1554
1789
|
try {
|
|
1555
1790
|
unlinkSync2(tmp);
|
|
@@ -2308,7 +2543,7 @@ function enrichPricingAsync(onComplete) {
|
|
|
2308
2543
|
const fetched = await fetchPricingCache();
|
|
2309
2544
|
const cache = fetched ?? loadPricingCache();
|
|
2310
2545
|
const changed = await withRegistryWriteLock(() => {
|
|
2311
|
-
const registry =
|
|
2546
|
+
const registry = loadRegistryStrict();
|
|
2312
2547
|
const updated = applyPricingToRegistryProviders(registry, cache);
|
|
2313
2548
|
if (updated) saveRegistry(registry);
|
|
2314
2549
|
return updated;
|
|
@@ -2795,6 +3030,7 @@ function localProvidersToServerModels(localProviders) {
|
|
|
2795
3030
|
npm: model.modelFormat === "openai" ? model.npm || "@ai-sdk/openai-compatible" : model.npm,
|
|
2796
3031
|
apiBaseUrl: model.apiBaseUrl,
|
|
2797
3032
|
apiKey: provider.apiKey,
|
|
3033
|
+
authRef: provider.authRef,
|
|
2798
3034
|
authType: provider.authType,
|
|
2799
3035
|
oauthAccountId: provider.oauthAccountId,
|
|
2800
3036
|
contextWindow: model.contextWindow,
|
|
@@ -2809,11 +3045,14 @@ function localProvidersToServerModels(localProviders) {
|
|
|
2809
3045
|
);
|
|
2810
3046
|
}
|
|
2811
3047
|
|
|
3048
|
+
// src/registry/add-template.ts
|
|
3049
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3050
|
+
|
|
2812
3051
|
// src/provider-factory.ts
|
|
2813
3052
|
import { wrapLanguageModel, extractReasoningMiddleware } from "ai";
|
|
2814
3053
|
|
|
2815
3054
|
// src/oauth/responses-websocket.ts
|
|
2816
|
-
import { createHash as
|
|
3055
|
+
import { createHash as createHash4 } from "crypto";
|
|
2817
3056
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
2818
3057
|
|
|
2819
3058
|
// src/outbound-proxy.ts
|
|
@@ -2877,6 +3116,27 @@ async function outboundWsProxyAgent(wsUrl) {
|
|
|
2877
3116
|
|
|
2878
3117
|
// src/upstream-error.ts
|
|
2879
3118
|
import { APICallError, RetryError } from "ai";
|
|
3119
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 5;
|
|
3120
|
+
var MAX_RETRY_AFTER_SECONDS = 60;
|
|
3121
|
+
function clampRetryAfterSeconds(value) {
|
|
3122
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
3123
|
+
return DEFAULT_RETRY_AFTER_SECONDS;
|
|
3124
|
+
}
|
|
3125
|
+
return Math.min(Math.round(value), MAX_RETRY_AFTER_SECONDS);
|
|
3126
|
+
}
|
|
3127
|
+
function numericRetryAfterSeconds(inner) {
|
|
3128
|
+
const data = inner.data;
|
|
3129
|
+
const fromBody = data?.error?.retry_after_seconds;
|
|
3130
|
+
if (typeof fromBody === "number" && Number.isFinite(fromBody) && fromBody >= 0) return fromBody;
|
|
3131
|
+
const fromHeader = inner.responseHeaders?.["retry-after"];
|
|
3132
|
+
if (typeof fromHeader === "string" && /^\d+$/.test(fromHeader.trim())) return Number(fromHeader.trim());
|
|
3133
|
+
for (const message of [data?.error?.message, inner.message]) {
|
|
3134
|
+
if (typeof message !== "string") continue;
|
|
3135
|
+
const match = /retry after (\d+)s\b/i.exec(message);
|
|
3136
|
+
if (match) return Number(match[1]);
|
|
3137
|
+
}
|
|
3138
|
+
return void 0;
|
|
3139
|
+
}
|
|
2880
3140
|
function sdkUpstreamErrorDetails(err) {
|
|
2881
3141
|
const retry = RetryError.isInstance(err) ? err : void 0;
|
|
2882
3142
|
const inner = retry?.lastError ?? err;
|
|
@@ -2888,11 +3148,14 @@ function sdkUpstreamErrorDetails(err) {
|
|
|
2888
3148
|
} catch {
|
|
2889
3149
|
}
|
|
2890
3150
|
}
|
|
3151
|
+
const rawRetryAfter = inner.statusCode === 429 ? numericRetryAfterSeconds(inner) : void 0;
|
|
3152
|
+
const retryAfterSeconds = rawRetryAfter === void 0 ? void 0 : clampRetryAfterSeconds(rawRetryAfter);
|
|
2891
3153
|
return {
|
|
2892
3154
|
statusCode: inner.statusCode,
|
|
2893
3155
|
errorContent: errorContent || inner.message,
|
|
2894
3156
|
isRetryable: inner.isRetryable,
|
|
2895
|
-
attemptCount: retry?.errors.length ?? 1
|
|
3157
|
+
attemptCount: retry?.errors.length ?? 1,
|
|
3158
|
+
...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
|
|
2896
3159
|
};
|
|
2897
3160
|
}
|
|
2898
3161
|
function isContextLengthExceededError(err, formattedMessage = "") {
|
|
@@ -3057,7 +3320,7 @@ function hasResponsesLiteHeader(headers) {
|
|
|
3057
3320
|
}
|
|
3058
3321
|
function authorizationHeaderFingerprint(headers) {
|
|
3059
3322
|
const authorization = Object.entries(headers).find(([key]) => key.toLowerCase() === "authorization")?.[1];
|
|
3060
|
-
return authorization ?
|
|
3323
|
+
return authorization ? createHash4("sha256").update(authorization).digest("hex") : "";
|
|
3061
3324
|
}
|
|
3062
3325
|
function bodyToString(body) {
|
|
3063
3326
|
if (body == null) return "";
|
|
@@ -3090,13 +3353,13 @@ function responsesWebSocketPromptFingerprint(payload) {
|
|
|
3090
3353
|
delete stable.previous_response_id;
|
|
3091
3354
|
delete stable.stream;
|
|
3092
3355
|
delete stable.background;
|
|
3093
|
-
return
|
|
3356
|
+
return createHash4("sha256").update(canonicalJson(stable)).digest("hex");
|
|
3094
3357
|
}
|
|
3095
3358
|
function responsesWebSocketPromptFieldHashes(payload) {
|
|
3096
3359
|
const hashes = {};
|
|
3097
3360
|
for (const key of Object.keys(payload).sort()) {
|
|
3098
3361
|
if (key === "input" || key === "previous_response_id" || key === "stream" || key === "background") continue;
|
|
3099
|
-
hashes[key] =
|
|
3362
|
+
hashes[key] = createHash4("sha256").update(canonicalJson(payload[key])).digest("hex").slice(0, 12);
|
|
3100
3363
|
}
|
|
3101
3364
|
return hashes;
|
|
3102
3365
|
}
|
|
@@ -3132,7 +3395,7 @@ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizat
|
|
|
3132
3395
|
promptCacheKey,
|
|
3133
3396
|
authorizationFingerprint
|
|
3134
3397
|
].join("");
|
|
3135
|
-
return
|
|
3398
|
+
return createHash4("sha256").update(material).digest("hex");
|
|
3136
3399
|
}
|
|
3137
3400
|
function inputArray(payload) {
|
|
3138
3401
|
return Array.isArray(payload.input) ? payload.input : [];
|
|
@@ -3163,7 +3426,7 @@ function conversationItemKind(value) {
|
|
|
3163
3426
|
return "object";
|
|
3164
3427
|
}
|
|
3165
3428
|
function conversationItemHash(value) {
|
|
3166
|
-
return
|
|
3429
|
+
return createHash4("sha256").update(canonicalJson(normalizeToolCallJson(value))).digest("hex").slice(0, 16);
|
|
3167
3430
|
}
|
|
3168
3431
|
function continuationMismatchDetails(entry, payload) {
|
|
3169
3432
|
const full = inputArray(payload);
|
|
@@ -3229,7 +3492,7 @@ function diagnosticTextFingerprint(field, value) {
|
|
|
3229
3492
|
if (typeof value !== "string" || value.length === 0) return {};
|
|
3230
3493
|
return {
|
|
3231
3494
|
[`${field}Bytes`]: Buffer.byteLength(value),
|
|
3232
|
-
[`${field}Hash`]:
|
|
3495
|
+
[`${field}Hash`]: createHash4("sha256").update(value).digest("hex").slice(0, 16)
|
|
3233
3496
|
};
|
|
3234
3497
|
}
|
|
3235
3498
|
function responseFailureDetails(event) {
|
|
@@ -3264,7 +3527,7 @@ function emitResponseErrorDiagnostic(entry, ctx, details) {
|
|
|
3264
3527
|
emitContextDiagnostic(entry, ctx, { event: "ws_response_error", ...details });
|
|
3265
3528
|
}
|
|
3266
3529
|
function diagnosticItemIdHash(value) {
|
|
3267
|
-
return typeof value === "string" && value.length > 0 ?
|
|
3530
|
+
return typeof value === "string" && value.length > 0 ? createHash4("sha256").update(value).digest("hex").slice(0, 16) : void 0;
|
|
3268
3531
|
}
|
|
3269
3532
|
function reasoningPartIndex(value) {
|
|
3270
3533
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
@@ -3499,7 +3762,7 @@ function deleteEntry(entry, closeSocket = true) {
|
|
|
3499
3762
|
}
|
|
3500
3763
|
}
|
|
3501
3764
|
}
|
|
3502
|
-
function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
|
|
3765
|
+
function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfterSeconds) {
|
|
3503
3766
|
if (ctx.closed || entry.current !== ctx) return;
|
|
3504
3767
|
entry.debug(`fail: ${message}`);
|
|
3505
3768
|
emitResponseErrorDiagnostic(entry, ctx, {
|
|
@@ -3514,12 +3777,64 @@ function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
|
|
|
3514
3777
|
type: statusCode === void 0 ? "transport_error" : anthropicErrorType(statusCode),
|
|
3515
3778
|
code: statusCode === void 0 ? "websocket_transport_error" : String(statusCode),
|
|
3516
3779
|
message,
|
|
3517
|
-
param: null
|
|
3780
|
+
param: null,
|
|
3781
|
+
...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
|
|
3518
3782
|
}
|
|
3519
3783
|
});
|
|
3520
3784
|
deleteEntry(entry);
|
|
3521
3785
|
closeContext(ctx);
|
|
3522
3786
|
}
|
|
3787
|
+
function retryTransportFailure(entry, ctx, diagnosticDetails) {
|
|
3788
|
+
if (ctx.closed || entry.current !== ctx || ctx.retried || ctx.frameCount !== 0 || ctx.emittedModelData) {
|
|
3789
|
+
return false;
|
|
3790
|
+
}
|
|
3791
|
+
ctx.retried = true;
|
|
3792
|
+
ctx.transportRetryPending = true;
|
|
3793
|
+
entry.debug("transport failed before any response frame; retrying once with full context");
|
|
3794
|
+
emitContextDiagnostic(entry, ctx, {
|
|
3795
|
+
event: "ws_transport_retry",
|
|
3796
|
+
outcome: "started",
|
|
3797
|
+
...diagnosticDetails
|
|
3798
|
+
});
|
|
3799
|
+
deleteEntry(entry);
|
|
3800
|
+
if (ctx.closed) {
|
|
3801
|
+
ctx.transportRetryPending = false;
|
|
3802
|
+
entry.debug("transport retry cancelled before replacement");
|
|
3803
|
+
emitContextDiagnostic(entry, ctx, {
|
|
3804
|
+
event: "ws_transport_retry",
|
|
3805
|
+
outcome: "cancelled"
|
|
3806
|
+
});
|
|
3807
|
+
return true;
|
|
3808
|
+
}
|
|
3809
|
+
resetContextForRetry(ctx);
|
|
3810
|
+
const replacement = ctx.createReplacement();
|
|
3811
|
+
if (ctx.closed) {
|
|
3812
|
+
ctx.transportRetryPending = false;
|
|
3813
|
+
deleteEntry(replacement);
|
|
3814
|
+
replacement.debug("transport retry cancelled while creating replacement");
|
|
3815
|
+
emitContextDiagnostic(replacement, ctx, {
|
|
3816
|
+
event: "ws_transport_retry",
|
|
3817
|
+
outcome: "cancelled"
|
|
3818
|
+
});
|
|
3819
|
+
return true;
|
|
3820
|
+
}
|
|
3821
|
+
dispatchContext(replacement, ctx);
|
|
3822
|
+
return true;
|
|
3823
|
+
}
|
|
3824
|
+
function handleTransportFailure(entry, ctx, message, diagnosticDetails) {
|
|
3825
|
+
if (retryTransportFailure(entry, ctx, diagnosticDetails)) return;
|
|
3826
|
+
if (ctx.closed || entry.current !== ctx) return;
|
|
3827
|
+
if (ctx.retried && ctx.frameCount === 0 && !ctx.emittedModelData) {
|
|
3828
|
+
ctx.transportRetryPending = false;
|
|
3829
|
+
entry.debug("transport retry exhausted before any response frame");
|
|
3830
|
+
emitContextDiagnostic(entry, ctx, {
|
|
3831
|
+
event: "ws_transport_retry",
|
|
3832
|
+
outcome: "exhausted",
|
|
3833
|
+
...diagnosticDetails
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
failContext(entry, ctx, message, diagnosticDetails);
|
|
3837
|
+
}
|
|
3523
3838
|
function cleanupExpiredConnections(now) {
|
|
3524
3839
|
const evictions = [];
|
|
3525
3840
|
for (const entry of connectionEntries()) {
|
|
@@ -3567,7 +3882,27 @@ function sendContext(entry, ctx) {
|
|
|
3567
3882
|
entry.debug(
|
|
3568
3883
|
`connection=${entry.debugId} key=${debugKey(entry.key)} sending ${outgoing.length}B payload` + (ctx.continued ? " (continuation)" : "")
|
|
3569
3884
|
);
|
|
3570
|
-
|
|
3885
|
+
try {
|
|
3886
|
+
entry.socket.send(outgoing, (error) => {
|
|
3887
|
+
if (!error) return;
|
|
3888
|
+
handleTransportFailure(entry, ctx, error.message, {
|
|
3889
|
+
source: "socket_send",
|
|
3890
|
+
failureMode: "callback",
|
|
3891
|
+
socketErrorName: boundedDiagnosticIdentifier(error.name),
|
|
3892
|
+
socketErrorCode: boundedDiagnosticIdentifier(error.code),
|
|
3893
|
+
...diagnosticTextFingerprint("errorMessage", error.message)
|
|
3894
|
+
});
|
|
3895
|
+
});
|
|
3896
|
+
} catch (error) {
|
|
3897
|
+
const failure = error instanceof Error ? error : new Error("WebSocket send failed");
|
|
3898
|
+
handleTransportFailure(entry, ctx, failure.message, {
|
|
3899
|
+
source: "socket_send",
|
|
3900
|
+
failureMode: "synchronous",
|
|
3901
|
+
socketErrorName: boundedDiagnosticIdentifier(failure.name),
|
|
3902
|
+
socketErrorCode: boundedDiagnosticIdentifier(failure.code),
|
|
3903
|
+
...diagnosticTextFingerprint("errorMessage", failure.message)
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3571
3906
|
}
|
|
3572
3907
|
function dispatchContext(entry, ctx) {
|
|
3573
3908
|
const now = entry.options.now();
|
|
@@ -3600,6 +3935,14 @@ function handleSocketMessage(entry, data) {
|
|
|
3600
3935
|
if (!ctx || ctx.closed) return;
|
|
3601
3936
|
const text4 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
|
|
3602
3937
|
ctx.frameCount += 1;
|
|
3938
|
+
if (ctx.transportRetryPending) {
|
|
3939
|
+
ctx.transportRetryPending = false;
|
|
3940
|
+
entry.debug("transport retry received its first response frame");
|
|
3941
|
+
emitContextDiagnostic(entry, ctx, {
|
|
3942
|
+
event: "ws_transport_retry",
|
|
3943
|
+
outcome: "recovered"
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3603
3946
|
let event;
|
|
3604
3947
|
try {
|
|
3605
3948
|
event = JSON.parse(text4);
|
|
@@ -3674,6 +4017,10 @@ function handleSocketMessage(entry, data) {
|
|
|
3674
4017
|
closeContext(ctx);
|
|
3675
4018
|
}
|
|
3676
4019
|
}
|
|
4020
|
+
function numericRetryAfterHeader(value) {
|
|
4021
|
+
const single = Array.isArray(value) ? value[0] : value;
|
|
4022
|
+
return typeof single === "string" && /^\d+$/.test(single.trim()) ? Number(single.trim()) : void 0;
|
|
4023
|
+
}
|
|
3677
4024
|
function createConnection(WebSocket, wsUrl, headers, persistent, key, options, debug, agent) {
|
|
3678
4025
|
const now = options.now();
|
|
3679
4026
|
const socket = new WebSocket(wsUrl, agent ? { headers, agent } : { headers });
|
|
@@ -3707,24 +4054,39 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
|
|
|
3707
4054
|
debug(`unexpected-response status=${statusCode}`);
|
|
3708
4055
|
response.resume();
|
|
3709
4056
|
const ctx = entry.current;
|
|
3710
|
-
if (ctx
|
|
3711
|
-
failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
|
|
3712
|
-
source: "unexpected_response",
|
|
3713
|
-
httpStatusCode: statusCode
|
|
3714
|
-
}, statusCode);
|
|
3715
|
-
} else {
|
|
4057
|
+
if (!ctx || ctx.closed) {
|
|
3716
4058
|
deleteEntry(entry);
|
|
4059
|
+
return;
|
|
3717
4060
|
}
|
|
4061
|
+
if (statusCode === 403) {
|
|
4062
|
+
const retryAfterSeconds = clampRetryAfterSeconds(
|
|
4063
|
+
numericRetryAfterHeader(response.headers["retry-after"])
|
|
4064
|
+
);
|
|
4065
|
+
failContext(entry, ctx, `OpenAI edge throttled the Responses WebSocket upgrade (HTTP 403); retry after ${retryAfterSeconds}s`, {
|
|
4066
|
+
source: "unexpected_response",
|
|
4067
|
+
httpStatusCode: statusCode,
|
|
4068
|
+
mappedStatusCode: 429,
|
|
4069
|
+
retryAfterSeconds
|
|
4070
|
+
}, 429, retryAfterSeconds);
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
|
|
4074
|
+
source: "unexpected_response",
|
|
4075
|
+
httpStatusCode: statusCode
|
|
4076
|
+
}, statusCode);
|
|
3718
4077
|
});
|
|
3719
4078
|
socket.on("message", (data) => handleSocketMessage(entry, data));
|
|
3720
4079
|
socket.on("error", (error) => {
|
|
3721
4080
|
const ctx = entry.current;
|
|
3722
|
-
if (ctx)
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
4081
|
+
if (ctx) {
|
|
4082
|
+
const details = {
|
|
4083
|
+
source: "socket_error",
|
|
4084
|
+
socketErrorName: boundedDiagnosticIdentifier(error.name),
|
|
4085
|
+
socketErrorCode: boundedDiagnosticIdentifier(error.code),
|
|
4086
|
+
...diagnosticTextFingerprint("errorMessage", error.message)
|
|
4087
|
+
};
|
|
4088
|
+
handleTransportFailure(entry, ctx, error.message, details);
|
|
4089
|
+
} else deleteEntry(entry);
|
|
3728
4090
|
});
|
|
3729
4091
|
socket.on("close", (code, reason) => {
|
|
3730
4092
|
entry.open = false;
|
|
@@ -3733,7 +4095,7 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
|
|
|
3733
4095
|
if (ctx && !ctx.closed) {
|
|
3734
4096
|
const reasonText = reason?.length ? reason.toString("utf8") : "";
|
|
3735
4097
|
const suffix = reasonText ? `: ${reasonText}` : "";
|
|
3736
|
-
|
|
4098
|
+
handleTransportFailure(entry, ctx, `WebSocket closed (${code})${suffix}`, {
|
|
3737
4099
|
source: "socket_close",
|
|
3738
4100
|
closeCode: code,
|
|
3739
4101
|
...diagnosticTextFingerprint("closeReason", reasonText)
|
|
@@ -3851,7 +4213,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3851
4213
|
keyTuple: {
|
|
3852
4214
|
wsUrl,
|
|
3853
4215
|
providerId: options.providerId ?? "openai",
|
|
3854
|
-
accountIdHash: options.accountId ?
|
|
4216
|
+
accountIdHash: options.accountId ? createHash4("sha256").update(options.accountId).digest("hex").slice(0, 16) : "",
|
|
3855
4217
|
model: typeof payload.model === "string" ? payload.model : void 0,
|
|
3856
4218
|
effort: typeof payload.reasoning?.effort === "string" ? String(payload.reasoning.effort).trim().toLowerCase() : "",
|
|
3857
4219
|
promptCacheKey: typeof payload.prompt_cache_key === "string" ? payload.prompt_cache_key : void 0
|
|
@@ -3908,6 +4270,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3908
4270
|
frameCount: 0,
|
|
3909
4271
|
pendingEvents: [],
|
|
3910
4272
|
emittedModelData: false,
|
|
4273
|
+
transportRetryPending: false,
|
|
3911
4274
|
outputByIndex: /* @__PURE__ */ new Map(),
|
|
3912
4275
|
outputIndexByItemId: /* @__PURE__ */ new Map(),
|
|
3913
4276
|
reasoningPartsByItemId: /* @__PURE__ */ new Map(),
|
|
@@ -3918,7 +4281,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3918
4281
|
WebSocket,
|
|
3919
4282
|
wsUrl,
|
|
3920
4283
|
headers,
|
|
3921
|
-
|
|
4284
|
+
persistent,
|
|
3922
4285
|
partitionKey,
|
|
3923
4286
|
resolvedOptions,
|
|
3924
4287
|
debug,
|
|
@@ -3966,7 +4329,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3966
4329
|
}
|
|
3967
4330
|
|
|
3968
4331
|
// src/oauth/claude-identity.ts
|
|
3969
|
-
import { createHash as
|
|
4332
|
+
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
3970
4333
|
var CLAUDE_CODE_CLI_VERSION = "2.1.195";
|
|
3971
4334
|
var CLAUDE_CODE_USER_AGENT = `claude-cli/${CLAUDE_CODE_CLI_VERSION} (external, cli)`;
|
|
3972
4335
|
var CLAUDE_CODE_ENTRYPOINT = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
|
|
@@ -3981,7 +4344,7 @@ function getOrCreateSessionId(seed) {
|
|
|
3981
4344
|
return id;
|
|
3982
4345
|
}
|
|
3983
4346
|
function uuidFromHash(input) {
|
|
3984
|
-
const h =
|
|
4347
|
+
const h = createHash5("sha256").update(input).digest("hex");
|
|
3985
4348
|
return [
|
|
3986
4349
|
h.slice(0, 8),
|
|
3987
4350
|
h.slice(8, 12),
|
|
@@ -3995,7 +4358,7 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
3995
4358
|
function resolveCliUserID(providerData, seed) {
|
|
3996
4359
|
const v = providerData?.cliUserID;
|
|
3997
4360
|
if (typeof v === "string" && HEX64_RE.test(v)) return v;
|
|
3998
|
-
return
|
|
4361
|
+
return createHash5("sha256").update(`cliUserID:${seed}`).digest("hex");
|
|
3999
4362
|
}
|
|
4000
4363
|
function resolveAccountUUID(providerData, seed) {
|
|
4001
4364
|
const v = providerData?.accountUUID;
|
|
@@ -4717,20 +5080,331 @@ function thinkingProviderOptions(npm) {
|
|
|
4717
5080
|
return void 0;
|
|
4718
5081
|
}
|
|
4719
5082
|
|
|
4720
|
-
// src/
|
|
5083
|
+
// src/registry/credential-cleanup-journal.ts
|
|
5084
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4721
5085
|
import {
|
|
4722
|
-
|
|
5086
|
+
closeSync as closeSync3,
|
|
4723
5087
|
existsSync as existsSync4,
|
|
5088
|
+
fstatSync as fstatSync2,
|
|
5089
|
+
fsyncSync as fsyncSync3,
|
|
5090
|
+
lstatSync,
|
|
4724
5091
|
mkdirSync as mkdirSync5,
|
|
5092
|
+
openSync as openSync3,
|
|
4725
5093
|
readFileSync as readFileSync6,
|
|
5094
|
+
renameSync as renameSync2,
|
|
4726
5095
|
unlinkSync as unlinkSync3,
|
|
4727
5096
|
writeFileSync as writeFileSync4
|
|
4728
5097
|
} from "fs";
|
|
4729
|
-
import {
|
|
4730
|
-
|
|
4731
|
-
import pc2 from "picocolors";
|
|
5098
|
+
import { dirname as dirname5 } from "path";
|
|
5099
|
+
var JOURNAL_SCHEMA_VERSION = 1;
|
|
4732
5100
|
var DIR_MODE2 = 448;
|
|
4733
5101
|
var FILE_MODE4 = 384;
|
|
5102
|
+
var MAX_JOURNAL_BYTES = 1024 * 1024;
|
|
5103
|
+
var MAX_PENDING_CREDENTIAL_DELETES = 1024;
|
|
5104
|
+
var MAX_CREDENTIAL_REF_BYTES = 4096;
|
|
5105
|
+
var CREDENTIAL_INSTANCE_SEPARATOR = "::credential::";
|
|
5106
|
+
var CREDENTIAL_INSTANCE_PATTERN = /^v1:[0-9a-f]{32}$/;
|
|
5107
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
5108
|
+
function emptyJournal() {
|
|
5109
|
+
return {
|
|
5110
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
5111
|
+
pendingCredentialDeletes: []
|
|
5112
|
+
};
|
|
5113
|
+
}
|
|
5114
|
+
function isStoredCredentialRef(value) {
|
|
5115
|
+
const parsed = parseAuthRef(value);
|
|
5116
|
+
return (parsed?.kind === "keyring" || parsed?.kind === "helper") && isManagedCredentialAccount(parsed.account);
|
|
5117
|
+
}
|
|
5118
|
+
function credentialAccountBase(account) {
|
|
5119
|
+
const separatorIndex = account.lastIndexOf(CREDENTIAL_INSTANCE_SEPARATOR);
|
|
5120
|
+
if (separatorIndex === -1) return account;
|
|
5121
|
+
if (separatorIndex === 0 || account.indexOf(CREDENTIAL_INSTANCE_SEPARATOR) !== separatorIndex || !CREDENTIAL_INSTANCE_PATTERN.test(
|
|
5122
|
+
account.slice(separatorIndex + CREDENTIAL_INSTANCE_SEPARATOR.length)
|
|
5123
|
+
)) {
|
|
5124
|
+
return null;
|
|
5125
|
+
}
|
|
5126
|
+
return account.slice(0, separatorIndex);
|
|
5127
|
+
}
|
|
5128
|
+
function isManagedCredentialAccount(account) {
|
|
5129
|
+
const base = credentialAccountBase(account);
|
|
5130
|
+
if (!base) return false;
|
|
5131
|
+
const oauth = /^oauth:provider:(.+)$/.exec(base);
|
|
5132
|
+
if (oauth) return isValidProviderId(oauth[1]);
|
|
5133
|
+
const provider = /^provider:([^:]+)(?::(.+))?$/.exec(base);
|
|
5134
|
+
if (!provider || !isValidProviderId(provider[1])) return false;
|
|
5135
|
+
const suffix = provider[2];
|
|
5136
|
+
if (!suffix) return true;
|
|
5137
|
+
if (UUID_PATTERN.test(suffix)) return true;
|
|
5138
|
+
return suffix.startsWith("replacement:") && UUID_PATTERN.test(suffix.slice("replacement:".length));
|
|
5139
|
+
}
|
|
5140
|
+
function normalizePendingCredentialDeletes(raw) {
|
|
5141
|
+
if (raw.length > MAX_PENDING_CREDENTIAL_DELETES) {
|
|
5142
|
+
throw new Error("Credential cleanup journal contains too many pending entries.");
|
|
5143
|
+
}
|
|
5144
|
+
const pending = [];
|
|
5145
|
+
for (const [index, value] of raw.entries()) {
|
|
5146
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > MAX_CREDENTIAL_REF_BYTES || !isStoredCredentialRef(value)) {
|
|
5147
|
+
throw new Error(`Credential cleanup journal has an invalid entry at index ${index}.`);
|
|
5148
|
+
}
|
|
5149
|
+
if (!pending.includes(value)) pending.push(value);
|
|
5150
|
+
}
|
|
5151
|
+
return pending;
|
|
5152
|
+
}
|
|
5153
|
+
function parseJournal(raw) {
|
|
5154
|
+
if (!raw || typeof raw !== "object") {
|
|
5155
|
+
throw new Error("Credential cleanup journal must be a JSON object.");
|
|
5156
|
+
}
|
|
5157
|
+
const data = raw;
|
|
5158
|
+
if (data.schemaVersion !== JOURNAL_SCHEMA_VERSION) {
|
|
5159
|
+
throw new Error("Unsupported credential cleanup journal schema.");
|
|
5160
|
+
}
|
|
5161
|
+
if (!Array.isArray(data.pendingCredentialDeletes)) {
|
|
5162
|
+
throw new Error("Credential cleanup journal is missing its pending list.");
|
|
5163
|
+
}
|
|
5164
|
+
return {
|
|
5165
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
5166
|
+
pendingCredentialDeletes: normalizePendingCredentialDeletes(
|
|
5167
|
+
data.pendingCredentialDeletes
|
|
5168
|
+
)
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
function readJournalUnlocked(path) {
|
|
5172
|
+
if (!existsSync4(path)) return emptyJournal();
|
|
5173
|
+
let fd;
|
|
5174
|
+
try {
|
|
5175
|
+
const before = lstatSync(path);
|
|
5176
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
5177
|
+
throw new Error("Credential cleanup journal must be a regular file.");
|
|
5178
|
+
}
|
|
5179
|
+
fd = openSync3(path, "r");
|
|
5180
|
+
const opened = fstatSync2(fd);
|
|
5181
|
+
if (before.dev !== opened.dev || before.ino !== opened.ino) {
|
|
5182
|
+
throw new Error("Credential cleanup journal changed while opening.");
|
|
5183
|
+
}
|
|
5184
|
+
if (typeof process.getuid === "function") {
|
|
5185
|
+
if (opened.uid !== process.getuid()) {
|
|
5186
|
+
throw new Error("Credential cleanup journal is owned by another user.");
|
|
5187
|
+
}
|
|
5188
|
+
if ((opened.mode & 63) !== 0) {
|
|
5189
|
+
throw new Error("Credential cleanup journal permissions are too broad.");
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
if (opened.size > MAX_JOURNAL_BYTES) {
|
|
5193
|
+
throw new Error("Credential cleanup journal is too large.");
|
|
5194
|
+
}
|
|
5195
|
+
return parseJournal(JSON.parse(readFileSync6(fd, "utf8")));
|
|
5196
|
+
} catch (error) {
|
|
5197
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5198
|
+
throw new Error(`Could not read credential cleanup journal: ${message}`);
|
|
5199
|
+
} finally {
|
|
5200
|
+
if (fd !== void 0) closeSync3(fd);
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
function syncParentDirectory2(path) {
|
|
5204
|
+
let fd;
|
|
5205
|
+
try {
|
|
5206
|
+
fd = openSync3(dirname5(path), "r");
|
|
5207
|
+
fsyncSync3(fd);
|
|
5208
|
+
} catch (error) {
|
|
5209
|
+
const code = error.code;
|
|
5210
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EPERM") throw error;
|
|
5211
|
+
} finally {
|
|
5212
|
+
if (fd !== void 0) closeSync3(fd);
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
function writeJournalUnlocked(journal, path) {
|
|
5216
|
+
assertRegistryWriteOwnership(path);
|
|
5217
|
+
ensureSecureAppHome();
|
|
5218
|
+
mkdirSync5(dirname5(path), { recursive: true, mode: DIR_MODE2 });
|
|
5219
|
+
const tmp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
|
|
5220
|
+
let fd;
|
|
5221
|
+
try {
|
|
5222
|
+
fd = openSync3(tmp, "wx", FILE_MODE4);
|
|
5223
|
+
writeFileSync4(fd, `${JSON.stringify(journal, null, 2)}
|
|
5224
|
+
`);
|
|
5225
|
+
fsyncSync3(fd);
|
|
5226
|
+
closeSync3(fd);
|
|
5227
|
+
fd = void 0;
|
|
5228
|
+
assertRegistryWriteOwnership(path);
|
|
5229
|
+
renameSync2(tmp, path);
|
|
5230
|
+
syncParentDirectory2(path);
|
|
5231
|
+
} finally {
|
|
5232
|
+
if (fd !== void 0) closeSync3(fd);
|
|
5233
|
+
try {
|
|
5234
|
+
unlinkSync3(tmp);
|
|
5235
|
+
} catch (error) {
|
|
5236
|
+
if (error.code !== "ENOENT") throw error;
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
function journalLockPath(path) {
|
|
5241
|
+
return `${path}.lock`;
|
|
5242
|
+
}
|
|
5243
|
+
async function loadPendingCredentialDeletes(path = getCredentialCleanupPath()) {
|
|
5244
|
+
ensureLegacyAppHomeMigrated();
|
|
5245
|
+
return withRegistryWriteLock(
|
|
5246
|
+
() => [...readJournalUnlocked(path).pendingCredentialDeletes],
|
|
5247
|
+
{ lockPath: journalLockPath(path) }
|
|
5248
|
+
);
|
|
5249
|
+
}
|
|
5250
|
+
async function updatePendingCredentialDeletes(update, path = getCredentialCleanupPath()) {
|
|
5251
|
+
ensureLegacyAppHomeMigrated();
|
|
5252
|
+
return withRegistryWriteLock(() => {
|
|
5253
|
+
const journal = readJournalUnlocked(path);
|
|
5254
|
+
const before = [...journal.pendingCredentialDeletes];
|
|
5255
|
+
const after = normalizePendingCredentialDeletes(update(before));
|
|
5256
|
+
if (after.length !== before.length || after.some((value, index) => value !== before[index])) {
|
|
5257
|
+
writeJournalUnlocked(
|
|
5258
|
+
{
|
|
5259
|
+
schemaVersion: JOURNAL_SCHEMA_VERSION,
|
|
5260
|
+
pendingCredentialDeletes: after
|
|
5261
|
+
},
|
|
5262
|
+
path
|
|
5263
|
+
);
|
|
5264
|
+
}
|
|
5265
|
+
return { before, after };
|
|
5266
|
+
}, { lockPath: journalLockPath(path) });
|
|
5267
|
+
}
|
|
5268
|
+
async function queueCredentialDelete(authRef) {
|
|
5269
|
+
if (!isStoredCredentialRef(authRef)) return false;
|
|
5270
|
+
const result = await updatePendingCredentialDeletes((pending) => pending.includes(authRef) ? pending : [...pending, authRef]);
|
|
5271
|
+
return result.after.includes(authRef);
|
|
5272
|
+
}
|
|
5273
|
+
async function cancelCredentialDelete(authRef) {
|
|
5274
|
+
const result = await updatePendingCredentialDeletes((pending) => pending.filter((candidate) => candidate !== authRef));
|
|
5275
|
+
return result.before.includes(authRef) && !result.after.includes(authRef);
|
|
5276
|
+
}
|
|
5277
|
+
|
|
5278
|
+
// src/registry/credential-lifecycle.ts
|
|
5279
|
+
function errorMessage(error) {
|
|
5280
|
+
return error instanceof Error ? error.message : String(error);
|
|
5281
|
+
}
|
|
5282
|
+
function appendError(errors, context, error) {
|
|
5283
|
+
errors.push(`${context}: ${errorMessage(error)}`);
|
|
5284
|
+
}
|
|
5285
|
+
function credentialIsReferenced(registry, authRef) {
|
|
5286
|
+
return registry.providers.some((provider) => provider.authRef === authRef);
|
|
5287
|
+
}
|
|
5288
|
+
async function journalCredentialWrite(authRef) {
|
|
5289
|
+
if (!await queueCredentialDelete(authRef)) {
|
|
5290
|
+
throw new Error("Credential reference is not managed by Clodex.");
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
async function reconcilePendingCredentialDelete(authRef) {
|
|
5294
|
+
if (!isStoredCredentialRef(authRef)) {
|
|
5295
|
+
try {
|
|
5296
|
+
await cancelCredentialDelete(authRef);
|
|
5297
|
+
return { deleted: false, cleared: true };
|
|
5298
|
+
} catch (error) {
|
|
5299
|
+
return {
|
|
5300
|
+
deleted: false,
|
|
5301
|
+
cleared: false,
|
|
5302
|
+
persistenceError: errorMessage(error)
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
try {
|
|
5307
|
+
return await withCredentialMutationLock(authRef, async () => {
|
|
5308
|
+
try {
|
|
5309
|
+
const clearedReferencedMarker = await withRegistryWriteLock(async () => {
|
|
5310
|
+
if (!credentialIsReferenced(loadRegistryStrict(), authRef)) return false;
|
|
5311
|
+
await cancelCredentialDelete(authRef);
|
|
5312
|
+
return true;
|
|
5313
|
+
});
|
|
5314
|
+
if (clearedReferencedMarker) {
|
|
5315
|
+
return { deleted: false, cleared: true };
|
|
5316
|
+
}
|
|
5317
|
+
} catch (error) {
|
|
5318
|
+
return {
|
|
5319
|
+
deleted: false,
|
|
5320
|
+
cleared: false,
|
|
5321
|
+
persistenceError: errorMessage(error)
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
let deleted = false;
|
|
5325
|
+
try {
|
|
5326
|
+
deleted = await deleteProviderCredential(authRef);
|
|
5327
|
+
} catch {
|
|
5328
|
+
deleted = false;
|
|
5329
|
+
}
|
|
5330
|
+
if (!deleted) return { deleted: false, cleared: false };
|
|
5331
|
+
try {
|
|
5332
|
+
await cancelCredentialDelete(authRef);
|
|
5333
|
+
return { deleted: true, cleared: true };
|
|
5334
|
+
} catch (error) {
|
|
5335
|
+
return {
|
|
5336
|
+
deleted: true,
|
|
5337
|
+
cleared: false,
|
|
5338
|
+
persistenceError: errorMessage(error)
|
|
5339
|
+
};
|
|
5340
|
+
}
|
|
5341
|
+
});
|
|
5342
|
+
} catch (error) {
|
|
5343
|
+
return {
|
|
5344
|
+
deleted: false,
|
|
5345
|
+
cleared: false,
|
|
5346
|
+
persistenceError: errorMessage(error)
|
|
5347
|
+
};
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
async function reconcilePendingCredentialDeletes() {
|
|
5351
|
+
let queued;
|
|
5352
|
+
try {
|
|
5353
|
+
queued = await loadPendingCredentialDeletes();
|
|
5354
|
+
} catch (error) {
|
|
5355
|
+
return {
|
|
5356
|
+
deleted: [],
|
|
5357
|
+
pending: [],
|
|
5358
|
+
persistenceError: `Could not read pending credential cleanup: ${errorMessage(error)}`
|
|
5359
|
+
};
|
|
5360
|
+
}
|
|
5361
|
+
const knownPending = new Set(queued);
|
|
5362
|
+
const deleted = [];
|
|
5363
|
+
const errors = [];
|
|
5364
|
+
for (const authRef of queued) {
|
|
5365
|
+
let result;
|
|
5366
|
+
try {
|
|
5367
|
+
result = await reconcilePendingCredentialDelete(authRef);
|
|
5368
|
+
} catch (error) {
|
|
5369
|
+
result = {
|
|
5370
|
+
deleted: false,
|
|
5371
|
+
cleared: false,
|
|
5372
|
+
persistenceError: errorMessage(error)
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
if (result.deleted) deleted.push(authRef);
|
|
5376
|
+
if (result.cleared) knownPending.delete(authRef);
|
|
5377
|
+
if (result.persistenceError) {
|
|
5378
|
+
appendError(errors, `Cleanup for ${authRef}`, result.persistenceError);
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
let pending = [...knownPending];
|
|
5382
|
+
try {
|
|
5383
|
+
pending = await loadPendingCredentialDeletes();
|
|
5384
|
+
} catch (error) {
|
|
5385
|
+
appendError(errors, "Could not confirm pending credential cleanup", error);
|
|
5386
|
+
}
|
|
5387
|
+
return {
|
|
5388
|
+
deleted,
|
|
5389
|
+
pending,
|
|
5390
|
+
...errors.length > 0 ? { persistenceError: errors.join("; ") } : {}
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
// src/trace-log.ts
|
|
5395
|
+
import {
|
|
5396
|
+
chmodSync as chmodSync4,
|
|
5397
|
+
existsSync as existsSync5,
|
|
5398
|
+
mkdirSync as mkdirSync6,
|
|
5399
|
+
readFileSync as readFileSync7,
|
|
5400
|
+
unlinkSync as unlinkSync4,
|
|
5401
|
+
writeFileSync as writeFileSync5
|
|
5402
|
+
} from "fs";
|
|
5403
|
+
import { createHash as createHash6 } from "crypto";
|
|
5404
|
+
import { join as join4 } from "path";
|
|
5405
|
+
import pc2 from "picocolors";
|
|
5406
|
+
var DIR_MODE3 = 448;
|
|
5407
|
+
var FILE_MODE5 = 384;
|
|
4734
5408
|
var CLAUDE_DEBUG_LOG = "claude-debug.log";
|
|
4735
5409
|
var PROXY_DEBUG_LOG = "proxy-debug.log";
|
|
4736
5410
|
var PROVIDER_DEBUG_LOG = "provider-debug.log";
|
|
@@ -4744,9 +5418,9 @@ function safeClaudeSessionId(value) {
|
|
|
4744
5418
|
}
|
|
4745
5419
|
function ensureLogsDir() {
|
|
4746
5420
|
const dir = getLogsPath();
|
|
4747
|
-
|
|
5421
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE3 });
|
|
4748
5422
|
try {
|
|
4749
|
-
chmodSync4(dir,
|
|
5423
|
+
chmodSync4(dir, DIR_MODE3);
|
|
4750
5424
|
} catch {
|
|
4751
5425
|
}
|
|
4752
5426
|
return dir;
|
|
@@ -4769,9 +5443,9 @@ function getInferenceRequestLogPath() {
|
|
|
4769
5443
|
}
|
|
4770
5444
|
function getSessionLogPath(label = "session", extension = "log") {
|
|
4771
5445
|
const dir = join4(ensureLogsDir(), INFERENCE_SESSION_DIR);
|
|
4772
|
-
|
|
5446
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE3 });
|
|
4773
5447
|
try {
|
|
4774
|
-
chmodSync4(dir,
|
|
5448
|
+
chmodSync4(dir, DIR_MODE3);
|
|
4775
5449
|
} catch {
|
|
4776
5450
|
}
|
|
4777
5451
|
const safeLabel = label.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "proxy";
|
|
@@ -4852,7 +5526,7 @@ function canonicalDiagnosticValue(value) {
|
|
|
4852
5526
|
);
|
|
4853
5527
|
}
|
|
4854
5528
|
function diagnosticHash(value) {
|
|
4855
|
-
return
|
|
5529
|
+
return createHash6("sha256").update(JSON.stringify(canonicalDiagnosticValue(value)) ?? "undefined").digest("hex").slice(0, 16);
|
|
4856
5530
|
}
|
|
4857
5531
|
function diagnosticBytes(value) {
|
|
4858
5532
|
return Buffer.byteLength(JSON.stringify(value) ?? "");
|
|
@@ -5031,9 +5705,9 @@ function makeTraceLogger(logPath) {
|
|
|
5031
5705
|
}
|
|
5032
5706
|
function resetTraceLog(path) {
|
|
5033
5707
|
ensureLogsDir();
|
|
5034
|
-
if (
|
|
5708
|
+
if (existsSync5(path)) {
|
|
5035
5709
|
try {
|
|
5036
|
-
|
|
5710
|
+
unlinkSync4(path);
|
|
5037
5711
|
} catch {
|
|
5038
5712
|
}
|
|
5039
5713
|
}
|
|
@@ -5063,15 +5737,15 @@ function writeSecureLogLine(path, line) {
|
|
|
5063
5737
|
ensureLogsDir();
|
|
5064
5738
|
const redacted = redactTraceLine(line);
|
|
5065
5739
|
try {
|
|
5066
|
-
|
|
5067
|
-
`, { flag: "a", mode:
|
|
5068
|
-
chmodSync4(path,
|
|
5740
|
+
writeFileSync5(path, `${redacted}
|
|
5741
|
+
`, { flag: "a", mode: FILE_MODE5 });
|
|
5742
|
+
chmodSync4(path, FILE_MODE5);
|
|
5069
5743
|
} catch {
|
|
5070
5744
|
}
|
|
5071
5745
|
}
|
|
5072
5746
|
function printTraceLog(debugLogPath) {
|
|
5073
|
-
if (!
|
|
5074
|
-
const raw =
|
|
5747
|
+
if (!existsSync5(debugLogPath)) return;
|
|
5748
|
+
const raw = readFileSync7(debugLogPath, "utf8");
|
|
5075
5749
|
const log12 = redactTraceLog(raw);
|
|
5076
5750
|
const errorLines = log12.split("\n").filter(
|
|
5077
5751
|
(l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status") || l.includes("resolveModel failed") || l.includes("resolveModel fallback")
|
|
@@ -5309,19 +5983,22 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5309
5983
|
if (!trimmedKey && !template.apiKeyOptional) {
|
|
5310
5984
|
return { added: false, error: "API key cannot be empty." };
|
|
5311
5985
|
}
|
|
5312
|
-
const
|
|
5313
|
-
const registry =
|
|
5986
|
+
const existingState = await withRegistryWriteLock(() => {
|
|
5987
|
+
const registry = loadRegistryStrict();
|
|
5314
5988
|
const existing = registry.providers.find((p13) => p13.id === template.id);
|
|
5315
5989
|
if (existing && !opts?.replaceExisting) {
|
|
5316
5990
|
return {
|
|
5317
|
-
|
|
5318
|
-
error:
|
|
5319
|
-
|
|
5991
|
+
existing: false,
|
|
5992
|
+
error: {
|
|
5993
|
+
added: false,
|
|
5994
|
+
error: `${template.name} is already configured.`,
|
|
5995
|
+
hint: `Remove it first with: clodex providers remove ${template.id}`
|
|
5996
|
+
}
|
|
5320
5997
|
};
|
|
5321
5998
|
}
|
|
5322
|
-
return null;
|
|
5999
|
+
return { existing: existing !== void 0, error: null };
|
|
5323
6000
|
});
|
|
5324
|
-
if (
|
|
6001
|
+
if (existingState.error) return existingState.error;
|
|
5325
6002
|
const fetched = await fetchTemplateModels(template, trimmedKey, opts?.baseUrl);
|
|
5326
6003
|
if (fetched.error || fetched.models.length === 0) {
|
|
5327
6004
|
return {
|
|
@@ -5345,9 +6022,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5345
6022
|
buildPricingIndex(pricingCache),
|
|
5346
6023
|
platform
|
|
5347
6024
|
);
|
|
5348
|
-
const authRef = trimmedKey ? credentialAuthRef(
|
|
5349
|
-
|
|
5350
|
-
|
|
6025
|
+
const authRef = trimmedKey ? credentialAuthRef(
|
|
6026
|
+
existingState.existing ? `provider:${template.id}:replacement:${randomUUID6()}` : `provider:${template.id}`
|
|
6027
|
+
) : "none:anonymous";
|
|
6028
|
+
const commitProvider = () => withRegistryWriteLock(async () => {
|
|
6029
|
+
const registry = loadRegistryStrict();
|
|
5351
6030
|
const existing = registry.providers.find((p13) => p13.id === template.id);
|
|
5352
6031
|
if (existing && !opts?.replaceExisting) {
|
|
5353
6032
|
return {
|
|
@@ -5382,12 +6061,28 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5382
6061
|
} else {
|
|
5383
6062
|
registry.providers.push(entry);
|
|
5384
6063
|
}
|
|
6064
|
+
if (existing?.authRef && existing.authRef !== authRef) {
|
|
6065
|
+
await queueCredentialDelete(existing.authRef);
|
|
6066
|
+
}
|
|
5385
6067
|
saveRegistry(registry);
|
|
5386
|
-
|
|
6068
|
+
let credentialCleanupPending = false;
|
|
6069
|
+
if (trimmedKey) {
|
|
6070
|
+
try {
|
|
6071
|
+
await cancelCredentialDelete(authRef);
|
|
6072
|
+
} catch {
|
|
6073
|
+
credentialCleanupPending = true;
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6076
|
+
return {
|
|
6077
|
+
added: true,
|
|
6078
|
+
provider: entry,
|
|
6079
|
+
modelCount: pricedModels.length,
|
|
6080
|
+
...credentialCleanupPending ? { credentialCleanupPending: true } : {}
|
|
6081
|
+
};
|
|
5387
6082
|
});
|
|
5388
6083
|
const result = trimmedKey ? await withCredentialMutationLock(authRef, async () => {
|
|
5389
|
-
const
|
|
5390
|
-
const registry =
|
|
6084
|
+
const prepareError = await withRegistryWriteLock(() => {
|
|
6085
|
+
const registry = loadRegistryStrict();
|
|
5391
6086
|
const existing = registry.providers.find((p13) => p13.id === template.id);
|
|
5392
6087
|
if (existing && !opts?.replaceExisting) {
|
|
5393
6088
|
return {
|
|
@@ -5398,7 +6093,8 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5398
6093
|
}
|
|
5399
6094
|
return null;
|
|
5400
6095
|
});
|
|
5401
|
-
if (
|
|
6096
|
+
if (prepareError) return prepareError;
|
|
6097
|
+
await journalCredentialWrite(authRef);
|
|
5402
6098
|
const saved = await saveProviderCredential(authRef, trimmedKey);
|
|
5403
6099
|
if (!saved) {
|
|
5404
6100
|
return {
|
|
@@ -5409,20 +6105,30 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5409
6105
|
}
|
|
5410
6106
|
return commitProvider();
|
|
5411
6107
|
}) : await commitProvider();
|
|
6108
|
+
if (result.added) {
|
|
6109
|
+
try {
|
|
6110
|
+
const cleanup = await reconcilePendingCredentialDeletes();
|
|
6111
|
+
result.credentialCleanupPending = cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
|
|
6112
|
+
} catch {
|
|
6113
|
+
result.credentialCleanupPending = true;
|
|
6114
|
+
}
|
|
6115
|
+
} else {
|
|
6116
|
+
try {
|
|
6117
|
+
const cleanup = await reconcilePendingCredentialDeletes();
|
|
6118
|
+
result.credentialCleanupPending = cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
|
|
6119
|
+
} catch {
|
|
6120
|
+
result.credentialCleanupPending = true;
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
result.credentialCleanupReconciled = true;
|
|
5412
6124
|
if (result.added) enrichPricingAsync();
|
|
5413
6125
|
return result;
|
|
5414
6126
|
}
|
|
5415
6127
|
|
|
5416
6128
|
// src/registry/crud.ts
|
|
5417
|
-
function credentialStillReferenced(authRef, remaining) {
|
|
5418
|
-
return remaining.some((p13) => p13.authRef === authRef);
|
|
5419
|
-
}
|
|
5420
|
-
function isStoredCredentialRef(authRef) {
|
|
5421
|
-
return authRef.startsWith("keyring:") || authRef.startsWith("helper:");
|
|
5422
|
-
}
|
|
5423
6129
|
async function removeProviderFromRegistry(id, opts) {
|
|
5424
|
-
const removal = await withRegistryWriteLock(() => {
|
|
5425
|
-
const registry =
|
|
6130
|
+
const removal = await withRegistryWriteLock(async () => {
|
|
6131
|
+
const registry = loadRegistryStrict();
|
|
5426
6132
|
const index = registry.providers.findIndex((p13) => p13.id === id);
|
|
5427
6133
|
if (index < 0) {
|
|
5428
6134
|
return {
|
|
@@ -5432,10 +6138,11 @@ async function removeProviderFromRegistry(id, opts) {
|
|
|
5432
6138
|
credentialDeleted: false,
|
|
5433
6139
|
error: `Provider not found: ${id}`
|
|
5434
6140
|
},
|
|
5435
|
-
|
|
6141
|
+
authRef: null
|
|
5436
6142
|
};
|
|
5437
6143
|
}
|
|
5438
6144
|
const [removedProvider] = registry.providers.splice(index, 1);
|
|
6145
|
+
const cleanupQueued = opts?.deleteCredential !== false ? await queueCredentialDelete(removedProvider.authRef) : false;
|
|
5439
6146
|
saveRegistry(registry);
|
|
5440
6147
|
return {
|
|
5441
6148
|
result: {
|
|
@@ -5444,30 +6151,24 @@ async function removeProviderFromRegistry(id, opts) {
|
|
|
5444
6151
|
name: removedProvider.name,
|
|
5445
6152
|
credentialDeleted: false
|
|
5446
6153
|
},
|
|
5447
|
-
|
|
6154
|
+
authRef: cleanupQueued ? removedProvider.authRef : null
|
|
5448
6155
|
};
|
|
5449
6156
|
});
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
authRefToDelete
|
|
5460
|
-
);
|
|
5461
|
-
if (!removal.result.credentialDeleted) {
|
|
5462
|
-
removal.result.error = `Provider ${removal.result.name ?? id} was removed, but credential cleanup failed for ${authRefToDelete}. The credential remains in the configured store and must be removed manually.`;
|
|
5463
|
-
}
|
|
5464
|
-
});
|
|
6157
|
+
if (removal.authRef) {
|
|
6158
|
+
try {
|
|
6159
|
+
const cleanup = await reconcilePendingCredentialDeletes();
|
|
6160
|
+
removal.result.credentialDeleted = cleanup.deleted.includes(removal.authRef);
|
|
6161
|
+
removal.result.credentialCleanupPending = cleanup.pending.includes(removal.authRef) || cleanup.persistenceError !== void 0;
|
|
6162
|
+
} catch {
|
|
6163
|
+
removal.result.credentialCleanupPending = true;
|
|
6164
|
+
}
|
|
6165
|
+
removal.result.credentialCleanupReconciled = true;
|
|
5465
6166
|
}
|
|
5466
6167
|
return removal.result;
|
|
5467
6168
|
}
|
|
5468
6169
|
function toggleProviderEnabled(id) {
|
|
5469
6170
|
return withRegistryWriteLockSync(() => {
|
|
5470
|
-
const registry =
|
|
6171
|
+
const registry = loadRegistryStrict();
|
|
5471
6172
|
const provider = registry.providers.find((p13) => p13.id === id);
|
|
5472
6173
|
if (!provider) return { toggled: false, error: `Provider not found: ${id}` };
|
|
5473
6174
|
provider.enabled = !provider.enabled;
|
|
@@ -5480,7 +6181,7 @@ function toggleProviderEnabled(id) {
|
|
|
5480
6181
|
import { isDeepStrictEqual } from "util";
|
|
5481
6182
|
|
|
5482
6183
|
// src/registry/custom-endpoint.ts
|
|
5483
|
-
import { randomUUID as
|
|
6184
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
5484
6185
|
|
|
5485
6186
|
// src/registry/url-security.ts
|
|
5486
6187
|
import { lookup } from "dns/promises";
|
|
@@ -6007,7 +6708,7 @@ function providerDiscoveryInputsMatch(current, started) {
|
|
|
6007
6708
|
return current.authRef === started.authRef && current.authType === started.authType && current.templateId === started.templateId && isDeepStrictEqual(current.api, started.api);
|
|
6008
6709
|
}
|
|
6009
6710
|
async function refreshProviderModels(providerId, apiKey, registry) {
|
|
6010
|
-
const workingRegistry = registry ??
|
|
6711
|
+
const workingRegistry = registry ?? loadRegistryStrict();
|
|
6011
6712
|
const provider = workingRegistry.providers.find((p13) => p13.id === providerId);
|
|
6012
6713
|
if (!provider) {
|
|
6013
6714
|
return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
|
|
@@ -6099,7 +6800,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
|
|
|
6099
6800
|
const platform = pricingPlatformForProvider(provider.templateId, provider.id);
|
|
6100
6801
|
const enriched = enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
|
|
6101
6802
|
await withRegistryWriteLock(() => {
|
|
6102
|
-
const currentRegistry =
|
|
6803
|
+
const currentRegistry = loadRegistryStrict();
|
|
6103
6804
|
const currentProvider = currentRegistry.providers.find((candidate) => candidate.id === providerId);
|
|
6104
6805
|
if (!currentProvider) throw new Error("Provider was removed while models were refreshing.");
|
|
6105
6806
|
if (currentProvider.authRef !== provider.authRef) {
|
|
@@ -6131,7 +6832,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
|
|
|
6131
6832
|
}
|
|
6132
6833
|
async function refreshAllProviderModels(resolveKey) {
|
|
6133
6834
|
const refreshed = [];
|
|
6134
|
-
const registry =
|
|
6835
|
+
const registry = loadRegistryStrict();
|
|
6135
6836
|
const enabledProviders = registry.providers.filter((p13) => p13.enabled);
|
|
6136
6837
|
for (const provider of enabledProviders) {
|
|
6137
6838
|
const key = await resolveRefreshCredential(provider, resolveKey);
|
|
@@ -6177,41 +6878,83 @@ function oauthDisplayName(registryId, fallbackName) {
|
|
|
6177
6878
|
if (registryId === "openai-oauth") return "OpenAI (ChatGPT)";
|
|
6178
6879
|
return fallbackName;
|
|
6179
6880
|
}
|
|
6180
|
-
async function
|
|
6181
|
-
|
|
6881
|
+
async function persistOAuthProvider(providerId, cred, authRef) {
|
|
6882
|
+
const registryProvider = await withCredentialMutationLock(authRef, async () => {
|
|
6182
6883
|
const registryId = toOAuthRegistryId(providerId);
|
|
6183
6884
|
const templateId = providerId.replace(/-oauth$/, "") || providerId;
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6885
|
+
await withRegistryWriteLock(() => {
|
|
6886
|
+
const registry = loadRegistryStrict();
|
|
6887
|
+
const previousEntry = registry.providers.find((provider) => provider.id === registryId);
|
|
6888
|
+
if (!previousEntry && !getTemplateById(templateId)) {
|
|
6889
|
+
throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
|
|
6890
|
+
}
|
|
6891
|
+
});
|
|
6892
|
+
await journalCredentialWrite(authRef);
|
|
6893
|
+
let diagMsg = "";
|
|
6894
|
+
const saved = await saveProviderCredential(
|
|
6895
|
+
authRef,
|
|
6896
|
+
oauthCredentialToKeychainJson(cred),
|
|
6897
|
+
(msg) => {
|
|
6898
|
+
diagMsg = msg;
|
|
6899
|
+
}
|
|
6900
|
+
);
|
|
6901
|
+
if (!saved) {
|
|
6902
|
+
throw new Error(`Could not save OAuth tokens to the credential store${diagMsg ? ` \u2014 ${diagMsg}` : " \u2014 check access and try again"}`);
|
|
6903
|
+
}
|
|
6904
|
+
const committed = await withRegistryWriteLock(async () => {
|
|
6905
|
+
const registry = loadRegistryStrict();
|
|
6906
|
+
const template = getTemplateById(templateId);
|
|
6907
|
+
const previousEntry = registry.providers.find((provider) => provider.id === registryId);
|
|
6908
|
+
if (!previousEntry && !template) {
|
|
6189
6909
|
throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
|
|
6190
6910
|
}
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6911
|
+
let entry;
|
|
6912
|
+
if (!previousEntry) {
|
|
6913
|
+
if (!template) throw new Error(`Provider "${providerId}" has no template`);
|
|
6914
|
+
const displayName = oauthDisplayName(registryId, template.name);
|
|
6915
|
+
entry = {
|
|
6916
|
+
id: registryId,
|
|
6917
|
+
templateId,
|
|
6918
|
+
name: displayName,
|
|
6919
|
+
enabled: true,
|
|
6920
|
+
authRef,
|
|
6921
|
+
authType: "oauth",
|
|
6922
|
+
api: {
|
|
6923
|
+
npm: template.npm,
|
|
6924
|
+
url: template.defaultBaseUrl ?? "",
|
|
6925
|
+
...template.headers ? { headers: template.headers } : {}
|
|
6926
|
+
},
|
|
6927
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6928
|
+
};
|
|
6929
|
+
} else {
|
|
6930
|
+
entry = { ...previousEntry, authType: "oauth", authRef, templateId };
|
|
6931
|
+
}
|
|
6932
|
+
const idx = registry.providers.findIndex((provider) => provider.id === registryId);
|
|
6933
|
+
if (idx >= 0) registry.providers[idx] = entry;
|
|
6934
|
+
else registry.providers.push(entry);
|
|
6935
|
+
if (previousEntry?.authRef && previousEntry.authRef !== authRef) {
|
|
6936
|
+
await queueCredentialDelete(previousEntry.authRef);
|
|
6937
|
+
}
|
|
6938
|
+
saveRegistry(registry);
|
|
6939
|
+
try {
|
|
6940
|
+
await cancelCredentialDelete(authRef);
|
|
6941
|
+
} catch {
|
|
6942
|
+
}
|
|
6943
|
+
return entry;
|
|
6944
|
+
});
|
|
6945
|
+
return committed;
|
|
6214
6946
|
});
|
|
6947
|
+
let credentialCleanupPending = true;
|
|
6948
|
+
try {
|
|
6949
|
+
const cleanup = await reconcilePendingCredentialDeletes();
|
|
6950
|
+
credentialCleanupPending = cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
|
|
6951
|
+
} catch {
|
|
6952
|
+
credentialCleanupPending = true;
|
|
6953
|
+
}
|
|
6954
|
+
return {
|
|
6955
|
+
registryProvider,
|
|
6956
|
+
credentialCleanupPending
|
|
6957
|
+
};
|
|
6215
6958
|
}
|
|
6216
6959
|
async function authenticateProvider(providerId, _options = {}) {
|
|
6217
6960
|
const registryId = toOAuthRegistryId(providerId);
|
|
@@ -6229,28 +6972,7 @@ async function authenticateProvider(providerId, _options = {}) {
|
|
|
6229
6972
|
);
|
|
6230
6973
|
}
|
|
6231
6974
|
const cred = await runNativeDeviceCode(providerId);
|
|
6232
|
-
const persisted = await
|
|
6233
|
-
let nativeDiagMsg = "";
|
|
6234
|
-
const saved = await saveProviderCredential(
|
|
6235
|
-
authRef,
|
|
6236
|
-
oauthCredentialToKeychainJson(cred),
|
|
6237
|
-
(msg) => {
|
|
6238
|
-
nativeDiagMsg = msg;
|
|
6239
|
-
}
|
|
6240
|
-
);
|
|
6241
|
-
if (!saved) {
|
|
6242
|
-
throw new Error(
|
|
6243
|
-
`Could not save OAuth tokens to the credential store${nativeDiagMsg ? ` \u2014 ${nativeDiagMsg}` : " \u2014 check access and try again"}`
|
|
6244
|
-
);
|
|
6245
|
-
}
|
|
6246
|
-
const registryProvider2 = await upsertOAuthProvider(
|
|
6247
|
-
providerId,
|
|
6248
|
-
cred,
|
|
6249
|
-
authRef
|
|
6250
|
-
);
|
|
6251
|
-
return { registryProvider: registryProvider2 };
|
|
6252
|
-
});
|
|
6253
|
-
const { registryProvider } = persisted;
|
|
6975
|
+
const persisted = await persistOAuthProvider(providerId, cred, authRef);
|
|
6254
6976
|
const refreshSpinner = p2.spinner();
|
|
6255
6977
|
refreshSpinner.start("Refreshing model list...");
|
|
6256
6978
|
try {
|
|
@@ -6259,7 +6981,12 @@ async function authenticateProvider(providerId, _options = {}) {
|
|
|
6259
6981
|
} catch {
|
|
6260
6982
|
refreshSpinner.stop("Could not refresh models \u2014 run clodex providers refresh-models later");
|
|
6261
6983
|
}
|
|
6262
|
-
return {
|
|
6984
|
+
return {
|
|
6985
|
+
providerId: registryId,
|
|
6986
|
+
credential: cred,
|
|
6987
|
+
registryProvider: persisted.registryProvider,
|
|
6988
|
+
credentialCleanupPending: persisted.credentialCleanupPending
|
|
6989
|
+
};
|
|
6263
6990
|
}
|
|
6264
6991
|
function providerAuthHelpText() {
|
|
6265
6992
|
return `${pc3.bold("clodex providers auth")} \u2014 sign in with OAuth
|
|
@@ -6569,6 +7296,39 @@ async function pickLocalModel(provider, conflicts, prefs) {
|
|
|
6569
7296
|
}
|
|
6570
7297
|
|
|
6571
7298
|
// src/providers-command.ts
|
|
7299
|
+
var CREDENTIAL_CLEANUP_PENDING_MESSAGE = "Credential cleanup is pending and will be retried by the next provider command.";
|
|
7300
|
+
function reportCredentialCleanup(pending, state, reconciled = false) {
|
|
7301
|
+
if (state) {
|
|
7302
|
+
state.reconciled ||= reconciled;
|
|
7303
|
+
state.pending ||= pending;
|
|
7304
|
+
return;
|
|
7305
|
+
}
|
|
7306
|
+
if (pending) {
|
|
7307
|
+
p4.log.warn(CREDENTIAL_CLEANUP_PENDING_MESSAGE);
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
async function reconcileCredentialCleanup() {
|
|
7311
|
+
try {
|
|
7312
|
+
const cleanup = await reconcilePendingCredentialDeletes();
|
|
7313
|
+
return cleanup.pending.length > 0 || cleanup.persistenceError !== void 0;
|
|
7314
|
+
} catch {
|
|
7315
|
+
return true;
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
async function runWithCredentialCleanup(run) {
|
|
7319
|
+
const state = {
|
|
7320
|
+
reconciled: false,
|
|
7321
|
+
pending: false
|
|
7322
|
+
};
|
|
7323
|
+
try {
|
|
7324
|
+
return await run(state);
|
|
7325
|
+
} finally {
|
|
7326
|
+
if (!state.reconciled) {
|
|
7327
|
+
state.pending = await reconcileCredentialCleanup();
|
|
7328
|
+
}
|
|
7329
|
+
reportCredentialCleanup(state.pending);
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
6572
7332
|
function parseProvidersArgs(args) {
|
|
6573
7333
|
if (args.length === 0) return { subcommand: "hub", showHelp: false };
|
|
6574
7334
|
const [first, ...rest] = args;
|
|
@@ -6632,10 +7392,11 @@ ${pc5.bold("Subcommands:")}
|
|
|
6632
7392
|
function providerLabel(name, modelCount, enabled) {
|
|
6633
7393
|
return `${fmtEnabledStar(enabled)} ${fmtProvider(name)} ${pc5.dim(`(${modelCount} model${modelCount === 1 ? "" : "s"})`)}`;
|
|
6634
7394
|
}
|
|
6635
|
-
async function
|
|
7395
|
+
async function runProvidersAuthWithCleanupState(providerId, method, cleanupState) {
|
|
6636
7396
|
try {
|
|
6637
7397
|
const result = await authenticateProvider(providerId, { method });
|
|
6638
7398
|
p4.log.success(`Signed in to ${result.registryProvider.name} \u2014 credential saved to the credential store.`);
|
|
7399
|
+
reportCredentialCleanup(result.credentialCleanupPending, cleanupState, true);
|
|
6639
7400
|
return 0;
|
|
6640
7401
|
} catch (err) {
|
|
6641
7402
|
if (err instanceof Error && err.message === "Cancelled") {
|
|
@@ -6646,6 +7407,9 @@ async function runProvidersAuth(providerId, method) {
|
|
|
6646
7407
|
return 1;
|
|
6647
7408
|
}
|
|
6648
7409
|
}
|
|
7410
|
+
async function runProvidersAuth(providerId, method) {
|
|
7411
|
+
return runProvidersAuthWithCleanupState(providerId, method);
|
|
7412
|
+
}
|
|
6649
7413
|
async function runProvidersRefreshModels(providerId) {
|
|
6650
7414
|
const resolveKey = async (provider) => resolveProviderCredential(provider.id, provider.authRef);
|
|
6651
7415
|
if (providerId) {
|
|
@@ -6723,7 +7487,7 @@ async function runProvidersList() {
|
|
|
6723
7487
|
console.log("");
|
|
6724
7488
|
return 0;
|
|
6725
7489
|
}
|
|
6726
|
-
async function runTemplateAddFlow() {
|
|
7490
|
+
async function runTemplateAddFlow(cleanupState) {
|
|
6727
7491
|
const registry = loadRegistry();
|
|
6728
7492
|
const configuredIds = registry.providers.map((p13) => p13.id);
|
|
6729
7493
|
const template = listAddableTemplates(configuredIds).find((t) => t.id === "openai") ?? getTemplateById("openai");
|
|
@@ -6749,6 +7513,11 @@ async function runTemplateAddFlow() {
|
|
|
6749
7513
|
spinner5.start(`Testing connection to ${template.name}...`);
|
|
6750
7514
|
const result = await addProviderFromTemplate(template, apiKey);
|
|
6751
7515
|
spinner5.stop("");
|
|
7516
|
+
reportCredentialCleanup(
|
|
7517
|
+
result.credentialCleanupPending === true,
|
|
7518
|
+
cleanupState,
|
|
7519
|
+
result.credentialCleanupReconciled === true
|
|
7520
|
+
);
|
|
6752
7521
|
if (!result.added) {
|
|
6753
7522
|
p4.log.error(result.error ?? "Could not add provider.");
|
|
6754
7523
|
if (result.hint) p4.log.info(result.hint);
|
|
@@ -6757,7 +7526,7 @@ async function runTemplateAddFlow() {
|
|
|
6757
7526
|
logConnected(template.name, result.modelCount ?? 0);
|
|
6758
7527
|
return 0;
|
|
6759
7528
|
}
|
|
6760
|
-
async function
|
|
7529
|
+
async function runProvidersAddWithCleanupState(cleanupState) {
|
|
6761
7530
|
const choice = await p4.select({
|
|
6762
7531
|
message: "Add a provider",
|
|
6763
7532
|
options: [
|
|
@@ -6777,11 +7546,16 @@ async function runProvidersAdd() {
|
|
|
6777
7546
|
p4.cancel("Cancelled.");
|
|
6778
7547
|
return 0;
|
|
6779
7548
|
}
|
|
6780
|
-
if (choice === "oauth")
|
|
6781
|
-
|
|
7549
|
+
if (choice === "oauth") {
|
|
7550
|
+
return runProvidersAuthWithCleanupState("openai", void 0, cleanupState);
|
|
7551
|
+
}
|
|
7552
|
+
if (choice === "apikey") return runTemplateAddFlow(cleanupState);
|
|
6782
7553
|
return 0;
|
|
6783
7554
|
}
|
|
6784
|
-
async function
|
|
7555
|
+
async function runProvidersAdd() {
|
|
7556
|
+
return runProvidersAddWithCleanupState();
|
|
7557
|
+
}
|
|
7558
|
+
async function runProvidersRemoveWithCleanupState(id, interactive = false, cleanupState) {
|
|
6785
7559
|
const registry = loadRegistry();
|
|
6786
7560
|
const provider = registry.providers.find((pr) => pr.id === id);
|
|
6787
7561
|
if (!provider) {
|
|
@@ -6799,6 +7573,11 @@ async function runProvidersRemove(id, interactive = false) {
|
|
|
6799
7573
|
}
|
|
6800
7574
|
}
|
|
6801
7575
|
const result = await removeProviderFromRegistry(id);
|
|
7576
|
+
reportCredentialCleanup(
|
|
7577
|
+
result.credentialCleanupPending === true,
|
|
7578
|
+
cleanupState,
|
|
7579
|
+
result.credentialCleanupReconciled === true
|
|
7580
|
+
);
|
|
6802
7581
|
if (!result.removed) {
|
|
6803
7582
|
p4.log.error(result.error ?? `Could not remove ${id}`);
|
|
6804
7583
|
return 1;
|
|
@@ -6813,6 +7592,9 @@ async function runProvidersRemove(id, interactive = false) {
|
|
|
6813
7592
|
}
|
|
6814
7593
|
return 0;
|
|
6815
7594
|
}
|
|
7595
|
+
async function runProvidersRemove(id, interactive = false) {
|
|
7596
|
+
return runProvidersRemoveWithCleanupState(id, interactive);
|
|
7597
|
+
}
|
|
6816
7598
|
function providerHubChoiceValue(entry) {
|
|
6817
7599
|
return `provider:${entry.id}`;
|
|
6818
7600
|
}
|
|
@@ -6946,16 +7728,24 @@ async function runProvidersCommand(args) {
|
|
|
6946
7728
|
console.log(providersHelpText());
|
|
6947
7729
|
return 0;
|
|
6948
7730
|
}
|
|
7731
|
+
const reconcilesDuringMutation = parsed.subcommand === "add" || parsed.subcommand === "remove" || parsed.subcommand === "auth" && !parsed.showHelp && parsed.removeId !== void 0;
|
|
7732
|
+
if (!reconcilesDuringMutation) {
|
|
7733
|
+
reportCredentialCleanup(await reconcileCredentialCleanup());
|
|
7734
|
+
}
|
|
6949
7735
|
if (parsed.subcommand === "list") return runProvidersList();
|
|
6950
|
-
if (parsed.subcommand === "add")
|
|
6951
|
-
|
|
7736
|
+
if (parsed.subcommand === "add") {
|
|
7737
|
+
return runWithCredentialCleanup((state) => runProvidersAddWithCleanupState(state));
|
|
7738
|
+
}
|
|
7739
|
+
if (parsed.subcommand === "remove" && parsed.removeId) {
|
|
7740
|
+
return runWithCredentialCleanup((state) => runProvidersRemoveWithCleanupState(parsed.removeId, false, state));
|
|
7741
|
+
}
|
|
6952
7742
|
if (parsed.subcommand === "refresh-models") return runProvidersRefreshModels(parsed.removeId);
|
|
6953
7743
|
if (parsed.subcommand === "auth") {
|
|
6954
7744
|
if (parsed.showHelp || !parsed.removeId) {
|
|
6955
7745
|
console.log(providerAuthHelpText());
|
|
6956
7746
|
return 0;
|
|
6957
7747
|
}
|
|
6958
|
-
return
|
|
7748
|
+
return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state));
|
|
6959
7749
|
}
|
|
6960
7750
|
relayIntro("Your OpenAI providers");
|
|
6961
7751
|
return runProvidersHub();
|
|
@@ -6996,7 +7786,7 @@ async function runFirstRunWizard(_trace = false) {
|
|
|
6996
7786
|
|
|
6997
7787
|
// src/proxy.ts
|
|
6998
7788
|
import { createServer } from "http";
|
|
6999
|
-
import { appendFileSync, openSync as
|
|
7789
|
+
import { appendFileSync, openSync as openSync4, writeSync as writeSync2, closeSync as closeSync4 } from "fs";
|
|
7000
7790
|
|
|
7001
7791
|
// src/http-utils.ts
|
|
7002
7792
|
import * as zlib from "zlib";
|
|
@@ -7073,6 +7863,12 @@ function localModelToRoute(lp, model) {
|
|
|
7073
7863
|
baseURL: model.apiBaseUrl,
|
|
7074
7864
|
providerId: lp.id,
|
|
7075
7865
|
authType: lp.authType,
|
|
7866
|
+
refreshToken: lp.authType === "oauth" && lp.authRef ? (rejectedAccessToken) => rejectedAccessToken === void 0 ? resolveProviderCredential(lp.id, lp.authRef) : resolveProviderCredential(
|
|
7867
|
+
lp.id,
|
|
7868
|
+
lp.authRef,
|
|
7869
|
+
void 0,
|
|
7870
|
+
{ rejectedAccessToken }
|
|
7871
|
+
) : void 0,
|
|
7076
7872
|
oauthAccountId: lp.oauthAccountId,
|
|
7077
7873
|
providerData: lp.providerData,
|
|
7078
7874
|
headers: lp.headers,
|
|
@@ -7374,14 +8170,29 @@ var UpstreamUnreachableError = class extends Error {
|
|
|
7374
8170
|
this.name = "UpstreamUnreachableError";
|
|
7375
8171
|
}
|
|
7376
8172
|
};
|
|
8173
|
+
async function resolveOAuthRetryReplacement(enabled, status, attempt, headersSent, apiKey, refreshToken) {
|
|
8174
|
+
if (!enabled || status !== 401 || attempt !== 0 || headersSent || !refreshToken) {
|
|
8175
|
+
return null;
|
|
8176
|
+
}
|
|
8177
|
+
const replacement = await refreshToken(apiKey).catch(() => null);
|
|
8178
|
+
return replacement && replacement !== apiKey ? replacement : null;
|
|
8179
|
+
}
|
|
7377
8180
|
async function fetchWithOAuthRetry(apiKey, request3, refreshToken) {
|
|
7378
8181
|
let response = await request3(apiKey);
|
|
7379
|
-
|
|
8182
|
+
const refreshed = await resolveOAuthRetryReplacement(
|
|
8183
|
+
true,
|
|
8184
|
+
response.status,
|
|
8185
|
+
0,
|
|
8186
|
+
false,
|
|
8187
|
+
apiKey,
|
|
8188
|
+
refreshToken
|
|
8189
|
+
);
|
|
8190
|
+
if (!refreshed) {
|
|
7380
8191
|
return { response, apiKey, refreshed: false };
|
|
7381
8192
|
}
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
8193
|
+
try {
|
|
8194
|
+
await response.body?.cancel?.();
|
|
8195
|
+
} catch {
|
|
7385
8196
|
}
|
|
7386
8197
|
response = await request3(refreshed);
|
|
7387
8198
|
return { response, apiKey: refreshed, refreshed: true };
|
|
@@ -7446,10 +8257,10 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
|
|
|
7446
8257
|
}
|
|
7447
8258
|
|
|
7448
8259
|
// src/proxy.ts
|
|
7449
|
-
import { randomUUID as
|
|
8260
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
7450
8261
|
|
|
7451
8262
|
// src/sdk-adapter.ts
|
|
7452
|
-
import { createHash as
|
|
8263
|
+
import { createHash as createHash7 } from "crypto";
|
|
7453
8264
|
import { streamText, generateText, tool, jsonSchema } from "ai";
|
|
7454
8265
|
|
|
7455
8266
|
// src/proxy-shared.ts
|
|
@@ -7613,7 +8424,7 @@ function extractClaudeSessionId(body, headerFallback) {
|
|
|
7613
8424
|
return validClaudeSessionId(headerFallback);
|
|
7614
8425
|
}
|
|
7615
8426
|
function claudeSessionPromptCacheKey(sessionId) {
|
|
7616
|
-
return "relay-session-" +
|
|
8427
|
+
return "relay-session-" + createHash7("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
7617
8428
|
}
|
|
7618
8429
|
function anthropicEffortFromRequest(body) {
|
|
7619
8430
|
const effort = body.output_config?.effort;
|
|
@@ -7623,7 +8434,7 @@ function anthropicEffortFromRequest(body) {
|
|
|
7623
8434
|
function openAiPromptCacheKey(system, tools) {
|
|
7624
8435
|
const toolSig = (tools ?? []).map((t) => `${t.name}${t.description ?? ""}${JSON.stringify(t.input_schema ?? {})}`).join("");
|
|
7625
8436
|
const material = `${system ?? ""}\0${toolSig}`;
|
|
7626
|
-
return "relay-" +
|
|
8437
|
+
return "relay-" + createHash7("sha256").update(material).digest("hex").slice(0, 32);
|
|
7627
8438
|
}
|
|
7628
8439
|
function supportsOpenAiPromptCacheBreakpoints(modelId) {
|
|
7629
8440
|
const match = modelId.toLowerCase().match(/^gpt-(\d+)(?:\.(\d+))?(?:-|$)/);
|
|
@@ -8486,12 +9297,12 @@ function createTranslationLifecycle(logPath, requestId, modelId, provider) {
|
|
|
8486
9297
|
function appendSecureLog(logPath, line) {
|
|
8487
9298
|
const redacted = redactTraceLine(line);
|
|
8488
9299
|
try {
|
|
8489
|
-
const fd =
|
|
9300
|
+
const fd = openSync4(logPath, "a", 384);
|
|
8490
9301
|
try {
|
|
8491
9302
|
writeSync2(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
|
|
8492
9303
|
`);
|
|
8493
9304
|
} finally {
|
|
8494
|
-
|
|
9305
|
+
closeSync4(fd);
|
|
8495
9306
|
}
|
|
8496
9307
|
} catch {
|
|
8497
9308
|
try {
|
|
@@ -8531,7 +9342,7 @@ function lookupRoute(byAlias, id) {
|
|
|
8531
9342
|
return void 0;
|
|
8532
9343
|
}
|
|
8533
9344
|
async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
|
|
8534
|
-
const proxyToken =
|
|
9345
|
+
const proxyToken = randomUUID8();
|
|
8535
9346
|
silenceSdkWarnings();
|
|
8536
9347
|
if (routes.length === 0) {
|
|
8537
9348
|
throw new Error("Proxy catalog requires at least one route");
|
|
@@ -8609,7 +9420,28 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
8609
9420
|
const relayRequestIdRaw = req.headers["x-relay-request-id"];
|
|
8610
9421
|
const relayRequestId = Array.isArray(relayRequestIdRaw) ? relayRequestIdRaw[0] : relayRequestIdRaw;
|
|
8611
9422
|
const route = lookupRoute(byAlias, originalModel) ?? defaultRoute;
|
|
8612
|
-
|
|
9423
|
+
if (messagesEndpoint === "count_tokens" && route.modelFormat !== "anthropic") {
|
|
9424
|
+
const inputTokens = estimateAnthropicInputTokens(anthropicBody);
|
|
9425
|
+
plog(() => `token-count: local estimate model=${originalModel} input_tokens=${inputTokens}`);
|
|
9426
|
+
res.setHeader("x-relay-token-count-source", "local-estimate");
|
|
9427
|
+
sendJson(res, 200, { input_tokens: inputTokens });
|
|
9428
|
+
return;
|
|
9429
|
+
}
|
|
9430
|
+
let apiKey = route.apiKey;
|
|
9431
|
+
if (route.authType === "oauth" && route.refreshToken) {
|
|
9432
|
+
try {
|
|
9433
|
+
const current = await route.refreshToken();
|
|
9434
|
+
if (!current) throw new Error("credential is missing");
|
|
9435
|
+
apiKey = current;
|
|
9436
|
+
route.apiKey = current;
|
|
9437
|
+
} catch (err) {
|
|
9438
|
+
plog(
|
|
9439
|
+
() => `oauth credential unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
9440
|
+
);
|
|
9441
|
+
anthropicError(res, 401, "OAuth credential is unavailable");
|
|
9442
|
+
return;
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
8613
9445
|
const upstreamUrl = route.upstreamUrl;
|
|
8614
9446
|
const routeAuthType = route.authType ?? "api";
|
|
8615
9447
|
plog(
|
|
@@ -8617,13 +9449,6 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
8617
9449
|
);
|
|
8618
9450
|
const usesSdkAdapter = isSdkMigratedNpm(route.npm);
|
|
8619
9451
|
if (messagesEndpoint === "count_tokens") {
|
|
8620
|
-
if (route.modelFormat !== "anthropic") {
|
|
8621
|
-
const inputTokens = estimateAnthropicInputTokens(anthropicBody);
|
|
8622
|
-
plog(() => `token-count: local estimate model=${originalModel} input_tokens=${inputTokens}`);
|
|
8623
|
-
res.setHeader("x-relay-token-count-source", "local-estimate");
|
|
8624
|
-
sendJson(res, 200, { input_tokens: inputTokens });
|
|
8625
|
-
return;
|
|
8626
|
-
}
|
|
8627
9452
|
if (!apiKey && routeAuthType !== "none") {
|
|
8628
9453
|
anthropicError(res, 401, "Missing API key");
|
|
8629
9454
|
return;
|
|
@@ -8712,7 +9537,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
8712
9537
|
originalModel,
|
|
8713
9538
|
route.providerId ?? route.aliasId.split(":")[1] ?? "unknown"
|
|
8714
9539
|
);
|
|
8715
|
-
|
|
9540
|
+
const runSdkRequest = async () => {
|
|
8716
9541
|
const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
|
|
8717
9542
|
const claudeSessionId = extractClaudeSessionId(anthropicBody, claudeSessionIdHeader);
|
|
8718
9543
|
const params = translateRequest(anthropicBody, route.npm, {
|
|
@@ -8818,18 +9643,35 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
8818
9643
|
translationLifecycle?.complete();
|
|
8819
9644
|
sendJson(res, 200, anthropicResponse);
|
|
8820
9645
|
}
|
|
8821
|
-
}
|
|
9646
|
+
};
|
|
9647
|
+
let sdkAttempt = 0;
|
|
9648
|
+
const handleSdkError = async (err) => {
|
|
8822
9649
|
if (clientAbort.signal.aborted) {
|
|
8823
9650
|
translationLifecycle?.cancel();
|
|
8824
|
-
return;
|
|
9651
|
+
return "cancelled";
|
|
9652
|
+
}
|
|
9653
|
+
const message = formatUpstreamError(err);
|
|
9654
|
+
const details = sdkUpstreamErrorDetails(err);
|
|
9655
|
+
const upstreamStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
9656
|
+
const replacement = await resolveOAuthRetryReplacement(
|
|
9657
|
+
openAiOAuth,
|
|
9658
|
+
upstreamStatus,
|
|
9659
|
+
sdkAttempt,
|
|
9660
|
+
res.headersSent,
|
|
9661
|
+
apiKey,
|
|
9662
|
+
route.refreshToken
|
|
9663
|
+
);
|
|
9664
|
+
if (replacement) {
|
|
9665
|
+
apiKey = replacement;
|
|
9666
|
+
route.apiKey = replacement;
|
|
9667
|
+
sdkAttempt += 1;
|
|
9668
|
+
plog(() => "sdk oauth credential replaced after 401; retrying once");
|
|
9669
|
+
return "retry";
|
|
8825
9670
|
}
|
|
8826
9671
|
translationLifecycle?.fail(
|
|
8827
9672
|
err instanceof Error ? err.name : "UpstreamError",
|
|
8828
9673
|
sdkTranslationErrorSignature(err)
|
|
8829
9674
|
);
|
|
8830
|
-
const message = formatUpstreamError(err);
|
|
8831
|
-
const details = sdkUpstreamErrorDetails(err);
|
|
8832
|
-
const upstreamStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
8833
9675
|
const contextLengthExceeded = upstreamStatus === 400 && isContextLengthExceededError(err, message);
|
|
8834
9676
|
const clientMessage = contextLengthExceeded ? anthropicPromptTooLongMessage(
|
|
8835
9677
|
anthropicBody,
|
|
@@ -8849,11 +9691,14 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
8849
9691
|
});
|
|
8850
9692
|
}
|
|
8851
9693
|
if (!res.headersSent) {
|
|
9694
|
+
if (details?.retryAfterSeconds !== void 0) {
|
|
9695
|
+
res.setHeader("retry-after", String(details.retryAfterSeconds));
|
|
9696
|
+
}
|
|
8852
9697
|
anthropicError(
|
|
8853
9698
|
res,
|
|
8854
9699
|
upstreamStatus === 500 ? 502 : upstreamStatus,
|
|
8855
9700
|
clientMessage,
|
|
8856
|
-
contextLengthExceeded ? relayRequestId ??
|
|
9701
|
+
contextLengthExceeded ? relayRequestId ?? randomUUID8() : void 0
|
|
8857
9702
|
);
|
|
8858
9703
|
} else {
|
|
8859
9704
|
const errorType = anthropicErrorType(upstreamStatus);
|
|
@@ -8861,12 +9706,24 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
|
|
|
8861
9706
|
data: ${JSON.stringify({
|
|
8862
9707
|
type: "error",
|
|
8863
9708
|
error: { type: errorType, message: clientMessage },
|
|
8864
|
-
...contextLengthExceeded ? { request_id: relayRequestId ??
|
|
9709
|
+
...contextLengthExceeded ? { request_id: relayRequestId ?? randomUUID8() } : {}
|
|
8865
9710
|
})}
|
|
8866
9711
|
|
|
8867
9712
|
`);
|
|
8868
9713
|
res.end();
|
|
8869
9714
|
}
|
|
9715
|
+
return "done";
|
|
9716
|
+
};
|
|
9717
|
+
for (; ; ) {
|
|
9718
|
+
try {
|
|
9719
|
+
await runSdkRequest();
|
|
9720
|
+
break;
|
|
9721
|
+
} catch (err) {
|
|
9722
|
+
const outcome = await handleSdkError(err);
|
|
9723
|
+
if (outcome === "retry") continue;
|
|
9724
|
+
if (outcome === "cancelled") return;
|
|
9725
|
+
break;
|
|
9726
|
+
}
|
|
8870
9727
|
}
|
|
8871
9728
|
return;
|
|
8872
9729
|
}
|
|
@@ -9059,7 +9916,7 @@ async function askSaveServerPassword() {
|
|
|
9059
9916
|
|
|
9060
9917
|
// src/server/router.ts
|
|
9061
9918
|
import { createServer as createServer2 } from "http";
|
|
9062
|
-
import { randomUUID as
|
|
9919
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
9063
9920
|
|
|
9064
9921
|
// src/openai-adapter.ts
|
|
9065
9922
|
import { tool as tool2, jsonSchema as jsonSchema2, streamText as streamText2, generateText as generateText2 } from "ai";
|
|
@@ -9261,6 +10118,30 @@ function auditInference(options, entry) {
|
|
|
9261
10118
|
function inferenceProvider(model) {
|
|
9262
10119
|
return model.providerId ?? String(model.sourceBackend);
|
|
9263
10120
|
}
|
|
10121
|
+
async function resolveModelApiKey(model, fallback, rejectedAccessToken) {
|
|
10122
|
+
if (model.authType === "oauth" && model.providerId && model.authRef) {
|
|
10123
|
+
let current;
|
|
10124
|
+
try {
|
|
10125
|
+
current = rejectedAccessToken === void 0 ? await resolveProviderCredential(model.providerId, model.authRef) : await resolveProviderCredential(
|
|
10126
|
+
model.providerId,
|
|
10127
|
+
model.authRef,
|
|
10128
|
+
void 0,
|
|
10129
|
+
{ rejectedAccessToken }
|
|
10130
|
+
);
|
|
10131
|
+
} catch (cause) {
|
|
10132
|
+
throw new Error(
|
|
10133
|
+
`OAuth credential is unavailable for ${model.providerId}`,
|
|
10134
|
+
{ cause }
|
|
10135
|
+
);
|
|
10136
|
+
}
|
|
10137
|
+
if (!current) {
|
|
10138
|
+
throw new Error(`OAuth credential is unavailable for ${model.providerId}`);
|
|
10139
|
+
}
|
|
10140
|
+
model.apiKey = current;
|
|
10141
|
+
return current;
|
|
10142
|
+
}
|
|
10143
|
+
return model.apiKey ?? fallback;
|
|
10144
|
+
}
|
|
9264
10145
|
function auditSdkError(options, requestedModelId, model, err, message) {
|
|
9265
10146
|
const details = sdkUpstreamErrorDetails(err);
|
|
9266
10147
|
const statusCode = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
@@ -9275,7 +10156,7 @@ function auditSdkError(options, requestedModelId, model, err, message) {
|
|
|
9275
10156
|
attemptCount: details?.attemptCount
|
|
9276
10157
|
});
|
|
9277
10158
|
}
|
|
9278
|
-
return statusCode;
|
|
10159
|
+
return { statusCode, retryAfterSeconds: details?.retryAfterSeconds };
|
|
9279
10160
|
}
|
|
9280
10161
|
function openAiEffort(body) {
|
|
9281
10162
|
if (typeof body.reasoning_effort === "string" && body.reasoning_effort.trim()) {
|
|
@@ -9319,7 +10200,16 @@ async function routeRequest(req, res, options, modelCache, plog) {
|
|
|
9319
10200
|
return;
|
|
9320
10201
|
}
|
|
9321
10202
|
if (req.method === "GET" && pathname === "/models") {
|
|
9322
|
-
sendJson(res, 200, {
|
|
10203
|
+
sendJson(res, 200, {
|
|
10204
|
+
models: options.catalog.list().map(({
|
|
10205
|
+
apiKey: _apiKey,
|
|
10206
|
+
authRef: _authRef,
|
|
10207
|
+
headers: _headers,
|
|
10208
|
+
oauthAccountId: _oauthAccountId,
|
|
10209
|
+
providerData: _providerData,
|
|
10210
|
+
...rest
|
|
10211
|
+
}) => rest)
|
|
10212
|
+
});
|
|
9323
10213
|
return;
|
|
9324
10214
|
}
|
|
9325
10215
|
if (req.method === "GET" && pathname === "/anthropic/v1/models") {
|
|
@@ -9354,7 +10244,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9354
10244
|
plog(`model not found: ${body.model}`);
|
|
9355
10245
|
return;
|
|
9356
10246
|
}
|
|
9357
|
-
const requestId =
|
|
10247
|
+
const requestId = randomUUID9();
|
|
9358
10248
|
const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
|
|
9359
10249
|
const claudeSessionId = extractClaudeSessionId(body, claudeSessionIdHeader);
|
|
9360
10250
|
if (options.webSocketDiagnosticsLogPath) {
|
|
@@ -9378,7 +10268,15 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9378
10268
|
return;
|
|
9379
10269
|
}
|
|
9380
10270
|
const messagesUrl = `${model.baseUrl}/v1/messages`;
|
|
9381
|
-
|
|
10271
|
+
let apiKey;
|
|
10272
|
+
try {
|
|
10273
|
+
apiKey = await resolveModelApiKey(model, options.apiKey);
|
|
10274
|
+
} catch (err) {
|
|
10275
|
+
sendJson(res, 401, {
|
|
10276
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
10277
|
+
});
|
|
10278
|
+
return;
|
|
10279
|
+
}
|
|
9382
10280
|
const betaHeaderRaw = req.headers["anthropic-beta"];
|
|
9383
10281
|
const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
|
|
9384
10282
|
const clientWantsStream = Boolean(body.stream);
|
|
@@ -9403,7 +10301,11 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9403
10301
|
claudeCodeSessionId = identity.sessionId;
|
|
9404
10302
|
effectiveBeta = selectBetaFlags(forwardBody, upstreamModelId(model), inboundBeta);
|
|
9405
10303
|
}
|
|
9406
|
-
const refreshToken = isOAuth && model.providerId ? () =>
|
|
10304
|
+
const refreshToken = isOAuth && model.providerId && model.authRef ? (rejectedAccessToken) => resolveModelApiKey(
|
|
10305
|
+
model,
|
|
10306
|
+
options.apiKey,
|
|
10307
|
+
rejectedAccessToken
|
|
10308
|
+
) : void 0;
|
|
9407
10309
|
plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
|
|
9408
10310
|
await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
|
|
9409
10311
|
inboundBeta: effectiveBeta,
|
|
@@ -9431,7 +10333,15 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9431
10333
|
sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
|
|
9432
10334
|
return;
|
|
9433
10335
|
}
|
|
9434
|
-
|
|
10336
|
+
let apiKey;
|
|
10337
|
+
try {
|
|
10338
|
+
apiKey = await resolveModelApiKey(model, options.apiKey);
|
|
10339
|
+
} catch (err) {
|
|
10340
|
+
sendJson(res, 401, {
|
|
10341
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
10342
|
+
});
|
|
10343
|
+
return;
|
|
10344
|
+
}
|
|
9435
10345
|
auditInference(options, {
|
|
9436
10346
|
requestId,
|
|
9437
10347
|
modelId: body.model,
|
|
@@ -9441,14 +10351,6 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9441
10351
|
route: "translated",
|
|
9442
10352
|
requestPreview: getLatestMessagePreview(body.messages, body.system)
|
|
9443
10353
|
});
|
|
9444
|
-
const languageModel = await getOrInitLanguageModel(
|
|
9445
|
-
modelCache,
|
|
9446
|
-
model,
|
|
9447
|
-
model.npm,
|
|
9448
|
-
model.apiBaseUrl,
|
|
9449
|
-
apiKey,
|
|
9450
|
-
options.webSocketDiagnosticsLogPath
|
|
9451
|
-
);
|
|
9452
10354
|
const npmMaxTools = maxToolsForNpm(model.npm);
|
|
9453
10355
|
const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
|
|
9454
10356
|
if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
|
|
@@ -9472,63 +10374,93 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9472
10374
|
const clientWantsStream = Boolean(body.stream);
|
|
9473
10375
|
const responseModelId = getResponseModelId(body.model, model, options);
|
|
9474
10376
|
plog(() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
res.write(chunk);
|
|
9486
|
-
};
|
|
9487
|
-
await withResponsesWebSocketDiagnosticContext(
|
|
9488
|
-
{ requestId, claudeSessionId },
|
|
9489
|
-
() => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
|
|
9490
|
-
initialInputTokens: estimateAnthropicInputTokens(body)
|
|
9491
|
-
})
|
|
9492
|
-
);
|
|
9493
|
-
if (!res.headersSent) writeStreamChunk("");
|
|
9494
|
-
res.end();
|
|
9495
|
-
} else {
|
|
9496
|
-
const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
|
|
9497
|
-
{ requestId, claudeSessionId },
|
|
9498
|
-
() => generateAnthropicResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth })
|
|
10377
|
+
let sdkAttempt = 0;
|
|
10378
|
+
for (; ; ) {
|
|
10379
|
+
try {
|
|
10380
|
+
const languageModel = await getOrInitLanguageModel(
|
|
10381
|
+
modelCache,
|
|
10382
|
+
model,
|
|
10383
|
+
model.npm,
|
|
10384
|
+
model.apiBaseUrl,
|
|
10385
|
+
apiKey,
|
|
10386
|
+
options.webSocketDiagnosticsLogPath
|
|
9499
10387
|
);
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
10388
|
+
if (clientWantsStream) {
|
|
10389
|
+
const writeStreamChunk = (chunk) => {
|
|
10390
|
+
if (!res.headersSent) {
|
|
10391
|
+
res.writeHead(200, {
|
|
10392
|
+
"Content-Type": "text/event-stream",
|
|
10393
|
+
"Cache-Control": "no-cache",
|
|
10394
|
+
"Connection": "keep-alive"
|
|
10395
|
+
});
|
|
10396
|
+
}
|
|
10397
|
+
res.write(chunk);
|
|
10398
|
+
};
|
|
10399
|
+
await withResponsesWebSocketDiagnosticContext(
|
|
10400
|
+
{ requestId, claudeSessionId },
|
|
10401
|
+
() => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
|
|
10402
|
+
initialInputTokens: estimateAnthropicInputTokens(body)
|
|
10403
|
+
})
|
|
10404
|
+
);
|
|
10405
|
+
if (!res.headersSent) writeStreamChunk("");
|
|
10406
|
+
res.end();
|
|
9518
10407
|
} else {
|
|
9519
|
-
|
|
10408
|
+
const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
|
|
10409
|
+
{ requestId, claudeSessionId },
|
|
10410
|
+
() => generateAnthropicResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth })
|
|
10411
|
+
);
|
|
10412
|
+
sendJson(res, 200, anthropicResponse);
|
|
9520
10413
|
}
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
10414
|
+
break;
|
|
10415
|
+
} catch (err) {
|
|
10416
|
+
const message = formatUpstreamError(err);
|
|
10417
|
+
const details = sdkUpstreamErrorDetails(err);
|
|
10418
|
+
const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
10419
|
+
const replacement = await resolveOAuthRetryReplacement(
|
|
10420
|
+
openAiOAuth,
|
|
10421
|
+
candidateStatus,
|
|
10422
|
+
sdkAttempt,
|
|
10423
|
+
res.headersSent,
|
|
10424
|
+
apiKey,
|
|
10425
|
+
(rejectedAccessToken) => resolveModelApiKey(model, options.apiKey, rejectedAccessToken)
|
|
10426
|
+
);
|
|
10427
|
+
if (replacement) {
|
|
10428
|
+
apiKey = replacement;
|
|
10429
|
+
sdkAttempt += 1;
|
|
10430
|
+
plog("sdk oauth credential replaced after 401; retrying once");
|
|
10431
|
+
continue;
|
|
10432
|
+
}
|
|
10433
|
+
const { statusCode: status, retryAfterSeconds } = auditSdkError(options, body.model, model, err, message);
|
|
10434
|
+
const contextLengthExceeded = status === 400 && isContextLengthExceededError(err, message);
|
|
10435
|
+
const clientMessage = contextLengthExceeded ? anthropicPromptTooLongMessage(
|
|
10436
|
+
body,
|
|
10437
|
+
resolveContextWindow(upstreamModelId(model), model.contextWindow)
|
|
10438
|
+
) : message;
|
|
10439
|
+
plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
|
|
10440
|
+
if (!res.headersSent) {
|
|
10441
|
+
if (contextLengthExceeded) {
|
|
10442
|
+
sendJson(res, 400, {
|
|
10443
|
+
type: "error",
|
|
10444
|
+
error: { type: "invalid_request_error", message: clientMessage },
|
|
10445
|
+
request_id: requestId
|
|
10446
|
+
});
|
|
10447
|
+
} else {
|
|
10448
|
+
if (retryAfterSeconds !== void 0) res.setHeader("retry-after", String(retryAfterSeconds));
|
|
10449
|
+
sendJson(res, status === 500 ? 502 : status, { error: { message: clientMessage } });
|
|
10450
|
+
}
|
|
10451
|
+
} else {
|
|
10452
|
+
const errorType = anthropicErrorType(status);
|
|
10453
|
+
res.write(`event: error
|
|
9524
10454
|
data: ${JSON.stringify({
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
10455
|
+
type: "error",
|
|
10456
|
+
error: { type: errorType, message: clientMessage },
|
|
10457
|
+
...contextLengthExceeded ? { request_id: requestId } : {}
|
|
10458
|
+
})}
|
|
9529
10459
|
|
|
9530
10460
|
`);
|
|
9531
|
-
|
|
10461
|
+
res.end();
|
|
10462
|
+
}
|
|
10463
|
+
break;
|
|
9532
10464
|
}
|
|
9533
10465
|
}
|
|
9534
10466
|
return;
|
|
@@ -9553,7 +10485,15 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9553
10485
|
return;
|
|
9554
10486
|
}
|
|
9555
10487
|
const completionsUrl = model.completionsUrl;
|
|
9556
|
-
|
|
10488
|
+
let apiKey2;
|
|
10489
|
+
try {
|
|
10490
|
+
apiKey2 = await resolveModelApiKey(model, options.apiKey);
|
|
10491
|
+
} catch (err) {
|
|
10492
|
+
sendJson(res, 401, {
|
|
10493
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
10494
|
+
});
|
|
10495
|
+
return;
|
|
10496
|
+
}
|
|
9557
10497
|
const forwardBody = body.model === upstreamModelId(model) ? body : { ...body, model: upstreamModelId(model) };
|
|
9558
10498
|
auditInference(options, {
|
|
9559
10499
|
modelId: body.model,
|
|
@@ -9562,9 +10502,19 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9562
10502
|
route: "passthrough",
|
|
9563
10503
|
requestPreview: getLatestMessagePreview(body.messages, body.system)
|
|
9564
10504
|
});
|
|
10505
|
+
const isOAuth = model.authType === "oauth";
|
|
10506
|
+
const refreshToken = isOAuth && model.providerId && model.authRef ? (rejectedAccessToken) => resolveModelApiKey(
|
|
10507
|
+
model,
|
|
10508
|
+
options.apiKey,
|
|
10509
|
+
rejectedAccessToken
|
|
10510
|
+
) : void 0;
|
|
9565
10511
|
await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
|
|
9566
10512
|
authType: model.authType ?? "api",
|
|
9567
10513
|
extraHeaders: model.headers,
|
|
10514
|
+
refreshToken,
|
|
10515
|
+
onTokenRefreshed: (refreshed) => {
|
|
10516
|
+
model.apiKey = refreshed;
|
|
10517
|
+
},
|
|
9568
10518
|
onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
|
|
9569
10519
|
modelId: body.model,
|
|
9570
10520
|
provider: inferenceProvider(model),
|
|
@@ -9580,7 +10530,15 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9580
10530
|
sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
|
|
9581
10531
|
return;
|
|
9582
10532
|
}
|
|
9583
|
-
|
|
10533
|
+
let apiKey;
|
|
10534
|
+
try {
|
|
10535
|
+
apiKey = await resolveModelApiKey(model, options.apiKey);
|
|
10536
|
+
} catch (err) {
|
|
10537
|
+
sendJson(res, 401, {
|
|
10538
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
10539
|
+
});
|
|
10540
|
+
return;
|
|
10541
|
+
}
|
|
9584
10542
|
auditInference(options, {
|
|
9585
10543
|
modelId: body.model,
|
|
9586
10544
|
effort: openAiEffort(body),
|
|
@@ -9589,42 +10547,70 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9589
10547
|
requestPreview: getLatestMessagePreview(body.messages, body.system)
|
|
9590
10548
|
});
|
|
9591
10549
|
const baseURL = model.modelFormat === "anthropic" ? model.baseUrl : model.apiBaseUrl;
|
|
9592
|
-
const languageModel = await getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey);
|
|
9593
10550
|
const openAiOAuth = npm === "@ai-sdk/openai" && model.authType === "oauth";
|
|
9594
10551
|
const params = translateOpenAiRequest(body, { openAiOAuth });
|
|
9595
10552
|
const clientWantsStream = Boolean(body.stream);
|
|
9596
10553
|
const responseModelId = getResponseModelId(body.model, model, options);
|
|
9597
10554
|
plog(() => `sdk-openai npm=${npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
10555
|
+
let sdkAttempt = 0;
|
|
10556
|
+
for (; ; ) {
|
|
10557
|
+
try {
|
|
10558
|
+
const languageModel = await getOrInitLanguageModel(
|
|
10559
|
+
modelCache,
|
|
10560
|
+
model,
|
|
10561
|
+
npm,
|
|
10562
|
+
baseURL,
|
|
10563
|
+
apiKey
|
|
10564
|
+
);
|
|
10565
|
+
if (clientWantsStream) {
|
|
10566
|
+
const writeStreamChunk = (chunk) => {
|
|
10567
|
+
if (!res.headersSent) {
|
|
10568
|
+
res.writeHead(200, {
|
|
10569
|
+
"Content-Type": "text/event-stream",
|
|
10570
|
+
"Cache-Control": "no-cache",
|
|
10571
|
+
"Connection": "keep-alive"
|
|
10572
|
+
});
|
|
10573
|
+
}
|
|
10574
|
+
res.write(chunk);
|
|
10575
|
+
};
|
|
10576
|
+
await streamOpenAiResponse(languageModel, params, responseModelId, writeStreamChunk);
|
|
10577
|
+
if (!res.headersSent) writeStreamChunk("");
|
|
10578
|
+
res.end();
|
|
10579
|
+
} else {
|
|
10580
|
+
const response = await generateOpenAiResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth });
|
|
10581
|
+
sendJson(res, 200, response);
|
|
10582
|
+
}
|
|
10583
|
+
break;
|
|
10584
|
+
} catch (err) {
|
|
10585
|
+
const message = formatUpstreamError(err);
|
|
10586
|
+
const details = sdkUpstreamErrorDetails(err);
|
|
10587
|
+
const candidateStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
10588
|
+
const replacement = await resolveOAuthRetryReplacement(
|
|
10589
|
+
openAiOAuth,
|
|
10590
|
+
candidateStatus,
|
|
10591
|
+
sdkAttempt,
|
|
10592
|
+
res.headersSent,
|
|
10593
|
+
apiKey,
|
|
10594
|
+
(rejectedAccessToken) => resolveModelApiKey(model, options.apiKey, rejectedAccessToken)
|
|
10595
|
+
);
|
|
10596
|
+
if (replacement) {
|
|
10597
|
+
apiKey = replacement;
|
|
10598
|
+
sdkAttempt += 1;
|
|
10599
|
+
plog("sdk oauth credential replaced after 401; retrying once");
|
|
10600
|
+
continue;
|
|
10601
|
+
}
|
|
10602
|
+
const { statusCode: status, retryAfterSeconds } = auditSdkError(options, body.model, model, err, message);
|
|
10603
|
+
plog(`sdk error npm=${model.npm} upstream=${upstreamModelId(model)}: ${message}`);
|
|
10604
|
+
if (!res.headersSent) {
|
|
10605
|
+
if (retryAfterSeconds !== void 0) res.setHeader("retry-after", String(retryAfterSeconds));
|
|
10606
|
+
sendJson(res, status === 500 ? 502 : status, { error: { message } });
|
|
10607
|
+
} else {
|
|
10608
|
+
res.write(`data: ${JSON.stringify({ error: { message, type: "upstream_error", code: status } })}
|
|
9625
10609
|
|
|
9626
10610
|
`);
|
|
9627
|
-
|
|
10611
|
+
res.end();
|
|
10612
|
+
}
|
|
10613
|
+
break;
|
|
9628
10614
|
}
|
|
9629
10615
|
}
|
|
9630
10616
|
}
|
|
@@ -9648,9 +10634,9 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
|
|
|
9648
10634
|
npm,
|
|
9649
10635
|
baseURL ?? ""
|
|
9650
10636
|
].join("");
|
|
9651
|
-
let
|
|
9652
|
-
if (!
|
|
9653
|
-
languageModel = await createLanguageModel({
|
|
10637
|
+
let cached = modelCache.get(cacheKey);
|
|
10638
|
+
if (!cached || cached.apiKey !== apiKey) {
|
|
10639
|
+
const languageModel = await createLanguageModel({
|
|
9654
10640
|
npm,
|
|
9655
10641
|
modelId: upstreamModelId(model),
|
|
9656
10642
|
apiKey,
|
|
@@ -9663,9 +10649,10 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
|
|
|
9663
10649
|
preferWebSockets: model.preferWebSockets,
|
|
9664
10650
|
onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
|
|
9665
10651
|
});
|
|
9666
|
-
|
|
10652
|
+
cached = { apiKey, languageModel };
|
|
10653
|
+
modelCache.set(cacheKey, cached);
|
|
9667
10654
|
}
|
|
9668
|
-
return languageModel;
|
|
10655
|
+
return cached.languageModel;
|
|
9669
10656
|
}
|
|
9670
10657
|
function getResponseModelId(bodyModel, model, options) {
|
|
9671
10658
|
if (typeof bodyModel === "string" && options.aliasNames?.has(bodyModel)) return bodyModel;
|
|
@@ -9808,14 +10795,14 @@ import * as p8 from "@clack/prompts";
|
|
|
9808
10795
|
import * as http from "http";
|
|
9809
10796
|
import * as https from "https";
|
|
9810
10797
|
import * as net from "net";
|
|
9811
|
-
import { randomUUID as
|
|
10798
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
9812
10799
|
import { URL as URL2 } from "url";
|
|
9813
10800
|
import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
|
|
9814
10801
|
|
|
9815
10802
|
// src/http-proxy/ca.ts
|
|
9816
10803
|
import { randomBytes } from "crypto";
|
|
9817
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
9818
|
-
import { dirname as
|
|
10804
|
+
import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
10805
|
+
import { dirname as dirname6, join as join5, resolve } from "path";
|
|
9819
10806
|
import forge from "node-forge";
|
|
9820
10807
|
var CERT_DIR = "http-proxy";
|
|
9821
10808
|
var CA_CERT_FILE = "clodex-ca.pem";
|
|
@@ -9841,15 +10828,15 @@ function certPaths() {
|
|
|
9841
10828
|
};
|
|
9842
10829
|
}
|
|
9843
10830
|
function writePrivate(path, value) {
|
|
9844
|
-
|
|
10831
|
+
writeFileSync6(path, value, { encoding: "utf8", mode: 384 });
|
|
9845
10832
|
chmodSync5(path, 384);
|
|
9846
10833
|
}
|
|
9847
10834
|
function writePublic(path, value) {
|
|
9848
|
-
|
|
10835
|
+
writeFileSync6(path, value, { encoding: "utf8", mode: 420 });
|
|
9849
10836
|
chmodSync5(path, 420);
|
|
9850
10837
|
}
|
|
9851
10838
|
function generateCertificates(paths) {
|
|
9852
|
-
|
|
10839
|
+
mkdirSync7(paths.dir, { recursive: true, mode: 448 });
|
|
9853
10840
|
chmodSync5(paths.dir, 448);
|
|
9854
10841
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
9855
10842
|
const caCert = forge.pki.createCertificate();
|
|
@@ -9890,8 +10877,8 @@ function generateCertificates(paths) {
|
|
|
9890
10877
|
}
|
|
9891
10878
|
function storedCertificatesAreCurrent(paths) {
|
|
9892
10879
|
try {
|
|
9893
|
-
const ca = forge.pki.certificateFromPem(
|
|
9894
|
-
const server = forge.pki.certificateFromPem(
|
|
10880
|
+
const ca = forge.pki.certificateFromPem(readFileSync8(paths.caCert, "utf8"));
|
|
10881
|
+
const server = forge.pki.certificateFromPem(readFileSync8(paths.serverCert, "utf8"));
|
|
9895
10882
|
const now = Date.now();
|
|
9896
10883
|
const renewalBuffer = 7 * 24 * 60 * 60 * 1e3;
|
|
9897
10884
|
return ca.validity.notBefore.getTime() <= now && ca.validity.notAfter.getTime() > now + renewalBuffer && server.validity.notBefore.getTime() <= now && server.validity.notAfter.getTime() > now + renewalBuffer && ca.verify(ca) && ca.verify(server);
|
|
@@ -9902,23 +10889,23 @@ function storedCertificatesAreCurrent(paths) {
|
|
|
9902
10889
|
function ensureHttpProxyCertificates() {
|
|
9903
10890
|
const paths = certPaths();
|
|
9904
10891
|
const required = [paths.caCert, paths.caKey, paths.serverCert, paths.serverKey, paths.version];
|
|
9905
|
-
const current = required.every(
|
|
10892
|
+
const current = required.every(existsSync6) && readFileSync8(paths.version, "utf8") === CERT_VERSION && storedCertificatesAreCurrent(paths);
|
|
9906
10893
|
if (!current) generateCertificates(paths);
|
|
9907
10894
|
return {
|
|
9908
10895
|
caCertPath: paths.caCert,
|
|
9909
|
-
caCert:
|
|
9910
|
-
serverCert:
|
|
9911
|
-
serverKey:
|
|
10896
|
+
caCert: readFileSync8(paths.caCert, "utf8"),
|
|
10897
|
+
serverCert: readFileSync8(paths.serverCert, "utf8"),
|
|
10898
|
+
serverKey: readFileSync8(paths.serverKey, "utf8")
|
|
9912
10899
|
};
|
|
9913
10900
|
}
|
|
9914
10901
|
function ensureHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
|
|
9915
10902
|
if (!additionalCaCertPath?.trim()) return relayCaCertPath;
|
|
9916
10903
|
try {
|
|
9917
10904
|
if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) return relayCaCertPath;
|
|
9918
|
-
const relayCa =
|
|
9919
|
-
const additionalCa =
|
|
10905
|
+
const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
|
|
10906
|
+
const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
|
|
9920
10907
|
if (!additionalCa) return relayCaCertPath;
|
|
9921
|
-
const combinedPath = join5(
|
|
10908
|
+
const combinedPath = join5(dirname6(relayCaCertPath), "combined-ca.pem");
|
|
9922
10909
|
writePublic(combinedPath, `${relayCa}
|
|
9923
10910
|
${additionalCa}
|
|
9924
10911
|
`);
|
|
@@ -10472,7 +11459,7 @@ async function startHttpProxy(options) {
|
|
|
10472
11459
|
}
|
|
10473
11460
|
const messagesEndpoint = anthropicMessagesEndpoint(req.url);
|
|
10474
11461
|
if (req.method === "POST" && messagesEndpoint) {
|
|
10475
|
-
const requestId =
|
|
11462
|
+
const requestId = randomUUID10();
|
|
10476
11463
|
let parsed = null;
|
|
10477
11464
|
let route;
|
|
10478
11465
|
try {
|
|
@@ -10720,12 +11707,13 @@ function waitForShutdown() {
|
|
|
10720
11707
|
}
|
|
10721
11708
|
async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = false, port, noDiscovery = false) {
|
|
10722
11709
|
const webSocketDiagnosticsLogPath = webSocketDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
|
|
11710
|
+
const inferenceLogPath = getInferenceRequestLogPath();
|
|
10723
11711
|
let started;
|
|
10724
11712
|
try {
|
|
10725
11713
|
started = await startConfiguredHttpProxy(
|
|
10726
11714
|
port ?? DEFAULT_SERVER_PORT,
|
|
10727
11715
|
debug,
|
|
10728
|
-
|
|
11716
|
+
inferenceLogPath,
|
|
10729
11717
|
void 0,
|
|
10730
11718
|
webSocketDiagnosticsLogPath
|
|
10731
11719
|
);
|
|
@@ -10734,6 +11722,13 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
|
|
|
10734
11722
|
return 1;
|
|
10735
11723
|
}
|
|
10736
11724
|
const { handle, loaded } = started;
|
|
11725
|
+
writeProxyLifecycleLog(inferenceLogPath, {
|
|
11726
|
+
event: "proxy_started",
|
|
11727
|
+
pid: process.pid,
|
|
11728
|
+
parentPid: process.ppid,
|
|
11729
|
+
host: handle.host,
|
|
11730
|
+
port: handle.port
|
|
11731
|
+
});
|
|
10737
11732
|
console.log("");
|
|
10738
11733
|
console.log(pc9.bold(pc9.green("clodex proxy-mode server running")));
|
|
10739
11734
|
console.log(` HTTPS_PROXY=http://127.0.0.1:${handle.port}`);
|
|
@@ -10761,8 +11756,23 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
|
|
|
10761
11756
|
});
|
|
10762
11757
|
}
|
|
10763
11758
|
await waitForShutdown();
|
|
11759
|
+
writeProxyLifecycleLog(inferenceLogPath, {
|
|
11760
|
+
event: "proxy_stopping",
|
|
11761
|
+
pid: process.pid,
|
|
11762
|
+
parentPid: process.ppid,
|
|
11763
|
+
host: handle.host,
|
|
11764
|
+
port: handle.port,
|
|
11765
|
+
reason: "shutdown signal received"
|
|
11766
|
+
});
|
|
10764
11767
|
if (!noDiscovery) unregisterServerRuntimeState();
|
|
10765
11768
|
await handle.close();
|
|
11769
|
+
writeProxyLifecycleLog(inferenceLogPath, {
|
|
11770
|
+
event: "proxy_stopped",
|
|
11771
|
+
pid: process.pid,
|
|
11772
|
+
parentPid: process.ppid,
|
|
11773
|
+
host: handle.host,
|
|
11774
|
+
port: handle.port
|
|
11775
|
+
});
|
|
10766
11776
|
return 0;
|
|
10767
11777
|
}
|
|
10768
11778
|
|
|
@@ -11375,17 +12385,17 @@ function planLaunchWizard(opts) {
|
|
|
11375
12385
|
}
|
|
11376
12386
|
|
|
11377
12387
|
// src/patcher.ts
|
|
11378
|
-
import { createHash as
|
|
12388
|
+
import { createHash as createHash8 } from "crypto";
|
|
11379
12389
|
import {
|
|
11380
12390
|
copyFileSync as copyFileSync2,
|
|
11381
|
-
existsSync as
|
|
11382
|
-
mkdirSync as
|
|
11383
|
-
readFileSync as
|
|
12391
|
+
existsSync as existsSync7,
|
|
12392
|
+
mkdirSync as mkdirSync8,
|
|
12393
|
+
readFileSync as readFileSync9,
|
|
11384
12394
|
statSync as statSync4,
|
|
11385
|
-
unlinkSync as
|
|
11386
|
-
writeFileSync as
|
|
11387
|
-
openSync as
|
|
11388
|
-
closeSync as
|
|
12395
|
+
unlinkSync as unlinkSync5,
|
|
12396
|
+
writeFileSync as writeFileSync7,
|
|
12397
|
+
openSync as openSync5,
|
|
12398
|
+
closeSync as closeSync5,
|
|
11389
12399
|
realpathSync
|
|
11390
12400
|
} from "fs";
|
|
11391
12401
|
import { homedir as homedir2 } from "os";
|
|
@@ -11582,7 +12592,7 @@ function getPatchLockPath() {
|
|
|
11582
12592
|
}
|
|
11583
12593
|
function readPatchManifest(path = getPatchManifestPath()) {
|
|
11584
12594
|
try {
|
|
11585
|
-
const parsed = JSON.parse(
|
|
12595
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
11586
12596
|
if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
|
|
11587
12597
|
return parsed;
|
|
11588
12598
|
}
|
|
@@ -11591,8 +12601,8 @@ function readPatchManifest(path = getPatchManifestPath()) {
|
|
|
11591
12601
|
return null;
|
|
11592
12602
|
}
|
|
11593
12603
|
function writePatchManifest(manifest, path = getPatchManifestPath()) {
|
|
11594
|
-
|
|
11595
|
-
|
|
12604
|
+
mkdirSync8(getAppHome(), { recursive: true, mode: 448 });
|
|
12605
|
+
writeFileSync7(path, `${JSON.stringify(manifest, null, 2)}
|
|
11596
12606
|
`, { encoding: "utf8", mode: 384 });
|
|
11597
12607
|
}
|
|
11598
12608
|
function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
|
|
@@ -11620,7 +12630,7 @@ function computePatchConfigHash(config) {
|
|
|
11620
12630
|
const entry = config[key];
|
|
11621
12631
|
return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
|
|
11622
12632
|
});
|
|
11623
|
-
return
|
|
12633
|
+
return createHash8("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
11624
12634
|
}
|
|
11625
12635
|
function buildDesiredPatchConfig() {
|
|
11626
12636
|
const prefs = loadPreferences();
|
|
@@ -11663,30 +12673,30 @@ function pidIsAlive(pid) {
|
|
|
11663
12673
|
function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
|
|
11664
12674
|
const now = opts.now ?? Date.now();
|
|
11665
12675
|
const isAlive = opts.isAlive ?? pidIsAlive;
|
|
11666
|
-
|
|
12676
|
+
mkdirSync8(join6(lockPath, ".."), { recursive: true, mode: 448 });
|
|
11667
12677
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
11668
12678
|
try {
|
|
11669
|
-
const fd =
|
|
12679
|
+
const fd = openSync5(lockPath, "wx");
|
|
11670
12680
|
const content = { pid: process.pid, startedAt: now };
|
|
11671
|
-
|
|
11672
|
-
|
|
12681
|
+
writeFileSync7(fd, JSON.stringify(content));
|
|
12682
|
+
closeSync5(fd);
|
|
11673
12683
|
return () => {
|
|
11674
12684
|
try {
|
|
11675
|
-
|
|
12685
|
+
unlinkSync5(lockPath);
|
|
11676
12686
|
} catch {
|
|
11677
12687
|
}
|
|
11678
12688
|
};
|
|
11679
12689
|
} catch {
|
|
11680
12690
|
let stale = false;
|
|
11681
12691
|
try {
|
|
11682
|
-
const existing = JSON.parse(
|
|
12692
|
+
const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
|
|
11683
12693
|
stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
|
|
11684
12694
|
} catch {
|
|
11685
12695
|
stale = true;
|
|
11686
12696
|
}
|
|
11687
12697
|
if (!stale) return null;
|
|
11688
12698
|
try {
|
|
11689
|
-
|
|
12699
|
+
unlinkSync5(lockPath);
|
|
11690
12700
|
} catch {
|
|
11691
12701
|
}
|
|
11692
12702
|
}
|
|
@@ -11694,12 +12704,12 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
|
|
|
11694
12704
|
return null;
|
|
11695
12705
|
}
|
|
11696
12706
|
function sha256File(path) {
|
|
11697
|
-
return
|
|
12707
|
+
return createHash8("sha256").update(readFileSync9(path)).digest("hex");
|
|
11698
12708
|
}
|
|
11699
12709
|
function resolveClaudeBinaryForPatch() {
|
|
11700
12710
|
const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
|
|
11701
12711
|
const nativeSymlink = join6(homedir2(), ".local", "bin", "claude");
|
|
11702
|
-
const source = envOverride?.trim() || (
|
|
12712
|
+
const source = envOverride?.trim() || (existsSync7(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
|
|
11703
12713
|
if (!source) return null;
|
|
11704
12714
|
let resolved;
|
|
11705
12715
|
try {
|
|
@@ -11734,13 +12744,13 @@ function summarizePatchResults(results) {
|
|
|
11734
12744
|
}
|
|
11735
12745
|
async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
11736
12746
|
const backup = pristineBackupPath(version, binaryPath);
|
|
11737
|
-
|
|
12747
|
+
mkdirSync8(backupDir(), { recursive: true });
|
|
11738
12748
|
if (opts.restoreFirst) {
|
|
11739
|
-
if (!
|
|
12749
|
+
if (!existsSync7(backup)) {
|
|
11740
12750
|
return { ok: false, message: `Cannot re-patch: pristine backup missing at ${backup}. Reinstall claude, then run clodex patch.` };
|
|
11741
12751
|
}
|
|
11742
12752
|
copyFileSync2(backup, binaryPath);
|
|
11743
|
-
} else if (!
|
|
12753
|
+
} else if (!existsSync7(backup)) {
|
|
11744
12754
|
copyFileSync2(binaryPath, backup);
|
|
11745
12755
|
}
|
|
11746
12756
|
copyFileSync2(backup, join6(backupDir(), "native-binary.backup"));
|
|
@@ -11796,14 +12806,14 @@ async function runPatchCommand(opts = {}) {
|
|
|
11796
12806
|
const { binaryPath, version } = resolved;
|
|
11797
12807
|
if (opts.restore) {
|
|
11798
12808
|
const manifest2 = readPatchManifest();
|
|
11799
|
-
const backup = manifest2?.backupPath &&
|
|
11800
|
-
if (!
|
|
12809
|
+
const backup = manifest2?.backupPath && existsSync7(manifest2.backupPath) ? manifest2.backupPath : pristineBackupPath(version, binaryPath);
|
|
12810
|
+
if (!existsSync7(backup)) {
|
|
11801
12811
|
p11.log.error(`No pristine backup found for claude ${version} (${backup}).`);
|
|
11802
12812
|
return 1;
|
|
11803
12813
|
}
|
|
11804
12814
|
copyFileSync2(backup, binaryPath);
|
|
11805
12815
|
try {
|
|
11806
|
-
|
|
12816
|
+
unlinkSync5(getPatchManifestPath());
|
|
11807
12817
|
} catch {
|
|
11808
12818
|
}
|
|
11809
12819
|
p11.log.success(`Restored pristine claude ${version} from ${backup}.`);
|
|
@@ -11836,7 +12846,7 @@ async function runPatchCommand(opts = {}) {
|
|
|
11836
12846
|
}
|
|
11837
12847
|
try {
|
|
11838
12848
|
const backup = pristineBackupPath(version, binaryPath);
|
|
11839
|
-
const restoreFirst =
|
|
12849
|
+
const restoreFirst = existsSync7(backup) && sha256File(backup) !== sha256File(binaryPath);
|
|
11840
12850
|
if (restoreFirst) {
|
|
11841
12851
|
p11.log.info("Binary differs from its pristine backup \u2014 restoring it before patching fresh.");
|
|
11842
12852
|
}
|