@bman654/clodex 1.2.1 → 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 +7 -2
- 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 +1572 -465
- package/dist/cli.js.map +1 -1
- package/docs/credential-helpers.md +42 -6
- 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;
|
|
@@ -2731,13 +2966,12 @@ function providersForPicker(providers) {
|
|
|
2731
2966
|
return providers.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true }));
|
|
2732
2967
|
}
|
|
2733
2968
|
async function resolveLocalProviderApiKey(provider) {
|
|
2734
|
-
if (provider.authRef === "none:anonymous") return "
|
|
2969
|
+
if (provider.authRef === "none:anonymous" || provider.authType === "none") return "";
|
|
2735
2970
|
const direct = provider.apiKey?.trim();
|
|
2736
2971
|
if (direct) return direct;
|
|
2737
|
-
if (provider.authType === "none") return "anonymous";
|
|
2738
2972
|
const template = getTemplateById(provider.id);
|
|
2739
2973
|
if (template?.apiKeyOptional || template?.anonymousFreeModels) {
|
|
2740
|
-
return "
|
|
2974
|
+
return "";
|
|
2741
2975
|
}
|
|
2742
2976
|
const reg = loadRegistry().providers.find((p13) => p13.id === provider.id);
|
|
2743
2977
|
const authRef = provider.authRef ?? reg?.authRef ?? oauthAuthRef(provider.id);
|
|
@@ -2796,6 +3030,7 @@ function localProvidersToServerModels(localProviders) {
|
|
|
2796
3030
|
npm: model.modelFormat === "openai" ? model.npm || "@ai-sdk/openai-compatible" : model.npm,
|
|
2797
3031
|
apiBaseUrl: model.apiBaseUrl,
|
|
2798
3032
|
apiKey: provider.apiKey,
|
|
3033
|
+
authRef: provider.authRef,
|
|
2799
3034
|
authType: provider.authType,
|
|
2800
3035
|
oauthAccountId: provider.oauthAccountId,
|
|
2801
3036
|
contextWindow: model.contextWindow,
|
|
@@ -2810,11 +3045,14 @@ function localProvidersToServerModels(localProviders) {
|
|
|
2810
3045
|
);
|
|
2811
3046
|
}
|
|
2812
3047
|
|
|
3048
|
+
// src/registry/add-template.ts
|
|
3049
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3050
|
+
|
|
2813
3051
|
// src/provider-factory.ts
|
|
2814
3052
|
import { wrapLanguageModel, extractReasoningMiddleware } from "ai";
|
|
2815
3053
|
|
|
2816
3054
|
// src/oauth/responses-websocket.ts
|
|
2817
|
-
import { createHash as
|
|
3055
|
+
import { createHash as createHash4 } from "crypto";
|
|
2818
3056
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
2819
3057
|
|
|
2820
3058
|
// src/outbound-proxy.ts
|
|
@@ -2878,6 +3116,27 @@ async function outboundWsProxyAgent(wsUrl) {
|
|
|
2878
3116
|
|
|
2879
3117
|
// src/upstream-error.ts
|
|
2880
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
|
+
}
|
|
2881
3140
|
function sdkUpstreamErrorDetails(err) {
|
|
2882
3141
|
const retry = RetryError.isInstance(err) ? err : void 0;
|
|
2883
3142
|
const inner = retry?.lastError ?? err;
|
|
@@ -2889,11 +3148,14 @@ function sdkUpstreamErrorDetails(err) {
|
|
|
2889
3148
|
} catch {
|
|
2890
3149
|
}
|
|
2891
3150
|
}
|
|
3151
|
+
const rawRetryAfter = inner.statusCode === 429 ? numericRetryAfterSeconds(inner) : void 0;
|
|
3152
|
+
const retryAfterSeconds = rawRetryAfter === void 0 ? void 0 : clampRetryAfterSeconds(rawRetryAfter);
|
|
2892
3153
|
return {
|
|
2893
3154
|
statusCode: inner.statusCode,
|
|
2894
3155
|
errorContent: errorContent || inner.message,
|
|
2895
3156
|
isRetryable: inner.isRetryable,
|
|
2896
|
-
attemptCount: retry?.errors.length ?? 1
|
|
3157
|
+
attemptCount: retry?.errors.length ?? 1,
|
|
3158
|
+
...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
|
|
2897
3159
|
};
|
|
2898
3160
|
}
|
|
2899
3161
|
function isContextLengthExceededError(err, formattedMessage = "") {
|
|
@@ -3058,7 +3320,7 @@ function hasResponsesLiteHeader(headers) {
|
|
|
3058
3320
|
}
|
|
3059
3321
|
function authorizationHeaderFingerprint(headers) {
|
|
3060
3322
|
const authorization = Object.entries(headers).find(([key]) => key.toLowerCase() === "authorization")?.[1];
|
|
3061
|
-
return authorization ?
|
|
3323
|
+
return authorization ? createHash4("sha256").update(authorization).digest("hex") : "";
|
|
3062
3324
|
}
|
|
3063
3325
|
function bodyToString(body) {
|
|
3064
3326
|
if (body == null) return "";
|
|
@@ -3091,13 +3353,13 @@ function responsesWebSocketPromptFingerprint(payload) {
|
|
|
3091
3353
|
delete stable.previous_response_id;
|
|
3092
3354
|
delete stable.stream;
|
|
3093
3355
|
delete stable.background;
|
|
3094
|
-
return
|
|
3356
|
+
return createHash4("sha256").update(canonicalJson(stable)).digest("hex");
|
|
3095
3357
|
}
|
|
3096
3358
|
function responsesWebSocketPromptFieldHashes(payload) {
|
|
3097
3359
|
const hashes = {};
|
|
3098
3360
|
for (const key of Object.keys(payload).sort()) {
|
|
3099
3361
|
if (key === "input" || key === "previous_response_id" || key === "stream" || key === "background") continue;
|
|
3100
|
-
hashes[key] =
|
|
3362
|
+
hashes[key] = createHash4("sha256").update(canonicalJson(payload[key])).digest("hex").slice(0, 12);
|
|
3101
3363
|
}
|
|
3102
3364
|
return hashes;
|
|
3103
3365
|
}
|
|
@@ -3133,7 +3395,7 @@ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizat
|
|
|
3133
3395
|
promptCacheKey,
|
|
3134
3396
|
authorizationFingerprint
|
|
3135
3397
|
].join("");
|
|
3136
|
-
return
|
|
3398
|
+
return createHash4("sha256").update(material).digest("hex");
|
|
3137
3399
|
}
|
|
3138
3400
|
function inputArray(payload) {
|
|
3139
3401
|
return Array.isArray(payload.input) ? payload.input : [];
|
|
@@ -3164,7 +3426,7 @@ function conversationItemKind(value) {
|
|
|
3164
3426
|
return "object";
|
|
3165
3427
|
}
|
|
3166
3428
|
function conversationItemHash(value) {
|
|
3167
|
-
return
|
|
3429
|
+
return createHash4("sha256").update(canonicalJson(normalizeToolCallJson(value))).digest("hex").slice(0, 16);
|
|
3168
3430
|
}
|
|
3169
3431
|
function continuationMismatchDetails(entry, payload) {
|
|
3170
3432
|
const full = inputArray(payload);
|
|
@@ -3230,7 +3492,7 @@ function diagnosticTextFingerprint(field, value) {
|
|
|
3230
3492
|
if (typeof value !== "string" || value.length === 0) return {};
|
|
3231
3493
|
return {
|
|
3232
3494
|
[`${field}Bytes`]: Buffer.byteLength(value),
|
|
3233
|
-
[`${field}Hash`]:
|
|
3495
|
+
[`${field}Hash`]: createHash4("sha256").update(value).digest("hex").slice(0, 16)
|
|
3234
3496
|
};
|
|
3235
3497
|
}
|
|
3236
3498
|
function responseFailureDetails(event) {
|
|
@@ -3265,7 +3527,7 @@ function emitResponseErrorDiagnostic(entry, ctx, details) {
|
|
|
3265
3527
|
emitContextDiagnostic(entry, ctx, { event: "ws_response_error", ...details });
|
|
3266
3528
|
}
|
|
3267
3529
|
function diagnosticItemIdHash(value) {
|
|
3268
|
-
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;
|
|
3269
3531
|
}
|
|
3270
3532
|
function reasoningPartIndex(value) {
|
|
3271
3533
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
@@ -3500,7 +3762,7 @@ function deleteEntry(entry, closeSocket = true) {
|
|
|
3500
3762
|
}
|
|
3501
3763
|
}
|
|
3502
3764
|
}
|
|
3503
|
-
function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
|
|
3765
|
+
function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAfterSeconds) {
|
|
3504
3766
|
if (ctx.closed || entry.current !== ctx) return;
|
|
3505
3767
|
entry.debug(`fail: ${message}`);
|
|
3506
3768
|
emitResponseErrorDiagnostic(entry, ctx, {
|
|
@@ -3515,12 +3777,64 @@ function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
|
|
|
3515
3777
|
type: statusCode === void 0 ? "transport_error" : anthropicErrorType(statusCode),
|
|
3516
3778
|
code: statusCode === void 0 ? "websocket_transport_error" : String(statusCode),
|
|
3517
3779
|
message,
|
|
3518
|
-
param: null
|
|
3780
|
+
param: null,
|
|
3781
|
+
...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
|
|
3519
3782
|
}
|
|
3520
3783
|
});
|
|
3521
3784
|
deleteEntry(entry);
|
|
3522
3785
|
closeContext(ctx);
|
|
3523
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
|
+
}
|
|
3524
3838
|
function cleanupExpiredConnections(now) {
|
|
3525
3839
|
const evictions = [];
|
|
3526
3840
|
for (const entry of connectionEntries()) {
|
|
@@ -3568,7 +3882,27 @@ function sendContext(entry, ctx) {
|
|
|
3568
3882
|
entry.debug(
|
|
3569
3883
|
`connection=${entry.debugId} key=${debugKey(entry.key)} sending ${outgoing.length}B payload` + (ctx.continued ? " (continuation)" : "")
|
|
3570
3884
|
);
|
|
3571
|
-
|
|
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
|
+
}
|
|
3572
3906
|
}
|
|
3573
3907
|
function dispatchContext(entry, ctx) {
|
|
3574
3908
|
const now = entry.options.now();
|
|
@@ -3601,6 +3935,14 @@ function handleSocketMessage(entry, data) {
|
|
|
3601
3935
|
if (!ctx || ctx.closed) return;
|
|
3602
3936
|
const text4 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
|
|
3603
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
|
+
}
|
|
3604
3946
|
let event;
|
|
3605
3947
|
try {
|
|
3606
3948
|
event = JSON.parse(text4);
|
|
@@ -3675,6 +4017,10 @@ function handleSocketMessage(entry, data) {
|
|
|
3675
4017
|
closeContext(ctx);
|
|
3676
4018
|
}
|
|
3677
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
|
+
}
|
|
3678
4024
|
function createConnection(WebSocket, wsUrl, headers, persistent, key, options, debug, agent) {
|
|
3679
4025
|
const now = options.now();
|
|
3680
4026
|
const socket = new WebSocket(wsUrl, agent ? { headers, agent } : { headers });
|
|
@@ -3708,24 +4054,39 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
|
|
|
3708
4054
|
debug(`unexpected-response status=${statusCode}`);
|
|
3709
4055
|
response.resume();
|
|
3710
4056
|
const ctx = entry.current;
|
|
3711
|
-
if (ctx
|
|
3712
|
-
failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
|
|
3713
|
-
source: "unexpected_response",
|
|
3714
|
-
httpStatusCode: statusCode
|
|
3715
|
-
}, statusCode);
|
|
3716
|
-
} else {
|
|
4057
|
+
if (!ctx || ctx.closed) {
|
|
3717
4058
|
deleteEntry(entry);
|
|
4059
|
+
return;
|
|
3718
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);
|
|
3719
4077
|
});
|
|
3720
4078
|
socket.on("message", (data) => handleSocketMessage(entry, data));
|
|
3721
4079
|
socket.on("error", (error) => {
|
|
3722
4080
|
const ctx = entry.current;
|
|
3723
|
-
if (ctx)
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
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);
|
|
3729
4090
|
});
|
|
3730
4091
|
socket.on("close", (code, reason) => {
|
|
3731
4092
|
entry.open = false;
|
|
@@ -3734,7 +4095,7 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
|
|
|
3734
4095
|
if (ctx && !ctx.closed) {
|
|
3735
4096
|
const reasonText = reason?.length ? reason.toString("utf8") : "";
|
|
3736
4097
|
const suffix = reasonText ? `: ${reasonText}` : "";
|
|
3737
|
-
|
|
4098
|
+
handleTransportFailure(entry, ctx, `WebSocket closed (${code})${suffix}`, {
|
|
3738
4099
|
source: "socket_close",
|
|
3739
4100
|
closeCode: code,
|
|
3740
4101
|
...diagnosticTextFingerprint("closeReason", reasonText)
|
|
@@ -3852,7 +4213,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3852
4213
|
keyTuple: {
|
|
3853
4214
|
wsUrl,
|
|
3854
4215
|
providerId: options.providerId ?? "openai",
|
|
3855
|
-
accountIdHash: options.accountId ?
|
|
4216
|
+
accountIdHash: options.accountId ? createHash4("sha256").update(options.accountId).digest("hex").slice(0, 16) : "",
|
|
3856
4217
|
model: typeof payload.model === "string" ? payload.model : void 0,
|
|
3857
4218
|
effort: typeof payload.reasoning?.effort === "string" ? String(payload.reasoning.effort).trim().toLowerCase() : "",
|
|
3858
4219
|
promptCacheKey: typeof payload.prompt_cache_key === "string" ? payload.prompt_cache_key : void 0
|
|
@@ -3909,6 +4270,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3909
4270
|
frameCount: 0,
|
|
3910
4271
|
pendingEvents: [],
|
|
3911
4272
|
emittedModelData: false,
|
|
4273
|
+
transportRetryPending: false,
|
|
3912
4274
|
outputByIndex: /* @__PURE__ */ new Map(),
|
|
3913
4275
|
outputIndexByItemId: /* @__PURE__ */ new Map(),
|
|
3914
4276
|
reasoningPartsByItemId: /* @__PURE__ */ new Map(),
|
|
@@ -3919,7 +4281,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3919
4281
|
WebSocket,
|
|
3920
4282
|
wsUrl,
|
|
3921
4283
|
headers,
|
|
3922
|
-
|
|
4284
|
+
persistent,
|
|
3923
4285
|
partitionKey,
|
|
3924
4286
|
resolvedOptions,
|
|
3925
4287
|
debug,
|
|
@@ -3967,7 +4329,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
|
|
|
3967
4329
|
}
|
|
3968
4330
|
|
|
3969
4331
|
// src/oauth/claude-identity.ts
|
|
3970
|
-
import { createHash as
|
|
4332
|
+
import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
|
|
3971
4333
|
var CLAUDE_CODE_CLI_VERSION = "2.1.195";
|
|
3972
4334
|
var CLAUDE_CODE_USER_AGENT = `claude-cli/${CLAUDE_CODE_CLI_VERSION} (external, cli)`;
|
|
3973
4335
|
var CLAUDE_CODE_ENTRYPOINT = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
|
|
@@ -3982,7 +4344,7 @@ function getOrCreateSessionId(seed) {
|
|
|
3982
4344
|
return id;
|
|
3983
4345
|
}
|
|
3984
4346
|
function uuidFromHash(input) {
|
|
3985
|
-
const h =
|
|
4347
|
+
const h = createHash5("sha256").update(input).digest("hex");
|
|
3986
4348
|
return [
|
|
3987
4349
|
h.slice(0, 8),
|
|
3988
4350
|
h.slice(8, 12),
|
|
@@ -3996,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;
|
|
|
3996
4358
|
function resolveCliUserID(providerData, seed) {
|
|
3997
4359
|
const v = providerData?.cliUserID;
|
|
3998
4360
|
if (typeof v === "string" && HEX64_RE.test(v)) return v;
|
|
3999
|
-
return
|
|
4361
|
+
return createHash5("sha256").update(`cliUserID:${seed}`).digest("hex");
|
|
4000
4362
|
}
|
|
4001
4363
|
function resolveAccountUUID(providerData, seed) {
|
|
4002
4364
|
const v = providerData?.accountUUID;
|
|
@@ -4080,6 +4442,12 @@ function injectClaudeIdentity(body, providerData, seed) {
|
|
|
4080
4442
|
return { sessionId, userId };
|
|
4081
4443
|
}
|
|
4082
4444
|
|
|
4445
|
+
// src/credential-headers.ts
|
|
4446
|
+
var CREDENTIAL_BEARING_HEADER = /(?:^|[-_])(?:authorization|api[-_]?key|cookie|token|secret|credential)(?:$|[-_])/i;
|
|
4447
|
+
function isCredentialBearingHeader(name) {
|
|
4448
|
+
return CREDENTIAL_BEARING_HEADER.test(name);
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4083
4451
|
// src/provider-factory.ts
|
|
4084
4452
|
var RESPONSES_ONLY_PREFIXES = [
|
|
4085
4453
|
"gpt-5-codex",
|
|
@@ -4089,6 +4457,15 @@ var RESPONSES_ONLY_PREFIXES = [
|
|
|
4089
4457
|
"o4"
|
|
4090
4458
|
];
|
|
4091
4459
|
var factoryCache = /* @__PURE__ */ new Map();
|
|
4460
|
+
var fetchWithoutCredentialHeaders = (input, init) => {
|
|
4461
|
+
const headers = new Headers(
|
|
4462
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
4463
|
+
);
|
|
4464
|
+
for (const name of [...headers.keys()]) {
|
|
4465
|
+
if (isCredentialBearingHeader(name)) headers.delete(name);
|
|
4466
|
+
}
|
|
4467
|
+
return fetch(input, { ...init, headers });
|
|
4468
|
+
};
|
|
4092
4469
|
function modelPrefersResponsesApi(modelId) {
|
|
4093
4470
|
const lower = modelId.toLowerCase();
|
|
4094
4471
|
if (RESPONSES_ONLY_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(`${prefix}-`))) {
|
|
@@ -4153,6 +4530,7 @@ async function createLanguageModel(spec) {
|
|
|
4153
4530
|
apiKey,
|
|
4154
4531
|
baseURL: "https://chatgpt.com/backend-api/codex",
|
|
4155
4532
|
headers: {
|
|
4533
|
+
...spec.headers,
|
|
4156
4534
|
...accountId ? { "ChatGPT-Account-Id": accountId } : {},
|
|
4157
4535
|
originator: "clodex",
|
|
4158
4536
|
// Responses-Lite models (backend prefer_websockets/use_responses_lite,
|
|
@@ -4170,7 +4548,11 @@ async function createLanguageModel(spec) {
|
|
|
4170
4548
|
onDiagnostic: spec.onWebSocketDiagnostic
|
|
4171
4549
|
})
|
|
4172
4550
|
} : {}
|
|
4173
|
-
} :
|
|
4551
|
+
} : spec.authType === "none" ? {
|
|
4552
|
+
apiKey: "",
|
|
4553
|
+
...spec.headers ? { headers: spec.headers } : {},
|
|
4554
|
+
fetch: fetchWithoutCredentialHeaders
|
|
4555
|
+
} : { apiKey, ...spec.headers ? { headers: spec.headers } : {} };
|
|
4174
4556
|
const openai = createOpenAI(oauthOptions);
|
|
4175
4557
|
return useResponsesEndpoint ? openai.responses(modelId) : openai.chat(modelId);
|
|
4176
4558
|
}
|
|
@@ -4190,7 +4572,7 @@ async function createLanguageModel(spec) {
|
|
|
4190
4572
|
).sessionId
|
|
4191
4573
|
}
|
|
4192
4574
|
} : {}
|
|
4193
|
-
} : { apiKey };
|
|
4575
|
+
} : spec.authType === "none" ? { apiKey: "", fetch: fetchWithoutCredentialHeaders } : { apiKey };
|
|
4194
4576
|
if (spec.headers) {
|
|
4195
4577
|
anthropicOptions.headers = { ...anthropicOptions.headers, ...spec.headers };
|
|
4196
4578
|
}
|
|
@@ -4206,7 +4588,8 @@ async function createLanguageModel(spec) {
|
|
|
4206
4588
|
const options = {
|
|
4207
4589
|
name: spec.providerId ?? "openai-compatible",
|
|
4208
4590
|
baseURL: baseURL ?? "",
|
|
4209
|
-
...apiKey.trim() ? { apiKey } : {},
|
|
4591
|
+
...spec.authType !== "none" && apiKey.trim() ? { apiKey } : {},
|
|
4592
|
+
...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
|
|
4210
4593
|
...spec.headers ? { headers: spec.headers } : {}
|
|
4211
4594
|
};
|
|
4212
4595
|
model = createOpenAICompatible({
|
|
@@ -4215,7 +4598,8 @@ async function createLanguageModel(spec) {
|
|
|
4215
4598
|
} else {
|
|
4216
4599
|
const create = await loadSdkProviderFactory(npm);
|
|
4217
4600
|
const provider = create({
|
|
4218
|
-
apiKey,
|
|
4601
|
+
apiKey: spec.authType === "none" ? "" : apiKey,
|
|
4602
|
+
...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
|
|
4219
4603
|
...baseURL ? { baseURL } : {},
|
|
4220
4604
|
...spec.headers ? { headers: spec.headers } : {}
|
|
4221
4605
|
});
|
|
@@ -4696,20 +5080,331 @@ function thinkingProviderOptions(npm) {
|
|
|
4696
5080
|
return void 0;
|
|
4697
5081
|
}
|
|
4698
5082
|
|
|
4699
|
-
// src/
|
|
5083
|
+
// src/registry/credential-cleanup-journal.ts
|
|
5084
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4700
5085
|
import {
|
|
4701
|
-
|
|
5086
|
+
closeSync as closeSync3,
|
|
4702
5087
|
existsSync as existsSync4,
|
|
5088
|
+
fstatSync as fstatSync2,
|
|
5089
|
+
fsyncSync as fsyncSync3,
|
|
5090
|
+
lstatSync,
|
|
4703
5091
|
mkdirSync as mkdirSync5,
|
|
5092
|
+
openSync as openSync3,
|
|
4704
5093
|
readFileSync as readFileSync6,
|
|
5094
|
+
renameSync as renameSync2,
|
|
4705
5095
|
unlinkSync as unlinkSync3,
|
|
4706
5096
|
writeFileSync as writeFileSync4
|
|
4707
5097
|
} from "fs";
|
|
4708
|
-
import {
|
|
4709
|
-
|
|
4710
|
-
import pc2 from "picocolors";
|
|
5098
|
+
import { dirname as dirname5 } from "path";
|
|
5099
|
+
var JOURNAL_SCHEMA_VERSION = 1;
|
|
4711
5100
|
var DIR_MODE2 = 448;
|
|
4712
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;
|
|
4713
5408
|
var CLAUDE_DEBUG_LOG = "claude-debug.log";
|
|
4714
5409
|
var PROXY_DEBUG_LOG = "proxy-debug.log";
|
|
4715
5410
|
var PROVIDER_DEBUG_LOG = "provider-debug.log";
|
|
@@ -4723,9 +5418,9 @@ function safeClaudeSessionId(value) {
|
|
|
4723
5418
|
}
|
|
4724
5419
|
function ensureLogsDir() {
|
|
4725
5420
|
const dir = getLogsPath();
|
|
4726
|
-
|
|
5421
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE3 });
|
|
4727
5422
|
try {
|
|
4728
|
-
chmodSync4(dir,
|
|
5423
|
+
chmodSync4(dir, DIR_MODE3);
|
|
4729
5424
|
} catch {
|
|
4730
5425
|
}
|
|
4731
5426
|
return dir;
|
|
@@ -4748,9 +5443,9 @@ function getInferenceRequestLogPath() {
|
|
|
4748
5443
|
}
|
|
4749
5444
|
function getSessionLogPath(label = "session", extension = "log") {
|
|
4750
5445
|
const dir = join4(ensureLogsDir(), INFERENCE_SESSION_DIR);
|
|
4751
|
-
|
|
5446
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE3 });
|
|
4752
5447
|
try {
|
|
4753
|
-
chmodSync4(dir,
|
|
5448
|
+
chmodSync4(dir, DIR_MODE3);
|
|
4754
5449
|
} catch {
|
|
4755
5450
|
}
|
|
4756
5451
|
const safeLabel = label.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "proxy";
|
|
@@ -4822,7 +5517,6 @@ function getLatestMessagePreview(messages, system) {
|
|
|
4822
5517
|
return compactLogValue(preview, REQUEST_PREVIEW_MAX + 20);
|
|
4823
5518
|
}
|
|
4824
5519
|
var REDACTED_DIAGNOSTIC_HEADER = "[REDACTED]";
|
|
4825
|
-
var SENSITIVE_DIAGNOSTIC_HEADER = /(?:^|[-_])(?:authorization|api[-_]?key|cookie|token|secret|credential)(?:$|[-_])/i;
|
|
4826
5520
|
var CONVERSATION_BODY_FIELDS = /* @__PURE__ */ new Set(["system", "messages", "tools"]);
|
|
4827
5521
|
function canonicalDiagnosticValue(value) {
|
|
4828
5522
|
if (Array.isArray(value)) return value.map(canonicalDiagnosticValue);
|
|
@@ -4832,7 +5526,7 @@ function canonicalDiagnosticValue(value) {
|
|
|
4832
5526
|
);
|
|
4833
5527
|
}
|
|
4834
5528
|
function diagnosticHash(value) {
|
|
4835
|
-
return
|
|
5529
|
+
return createHash6("sha256").update(JSON.stringify(canonicalDiagnosticValue(value)) ?? "undefined").digest("hex").slice(0, 16);
|
|
4836
5530
|
}
|
|
4837
5531
|
function diagnosticBytes(value) {
|
|
4838
5532
|
return Buffer.byteLength(JSON.stringify(value) ?? "");
|
|
@@ -4841,7 +5535,7 @@ function sanitizeDiagnosticHeaders(headers) {
|
|
|
4841
5535
|
const out = {};
|
|
4842
5536
|
for (const [name, value] of Object.entries(headers).sort(([left], [right]) => left.localeCompare(right))) {
|
|
4843
5537
|
if (value === void 0) continue;
|
|
4844
|
-
out[name.toLowerCase()] =
|
|
5538
|
+
out[name.toLowerCase()] = isCredentialBearingHeader(name) ? REDACTED_DIAGNOSTIC_HEADER : value;
|
|
4845
5539
|
}
|
|
4846
5540
|
return out;
|
|
4847
5541
|
}
|
|
@@ -5011,9 +5705,9 @@ function makeTraceLogger(logPath) {
|
|
|
5011
5705
|
}
|
|
5012
5706
|
function resetTraceLog(path) {
|
|
5013
5707
|
ensureLogsDir();
|
|
5014
|
-
if (
|
|
5708
|
+
if (existsSync5(path)) {
|
|
5015
5709
|
try {
|
|
5016
|
-
|
|
5710
|
+
unlinkSync4(path);
|
|
5017
5711
|
} catch {
|
|
5018
5712
|
}
|
|
5019
5713
|
}
|
|
@@ -5043,15 +5737,15 @@ function writeSecureLogLine(path, line) {
|
|
|
5043
5737
|
ensureLogsDir();
|
|
5044
5738
|
const redacted = redactTraceLine(line);
|
|
5045
5739
|
try {
|
|
5046
|
-
|
|
5047
|
-
`, { flag: "a", mode:
|
|
5048
|
-
chmodSync4(path,
|
|
5740
|
+
writeFileSync5(path, `${redacted}
|
|
5741
|
+
`, { flag: "a", mode: FILE_MODE5 });
|
|
5742
|
+
chmodSync4(path, FILE_MODE5);
|
|
5049
5743
|
} catch {
|
|
5050
5744
|
}
|
|
5051
5745
|
}
|
|
5052
5746
|
function printTraceLog(debugLogPath) {
|
|
5053
|
-
if (!
|
|
5054
|
-
const raw =
|
|
5747
|
+
if (!existsSync5(debugLogPath)) return;
|
|
5748
|
+
const raw = readFileSync7(debugLogPath, "utf8");
|
|
5055
5749
|
const log12 = redactTraceLog(raw);
|
|
5056
5750
|
const errorLines = log12.split("\n").filter(
|
|
5057
5751
|
(l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status") || l.includes("resolveModel failed") || l.includes("resolveModel fallback")
|
|
@@ -5289,19 +5983,22 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5289
5983
|
if (!trimmedKey && !template.apiKeyOptional) {
|
|
5290
5984
|
return { added: false, error: "API key cannot be empty." };
|
|
5291
5985
|
}
|
|
5292
|
-
const
|
|
5293
|
-
const registry =
|
|
5986
|
+
const existingState = await withRegistryWriteLock(() => {
|
|
5987
|
+
const registry = loadRegistryStrict();
|
|
5294
5988
|
const existing = registry.providers.find((p13) => p13.id === template.id);
|
|
5295
5989
|
if (existing && !opts?.replaceExisting) {
|
|
5296
5990
|
return {
|
|
5297
|
-
|
|
5298
|
-
error:
|
|
5299
|
-
|
|
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
|
+
}
|
|
5300
5997
|
};
|
|
5301
5998
|
}
|
|
5302
|
-
return null;
|
|
5999
|
+
return { existing: existing !== void 0, error: null };
|
|
5303
6000
|
});
|
|
5304
|
-
if (
|
|
6001
|
+
if (existingState.error) return existingState.error;
|
|
5305
6002
|
const fetched = await fetchTemplateModels(template, trimmedKey, opts?.baseUrl);
|
|
5306
6003
|
if (fetched.error || fetched.models.length === 0) {
|
|
5307
6004
|
return {
|
|
@@ -5325,9 +6022,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5325
6022
|
buildPricingIndex(pricingCache),
|
|
5326
6023
|
platform
|
|
5327
6024
|
);
|
|
5328
|
-
const authRef = trimmedKey ? credentialAuthRef(
|
|
5329
|
-
|
|
5330
|
-
|
|
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();
|
|
5331
6030
|
const existing = registry.providers.find((p13) => p13.id === template.id);
|
|
5332
6031
|
if (existing && !opts?.replaceExisting) {
|
|
5333
6032
|
return {
|
|
@@ -5362,12 +6061,28 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5362
6061
|
} else {
|
|
5363
6062
|
registry.providers.push(entry);
|
|
5364
6063
|
}
|
|
6064
|
+
if (existing?.authRef && existing.authRef !== authRef) {
|
|
6065
|
+
await queueCredentialDelete(existing.authRef);
|
|
6066
|
+
}
|
|
5365
6067
|
saveRegistry(registry);
|
|
5366
|
-
|
|
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
|
+
};
|
|
5367
6082
|
});
|
|
5368
6083
|
const result = trimmedKey ? await withCredentialMutationLock(authRef, async () => {
|
|
5369
|
-
const
|
|
5370
|
-
const registry =
|
|
6084
|
+
const prepareError = await withRegistryWriteLock(() => {
|
|
6085
|
+
const registry = loadRegistryStrict();
|
|
5371
6086
|
const existing = registry.providers.find((p13) => p13.id === template.id);
|
|
5372
6087
|
if (existing && !opts?.replaceExisting) {
|
|
5373
6088
|
return {
|
|
@@ -5378,7 +6093,8 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5378
6093
|
}
|
|
5379
6094
|
return null;
|
|
5380
6095
|
});
|
|
5381
|
-
if (
|
|
6096
|
+
if (prepareError) return prepareError;
|
|
6097
|
+
await journalCredentialWrite(authRef);
|
|
5382
6098
|
const saved = await saveProviderCredential(authRef, trimmedKey);
|
|
5383
6099
|
if (!saved) {
|
|
5384
6100
|
return {
|
|
@@ -5389,17 +6105,30 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
5389
6105
|
}
|
|
5390
6106
|
return commitProvider();
|
|
5391
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;
|
|
5392
6124
|
if (result.added) enrichPricingAsync();
|
|
5393
6125
|
return result;
|
|
5394
6126
|
}
|
|
5395
6127
|
|
|
5396
6128
|
// src/registry/crud.ts
|
|
5397
|
-
function credentialStillReferenced(authRef, remaining) {
|
|
5398
|
-
return remaining.some((p13) => p13.authRef === authRef);
|
|
5399
|
-
}
|
|
5400
6129
|
async function removeProviderFromRegistry(id, opts) {
|
|
5401
|
-
const removal = await withRegistryWriteLock(() => {
|
|
5402
|
-
const registry =
|
|
6130
|
+
const removal = await withRegistryWriteLock(async () => {
|
|
6131
|
+
const registry = loadRegistryStrict();
|
|
5403
6132
|
const index = registry.providers.findIndex((p13) => p13.id === id);
|
|
5404
6133
|
if (index < 0) {
|
|
5405
6134
|
return {
|
|
@@ -5409,10 +6138,11 @@ async function removeProviderFromRegistry(id, opts) {
|
|
|
5409
6138
|
credentialDeleted: false,
|
|
5410
6139
|
error: `Provider not found: ${id}`
|
|
5411
6140
|
},
|
|
5412
|
-
|
|
6141
|
+
authRef: null
|
|
5413
6142
|
};
|
|
5414
6143
|
}
|
|
5415
6144
|
const [removedProvider] = registry.providers.splice(index, 1);
|
|
6145
|
+
const cleanupQueued = opts?.deleteCredential !== false ? await queueCredentialDelete(removedProvider.authRef) : false;
|
|
5416
6146
|
saveRegistry(registry);
|
|
5417
6147
|
return {
|
|
5418
6148
|
result: {
|
|
@@ -5421,30 +6151,24 @@ async function removeProviderFromRegistry(id, opts) {
|
|
|
5421
6151
|
name: removedProvider.name,
|
|
5422
6152
|
credentialDeleted: false
|
|
5423
6153
|
},
|
|
5424
|
-
|
|
6154
|
+
authRef: cleanupQueued ? removedProvider.authRef : null
|
|
5425
6155
|
};
|
|
5426
6156
|
});
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
authRefToDelete
|
|
5437
|
-
);
|
|
5438
|
-
if (!removal.result.credentialDeleted) {
|
|
5439
|
-
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.`;
|
|
5440
|
-
}
|
|
5441
|
-
});
|
|
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;
|
|
5442
6166
|
}
|
|
5443
6167
|
return removal.result;
|
|
5444
6168
|
}
|
|
5445
6169
|
function toggleProviderEnabled(id) {
|
|
5446
6170
|
return withRegistryWriteLockSync(() => {
|
|
5447
|
-
const registry =
|
|
6171
|
+
const registry = loadRegistryStrict();
|
|
5448
6172
|
const provider = registry.providers.find((p13) => p13.id === id);
|
|
5449
6173
|
if (!provider) return { toggled: false, error: `Provider not found: ${id}` };
|
|
5450
6174
|
provider.enabled = !provider.enabled;
|
|
@@ -5457,7 +6181,7 @@ function toggleProviderEnabled(id) {
|
|
|
5457
6181
|
import { isDeepStrictEqual } from "util";
|
|
5458
6182
|
|
|
5459
6183
|
// src/registry/custom-endpoint.ts
|
|
5460
|
-
import { randomUUID as
|
|
6184
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
5461
6185
|
|
|
5462
6186
|
// src/registry/url-security.ts
|
|
5463
6187
|
import { lookup } from "dns/promises";
|
|
@@ -5984,7 +6708,7 @@ function providerDiscoveryInputsMatch(current, started) {
|
|
|
5984
6708
|
return current.authRef === started.authRef && current.authType === started.authType && current.templateId === started.templateId && isDeepStrictEqual(current.api, started.api);
|
|
5985
6709
|
}
|
|
5986
6710
|
async function refreshProviderModels(providerId, apiKey, registry) {
|
|
5987
|
-
const workingRegistry = registry ??
|
|
6711
|
+
const workingRegistry = registry ?? loadRegistryStrict();
|
|
5988
6712
|
const provider = workingRegistry.providers.find((p13) => p13.id === providerId);
|
|
5989
6713
|
if (!provider) {
|
|
5990
6714
|
return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
|
|
@@ -6076,7 +6800,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
|
|
|
6076
6800
|
const platform = pricingPlatformForProvider(provider.templateId, provider.id);
|
|
6077
6801
|
const enriched = enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
|
|
6078
6802
|
await withRegistryWriteLock(() => {
|
|
6079
|
-
const currentRegistry =
|
|
6803
|
+
const currentRegistry = loadRegistryStrict();
|
|
6080
6804
|
const currentProvider = currentRegistry.providers.find((candidate) => candidate.id === providerId);
|
|
6081
6805
|
if (!currentProvider) throw new Error("Provider was removed while models were refreshing.");
|
|
6082
6806
|
if (currentProvider.authRef !== provider.authRef) {
|
|
@@ -6108,7 +6832,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
|
|
|
6108
6832
|
}
|
|
6109
6833
|
async function refreshAllProviderModels(resolveKey) {
|
|
6110
6834
|
const refreshed = [];
|
|
6111
|
-
const registry =
|
|
6835
|
+
const registry = loadRegistryStrict();
|
|
6112
6836
|
const enabledProviders = registry.providers.filter((p13) => p13.enabled);
|
|
6113
6837
|
for (const provider of enabledProviders) {
|
|
6114
6838
|
const key = await resolveRefreshCredential(provider, resolveKey);
|
|
@@ -6154,41 +6878,83 @@ function oauthDisplayName(registryId, fallbackName) {
|
|
|
6154
6878
|
if (registryId === "openai-oauth") return "OpenAI (ChatGPT)";
|
|
6155
6879
|
return fallbackName;
|
|
6156
6880
|
}
|
|
6157
|
-
async function
|
|
6158
|
-
|
|
6881
|
+
async function persistOAuthProvider(providerId, cred, authRef) {
|
|
6882
|
+
const registryProvider = await withCredentialMutationLock(authRef, async () => {
|
|
6159
6883
|
const registryId = toOAuthRegistryId(providerId);
|
|
6160
6884
|
const templateId = providerId.replace(/-oauth$/, "") || providerId;
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
if (!template) {
|
|
6885
|
+
await withRegistryWriteLock(() => {
|
|
6886
|
+
const registry = loadRegistryStrict();
|
|
6887
|
+
const previousEntry = registry.providers.find((provider) => provider.id === registryId);
|
|
6888
|
+
if (!previousEntry && !getTemplateById(templateId)) {
|
|
6166
6889
|
throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
|
|
6167
6890
|
}
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
},
|
|
6181
|
-
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6182
|
-
};
|
|
6183
|
-
} else {
|
|
6184
|
-
entry = { ...entry, authType: "oauth", authRef, templateId };
|
|
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"}`);
|
|
6185
6903
|
}
|
|
6186
|
-
const
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
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) {
|
|
6909
|
+
throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
|
|
6910
|
+
}
|
|
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;
|
|
6191
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
|
+
};
|
|
6192
6958
|
}
|
|
6193
6959
|
async function authenticateProvider(providerId, _options = {}) {
|
|
6194
6960
|
const registryId = toOAuthRegistryId(providerId);
|
|
@@ -6206,26 +6972,7 @@ async function authenticateProvider(providerId, _options = {}) {
|
|
|
6206
6972
|
);
|
|
6207
6973
|
}
|
|
6208
6974
|
const cred = await runNativeDeviceCode(providerId);
|
|
6209
|
-
const persisted = await
|
|
6210
|
-
let nativeDiagMsg = "";
|
|
6211
|
-
const saved = await saveProviderCredential(
|
|
6212
|
-
authRef,
|
|
6213
|
-
oauthCredentialToKeychainJson(cred),
|
|
6214
|
-
(msg) => {
|
|
6215
|
-
nativeDiagMsg = msg;
|
|
6216
|
-
}
|
|
6217
|
-
);
|
|
6218
|
-
const registryProvider2 = await upsertOAuthProvider(
|
|
6219
|
-
providerId,
|
|
6220
|
-
cred,
|
|
6221
|
-
authRef
|
|
6222
|
-
);
|
|
6223
|
-
return { saved, nativeDiagMsg, registryProvider: registryProvider2 };
|
|
6224
|
-
});
|
|
6225
|
-
if (!persisted.saved) {
|
|
6226
|
-
p2.log.warn(`Could not save OAuth tokens to the credential store \u2014 ${persisted.nativeDiagMsg || "session may not persist."}`);
|
|
6227
|
-
}
|
|
6228
|
-
const { registryProvider } = persisted;
|
|
6975
|
+
const persisted = await persistOAuthProvider(providerId, cred, authRef);
|
|
6229
6976
|
const refreshSpinner = p2.spinner();
|
|
6230
6977
|
refreshSpinner.start("Refreshing model list...");
|
|
6231
6978
|
try {
|
|
@@ -6234,7 +6981,12 @@ async function authenticateProvider(providerId, _options = {}) {
|
|
|
6234
6981
|
} catch {
|
|
6235
6982
|
refreshSpinner.stop("Could not refresh models \u2014 run clodex providers refresh-models later");
|
|
6236
6983
|
}
|
|
6237
|
-
return {
|
|
6984
|
+
return {
|
|
6985
|
+
providerId: registryId,
|
|
6986
|
+
credential: cred,
|
|
6987
|
+
registryProvider: persisted.registryProvider,
|
|
6988
|
+
credentialCleanupPending: persisted.credentialCleanupPending
|
|
6989
|
+
};
|
|
6238
6990
|
}
|
|
6239
6991
|
function providerAuthHelpText() {
|
|
6240
6992
|
return `${pc3.bold("clodex providers auth")} \u2014 sign in with OAuth
|
|
@@ -6544,6 +7296,39 @@ async function pickLocalModel(provider, conflicts, prefs) {
|
|
|
6544
7296
|
}
|
|
6545
7297
|
|
|
6546
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
|
+
}
|
|
6547
7332
|
function parseProvidersArgs(args) {
|
|
6548
7333
|
if (args.length === 0) return { subcommand: "hub", showHelp: false };
|
|
6549
7334
|
const [first, ...rest] = args;
|
|
@@ -6607,10 +7392,11 @@ ${pc5.bold("Subcommands:")}
|
|
|
6607
7392
|
function providerLabel(name, modelCount, enabled) {
|
|
6608
7393
|
return `${fmtEnabledStar(enabled)} ${fmtProvider(name)} ${pc5.dim(`(${modelCount} model${modelCount === 1 ? "" : "s"})`)}`;
|
|
6609
7394
|
}
|
|
6610
|
-
async function
|
|
7395
|
+
async function runProvidersAuthWithCleanupState(providerId, method, cleanupState) {
|
|
6611
7396
|
try {
|
|
6612
7397
|
const result = await authenticateProvider(providerId, { method });
|
|
6613
7398
|
p4.log.success(`Signed in to ${result.registryProvider.name} \u2014 credential saved to the credential store.`);
|
|
7399
|
+
reportCredentialCleanup(result.credentialCleanupPending, cleanupState, true);
|
|
6614
7400
|
return 0;
|
|
6615
7401
|
} catch (err) {
|
|
6616
7402
|
if (err instanceof Error && err.message === "Cancelled") {
|
|
@@ -6621,6 +7407,9 @@ async function runProvidersAuth(providerId, method) {
|
|
|
6621
7407
|
return 1;
|
|
6622
7408
|
}
|
|
6623
7409
|
}
|
|
7410
|
+
async function runProvidersAuth(providerId, method) {
|
|
7411
|
+
return runProvidersAuthWithCleanupState(providerId, method);
|
|
7412
|
+
}
|
|
6624
7413
|
async function runProvidersRefreshModels(providerId) {
|
|
6625
7414
|
const resolveKey = async (provider) => resolveProviderCredential(provider.id, provider.authRef);
|
|
6626
7415
|
if (providerId) {
|
|
@@ -6698,7 +7487,7 @@ async function runProvidersList() {
|
|
|
6698
7487
|
console.log("");
|
|
6699
7488
|
return 0;
|
|
6700
7489
|
}
|
|
6701
|
-
async function runTemplateAddFlow() {
|
|
7490
|
+
async function runTemplateAddFlow(cleanupState) {
|
|
6702
7491
|
const registry = loadRegistry();
|
|
6703
7492
|
const configuredIds = registry.providers.map((p13) => p13.id);
|
|
6704
7493
|
const template = listAddableTemplates(configuredIds).find((t) => t.id === "openai") ?? getTemplateById("openai");
|
|
@@ -6724,6 +7513,11 @@ async function runTemplateAddFlow() {
|
|
|
6724
7513
|
spinner5.start(`Testing connection to ${template.name}...`);
|
|
6725
7514
|
const result = await addProviderFromTemplate(template, apiKey);
|
|
6726
7515
|
spinner5.stop("");
|
|
7516
|
+
reportCredentialCleanup(
|
|
7517
|
+
result.credentialCleanupPending === true,
|
|
7518
|
+
cleanupState,
|
|
7519
|
+
result.credentialCleanupReconciled === true
|
|
7520
|
+
);
|
|
6727
7521
|
if (!result.added) {
|
|
6728
7522
|
p4.log.error(result.error ?? "Could not add provider.");
|
|
6729
7523
|
if (result.hint) p4.log.info(result.hint);
|
|
@@ -6732,7 +7526,7 @@ async function runTemplateAddFlow() {
|
|
|
6732
7526
|
logConnected(template.name, result.modelCount ?? 0);
|
|
6733
7527
|
return 0;
|
|
6734
7528
|
}
|
|
6735
|
-
async function
|
|
7529
|
+
async function runProvidersAddWithCleanupState(cleanupState) {
|
|
6736
7530
|
const choice = await p4.select({
|
|
6737
7531
|
message: "Add a provider",
|
|
6738
7532
|
options: [
|
|
@@ -6752,11 +7546,16 @@ async function runProvidersAdd() {
|
|
|
6752
7546
|
p4.cancel("Cancelled.");
|
|
6753
7547
|
return 0;
|
|
6754
7548
|
}
|
|
6755
|
-
if (choice === "oauth")
|
|
6756
|
-
|
|
7549
|
+
if (choice === "oauth") {
|
|
7550
|
+
return runProvidersAuthWithCleanupState("openai", void 0, cleanupState);
|
|
7551
|
+
}
|
|
7552
|
+
if (choice === "apikey") return runTemplateAddFlow(cleanupState);
|
|
6757
7553
|
return 0;
|
|
6758
7554
|
}
|
|
6759
|
-
async function
|
|
7555
|
+
async function runProvidersAdd() {
|
|
7556
|
+
return runProvidersAddWithCleanupState();
|
|
7557
|
+
}
|
|
7558
|
+
async function runProvidersRemoveWithCleanupState(id, interactive = false, cleanupState) {
|
|
6760
7559
|
const registry = loadRegistry();
|
|
6761
7560
|
const provider = registry.providers.find((pr) => pr.id === id);
|
|
6762
7561
|
if (!provider) {
|
|
@@ -6774,6 +7573,11 @@ async function runProvidersRemove(id, interactive = false) {
|
|
|
6774
7573
|
}
|
|
6775
7574
|
}
|
|
6776
7575
|
const result = await removeProviderFromRegistry(id);
|
|
7576
|
+
reportCredentialCleanup(
|
|
7577
|
+
result.credentialCleanupPending === true,
|
|
7578
|
+
cleanupState,
|
|
7579
|
+
result.credentialCleanupReconciled === true
|
|
7580
|
+
);
|
|
6777
7581
|
if (!result.removed) {
|
|
6778
7582
|
p4.log.error(result.error ?? `Could not remove ${id}`);
|
|
6779
7583
|
return 1;
|
|
@@ -6788,6 +7592,9 @@ async function runProvidersRemove(id, interactive = false) {
|
|
|
6788
7592
|
}
|
|
6789
7593
|
return 0;
|
|
6790
7594
|
}
|
|
7595
|
+
async function runProvidersRemove(id, interactive = false) {
|
|
7596
|
+
return runProvidersRemoveWithCleanupState(id, interactive);
|
|
7597
|
+
}
|
|
6791
7598
|
function providerHubChoiceValue(entry) {
|
|
6792
7599
|
return `provider:${entry.id}`;
|
|
6793
7600
|
}
|
|
@@ -6921,16 +7728,24 @@ async function runProvidersCommand(args) {
|
|
|
6921
7728
|
console.log(providersHelpText());
|
|
6922
7729
|
return 0;
|
|
6923
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
|
+
}
|
|
6924
7735
|
if (parsed.subcommand === "list") return runProvidersList();
|
|
6925
|
-
if (parsed.subcommand === "add")
|
|
6926
|
-
|
|
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
|
+
}
|
|
6927
7742
|
if (parsed.subcommand === "refresh-models") return runProvidersRefreshModels(parsed.removeId);
|
|
6928
7743
|
if (parsed.subcommand === "auth") {
|
|
6929
7744
|
if (parsed.showHelp || !parsed.removeId) {
|
|
6930
7745
|
console.log(providerAuthHelpText());
|
|
6931
7746
|
return 0;
|
|
6932
7747
|
}
|
|
6933
|
-
return
|
|
7748
|
+
return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state));
|
|
6934
7749
|
}
|
|
6935
7750
|
relayIntro("Your OpenAI providers");
|
|
6936
7751
|
return runProvidersHub();
|
|
@@ -6971,7 +7786,7 @@ async function runFirstRunWizard(_trace = false) {
|
|
|
6971
7786
|
|
|
6972
7787
|
// src/proxy.ts
|
|
6973
7788
|
import { createServer } from "http";
|
|
6974
|
-
import { appendFileSync, openSync as
|
|
7789
|
+
import { appendFileSync, openSync as openSync4, writeSync as writeSync2, closeSync as closeSync4 } from "fs";
|
|
6975
7790
|
|
|
6976
7791
|
// src/http-utils.ts
|
|
6977
7792
|
import * as zlib from "zlib";
|
|
@@ -7048,6 +7863,12 @@ function localModelToRoute(lp, model) {
|
|
|
7048
7863
|
baseURL: model.apiBaseUrl,
|
|
7049
7864
|
providerId: lp.id,
|
|
7050
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,
|
|
7051
7872
|
oauthAccountId: lp.oauthAccountId,
|
|
7052
7873
|
providerData: lp.providerData,
|
|
7053
7874
|
headers: lp.headers,
|
|
@@ -7138,7 +7959,7 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX
|
|
|
7138
7959
|
continue;
|
|
7139
7960
|
}
|
|
7140
7961
|
const route = localModelToRoute(provider, model);
|
|
7141
|
-
if (!route || !route.apiKey.trim()) {
|
|
7962
|
+
if (!route || !route.apiKey.trim() && route.authType !== "none") {
|
|
7142
7963
|
unavailable.push(favorite);
|
|
7143
7964
|
continue;
|
|
7144
7965
|
}
|
|
@@ -7319,13 +8140,21 @@ function extractBearerToken(value) {
|
|
|
7319
8140
|
// src/upstream-forward.ts
|
|
7320
8141
|
function anthropicUpstreamHeaders(apiKey, stream = false, inboundBeta, authType, claudeCodeSessionId, extraHeaders) {
|
|
7321
8142
|
const key = sanitizeCredential(apiKey) ?? apiKey.trim();
|
|
7322
|
-
const
|
|
8143
|
+
const resolvedAuthType = authType ?? "api";
|
|
8144
|
+
const isOAuth = resolvedAuthType === "oauth";
|
|
8145
|
+
const forwardedExtraHeaders = resolvedAuthType === "none" ? Object.fromEntries(
|
|
8146
|
+
Object.entries(extraHeaders ?? {}).filter(
|
|
8147
|
+
([name]) => !isCredentialBearingHeader(name)
|
|
8148
|
+
)
|
|
8149
|
+
) : extraHeaders;
|
|
7323
8150
|
const headers = {
|
|
7324
|
-
...
|
|
8151
|
+
...forwardedExtraHeaders,
|
|
7325
8152
|
"Content-Type": "application/json",
|
|
7326
8153
|
"anthropic-version": "2023-06-01",
|
|
7327
|
-
|
|
7328
|
-
|
|
8154
|
+
...resolvedAuthType === "none" ? {} : {
|
|
8155
|
+
Authorization: `Bearer ${key}`,
|
|
8156
|
+
...isOAuth ? {} : { "x-api-key": key }
|
|
8157
|
+
},
|
|
7329
8158
|
...isOAuth ? { "User-Agent": CLAUDE_CODE_USER_AGENT, "x-app": "cli" } : {},
|
|
7330
8159
|
...isOAuth && claudeCodeSessionId ? { "X-Claude-Code-Session-Id": claudeCodeSessionId } : {},
|
|
7331
8160
|
...stream ? { Accept: "text/event-stream" } : {}
|
|
@@ -7341,14 +8170,29 @@ var UpstreamUnreachableError = class extends Error {
|
|
|
7341
8170
|
this.name = "UpstreamUnreachableError";
|
|
7342
8171
|
}
|
|
7343
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
|
+
}
|
|
7344
8180
|
async function fetchWithOAuthRetry(apiKey, request3, refreshToken) {
|
|
7345
8181
|
let response = await request3(apiKey);
|
|
7346
|
-
|
|
8182
|
+
const refreshed = await resolveOAuthRetryReplacement(
|
|
8183
|
+
true,
|
|
8184
|
+
response.status,
|
|
8185
|
+
0,
|
|
8186
|
+
false,
|
|
8187
|
+
apiKey,
|
|
8188
|
+
refreshToken
|
|
8189
|
+
);
|
|
8190
|
+
if (!refreshed) {
|
|
7347
8191
|
return { response, apiKey, refreshed: false };
|
|
7348
8192
|
}
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
8193
|
+
try {
|
|
8194
|
+
await response.body?.cancel?.();
|
|
8195
|
+
} catch {
|
|
7352
8196
|
}
|
|
7353
8197
|
response = await request3(refreshed);
|
|
7354
8198
|
return { response, apiKey: refreshed, refreshed: true };
|
|
@@ -7413,10 +8257,10 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
|
|
|
7413
8257
|
}
|
|
7414
8258
|
|
|
7415
8259
|
// src/proxy.ts
|
|
7416
|
-
import { randomUUID as
|
|
8260
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
7417
8261
|
|
|
7418
8262
|
// src/sdk-adapter.ts
|
|
7419
|
-
import { createHash as
|
|
8263
|
+
import { createHash as createHash7 } from "crypto";
|
|
7420
8264
|
import { streamText, generateText, tool, jsonSchema } from "ai";
|
|
7421
8265
|
|
|
7422
8266
|
// src/proxy-shared.ts
|
|
@@ -7580,7 +8424,7 @@ function extractClaudeSessionId(body, headerFallback) {
|
|
|
7580
8424
|
return validClaudeSessionId(headerFallback);
|
|
7581
8425
|
}
|
|
7582
8426
|
function claudeSessionPromptCacheKey(sessionId) {
|
|
7583
|
-
return "relay-session-" +
|
|
8427
|
+
return "relay-session-" + createHash7("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
7584
8428
|
}
|
|
7585
8429
|
function anthropicEffortFromRequest(body) {
|
|
7586
8430
|
const effort = body.output_config?.effort;
|
|
@@ -7590,7 +8434,7 @@ function anthropicEffortFromRequest(body) {
|
|
|
7590
8434
|
function openAiPromptCacheKey(system, tools) {
|
|
7591
8435
|
const toolSig = (tools ?? []).map((t) => `${t.name}${t.description ?? ""}${JSON.stringify(t.input_schema ?? {})}`).join("");
|
|
7592
8436
|
const material = `${system ?? ""}\0${toolSig}`;
|
|
7593
|
-
return "relay-" +
|
|
8437
|
+
return "relay-" + createHash7("sha256").update(material).digest("hex").slice(0, 32);
|
|
7594
8438
|
}
|
|
7595
8439
|
function supportsOpenAiPromptCacheBreakpoints(modelId) {
|
|
7596
8440
|
const match = modelId.toLowerCase().match(/^gpt-(\d+)(?:\.(\d+))?(?:-|$)/);
|
|
@@ -8297,6 +9141,77 @@ function anthropicPromptTooLongMessage(body, contextWindow) {
|
|
|
8297
9141
|
return `prompt is too long: ${promptTokens} tokens > ${maximum} maximum`;
|
|
8298
9142
|
}
|
|
8299
9143
|
|
|
9144
|
+
// src/listener-ready.ts
|
|
9145
|
+
import { connect } from "net";
|
|
9146
|
+
import { setTimeout as delay } from "timers/promises";
|
|
9147
|
+
var LISTENER_READY_TIMEOUT_MS = 1e3;
|
|
9148
|
+
var LISTENER_READY_RETRY_MS = 5;
|
|
9149
|
+
function connectHost(address) {
|
|
9150
|
+
if (address === "0.0.0.0") return "127.0.0.1";
|
|
9151
|
+
if (address === "::") return "::1";
|
|
9152
|
+
return address;
|
|
9153
|
+
}
|
|
9154
|
+
function tcpListenerUrlHost(address) {
|
|
9155
|
+
const host = connectHost(address);
|
|
9156
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
9157
|
+
}
|
|
9158
|
+
function probeTcpListener(host, port, timeoutMs) {
|
|
9159
|
+
return new Promise((resolve2) => {
|
|
9160
|
+
const socket = connect({ host, port });
|
|
9161
|
+
let settled = false;
|
|
9162
|
+
const finish = (ready) => {
|
|
9163
|
+
if (settled) return;
|
|
9164
|
+
settled = true;
|
|
9165
|
+
socket.destroy();
|
|
9166
|
+
resolve2(ready);
|
|
9167
|
+
};
|
|
9168
|
+
socket.once("connect", () => finish(true));
|
|
9169
|
+
socket.once("error", () => finish(false));
|
|
9170
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
9171
|
+
});
|
|
9172
|
+
}
|
|
9173
|
+
async function closeAfterReadinessFailure(server) {
|
|
9174
|
+
if (!server.listening) return;
|
|
9175
|
+
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
9176
|
+
}
|
|
9177
|
+
async function listenTcpServer(server, port, host) {
|
|
9178
|
+
await new Promise((resolve2, reject) => {
|
|
9179
|
+
const cleanup = () => server.off("error", onError);
|
|
9180
|
+
const onError = (error) => {
|
|
9181
|
+
cleanup();
|
|
9182
|
+
reject(error);
|
|
9183
|
+
};
|
|
9184
|
+
server.once("error", onError);
|
|
9185
|
+
try {
|
|
9186
|
+
server.listen(port, host, () => {
|
|
9187
|
+
cleanup();
|
|
9188
|
+
resolve2();
|
|
9189
|
+
});
|
|
9190
|
+
} catch (error) {
|
|
9191
|
+
cleanup();
|
|
9192
|
+
reject(error);
|
|
9193
|
+
}
|
|
9194
|
+
});
|
|
9195
|
+
const address = server.address();
|
|
9196
|
+
if (!address || typeof address === "string") {
|
|
9197
|
+
await closeAfterReadinessFailure(server);
|
|
9198
|
+
throw new Error("TCP server did not bind to a network address");
|
|
9199
|
+
}
|
|
9200
|
+
const probeHost = connectHost(address.address);
|
|
9201
|
+
const deadline = Date.now() + LISTENER_READY_TIMEOUT_MS;
|
|
9202
|
+
while (Date.now() < deadline) {
|
|
9203
|
+
const remaining = deadline - Date.now();
|
|
9204
|
+
if (await probeTcpListener(probeHost, address.port, Math.min(remaining, 50))) {
|
|
9205
|
+
return address;
|
|
9206
|
+
}
|
|
9207
|
+
await delay(Math.min(LISTENER_READY_RETRY_MS, Math.max(1, deadline - Date.now())));
|
|
9208
|
+
}
|
|
9209
|
+
await closeAfterReadinessFailure(server);
|
|
9210
|
+
throw new Error(
|
|
9211
|
+
`TCP listener did not become reachable within ${LISTENER_READY_TIMEOUT_MS}ms: ${probeHost}:${address.port}`
|
|
9212
|
+
);
|
|
9213
|
+
}
|
|
9214
|
+
|
|
8300
9215
|
// src/proxy.ts
|
|
8301
9216
|
var STREAM_KEEPALIVE_INTERVAL_MS = 2e4;
|
|
8302
9217
|
var STREAM_KEEPALIVE_PING = 'event: ping\ndata: {"type":"ping"}\n\n';
|
|
@@ -8382,12 +9297,12 @@ function createTranslationLifecycle(logPath, requestId, modelId, provider) {
|
|
|
8382
9297
|
function appendSecureLog(logPath, line) {
|
|
8383
9298
|
const redacted = redactTraceLine(line);
|
|
8384
9299
|
try {
|
|
8385
|
-
const fd =
|
|
9300
|
+
const fd = openSync4(logPath, "a", 384);
|
|
8386
9301
|
try {
|
|
8387
9302
|
writeSync2(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
|
|
8388
9303
|
`);
|
|
8389
9304
|
} finally {
|
|
8390
|
-
|
|
9305
|
+
closeSync4(fd);
|
|
8391
9306
|
}
|
|
8392
9307
|
} catch {
|
|
8393
9308
|
try {
|
|
@@ -8426,11 +9341,11 @@ function lookupRoute(byAlias, id) {
|
|
|
8426
9341
|
}
|
|
8427
9342
|
return void 0;
|
|
8428
9343
|
}
|
|
8429
|
-
function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
|
|
8430
|
-
const proxyToken =
|
|
9344
|
+
async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
|
|
9345
|
+
const proxyToken = randomUUID8();
|
|
8431
9346
|
silenceSdkWarnings();
|
|
8432
9347
|
if (routes.length === 0) {
|
|
8433
|
-
|
|
9348
|
+
throw new Error("Proxy catalog requires at least one route");
|
|
8434
9349
|
}
|
|
8435
9350
|
const byAlias = new Map(routes.map((r) => [r.aliasId, r]));
|
|
8436
9351
|
for (const alias of modelAliases ?? []) {
|
|
@@ -8505,21 +9420,36 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8505
9420
|
const relayRequestIdRaw = req.headers["x-relay-request-id"];
|
|
8506
9421
|
const relayRequestId = Array.isArray(relayRequestIdRaw) ? relayRequestIdRaw[0] : relayRequestIdRaw;
|
|
8507
9422
|
const route = lookupRoute(byAlias, originalModel) ?? defaultRoute;
|
|
8508
|
-
|
|
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
|
+
}
|
|
8509
9445
|
const upstreamUrl = route.upstreamUrl;
|
|
9446
|
+
const routeAuthType = route.authType ?? "api";
|
|
8510
9447
|
plog(
|
|
8511
|
-
() => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${apiKey ? `len:${apiKey.length}` : "MISSING"}`
|
|
9448
|
+
() => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${routeAuthType === "none" ? "none" : apiKey ? `len:${apiKey.length}` : "MISSING"}`
|
|
8512
9449
|
);
|
|
8513
9450
|
const usesSdkAdapter = isSdkMigratedNpm(route.npm);
|
|
8514
9451
|
if (messagesEndpoint === "count_tokens") {
|
|
8515
|
-
if (
|
|
8516
|
-
const inputTokens = estimateAnthropicInputTokens(anthropicBody);
|
|
8517
|
-
plog(() => `token-count: local estimate model=${originalModel} input_tokens=${inputTokens}`);
|
|
8518
|
-
res.setHeader("x-relay-token-count-source", "local-estimate");
|
|
8519
|
-
sendJson(res, 200, { input_tokens: inputTokens });
|
|
8520
|
-
return;
|
|
8521
|
-
}
|
|
8522
|
-
if (!apiKey) {
|
|
9452
|
+
if (!apiKey && routeAuthType !== "none") {
|
|
8523
9453
|
anthropicError(res, 401, "Missing API key");
|
|
8524
9454
|
return;
|
|
8525
9455
|
}
|
|
@@ -8527,11 +9457,11 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8527
9457
|
const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
|
|
8528
9458
|
const forwardBody = { ...anthropicBody, model: route.realModelId };
|
|
8529
9459
|
const targetUrl = `${upstreamUrl}/v1/messages/count_tokens`;
|
|
8530
|
-
const isOAuth =
|
|
9460
|
+
const isOAuth = routeAuthType === "oauth";
|
|
8531
9461
|
try {
|
|
8532
9462
|
await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, false, {
|
|
8533
9463
|
inboundBeta,
|
|
8534
|
-
authType:
|
|
9464
|
+
authType: routeAuthType,
|
|
8535
9465
|
log: (message) => plog(message),
|
|
8536
9466
|
extraHeaders: route.headers,
|
|
8537
9467
|
refreshToken: route.refreshToken,
|
|
@@ -8548,7 +9478,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8548
9478
|
}
|
|
8549
9479
|
return;
|
|
8550
9480
|
}
|
|
8551
|
-
if (!apiKey && !usesSdkAdapter) {
|
|
9481
|
+
if (!apiKey && routeAuthType !== "none" && !usesSdkAdapter) {
|
|
8552
9482
|
anthropicError(res, 401, "Missing API key");
|
|
8553
9483
|
return;
|
|
8554
9484
|
}
|
|
@@ -8557,7 +9487,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8557
9487
|
const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
|
|
8558
9488
|
const forwardBody = { ...anthropicBody, model: route.realModelId };
|
|
8559
9489
|
const targetUrl = `${upstreamUrl}/v1/messages`;
|
|
8560
|
-
const isOAuth =
|
|
9490
|
+
const isOAuth = routeAuthType === "oauth";
|
|
8561
9491
|
let effectiveBeta = inboundBeta;
|
|
8562
9492
|
let claudeCodeSessionId;
|
|
8563
9493
|
if (isOAuth) {
|
|
@@ -8574,7 +9504,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8574
9504
|
try {
|
|
8575
9505
|
await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream, {
|
|
8576
9506
|
inboundBeta: effectiveBeta,
|
|
8577
|
-
authType:
|
|
9507
|
+
authType: routeAuthType,
|
|
8578
9508
|
log: (message) => plog(message),
|
|
8579
9509
|
claudeCodeSessionId,
|
|
8580
9510
|
extraHeaders: route.headers,
|
|
@@ -8607,7 +9537,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8607
9537
|
originalModel,
|
|
8608
9538
|
route.providerId ?? route.aliasId.split(":")[1] ?? "unknown"
|
|
8609
9539
|
);
|
|
8610
|
-
|
|
9540
|
+
const runSdkRequest = async () => {
|
|
8611
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"];
|
|
8612
9542
|
const claudeSessionId = extractClaudeSessionId(anthropicBody, claudeSessionIdHeader);
|
|
8613
9543
|
const params = translateRequest(anthropicBody, route.npm, {
|
|
@@ -8713,18 +9643,35 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8713
9643
|
translationLifecycle?.complete();
|
|
8714
9644
|
sendJson(res, 200, anthropicResponse);
|
|
8715
9645
|
}
|
|
8716
|
-
}
|
|
9646
|
+
};
|
|
9647
|
+
let sdkAttempt = 0;
|
|
9648
|
+
const handleSdkError = async (err) => {
|
|
8717
9649
|
if (clientAbort.signal.aborted) {
|
|
8718
9650
|
translationLifecycle?.cancel();
|
|
8719
|
-
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";
|
|
8720
9670
|
}
|
|
8721
9671
|
translationLifecycle?.fail(
|
|
8722
9672
|
err instanceof Error ? err.name : "UpstreamError",
|
|
8723
9673
|
sdkTranslationErrorSignature(err)
|
|
8724
9674
|
);
|
|
8725
|
-
const message = formatUpstreamError(err);
|
|
8726
|
-
const details = sdkUpstreamErrorDetails(err);
|
|
8727
|
-
const upstreamStatus = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
8728
9675
|
const contextLengthExceeded = upstreamStatus === 400 && isContextLengthExceededError(err, message);
|
|
8729
9676
|
const clientMessage = contextLengthExceeded ? anthropicPromptTooLongMessage(
|
|
8730
9677
|
anthropicBody,
|
|
@@ -8744,11 +9691,14 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8744
9691
|
});
|
|
8745
9692
|
}
|
|
8746
9693
|
if (!res.headersSent) {
|
|
9694
|
+
if (details?.retryAfterSeconds !== void 0) {
|
|
9695
|
+
res.setHeader("retry-after", String(details.retryAfterSeconds));
|
|
9696
|
+
}
|
|
8747
9697
|
anthropicError(
|
|
8748
9698
|
res,
|
|
8749
9699
|
upstreamStatus === 500 ? 502 : upstreamStatus,
|
|
8750
9700
|
clientMessage,
|
|
8751
|
-
contextLengthExceeded ? relayRequestId ??
|
|
9701
|
+
contextLengthExceeded ? relayRequestId ?? randomUUID8() : void 0
|
|
8752
9702
|
);
|
|
8753
9703
|
} else {
|
|
8754
9704
|
const errorType = anthropicErrorType(upstreamStatus);
|
|
@@ -8756,12 +9706,24 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
8756
9706
|
data: ${JSON.stringify({
|
|
8757
9707
|
type: "error",
|
|
8758
9708
|
error: { type: errorType, message: clientMessage },
|
|
8759
|
-
...contextLengthExceeded ? { request_id: relayRequestId ??
|
|
9709
|
+
...contextLengthExceeded ? { request_id: relayRequestId ?? randomUUID8() } : {}
|
|
8760
9710
|
})}
|
|
8761
9711
|
|
|
8762
9712
|
`);
|
|
8763
9713
|
res.end();
|
|
8764
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
|
+
}
|
|
8765
9727
|
}
|
|
8766
9728
|
return;
|
|
8767
9729
|
}
|
|
@@ -8770,26 +9732,26 @@ data: ${JSON.stringify({
|
|
|
8770
9732
|
}
|
|
8771
9733
|
anthropicError(res, 404, `Unknown endpoint: ${req.method} ${req.url}`);
|
|
8772
9734
|
});
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
server
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
}
|
|
8792
|
-
}
|
|
9735
|
+
let address;
|
|
9736
|
+
try {
|
|
9737
|
+
address = await listenTcpServer(server, 0, "127.0.0.1");
|
|
9738
|
+
} catch (error) {
|
|
9739
|
+
process.off("unhandledRejection", onRejection);
|
|
9740
|
+
process.off("uncaughtException", onException);
|
|
9741
|
+
throw error;
|
|
9742
|
+
}
|
|
9743
|
+
plog(
|
|
9744
|
+
() => `started on port ${address.port}, catalog=${routes.length} model(s), default=${defaultRoute.aliasId}`
|
|
9745
|
+
);
|
|
9746
|
+
return {
|
|
9747
|
+
port: address.port,
|
|
9748
|
+
token: proxyToken,
|
|
9749
|
+
close: () => {
|
|
9750
|
+
process.off("unhandledRejection", onRejection);
|
|
9751
|
+
process.off("uncaughtException", onException);
|
|
9752
|
+
server.close();
|
|
9753
|
+
}
|
|
9754
|
+
};
|
|
8793
9755
|
}
|
|
8794
9756
|
function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk, apiKey) {
|
|
8795
9757
|
const bareModelId = stripOneMContextSuffix(modelId);
|
|
@@ -8812,7 +9774,8 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
|
|
|
8812
9774
|
reasoning: sdk?.reasoning,
|
|
8813
9775
|
interleavedReasoningField: sdk?.interleavedReasoningField,
|
|
8814
9776
|
useResponsesLite: sdk?.useResponsesLite,
|
|
8815
|
-
preferWebSockets: sdk?.preferWebSockets
|
|
9777
|
+
preferWebSockets: sdk?.preferWebSockets,
|
|
9778
|
+
headers: sdk?.headers
|
|
8816
9779
|
}], clientModelId, debug);
|
|
8817
9780
|
}
|
|
8818
9781
|
|
|
@@ -8953,7 +9916,7 @@ async function askSaveServerPassword() {
|
|
|
8953
9916
|
|
|
8954
9917
|
// src/server/router.ts
|
|
8955
9918
|
import { createServer as createServer2 } from "http";
|
|
8956
|
-
import { randomUUID as
|
|
9919
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
8957
9920
|
|
|
8958
9921
|
// src/openai-adapter.ts
|
|
8959
9922
|
import { tool as tool2, jsonSchema as jsonSchema2, streamText as streamText2, generateText as generateText2 } from "ai";
|
|
@@ -9155,6 +10118,30 @@ function auditInference(options, entry) {
|
|
|
9155
10118
|
function inferenceProvider(model) {
|
|
9156
10119
|
return model.providerId ?? String(model.sourceBackend);
|
|
9157
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
|
+
}
|
|
9158
10145
|
function auditSdkError(options, requestedModelId, model, err, message) {
|
|
9159
10146
|
const details = sdkUpstreamErrorDetails(err);
|
|
9160
10147
|
const statusCode = details?.statusCode ?? upstreamHttpStatus(err, message);
|
|
@@ -9169,7 +10156,7 @@ function auditSdkError(options, requestedModelId, model, err, message) {
|
|
|
9169
10156
|
attemptCount: details?.attemptCount
|
|
9170
10157
|
});
|
|
9171
10158
|
}
|
|
9172
|
-
return statusCode;
|
|
10159
|
+
return { statusCode, retryAfterSeconds: details?.retryAfterSeconds };
|
|
9173
10160
|
}
|
|
9174
10161
|
function openAiEffort(body) {
|
|
9175
10162
|
if (typeof body.reasoning_effort === "string" && body.reasoning_effort.trim()) {
|
|
@@ -9188,21 +10175,11 @@ async function startServer(options) {
|
|
|
9188
10175
|
const server = createServer2((req, res) => {
|
|
9189
10176
|
void routeRequest(req, res, options, languageModelCache, plog);
|
|
9190
10177
|
});
|
|
9191
|
-
await
|
|
9192
|
-
server.once("error", reject);
|
|
9193
|
-
server.listen(options.port, options.host, () => {
|
|
9194
|
-
server.off("error", reject);
|
|
9195
|
-
resolve2();
|
|
9196
|
-
});
|
|
9197
|
-
});
|
|
9198
|
-
const address = server.address();
|
|
9199
|
-
if (!address || typeof address === "string") {
|
|
9200
|
-
throw new Error("Server did not bind to a TCP port");
|
|
9201
|
-
}
|
|
10178
|
+
const address = await listenTcpServer(server, options.port, options.host);
|
|
9202
10179
|
return {
|
|
9203
10180
|
host: options.host,
|
|
9204
10181
|
port: address.port,
|
|
9205
|
-
url: `http://${
|
|
10182
|
+
url: `http://${tcpListenerUrlHost(address.address)}:${address.port}`,
|
|
9206
10183
|
server,
|
|
9207
10184
|
inferenceLogPath: options.inferenceLogPath,
|
|
9208
10185
|
close: () => new Promise((resolve2, reject) => {
|
|
@@ -9223,7 +10200,16 @@ async function routeRequest(req, res, options, modelCache, plog) {
|
|
|
9223
10200
|
return;
|
|
9224
10201
|
}
|
|
9225
10202
|
if (req.method === "GET" && pathname === "/models") {
|
|
9226
|
-
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
|
+
});
|
|
9227
10213
|
return;
|
|
9228
10214
|
}
|
|
9229
10215
|
if (req.method === "GET" && pathname === "/anthropic/v1/models") {
|
|
@@ -9258,7 +10244,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9258
10244
|
plog(`model not found: ${body.model}`);
|
|
9259
10245
|
return;
|
|
9260
10246
|
}
|
|
9261
|
-
const requestId =
|
|
10247
|
+
const requestId = randomUUID9();
|
|
9262
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"];
|
|
9263
10249
|
const claudeSessionId = extractClaudeSessionId(body, claudeSessionIdHeader);
|
|
9264
10250
|
if (options.webSocketDiagnosticsLogPath) {
|
|
@@ -9282,12 +10268,21 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9282
10268
|
return;
|
|
9283
10269
|
}
|
|
9284
10270
|
const messagesUrl = `${model.baseUrl}/v1/messages`;
|
|
9285
|
-
|
|
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
|
+
}
|
|
9286
10280
|
const betaHeaderRaw = req.headers["anthropic-beta"];
|
|
9287
10281
|
const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
|
|
9288
10282
|
const clientWantsStream = Boolean(body.stream);
|
|
9289
10283
|
const forwardBody = { ...body, model: upstreamModelId(model) };
|
|
9290
|
-
const
|
|
10284
|
+
const authType = model.authType ?? "api";
|
|
10285
|
+
const isOAuth = authType === "oauth";
|
|
9291
10286
|
auditInference(options, {
|
|
9292
10287
|
requestId,
|
|
9293
10288
|
modelId: body.model,
|
|
@@ -9306,11 +10301,15 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9306
10301
|
claudeCodeSessionId = identity.sessionId;
|
|
9307
10302
|
effectiveBeta = selectBetaFlags(forwardBody, upstreamModelId(model), inboundBeta);
|
|
9308
10303
|
}
|
|
9309
|
-
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;
|
|
9310
10309
|
plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
|
|
9311
10310
|
await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
|
|
9312
10311
|
inboundBeta: effectiveBeta,
|
|
9313
|
-
authType
|
|
10312
|
+
authType,
|
|
9314
10313
|
log: (message) => plog(message),
|
|
9315
10314
|
claudeCodeSessionId,
|
|
9316
10315
|
extraHeaders: model.headers,
|
|
@@ -9334,7 +10333,15 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9334
10333
|
sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
|
|
9335
10334
|
return;
|
|
9336
10335
|
}
|
|
9337
|
-
|
|
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
|
+
}
|
|
9338
10345
|
auditInference(options, {
|
|
9339
10346
|
requestId,
|
|
9340
10347
|
modelId: body.model,
|
|
@@ -9344,14 +10351,6 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9344
10351
|
route: "translated",
|
|
9345
10352
|
requestPreview: getLatestMessagePreview(body.messages, body.system)
|
|
9346
10353
|
});
|
|
9347
|
-
const languageModel = await getOrInitLanguageModel(
|
|
9348
|
-
modelCache,
|
|
9349
|
-
model,
|
|
9350
|
-
model.npm,
|
|
9351
|
-
model.apiBaseUrl,
|
|
9352
|
-
apiKey,
|
|
9353
|
-
options.webSocketDiagnosticsLogPath
|
|
9354
|
-
);
|
|
9355
10354
|
const npmMaxTools = maxToolsForNpm(model.npm);
|
|
9356
10355
|
const toolCount = Array.isArray(body.tools) ? body.tools.length : 0;
|
|
9357
10356
|
if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
|
|
@@ -9375,63 +10374,93 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9375
10374
|
const clientWantsStream = Boolean(body.stream);
|
|
9376
10375
|
const responseModelId = getResponseModelId(body.model, model, options);
|
|
9377
10376
|
plog(() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
res.write(chunk);
|
|
9389
|
-
};
|
|
9390
|
-
await withResponsesWebSocketDiagnosticContext(
|
|
9391
|
-
{ requestId, claudeSessionId },
|
|
9392
|
-
() => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
|
|
9393
|
-
initialInputTokens: estimateAnthropicInputTokens(body)
|
|
9394
|
-
})
|
|
9395
|
-
);
|
|
9396
|
-
if (!res.headersSent) writeStreamChunk("");
|
|
9397
|
-
res.end();
|
|
9398
|
-
} else {
|
|
9399
|
-
const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
|
|
9400
|
-
{ requestId, claudeSessionId },
|
|
9401
|
-
() => 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
|
|
9402
10387
|
);
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
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();
|
|
9421
10407
|
} else {
|
|
9422
|
-
|
|
10408
|
+
const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
|
|
10409
|
+
{ requestId, claudeSessionId },
|
|
10410
|
+
() => generateAnthropicResponse(languageModel, params, responseModelId, { forceStream: openAiOAuth })
|
|
10411
|
+
);
|
|
10412
|
+
sendJson(res, 200, anthropicResponse);
|
|
9423
10413
|
}
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
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
|
|
9427
10454
|
data: ${JSON.stringify({
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
10455
|
+
type: "error",
|
|
10456
|
+
error: { type: errorType, message: clientMessage },
|
|
10457
|
+
...contextLengthExceeded ? { request_id: requestId } : {}
|
|
10458
|
+
})}
|
|
9432
10459
|
|
|
9433
10460
|
`);
|
|
9434
|
-
|
|
10461
|
+
res.end();
|
|
10462
|
+
}
|
|
10463
|
+
break;
|
|
9435
10464
|
}
|
|
9436
10465
|
}
|
|
9437
10466
|
return;
|
|
@@ -9456,7 +10485,15 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9456
10485
|
return;
|
|
9457
10486
|
}
|
|
9458
10487
|
const completionsUrl = model.completionsUrl;
|
|
9459
|
-
|
|
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
|
+
}
|
|
9460
10497
|
const forwardBody = body.model === upstreamModelId(model) ? body : { ...body, model: upstreamModelId(model) };
|
|
9461
10498
|
auditInference(options, {
|
|
9462
10499
|
modelId: body.model,
|
|
@@ -9465,7 +10502,19 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9465
10502
|
route: "passthrough",
|
|
9466
10503
|
requestPreview: getLatestMessagePreview(body.messages, body.system)
|
|
9467
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;
|
|
9468
10511
|
await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
|
|
10512
|
+
authType: model.authType ?? "api",
|
|
10513
|
+
extraHeaders: model.headers,
|
|
10514
|
+
refreshToken,
|
|
10515
|
+
onTokenRefreshed: (refreshed) => {
|
|
10516
|
+
model.apiKey = refreshed;
|
|
10517
|
+
},
|
|
9469
10518
|
onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
|
|
9470
10519
|
modelId: body.model,
|
|
9471
10520
|
provider: inferenceProvider(model),
|
|
@@ -9481,7 +10530,15 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9481
10530
|
sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
|
|
9482
10531
|
return;
|
|
9483
10532
|
}
|
|
9484
|
-
|
|
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
|
+
}
|
|
9485
10542
|
auditInference(options, {
|
|
9486
10543
|
modelId: body.model,
|
|
9487
10544
|
effort: openAiEffort(body),
|
|
@@ -9490,42 +10547,70 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
|
|
|
9490
10547
|
requestPreview: getLatestMessagePreview(body.messages, body.system)
|
|
9491
10548
|
});
|
|
9492
10549
|
const baseURL = model.modelFormat === "anthropic" ? model.baseUrl : model.apiBaseUrl;
|
|
9493
|
-
const languageModel = await getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey);
|
|
9494
10550
|
const openAiOAuth = npm === "@ai-sdk/openai" && model.authType === "oauth";
|
|
9495
10551
|
const params = translateOpenAiRequest(body, { openAiOAuth });
|
|
9496
10552
|
const clientWantsStream = Boolean(body.stream);
|
|
9497
10553
|
const responseModelId = getResponseModelId(body.model, model, options);
|
|
9498
10554
|
plog(() => `sdk-openai npm=${npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
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 } })}
|
|
9526
10609
|
|
|
9527
10610
|
`);
|
|
9528
|
-
|
|
10611
|
+
res.end();
|
|
10612
|
+
}
|
|
10613
|
+
break;
|
|
9529
10614
|
}
|
|
9530
10615
|
}
|
|
9531
10616
|
}
|
|
@@ -9549,9 +10634,9 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
|
|
|
9549
10634
|
npm,
|
|
9550
10635
|
baseURL ?? ""
|
|
9551
10636
|
].join("");
|
|
9552
|
-
let
|
|
9553
|
-
if (!
|
|
9554
|
-
languageModel = await createLanguageModel({
|
|
10637
|
+
let cached = modelCache.get(cacheKey);
|
|
10638
|
+
if (!cached || cached.apiKey !== apiKey) {
|
|
10639
|
+
const languageModel = await createLanguageModel({
|
|
9555
10640
|
npm,
|
|
9556
10641
|
modelId: upstreamModelId(model),
|
|
9557
10642
|
apiKey,
|
|
@@ -9564,9 +10649,10 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
|
|
|
9564
10649
|
preferWebSockets: model.preferWebSockets,
|
|
9565
10650
|
onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
|
|
9566
10651
|
});
|
|
9567
|
-
|
|
10652
|
+
cached = { apiKey, languageModel };
|
|
10653
|
+
modelCache.set(cacheKey, cached);
|
|
9568
10654
|
}
|
|
9569
|
-
return languageModel;
|
|
10655
|
+
return cached.languageModel;
|
|
9570
10656
|
}
|
|
9571
10657
|
function getResponseModelId(bodyModel, model, options) {
|
|
9572
10658
|
if (typeof bodyModel === "string" && options.aliasNames?.has(bodyModel)) return bodyModel;
|
|
@@ -9709,14 +10795,14 @@ import * as p8 from "@clack/prompts";
|
|
|
9709
10795
|
import * as http from "http";
|
|
9710
10796
|
import * as https from "https";
|
|
9711
10797
|
import * as net from "net";
|
|
9712
|
-
import { randomUUID as
|
|
10798
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
9713
10799
|
import { URL as URL2 } from "url";
|
|
9714
10800
|
import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
|
|
9715
10801
|
|
|
9716
10802
|
// src/http-proxy/ca.ts
|
|
9717
10803
|
import { randomBytes } from "crypto";
|
|
9718
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
9719
|
-
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";
|
|
9720
10806
|
import forge from "node-forge";
|
|
9721
10807
|
var CERT_DIR = "http-proxy";
|
|
9722
10808
|
var CA_CERT_FILE = "clodex-ca.pem";
|
|
@@ -9742,15 +10828,15 @@ function certPaths() {
|
|
|
9742
10828
|
};
|
|
9743
10829
|
}
|
|
9744
10830
|
function writePrivate(path, value) {
|
|
9745
|
-
|
|
10831
|
+
writeFileSync6(path, value, { encoding: "utf8", mode: 384 });
|
|
9746
10832
|
chmodSync5(path, 384);
|
|
9747
10833
|
}
|
|
9748
10834
|
function writePublic(path, value) {
|
|
9749
|
-
|
|
10835
|
+
writeFileSync6(path, value, { encoding: "utf8", mode: 420 });
|
|
9750
10836
|
chmodSync5(path, 420);
|
|
9751
10837
|
}
|
|
9752
10838
|
function generateCertificates(paths) {
|
|
9753
|
-
|
|
10839
|
+
mkdirSync7(paths.dir, { recursive: true, mode: 448 });
|
|
9754
10840
|
chmodSync5(paths.dir, 448);
|
|
9755
10841
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
9756
10842
|
const caCert = forge.pki.createCertificate();
|
|
@@ -9791,8 +10877,8 @@ function generateCertificates(paths) {
|
|
|
9791
10877
|
}
|
|
9792
10878
|
function storedCertificatesAreCurrent(paths) {
|
|
9793
10879
|
try {
|
|
9794
|
-
const ca = forge.pki.certificateFromPem(
|
|
9795
|
-
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"));
|
|
9796
10882
|
const now = Date.now();
|
|
9797
10883
|
const renewalBuffer = 7 * 24 * 60 * 60 * 1e3;
|
|
9798
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);
|
|
@@ -9803,23 +10889,23 @@ function storedCertificatesAreCurrent(paths) {
|
|
|
9803
10889
|
function ensureHttpProxyCertificates() {
|
|
9804
10890
|
const paths = certPaths();
|
|
9805
10891
|
const required = [paths.caCert, paths.caKey, paths.serverCert, paths.serverKey, paths.version];
|
|
9806
|
-
const current = required.every(
|
|
10892
|
+
const current = required.every(existsSync6) && readFileSync8(paths.version, "utf8") === CERT_VERSION && storedCertificatesAreCurrent(paths);
|
|
9807
10893
|
if (!current) generateCertificates(paths);
|
|
9808
10894
|
return {
|
|
9809
10895
|
caCertPath: paths.caCert,
|
|
9810
|
-
caCert:
|
|
9811
|
-
serverCert:
|
|
9812
|
-
serverKey:
|
|
10896
|
+
caCert: readFileSync8(paths.caCert, "utf8"),
|
|
10897
|
+
serverCert: readFileSync8(paths.serverCert, "utf8"),
|
|
10898
|
+
serverKey: readFileSync8(paths.serverKey, "utf8")
|
|
9813
10899
|
};
|
|
9814
10900
|
}
|
|
9815
10901
|
function ensureHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
|
|
9816
10902
|
if (!additionalCaCertPath?.trim()) return relayCaCertPath;
|
|
9817
10903
|
try {
|
|
9818
10904
|
if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) return relayCaCertPath;
|
|
9819
|
-
const relayCa =
|
|
9820
|
-
const additionalCa =
|
|
10905
|
+
const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
|
|
10906
|
+
const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
|
|
9821
10907
|
if (!additionalCa) return relayCaCertPath;
|
|
9822
|
-
const combinedPath = join5(
|
|
10908
|
+
const combinedPath = join5(dirname6(relayCaCertPath), "combined-ca.pem");
|
|
9823
10909
|
writePublic(combinedPath, `${relayCa}
|
|
9824
10910
|
${additionalCa}
|
|
9825
10911
|
`);
|
|
@@ -10373,7 +11459,7 @@ async function startHttpProxy(options) {
|
|
|
10373
11459
|
}
|
|
10374
11460
|
const messagesEndpoint = anthropicMessagesEndpoint(req.url);
|
|
10375
11461
|
if (req.method === "POST" && messagesEndpoint) {
|
|
10376
|
-
const requestId =
|
|
11462
|
+
const requestId = randomUUID10();
|
|
10377
11463
|
let parsed = null;
|
|
10378
11464
|
let route;
|
|
10379
11465
|
try {
|
|
@@ -10501,23 +11587,17 @@ async function startHttpProxy(options) {
|
|
|
10501
11587
|
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n", () => clientSocket.destroy());
|
|
10502
11588
|
});
|
|
10503
11589
|
});
|
|
11590
|
+
let address;
|
|
10504
11591
|
try {
|
|
10505
|
-
await
|
|
10506
|
-
proxyServer
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
});
|
|
10511
|
-
});
|
|
11592
|
+
address = await listenTcpServer(
|
|
11593
|
+
proxyServer,
|
|
11594
|
+
options.port ?? 0,
|
|
11595
|
+
options.host ?? "127.0.0.1"
|
|
11596
|
+
);
|
|
10512
11597
|
} catch (err) {
|
|
10513
11598
|
adapter?.close();
|
|
10514
11599
|
throw err;
|
|
10515
11600
|
}
|
|
10516
|
-
const address = proxyServer.address();
|
|
10517
|
-
if (!address || typeof address === "string") {
|
|
10518
|
-
adapter?.close();
|
|
10519
|
-
throw new Error("HTTP proxy did not bind to a TCP port");
|
|
10520
|
-
}
|
|
10521
11601
|
return {
|
|
10522
11602
|
host: options.host ?? "127.0.0.1",
|
|
10523
11603
|
port: address.port,
|
|
@@ -10627,12 +11707,13 @@ function waitForShutdown() {
|
|
|
10627
11707
|
}
|
|
10628
11708
|
async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = false, port, noDiscovery = false) {
|
|
10629
11709
|
const webSocketDiagnosticsLogPath = webSocketDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
|
|
11710
|
+
const inferenceLogPath = getInferenceRequestLogPath();
|
|
10630
11711
|
let started;
|
|
10631
11712
|
try {
|
|
10632
11713
|
started = await startConfiguredHttpProxy(
|
|
10633
11714
|
port ?? DEFAULT_SERVER_PORT,
|
|
10634
11715
|
debug,
|
|
10635
|
-
|
|
11716
|
+
inferenceLogPath,
|
|
10636
11717
|
void 0,
|
|
10637
11718
|
webSocketDiagnosticsLogPath
|
|
10638
11719
|
);
|
|
@@ -10641,6 +11722,13 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
|
|
|
10641
11722
|
return 1;
|
|
10642
11723
|
}
|
|
10643
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
|
+
});
|
|
10644
11732
|
console.log("");
|
|
10645
11733
|
console.log(pc9.bold(pc9.green("clodex proxy-mode server running")));
|
|
10646
11734
|
console.log(` HTTPS_PROXY=http://127.0.0.1:${handle.port}`);
|
|
@@ -10668,8 +11756,23 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
|
|
|
10668
11756
|
});
|
|
10669
11757
|
}
|
|
10670
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
|
+
});
|
|
10671
11767
|
if (!noDiscovery) unregisterServerRuntimeState();
|
|
10672
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
|
+
});
|
|
10673
11776
|
return 0;
|
|
10674
11777
|
}
|
|
10675
11778
|
|
|
@@ -11282,17 +12385,17 @@ function planLaunchWizard(opts) {
|
|
|
11282
12385
|
}
|
|
11283
12386
|
|
|
11284
12387
|
// src/patcher.ts
|
|
11285
|
-
import { createHash as
|
|
12388
|
+
import { createHash as createHash8 } from "crypto";
|
|
11286
12389
|
import {
|
|
11287
12390
|
copyFileSync as copyFileSync2,
|
|
11288
|
-
existsSync as
|
|
11289
|
-
mkdirSync as
|
|
11290
|
-
readFileSync as
|
|
12391
|
+
existsSync as existsSync7,
|
|
12392
|
+
mkdirSync as mkdirSync8,
|
|
12393
|
+
readFileSync as readFileSync9,
|
|
11291
12394
|
statSync as statSync4,
|
|
11292
|
-
unlinkSync as
|
|
11293
|
-
writeFileSync as
|
|
11294
|
-
openSync as
|
|
11295
|
-
closeSync as
|
|
12395
|
+
unlinkSync as unlinkSync5,
|
|
12396
|
+
writeFileSync as writeFileSync7,
|
|
12397
|
+
openSync as openSync5,
|
|
12398
|
+
closeSync as closeSync5,
|
|
11296
12399
|
realpathSync
|
|
11297
12400
|
} from "fs";
|
|
11298
12401
|
import { homedir as homedir2 } from "os";
|
|
@@ -11489,7 +12592,7 @@ function getPatchLockPath() {
|
|
|
11489
12592
|
}
|
|
11490
12593
|
function readPatchManifest(path = getPatchManifestPath()) {
|
|
11491
12594
|
try {
|
|
11492
|
-
const parsed = JSON.parse(
|
|
12595
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
11493
12596
|
if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
|
|
11494
12597
|
return parsed;
|
|
11495
12598
|
}
|
|
@@ -11498,8 +12601,8 @@ function readPatchManifest(path = getPatchManifestPath()) {
|
|
|
11498
12601
|
return null;
|
|
11499
12602
|
}
|
|
11500
12603
|
function writePatchManifest(manifest, path = getPatchManifestPath()) {
|
|
11501
|
-
|
|
11502
|
-
|
|
12604
|
+
mkdirSync8(getAppHome(), { recursive: true, mode: 448 });
|
|
12605
|
+
writeFileSync7(path, `${JSON.stringify(manifest, null, 2)}
|
|
11503
12606
|
`, { encoding: "utf8", mode: 384 });
|
|
11504
12607
|
}
|
|
11505
12608
|
function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
|
|
@@ -11527,7 +12630,7 @@ function computePatchConfigHash(config) {
|
|
|
11527
12630
|
const entry = config[key];
|
|
11528
12631
|
return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
|
|
11529
12632
|
});
|
|
11530
|
-
return
|
|
12633
|
+
return createHash8("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
11531
12634
|
}
|
|
11532
12635
|
function buildDesiredPatchConfig() {
|
|
11533
12636
|
const prefs = loadPreferences();
|
|
@@ -11570,30 +12673,30 @@ function pidIsAlive(pid) {
|
|
|
11570
12673
|
function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
|
|
11571
12674
|
const now = opts.now ?? Date.now();
|
|
11572
12675
|
const isAlive = opts.isAlive ?? pidIsAlive;
|
|
11573
|
-
|
|
12676
|
+
mkdirSync8(join6(lockPath, ".."), { recursive: true, mode: 448 });
|
|
11574
12677
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
11575
12678
|
try {
|
|
11576
|
-
const fd =
|
|
12679
|
+
const fd = openSync5(lockPath, "wx");
|
|
11577
12680
|
const content = { pid: process.pid, startedAt: now };
|
|
11578
|
-
|
|
11579
|
-
|
|
12681
|
+
writeFileSync7(fd, JSON.stringify(content));
|
|
12682
|
+
closeSync5(fd);
|
|
11580
12683
|
return () => {
|
|
11581
12684
|
try {
|
|
11582
|
-
|
|
12685
|
+
unlinkSync5(lockPath);
|
|
11583
12686
|
} catch {
|
|
11584
12687
|
}
|
|
11585
12688
|
};
|
|
11586
12689
|
} catch {
|
|
11587
12690
|
let stale = false;
|
|
11588
12691
|
try {
|
|
11589
|
-
const existing = JSON.parse(
|
|
12692
|
+
const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
|
|
11590
12693
|
stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
|
|
11591
12694
|
} catch {
|
|
11592
12695
|
stale = true;
|
|
11593
12696
|
}
|
|
11594
12697
|
if (!stale) return null;
|
|
11595
12698
|
try {
|
|
11596
|
-
|
|
12699
|
+
unlinkSync5(lockPath);
|
|
11597
12700
|
} catch {
|
|
11598
12701
|
}
|
|
11599
12702
|
}
|
|
@@ -11601,12 +12704,12 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
|
|
|
11601
12704
|
return null;
|
|
11602
12705
|
}
|
|
11603
12706
|
function sha256File(path) {
|
|
11604
|
-
return
|
|
12707
|
+
return createHash8("sha256").update(readFileSync9(path)).digest("hex");
|
|
11605
12708
|
}
|
|
11606
12709
|
function resolveClaudeBinaryForPatch() {
|
|
11607
12710
|
const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
|
|
11608
12711
|
const nativeSymlink = join6(homedir2(), ".local", "bin", "claude");
|
|
11609
|
-
const source = envOverride?.trim() || (
|
|
12712
|
+
const source = envOverride?.trim() || (existsSync7(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
|
|
11610
12713
|
if (!source) return null;
|
|
11611
12714
|
let resolved;
|
|
11612
12715
|
try {
|
|
@@ -11641,13 +12744,13 @@ function summarizePatchResults(results) {
|
|
|
11641
12744
|
}
|
|
11642
12745
|
async function applyPatch(binaryPath, version, desired, configHash, opts) {
|
|
11643
12746
|
const backup = pristineBackupPath(version, binaryPath);
|
|
11644
|
-
|
|
12747
|
+
mkdirSync8(backupDir(), { recursive: true });
|
|
11645
12748
|
if (opts.restoreFirst) {
|
|
11646
|
-
if (!
|
|
12749
|
+
if (!existsSync7(backup)) {
|
|
11647
12750
|
return { ok: false, message: `Cannot re-patch: pristine backup missing at ${backup}. Reinstall claude, then run clodex patch.` };
|
|
11648
12751
|
}
|
|
11649
12752
|
copyFileSync2(backup, binaryPath);
|
|
11650
|
-
} else if (!
|
|
12753
|
+
} else if (!existsSync7(backup)) {
|
|
11651
12754
|
copyFileSync2(binaryPath, backup);
|
|
11652
12755
|
}
|
|
11653
12756
|
copyFileSync2(backup, join6(backupDir(), "native-binary.backup"));
|
|
@@ -11703,14 +12806,14 @@ async function runPatchCommand(opts = {}) {
|
|
|
11703
12806
|
const { binaryPath, version } = resolved;
|
|
11704
12807
|
if (opts.restore) {
|
|
11705
12808
|
const manifest2 = readPatchManifest();
|
|
11706
|
-
const backup = manifest2?.backupPath &&
|
|
11707
|
-
if (!
|
|
12809
|
+
const backup = manifest2?.backupPath && existsSync7(manifest2.backupPath) ? manifest2.backupPath : pristineBackupPath(version, binaryPath);
|
|
12810
|
+
if (!existsSync7(backup)) {
|
|
11708
12811
|
p11.log.error(`No pristine backup found for claude ${version} (${backup}).`);
|
|
11709
12812
|
return 1;
|
|
11710
12813
|
}
|
|
11711
12814
|
copyFileSync2(backup, binaryPath);
|
|
11712
12815
|
try {
|
|
11713
|
-
|
|
12816
|
+
unlinkSync5(getPatchManifestPath());
|
|
11714
12817
|
} catch {
|
|
11715
12818
|
}
|
|
11716
12819
|
p11.log.success(`Restored pristine claude ${version} from ${backup}.`);
|
|
@@ -11743,7 +12846,7 @@ async function runPatchCommand(opts = {}) {
|
|
|
11743
12846
|
}
|
|
11744
12847
|
try {
|
|
11745
12848
|
const backup = pristineBackupPath(version, binaryPath);
|
|
11746
|
-
const restoreFirst =
|
|
12849
|
+
const restoreFirst = existsSync7(backup) && sha256File(backup) !== sha256File(binaryPath);
|
|
11747
12850
|
if (restoreFirst) {
|
|
11748
12851
|
p11.log.info("Binary differs from its pristine backup \u2014 restoring it before patching fresh.");
|
|
11749
12852
|
}
|
|
@@ -12836,7 +13939,8 @@ Error: ${launchPlan.error}
|
|
|
12836
13939
|
return 0;
|
|
12837
13940
|
}
|
|
12838
13941
|
const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
|
|
12839
|
-
|
|
13942
|
+
const anonymousProvider = activeProvider.authType === "none";
|
|
13943
|
+
if (!anonymousProvider && !launchApiKey?.trim()) {
|
|
12840
13944
|
p12.log.error(
|
|
12841
13945
|
`No credential found for ${activeProvider.name}. Add a key or sign in with clodex providers.`
|
|
12842
13946
|
);
|
|
@@ -12845,7 +13949,8 @@ Error: ${launchPlan.error}
|
|
|
12845
13949
|
let proxyHandle = null;
|
|
12846
13950
|
let childEnv;
|
|
12847
13951
|
const isOAuthAnthropic = selectedModel.modelFormat === "anthropic" && activeProvider.authType === "oauth";
|
|
12848
|
-
|
|
13952
|
+
const usesAnthropicProxy = selectedModel.modelFormat === "anthropic" && (isOAuthAnthropic || anonymousProvider);
|
|
13953
|
+
if (usesAnthropicProxy) {
|
|
12849
13954
|
try {
|
|
12850
13955
|
proxyHandle = await startProxy(
|
|
12851
13956
|
selectedModel.baseUrl ?? "https://api.anthropic.com",
|
|
@@ -12854,16 +13959,17 @@ Error: ${launchPlan.error}
|
|
|
12854
13959
|
selectedModel.contextWindow,
|
|
12855
13960
|
{
|
|
12856
13961
|
providerId: activeProvider.id,
|
|
12857
|
-
authType:
|
|
13962
|
+
authType: activeProvider.authType,
|
|
12858
13963
|
oauthAccountId: activeProvider.oauthAccountId,
|
|
12859
13964
|
providerData: activeProvider.providerData,
|
|
12860
|
-
modelFormat: "anthropic"
|
|
13965
|
+
modelFormat: "anthropic",
|
|
13966
|
+
headers: activeProvider.headers
|
|
12861
13967
|
},
|
|
12862
|
-
launchApiKey
|
|
13968
|
+
launchApiKey ?? ""
|
|
12863
13969
|
);
|
|
12864
|
-
if (!isAgentStdoutMode()) p12.log.info(`
|
|
13970
|
+
if (!isAgentStdoutMode()) p12.log.info(`Anthropic proxy started on port ${proxyHandle.port}`);
|
|
12865
13971
|
} catch (err) {
|
|
12866
|
-
p12.log.error(`Failed to start
|
|
13972
|
+
p12.log.error(`Failed to start Anthropic proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
12867
13973
|
return 1;
|
|
12868
13974
|
}
|
|
12869
13975
|
childEnv = buildChildEnv(
|
|
@@ -12877,7 +13983,7 @@ Error: ${launchPlan.error}
|
|
|
12877
13983
|
childEnv = buildChildEnv(
|
|
12878
13984
|
selectedModel.baseUrl,
|
|
12879
13985
|
selectedModel.id,
|
|
12880
|
-
launchApiKey,
|
|
13986
|
+
launchApiKey ?? "",
|
|
12881
13987
|
void 0,
|
|
12882
13988
|
selectedModel.contextWindow
|
|
12883
13989
|
);
|
|
@@ -12899,9 +14005,10 @@ Error: ${launchPlan.error}
|
|
|
12899
14005
|
reasoning: selectedModel.reasoning,
|
|
12900
14006
|
interleavedReasoningField: selectedModel.interleavedReasoningField,
|
|
12901
14007
|
useResponsesLite: selectedModel.useResponsesLite,
|
|
12902
|
-
preferWebSockets: selectedModel.preferWebSockets
|
|
14008
|
+
preferWebSockets: selectedModel.preferWebSockets,
|
|
14009
|
+
headers: activeProvider.headers
|
|
12903
14010
|
},
|
|
12904
|
-
launchApiKey
|
|
14011
|
+
launchApiKey ?? ""
|
|
12905
14012
|
);
|
|
12906
14013
|
if (!isAgentStdoutMode()) {
|
|
12907
14014
|
p12.log.info(
|
|
@@ -12920,7 +14027,7 @@ Error: ${launchPlan.error}
|
|
|
12920
14027
|
selectedModel.contextWindow
|
|
12921
14028
|
);
|
|
12922
14029
|
}
|
|
12923
|
-
if (selectedModel.modelFormat === "anthropic" && !
|
|
14030
|
+
if (selectedModel.modelFormat === "anthropic" && !usesAnthropicProxy) {
|
|
12924
14031
|
childEnv["CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"] = "1";
|
|
12925
14032
|
}
|
|
12926
14033
|
const debugLogPath = prepareClaudeTraceLog();
|