@juspay/neurolink 10.4.3 → 10.4.4
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/CHANGELOG.md +6 -0
- package/dist/auth/tokenStore.d.ts +7 -0
- package/dist/auth/tokenStore.js +54 -0
- package/dist/browser/neurolink.min.js +374 -374
- package/dist/cli/commands/proxy.d.ts +1 -0
- package/dist/cli/commands/proxy.js +183 -24
- package/dist/lib/auth/tokenStore.d.ts +7 -0
- package/dist/lib/auth/tokenStore.js +54 -0
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
- package/dist/lib/proxy/rawStreamCapture.js +47 -1
- package/dist/lib/proxy/sseInterceptor.js +47 -1
- package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
- package/dist/lib/proxy/tokenRefresh.js +70 -15
- package/dist/lib/proxy/usageStats.d.ts +25 -3
- package/dist/lib/proxy/usageStats.js +546 -55
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +198 -222
- package/dist/lib/types/proxy.d.ts +60 -1
- package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/proxy/proxyTranslationEngine.js +56 -12
- package/dist/proxy/rawStreamCapture.js +47 -1
- package/dist/proxy/sseInterceptor.js +47 -1
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +70 -15
- package/dist/proxy/usageStats.d.ts +25 -3
- package/dist/proxy/usageStats.js +546 -55
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/server/routes/claudeProxyRoutes.js +198 -222
- package/dist/types/proxy.d.ts +60 -1
- package/package.json +1 -1
|
@@ -57,6 +57,7 @@ export declare function createProxyStartApp(params: {
|
|
|
57
57
|
readiness: ProxyReadinessState;
|
|
58
58
|
}>;
|
|
59
59
|
export declare const proxyStartCommand: CommandModule<object, ProxyStartArgs>;
|
|
60
|
+
export declare function sanitizeProxyStatusTerminalErrorMessage(message: string): string;
|
|
60
61
|
export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
|
|
61
62
|
export declare const proxyTelemetryCommand: CommandModule<object, ProxyTelemetryArgs>;
|
|
62
63
|
export declare const proxyGuardCommand: CommandModule<object, ProxyGuardArgs>;
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { stripVTControlCharacters } from "node:util";
|
|
15
16
|
import chalk from "chalk";
|
|
16
17
|
import ora from "ora";
|
|
17
18
|
import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
|
|
18
19
|
import { logger } from "../../lib/utils/logger.js";
|
|
19
|
-
import { sanitizeForLog } from "../../lib/utils/logSanitize.js";
|
|
20
|
+
import { redactUrlsInText, sanitizeForLog, } from "../../lib/utils/logSanitize.js";
|
|
20
21
|
import { withTimeout } from "../../lib/utils/async/withTimeout.js";
|
|
21
22
|
import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
|
|
22
23
|
import { configureProxyKeepAliveDispatcher } from "../../lib/proxy/proxyDispatcher.js";
|
|
@@ -39,9 +40,11 @@ import { fileURLToPath } from "node:url";
|
|
|
39
40
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
40
41
|
const _require = createRequire(import.meta.url);
|
|
41
42
|
const PROXY_VERSION = packageJson.version;
|
|
43
|
+
const PROXY_INTERNAL_ACCOUNT_LABEL = "proxy/internal";
|
|
42
44
|
const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
|
|
43
45
|
const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
44
46
|
const LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS = 5_000;
|
|
47
|
+
const PROXY_STATUS_TOKEN_READ_TIMEOUT_MS = 2_000;
|
|
45
48
|
let legacyStatusAccountCache;
|
|
46
49
|
// Allowed drift between a pid's OS-reported start time and the persisted
|
|
47
50
|
// ProxySupervisorState.startTime before processLooksLikeProxySupervisor
|
|
@@ -353,6 +356,15 @@ async function resolveLegacyStatusAccountLabel(storedAnthropicAccountCount) {
|
|
|
353
356
|
};
|
|
354
357
|
return label;
|
|
355
358
|
}
|
|
359
|
+
function deriveAccountAllowance(accountKey, now, allowlist, expirations, cooldowns) {
|
|
360
|
+
const allowed = isAccountAllowed(accountKey, allowlist);
|
|
361
|
+
const expiresAt = expirations.get(accountKey);
|
|
362
|
+
return {
|
|
363
|
+
allowed,
|
|
364
|
+
expired: expiresAt !== undefined && expiresAt <= now,
|
|
365
|
+
cooling: (cooldowns[accountKey]?.coolingUntil ?? 0) > now,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
356
368
|
/**
|
|
357
369
|
* Check if the launchd service is loaded and actively managing the proxy.
|
|
358
370
|
* Returns true if launchctl reports the service as running.
|
|
@@ -1248,7 +1260,13 @@ export async function createProxyStartApp(params) {
|
|
|
1248
1260
|
const recordRuntimeError = async (metadata, status, errorType, errorMessage, options) => {
|
|
1249
1261
|
const clientMessage = options?.clientMessage ?? errorMessage;
|
|
1250
1262
|
const clientErrorType = options?.clientErrorType ?? errorType;
|
|
1251
|
-
recordFinalError(status
|
|
1263
|
+
recordFinalError(status, undefined, undefined, {
|
|
1264
|
+
requestId: metadata.requestId,
|
|
1265
|
+
errorType,
|
|
1266
|
+
errorCode: options?.errorCode,
|
|
1267
|
+
terminalOutcome: "handler_error",
|
|
1268
|
+
message: errorMessage,
|
|
1269
|
+
});
|
|
1252
1270
|
await Promise.all([
|
|
1253
1271
|
logRequest({
|
|
1254
1272
|
timestamp: new Date().toISOString(),
|
|
@@ -1522,21 +1540,45 @@ export async function createProxyStartApp(params) {
|
|
|
1522
1540
|
const activeAccountAllowlist = runtimeConfig
|
|
1523
1541
|
? runtimeConfig.accountAllowlist
|
|
1524
1542
|
: params.accountAllowlist;
|
|
1525
|
-
const {
|
|
1543
|
+
const { getReconciledUsageSnapshot, getUsageStatsPersistenceStatus } = await import("../../lib/proxy/usageStats.js");
|
|
1526
1544
|
const { loadAccountCooldowns } = await import("../../lib/proxy/accountCooldown.js");
|
|
1527
|
-
const
|
|
1545
|
+
const usageSnapshot = await getReconciledUsageSnapshot();
|
|
1546
|
+
const { stats, terminalErrors } = usageSnapshot;
|
|
1547
|
+
const terminalErrorDetailsComparable = usageSnapshot.statsVersion === usageSnapshot.terminalErrorsVersion;
|
|
1548
|
+
const lastTerminalError = terminalErrors.recent.at(-1) ?? null;
|
|
1549
|
+
const terminalErrorDetailsMissing = terminalErrorDetailsComparable
|
|
1550
|
+
? Math.max(0, stats.totalErrors - terminalErrors.totalErrors)
|
|
1551
|
+
: undefined;
|
|
1552
|
+
const terminalErrorDetailsExcess = terminalErrorDetailsComparable
|
|
1553
|
+
? Math.max(0, terminalErrors.totalErrors - stats.totalErrors)
|
|
1554
|
+
: undefined;
|
|
1528
1555
|
const runtimeState = loadProxyState();
|
|
1529
1556
|
const supervisorState = loadProxySupervisorState();
|
|
1530
1557
|
const updateState = loadUpdateState();
|
|
1531
1558
|
const cooldowns = await loadAccountCooldowns();
|
|
1532
1559
|
const storedAccountKeys = new Set();
|
|
1560
|
+
const storedAccountExpirations = new Map();
|
|
1533
1561
|
const disabledAccountKeys = new Set();
|
|
1534
1562
|
let accountInventoryLoaded = false;
|
|
1535
1563
|
try {
|
|
1536
1564
|
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
1537
|
-
|
|
1538
|
-
|
|
1565
|
+
const storedKeys = await tokenStore.listByPrefix("anthropic:");
|
|
1566
|
+
for (const key of storedKeys) {
|
|
1567
|
+
const normalizedKey = normalizeAnthropicAccountKey(key);
|
|
1568
|
+
storedAccountKeys.add(normalizedKey);
|
|
1539
1569
|
}
|
|
1570
|
+
await Promise.all(storedKeys.map(async (key) => {
|
|
1571
|
+
const normalizedKey = normalizeAnthropicAccountKey(key);
|
|
1572
|
+
try {
|
|
1573
|
+
const tokens = await withTimeout(tokenStore.peekTokens(key), PROXY_STATUS_TOKEN_READ_TIMEOUT_MS, "[proxy] /status token inspection timed out");
|
|
1574
|
+
if (tokens) {
|
|
1575
|
+
storedAccountExpirations.set(normalizedKey, tokens.expiresAt);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
catch (err) {
|
|
1579
|
+
logger.debug(`[proxy] /status: failed to inspect token metadata for ${normalizedKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1580
|
+
}
|
|
1581
|
+
}));
|
|
1540
1582
|
for (const key of await tokenStore.listDisabled()) {
|
|
1541
1583
|
disabledAccountKeys.add(normalizeAnthropicAccountKey(key));
|
|
1542
1584
|
}
|
|
@@ -1567,16 +1609,22 @@ export async function createProxyStartApp(params) {
|
|
|
1567
1609
|
? ENV_ANTHROPIC_ACCOUNT_KEY
|
|
1568
1610
|
: normalizedKey;
|
|
1569
1611
|
const isStored = storedAccountKeys.has(accountKey) || isLegacyAccount;
|
|
1570
|
-
const cooling = (
|
|
1571
|
-
const accountStatus =
|
|
1572
|
-
? "
|
|
1573
|
-
:
|
|
1574
|
-
? "
|
|
1575
|
-
:
|
|
1576
|
-
? "
|
|
1577
|
-
:
|
|
1578
|
-
? "
|
|
1579
|
-
:
|
|
1612
|
+
const { allowed, expired, cooling } = deriveAccountAllowance(accountKey, now, activeAccountAllowlist, storedAccountExpirations, cooldowns);
|
|
1613
|
+
const accountStatus = account.type === "internal"
|
|
1614
|
+
? "internal"
|
|
1615
|
+
: disabledAccountKeys.has(accountKey)
|
|
1616
|
+
? "disabled"
|
|
1617
|
+
: expired
|
|
1618
|
+
? "expired"
|
|
1619
|
+
: !allowed
|
|
1620
|
+
? "excluded"
|
|
1621
|
+
: accountInventoryLoaded &&
|
|
1622
|
+
account.type === "oauth" &&
|
|
1623
|
+
!isStored
|
|
1624
|
+
? "removed"
|
|
1625
|
+
: cooling
|
|
1626
|
+
? "cooling"
|
|
1627
|
+
: "active";
|
|
1580
1628
|
return {
|
|
1581
1629
|
label: account.label,
|
|
1582
1630
|
type: account.type,
|
|
@@ -1590,8 +1638,43 @@ export async function createProxyStartApp(params) {
|
|
|
1590
1638
|
quotaRateLimits: account.quotaRateLimitCount,
|
|
1591
1639
|
cooling,
|
|
1592
1640
|
status: accountStatus,
|
|
1641
|
+
allowed: account.type === "internal" ? undefined : allowed,
|
|
1642
|
+
expired: account.type === "oauth" ? expired : undefined,
|
|
1593
1643
|
};
|
|
1594
1644
|
});
|
|
1645
|
+
const representedAccountKeys = new Set(accountRows
|
|
1646
|
+
.filter((account) => account.type === "oauth")
|
|
1647
|
+
.map((account) => normalizeAnthropicAccountKey(account.label)));
|
|
1648
|
+
for (const accountKey of storedAccountKeys) {
|
|
1649
|
+
if (representedAccountKeys.has(accountKey)) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const { allowed, expired, cooling } = deriveAccountAllowance(accountKey, now, activeAccountAllowlist, storedAccountExpirations, cooldowns);
|
|
1653
|
+
accountRows.push({
|
|
1654
|
+
label: accountKey.slice("anthropic:".length),
|
|
1655
|
+
type: "oauth",
|
|
1656
|
+
attempts: 0,
|
|
1657
|
+
requests: 0,
|
|
1658
|
+
success: 0,
|
|
1659
|
+
errors: 0,
|
|
1660
|
+
attemptErrors: 0,
|
|
1661
|
+
rateLimits: 0,
|
|
1662
|
+
transientRateLimits: 0,
|
|
1663
|
+
quotaRateLimits: 0,
|
|
1664
|
+
cooling,
|
|
1665
|
+
status: disabledAccountKeys.has(accountKey)
|
|
1666
|
+
? "disabled"
|
|
1667
|
+
: expired
|
|
1668
|
+
? "expired"
|
|
1669
|
+
: !allowed
|
|
1670
|
+
? "excluded"
|
|
1671
|
+
: cooling
|
|
1672
|
+
? "cooling"
|
|
1673
|
+
: "active",
|
|
1674
|
+
allowed,
|
|
1675
|
+
expired,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1595
1678
|
const attributed = accountRows.reduce((total, account) => ({
|
|
1596
1679
|
attempts: total.attempts + (account.attempts ?? 0),
|
|
1597
1680
|
requests: total.requests + (account.requests ?? 0),
|
|
@@ -1622,13 +1705,33 @@ export async function createProxyStartApp(params) {
|
|
|
1622
1705
|
quotaRateLimits: Math.max(0, stats.totalQuotaRateLimits - attributed.quotaRateLimits),
|
|
1623
1706
|
};
|
|
1624
1707
|
if (Object.values(unattributed).some((count) => count > 0)) {
|
|
1625
|
-
accountRows.
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1708
|
+
const internalRow = accountRows.find((account) => account.label === PROXY_INTERNAL_ACCOUNT_LABEL);
|
|
1709
|
+
if (internalRow) {
|
|
1710
|
+
internalRow.attempts =
|
|
1711
|
+
(internalRow.attempts ?? 0) + unattributed.attempts;
|
|
1712
|
+
internalRow.requests =
|
|
1713
|
+
(internalRow.requests ?? 0) + unattributed.requests;
|
|
1714
|
+
internalRow.success = (internalRow.success ?? 0) + unattributed.success;
|
|
1715
|
+
internalRow.errors = (internalRow.errors ?? 0) + unattributed.errors;
|
|
1716
|
+
internalRow.attemptErrors =
|
|
1717
|
+
(internalRow.attemptErrors ?? 0) + unattributed.attemptErrors;
|
|
1718
|
+
internalRow.rateLimits =
|
|
1719
|
+
(internalRow.rateLimits ?? 0) + unattributed.rateLimits;
|
|
1720
|
+
internalRow.transientRateLimits =
|
|
1721
|
+
(internalRow.transientRateLimits ?? 0) +
|
|
1722
|
+
unattributed.transientRateLimits;
|
|
1723
|
+
internalRow.quotaRateLimits =
|
|
1724
|
+
(internalRow.quotaRateLimits ?? 0) + unattributed.quotaRateLimits;
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
accountRows.push({
|
|
1728
|
+
label: PROXY_INTERNAL_ACCOUNT_LABEL,
|
|
1729
|
+
type: "internal",
|
|
1730
|
+
...unattributed,
|
|
1731
|
+
cooling: false,
|
|
1732
|
+
status: "internal",
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1632
1735
|
}
|
|
1633
1736
|
return c.json({
|
|
1634
1737
|
status: "running",
|
|
@@ -1652,6 +1755,11 @@ export async function createProxyStartApp(params) {
|
|
|
1652
1755
|
totalRateLimits: stats.totalRateLimits,
|
|
1653
1756
|
totalTransientRateLimits: stats.totalTransientRateLimits,
|
|
1654
1757
|
totalQuotaRateLimits: stats.totalQuotaRateLimits,
|
|
1758
|
+
terminalErrors,
|
|
1759
|
+
lastTerminalError,
|
|
1760
|
+
terminalErrorDetailsComparable,
|
|
1761
|
+
terminalErrorDetailsMissing,
|
|
1762
|
+
terminalErrorDetailsExcess,
|
|
1655
1763
|
accounts: accountRows,
|
|
1656
1764
|
primaryAccount,
|
|
1657
1765
|
persistence: getUsageStatsPersistenceStatus(),
|
|
@@ -2436,6 +2544,12 @@ async function startProxyCommandHandler(argv) {
|
|
|
2436
2544
|
else if (usageStatsPersistence.lastRecoveryAt) {
|
|
2437
2545
|
logger.warn(`[proxy] recovered corrupt usage statistics state at ${new Date(usageStatsPersistence.lastRecoveryAt).toISOString()}`);
|
|
2438
2546
|
}
|
|
2547
|
+
if (usageStatsPersistence.terminalErrorsLastError) {
|
|
2548
|
+
logger.warn(`[proxy] terminal-error persistence unavailable: ${usageStatsPersistence.terminalErrorsLastError}`);
|
|
2549
|
+
}
|
|
2550
|
+
else if (usageStatsPersistence.terminalErrorsLastRecoveryAt) {
|
|
2551
|
+
logger.warn(`[proxy] recovered corrupt terminal-error state at ${new Date(usageStatsPersistence.terminalErrorsLastRecoveryAt).toISOString()}`);
|
|
2552
|
+
}
|
|
2439
2553
|
const baseEnv = { ...process.env };
|
|
2440
2554
|
const envResolution = resolveProxyEnvFile({
|
|
2441
2555
|
explicitEnvFile: argv.envFile,
|
|
@@ -2588,8 +2702,17 @@ export const proxyStartCommand = {
|
|
|
2588
2702
|
// =============================================================================
|
|
2589
2703
|
// STATUS DISPLAY HELPERS
|
|
2590
2704
|
// =============================================================================
|
|
2705
|
+
export function sanitizeProxyStatusTerminalErrorMessage(message) {
|
|
2706
|
+
return stripVTControlCharacters(sanitizeForLog(redactUrlsInText(message), 700))
|
|
2707
|
+
.replace(/\s+/g, " ")
|
|
2708
|
+
.trim()
|
|
2709
|
+
.slice(0, 200);
|
|
2710
|
+
}
|
|
2591
2711
|
function printStatusStats(stats) {
|
|
2592
2712
|
console.info(`\n Stats:`);
|
|
2713
|
+
if (stats.startedAt !== undefined) {
|
|
2714
|
+
console.info(` Since: ${new Date(stats.startedAt).toISOString()}`);
|
|
2715
|
+
}
|
|
2593
2716
|
if (stats.totalAttempts !== undefined) {
|
|
2594
2717
|
console.info(` Attempts: ${stats.totalAttempts}`);
|
|
2595
2718
|
}
|
|
@@ -2602,6 +2725,32 @@ function printStatusStats(stats) {
|
|
|
2602
2725
|
stats.totalQuotaRateLimits !== undefined
|
|
2603
2726
|
? ` (${stats.totalTransientRateLimits} transient, ${stats.totalQuotaRateLimits} quota)`
|
|
2604
2727
|
: ""));
|
|
2728
|
+
if (stats.terminalErrors) {
|
|
2729
|
+
const causes = Object.entries(stats.terminalErrors.counts)
|
|
2730
|
+
.filter(([, count]) => count > 0)
|
|
2731
|
+
.map(([category, count]) => `${category}=${count}`)
|
|
2732
|
+
.join(", ");
|
|
2733
|
+
console.info(` Error details: ${stats.terminalErrors.totalErrors}/${stats.totalErrors}` +
|
|
2734
|
+
(causes ? ` (${causes})` : ""));
|
|
2735
|
+
console.info(` Detail since: ${new Date(stats.terminalErrors.startedAt).toISOString()}`);
|
|
2736
|
+
if ((stats.terminalErrorDetailsMissing ?? 0) > 0 ||
|
|
2737
|
+
(stats.terminalErrorDetailsExcess ?? 0) > 0) {
|
|
2738
|
+
console.info(` Detail gap: ${stats.terminalErrorDetailsMissing ?? 0} missing, ${stats.terminalErrorDetailsExcess ?? 0} awaiting counter reconciliation`);
|
|
2739
|
+
}
|
|
2740
|
+
else if (stats.terminalErrorDetailsComparable === false) {
|
|
2741
|
+
console.info(" Detail gap: unavailable during snapshot reconciliation");
|
|
2742
|
+
}
|
|
2743
|
+
const lastError = stats.lastTerminalError ?? stats.terminalErrors.recent.at(-1);
|
|
2744
|
+
if (lastError) {
|
|
2745
|
+
const cause = lastError.errorType ?? lastError.category;
|
|
2746
|
+
const code = lastError.errorCode ? `/${lastError.errorCode}` : "";
|
|
2747
|
+
const account = lastError.account ? ` account=${lastError.account}` : "";
|
|
2748
|
+
console.info(` Last error: ${new Date(lastError.at).toISOString()} ${cause}${code} status=${lastError.status}${account}`);
|
|
2749
|
+
if (lastError.message) {
|
|
2750
|
+
console.info(` Last cause: ${sanitizeProxyStatusTerminalErrorMessage(lastError.message)}`);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2605
2754
|
if (stats.accounts?.length) {
|
|
2606
2755
|
console.info(`\n Accounts:`);
|
|
2607
2756
|
const headers = [
|
|
@@ -2620,7 +2769,17 @@ function printStatusStats(stats) {
|
|
|
2620
2769
|
String(account.success ?? 0),
|
|
2621
2770
|
String(account.errors ?? 0),
|
|
2622
2771
|
String(account.rateLimits ?? 0),
|
|
2623
|
-
|
|
2772
|
+
(() => {
|
|
2773
|
+
const status = account.status ?? (account.cooling ? "cooling" : "active");
|
|
2774
|
+
const states = [status];
|
|
2775
|
+
if (account.expired && status !== "expired") {
|
|
2776
|
+
states.push("expired");
|
|
2777
|
+
}
|
|
2778
|
+
if (account.allowed === false && status !== "excluded") {
|
|
2779
|
+
states.push("excluded");
|
|
2780
|
+
}
|
|
2781
|
+
return states.join(", ");
|
|
2782
|
+
})(),
|
|
2624
2783
|
]);
|
|
2625
2784
|
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
|
|
2626
2785
|
const numericColumns = new Set([2, 3, 4, 5]);
|
|
@@ -96,6 +96,8 @@ export declare class TokenStore {
|
|
|
96
96
|
* @throws TokenStoreError if reading fails (other than file not found)
|
|
97
97
|
*/
|
|
98
98
|
loadTokens(provider: string): Promise<StoredOAuthTokens | null>;
|
|
99
|
+
/** Reads tokens without updating access metadata or rewriting the store. */
|
|
100
|
+
peekTokens(provider: string): Promise<StoredOAuthTokens | null>;
|
|
99
101
|
/**
|
|
100
102
|
* Internal load without mutex — callers must already hold the mutex.
|
|
101
103
|
*/
|
|
@@ -179,6 +181,11 @@ export declare class TokenStore {
|
|
|
179
181
|
* @param reason - Optional human-readable reason (e.g., "refresh_failed")
|
|
180
182
|
*/
|
|
181
183
|
markDisabled(provider: string, reason?: string): Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Disables an account only when the persisted credentials still match the
|
|
186
|
+
* request that observed the authentication failure.
|
|
187
|
+
*/
|
|
188
|
+
markDisabledIfCurrent(provider: string, expectedTokens: Pick<StoredOAuthTokens, "accessToken" | "refreshToken" | "expiresAt">, reason?: string): Promise<boolean>;
|
|
182
189
|
/**
|
|
183
190
|
* Re-enables a previously disabled provider (persisted to disk).
|
|
184
191
|
*
|
|
@@ -213,6 +213,22 @@ export class TokenStore {
|
|
|
213
213
|
return result;
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
|
+
/** Reads tokens without updating access metadata or rewriting the store. */
|
|
217
|
+
async peekTokens(provider) {
|
|
218
|
+
return this._mutex.runExclusive(async () => {
|
|
219
|
+
try {
|
|
220
|
+
const storageData = await this.loadStorageData();
|
|
221
|
+
const tokens = storageData.providers[provider]?.tokens;
|
|
222
|
+
return tokens ? { ...tokens } : null;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof TokenStoreError && error.code === "NOT_FOUND") {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
216
232
|
/**
|
|
217
233
|
* Internal load without mutex — callers must already hold the mutex.
|
|
218
234
|
*/
|
|
@@ -539,6 +555,44 @@ export class TokenStore {
|
|
|
539
555
|
});
|
|
540
556
|
});
|
|
541
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Disables an account only when the persisted credentials still match the
|
|
560
|
+
* request that observed the authentication failure.
|
|
561
|
+
*/
|
|
562
|
+
async markDisabledIfCurrent(provider, expectedTokens, reason) {
|
|
563
|
+
return this._mutex.runExclusive(async () => {
|
|
564
|
+
let storageData;
|
|
565
|
+
try {
|
|
566
|
+
storageData = await this.loadStorageData();
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
if (error instanceof TokenStoreError && error.code === "NOT_FOUND") {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
const providerData = storageData.providers[provider];
|
|
575
|
+
if (!providerData) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
const current = providerData.tokens;
|
|
579
|
+
if (current.accessToken !== expectedTokens.accessToken ||
|
|
580
|
+
current.refreshToken !== expectedTokens.refreshToken ||
|
|
581
|
+
current.expiresAt !== expectedTokens.expiresAt) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
providerData.disabled = true;
|
|
585
|
+
providerData.disabledAt = Date.now();
|
|
586
|
+
providerData.disabledReason = reason;
|
|
587
|
+
storageData.lastModified = Date.now();
|
|
588
|
+
await this.saveStorageData(storageData);
|
|
589
|
+
logger.info("Provider marked as disabled", {
|
|
590
|
+
provider,
|
|
591
|
+
reason: reason ?? "unspecified",
|
|
592
|
+
});
|
|
593
|
+
return true;
|
|
594
|
+
});
|
|
595
|
+
}
|
|
542
596
|
/**
|
|
543
597
|
* Re-enables a previously disabled provider (persisted to disk).
|
|
544
598
|
*
|
|
@@ -77,6 +77,7 @@ export declare function handleTranslatedJsonRequest(args: {
|
|
|
77
77
|
attempts: ProxyTranslationAttempt[];
|
|
78
78
|
tracer?: ProxyTracer;
|
|
79
79
|
requestStartTime: number;
|
|
80
|
+
terminalFailureStatus?: number;
|
|
80
81
|
}): Promise<unknown>;
|
|
81
82
|
/**
|
|
82
83
|
* Build the /v1/models response in OpenAI list format.
|
|
@@ -247,6 +247,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
247
247
|
let keepAliveTimer;
|
|
248
248
|
let cancelled = false;
|
|
249
249
|
let succeeded = false;
|
|
250
|
+
let streamInterruptedAfterOutput = false;
|
|
250
251
|
let translatedModel;
|
|
251
252
|
let finalStreamError = "No translation providers succeeded";
|
|
252
253
|
let upstreamIterator;
|
|
@@ -267,6 +268,9 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
267
268
|
}, KEEPALIVE_INTERVAL_MS);
|
|
268
269
|
try {
|
|
269
270
|
for (let attemptIndex = 0; attemptIndex < attempts.length; attemptIndex++) {
|
|
271
|
+
if (cancelled) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
270
274
|
const attempt = attempts[attemptIndex];
|
|
271
275
|
lastAttemptLabel = attempt.label ?? "translation";
|
|
272
276
|
logger.always(`[proxy:${format}] attempt ${attemptIndex + 1}/${attempts.length}: ${attempt.label}`);
|
|
@@ -295,10 +299,14 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
}
|
|
302
|
+
if (cancelled) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
298
305
|
const toolCalls = streamResult.toolCalls ?? [];
|
|
299
306
|
if (!hasTranslatedOutput(collectedText, toolCalls)) {
|
|
300
307
|
finalStreamError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
301
308
|
logger.debug(`${tag} translation attempt ${attempt.label} returned no content or tool calls`);
|
|
309
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
302
310
|
continue;
|
|
303
311
|
}
|
|
304
312
|
if (!cancelled && toolCalls.length) {
|
|
@@ -331,7 +339,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
331
339
|
tracer?.recordMetrics();
|
|
332
340
|
translatedModel = streamResult.model;
|
|
333
341
|
succeeded = true;
|
|
334
|
-
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
335
342
|
return;
|
|
336
343
|
}
|
|
337
344
|
catch (streamErr) {
|
|
@@ -343,6 +350,8 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
343
350
|
? streamErr.message
|
|
344
351
|
: String(streamErr);
|
|
345
352
|
if (collectedText.trim().length > 0) {
|
|
353
|
+
streamInterruptedAfterOutput = true;
|
|
354
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
346
355
|
logger.always(`${tag} mid-stream error: ${finalStreamError}`);
|
|
347
356
|
for (const frame of serializer.emitError(`Upstream stream interrupted: ${finalStreamError}`)) {
|
|
348
357
|
controller.enqueue(encoder.encode(frame));
|
|
@@ -354,7 +363,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
354
363
|
}
|
|
355
364
|
}
|
|
356
365
|
// All attempts exhausted
|
|
357
|
-
recordFinalError(500, lastAttemptLabel, "translation");
|
|
358
366
|
if (!cancelled) {
|
|
359
367
|
logger.always(`${tag} all translation attempts failed: ${finalStreamError}`);
|
|
360
368
|
for (const frame of serializer.emitError(finalStreamError)) {
|
|
@@ -372,12 +380,38 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
372
380
|
if (tracer && translatedModel && translatedModel !== requestModel) {
|
|
373
381
|
tracer.setModelSubstitution(requestModel, translatedModel);
|
|
374
382
|
}
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
const terminalStatus = cancelled
|
|
384
|
+
? 499
|
|
385
|
+
: succeeded
|
|
386
|
+
? 200
|
|
387
|
+
: streamInterruptedAfterOutput
|
|
388
|
+
? 502
|
|
389
|
+
: 500;
|
|
390
|
+
const terminalErrorType = cancelled
|
|
391
|
+
? "client_cancelled"
|
|
392
|
+
: streamInterruptedAfterOutput
|
|
393
|
+
? "stream_error"
|
|
394
|
+
: succeeded
|
|
395
|
+
? undefined
|
|
396
|
+
: "generation_error";
|
|
397
|
+
const terminalErrorMessage = cancelled
|
|
398
|
+
? "Client cancelled the streaming response"
|
|
399
|
+
: succeeded
|
|
400
|
+
? undefined
|
|
401
|
+
: finalStreamError;
|
|
402
|
+
if (terminalErrorType && terminalErrorMessage) {
|
|
403
|
+
tracer?.setError(terminalErrorType, terminalErrorMessage.slice(0, 500));
|
|
404
|
+
recordFinalError(terminalStatus, lastAttemptLabel, "translation", {
|
|
405
|
+
requestId: ctx.requestId,
|
|
406
|
+
errorType: terminalErrorType,
|
|
407
|
+
terminalOutcome: terminalErrorType,
|
|
408
|
+
message: terminalErrorMessage,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
377
413
|
}
|
|
378
|
-
|
|
379
|
-
// responseStatus below (success path is 200, exhausted-attempts path is 500).
|
|
380
|
-
tracer?.end(succeeded ? 200 : 500, Date.now() - requestStartTime);
|
|
414
|
+
tracer?.end(terminalStatus, Date.now() - requestStartTime);
|
|
381
415
|
const traceCtx = tracer?.getTraceContext();
|
|
382
416
|
logRequest({
|
|
383
417
|
timestamp: new Date().toISOString(),
|
|
@@ -389,8 +423,12 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
389
423
|
toolCount: Object.keys(parsed.tools).length,
|
|
390
424
|
account: "translation",
|
|
391
425
|
accountType: "translation",
|
|
392
|
-
responseStatus:
|
|
426
|
+
responseStatus: terminalStatus,
|
|
393
427
|
responseTimeMs: Date.now() - requestStartTime,
|
|
428
|
+
...(terminalErrorType ? { errorType: terminalErrorType } : {}),
|
|
429
|
+
...(terminalErrorMessage
|
|
430
|
+
? { errorMessage: terminalErrorMessage.slice(0, 500) }
|
|
431
|
+
: {}),
|
|
394
432
|
...(traceCtx?.traceId ? { traceId: traceCtx.traceId } : {}),
|
|
395
433
|
...(traceCtx?.spanId ? { spanId: traceCtx.spanId } : {}),
|
|
396
434
|
});
|
|
@@ -424,7 +462,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
424
462
|
* Handles a translated non-streaming request for either Claude or OpenAI format.
|
|
425
463
|
*/
|
|
426
464
|
export async function handleTranslatedJsonRequest(args) {
|
|
427
|
-
const { ctx, format, requestModel, parsed, attempts, tracer, requestStartTime, } = args;
|
|
465
|
+
const { ctx, format, requestModel, parsed, attempts, tracer, requestStartTime, terminalFailureStatus = 500, } = args;
|
|
428
466
|
const tag = logTag(format);
|
|
429
467
|
let lastAttemptError = "No translation providers succeeded";
|
|
430
468
|
let lastAttemptLabel = "translation";
|
|
@@ -448,6 +486,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
448
486
|
if (!hasTranslatedOutput(collectedText, streamResult.toolCalls)) {
|
|
449
487
|
lastAttemptError = `Translated provider ${attempt.label} returned no content or tool calls`;
|
|
450
488
|
logger.debug(`${tag} translation attempt ${attempt.label} returned no content or tool calls`);
|
|
489
|
+
recordAttemptError(lastAttemptLabel, "translation", 502);
|
|
451
490
|
continue;
|
|
452
491
|
}
|
|
453
492
|
const internal = {
|
|
@@ -505,9 +544,14 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
505
544
|
recordAttemptError(lastAttemptLabel, "translation", 500);
|
|
506
545
|
}
|
|
507
546
|
}
|
|
508
|
-
recordFinalError(
|
|
547
|
+
recordFinalError(terminalFailureStatus, lastAttemptLabel, "translation", {
|
|
548
|
+
requestId: ctx.requestId,
|
|
549
|
+
errorType: "generation_error",
|
|
550
|
+
terminalOutcome: "handler_error",
|
|
551
|
+
message: lastAttemptError,
|
|
552
|
+
});
|
|
509
553
|
tracer?.setError("generation_error", lastAttemptError.slice(0, 500));
|
|
510
|
-
tracer?.end(
|
|
554
|
+
tracer?.end(terminalFailureStatus, Date.now() - requestStartTime);
|
|
511
555
|
const traceCtx = tracer?.getTraceContext();
|
|
512
556
|
logRequest({
|
|
513
557
|
timestamp: new Date().toISOString(),
|
|
@@ -519,7 +563,7 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
519
563
|
toolCount: Object.keys(parsed.tools).length,
|
|
520
564
|
account: "translation",
|
|
521
565
|
accountType: "translation",
|
|
522
|
-
responseStatus:
|
|
566
|
+
responseStatus: terminalFailureStatus,
|
|
523
567
|
responseTimeMs: Date.now() - requestStartTime,
|
|
524
568
|
errorType: "generation_error",
|
|
525
569
|
errorMessage: lastAttemptError.slice(0, 500),
|
|
@@ -60,7 +60,11 @@ export function createRawStreamCapture() {
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
const innerWriter = transform.writable.getWriter();
|
|
63
|
+
let writableController;
|
|
63
64
|
const writable = new WritableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
writableController = controller;
|
|
67
|
+
},
|
|
64
68
|
write(chunk) {
|
|
65
69
|
return innerWriter.write(chunk);
|
|
66
70
|
},
|
|
@@ -72,9 +76,51 @@ export function createRawStreamCapture() {
|
|
|
72
76
|
return innerWriter.abort(reason);
|
|
73
77
|
},
|
|
74
78
|
});
|
|
79
|
+
// A downstream reader cancellation errors the TransformStream's writable
|
|
80
|
+
// side, but this wrapper otherwise hides that state from the upstream pipe.
|
|
81
|
+
// Mirror it onto the wrapper so cancellation reaches the source reader.
|
|
82
|
+
void innerWriter.closed.catch((reason) => {
|
|
83
|
+
settle();
|
|
84
|
+
try {
|
|
85
|
+
writableController.error(reason);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// The wrapper may already be closing or aborted.
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const innerReader = transform.readable.getReader();
|
|
92
|
+
const readable = new ReadableStream({
|
|
93
|
+
async pull(controller) {
|
|
94
|
+
try {
|
|
95
|
+
const { done, value } = await innerReader.read();
|
|
96
|
+
if (done) {
|
|
97
|
+
controller.close();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
controller.enqueue(value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
controller.error(error);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async cancel(reason) {
|
|
108
|
+
settle();
|
|
109
|
+
try {
|
|
110
|
+
writableController.error(reason);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// The wrapper may already be closing or aborted.
|
|
114
|
+
}
|
|
115
|
+
await Promise.allSettled([
|
|
116
|
+
innerReader.cancel(reason),
|
|
117
|
+
innerWriter.abort(reason),
|
|
118
|
+
]);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
75
121
|
return {
|
|
76
122
|
stream: {
|
|
77
|
-
readable
|
|
123
|
+
readable,
|
|
78
124
|
writable,
|
|
79
125
|
},
|
|
80
126
|
capture,
|