@juspay/neurolink 12.17.1 → 12.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +421 -412
- package/dist/cli/commands/proxy.js +455 -259
- package/dist/constants/contextWindows.d.ts +2 -0
- package/dist/constants/contextWindows.js +4 -0
- package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
- package/dist/proxy/anthropicBudgetResponse.js +153 -0
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
- package/dist/proxy/bodyCaptureProcessing.js +23 -4
- package/dist/proxy/bodyCaptureWorker.js +20 -2
- package/dist/proxy/codexFallback.d.ts +10 -0
- package/dist/proxy/codexFallback.js +108 -24
- package/dist/proxy/codexUsage.js +2 -0
- package/dist/proxy/globalInstaller.d.ts +32 -1
- package/dist/proxy/globalInstaller.js +386 -3
- package/dist/proxy/otelLogSink.d.ts +3 -3
- package/dist/proxy/otelLogSink.js +137 -144
- package/dist/proxy/otlpLogTransport.d.ts +3 -0
- package/dist/proxy/otlpLogTransport.js +334 -0
- package/dist/proxy/proxyActivity.d.ts +22 -2
- package/dist/proxy/proxyActivity.js +114 -9
- package/dist/proxy/proxyAnalysis.js +82 -52
- package/dist/proxy/proxyClientCancellation.d.ts +7 -0
- package/dist/proxy/proxyClientCancellation.js +23 -0
- package/dist/proxy/proxyContextPreflight.d.ts +25 -0
- package/dist/proxy/proxyContextPreflight.js +337 -0
- package/dist/proxy/proxyFailureDetails.d.ts +19 -0
- package/dist/proxy/proxyFailureDetails.js +96 -0
- package/dist/proxy/proxyLifecycle.js +7 -0
- package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
- package/dist/proxy/proxyRouteBodyCapture.js +112 -0
- package/dist/proxy/proxyTokenBudget.d.ts +40 -0
- package/dist/proxy/proxyTokenBudget.js +523 -0
- package/dist/proxy/proxyTracer.d.ts +3 -2
- package/dist/proxy/proxyTracer.js +46 -20
- package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
- package/dist/proxy/proxyTranslationEngine.js +736 -376
- package/dist/proxy/requestLogger.d.ts +1 -0
- package/dist/proxy/requestLogger.js +107 -5
- package/dist/proxy/restartControl.js +9 -4
- package/dist/proxy/rollingWorkerProcess.js +65 -0
- package/dist/proxy/rollingWorkerSupervisor.js +7 -0
- package/dist/proxy/updateCoordinator.d.ts +14 -1
- package/dist/proxy/updateCoordinator.js +57 -2
- package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
- package/dist/server/routes/claudeProxyRoutes.js +809 -577
- package/dist/server/routes/codexProxyRoutes.js +191 -9
- package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
- package/dist/server/routes/openaiProxyRoutes.js +261 -69
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/codex.d.ts +5 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/proxy.d.ts +148 -3
- package/dist/types/proxy.js +0 -15
- package/dist/types/proxyBudget.d.ts +74 -0
- package/dist/types/proxyBudget.js +1 -0
- package/dist/types/proxyContext.d.ts +35 -0
- package/dist/types/proxyContext.js +1 -0
- package/docs/assets/dashboards/README.md +37 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
- package/docs-site/static/search-index.json +1 -1
- package/package.json +3 -1
- package/scripts/observability/NATIVE-DURABLE.md +63 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
- package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
- package/scripts/observability/proxy-telemetry-check.mjs +396 -61
- package/scripts/observability/query-proxy-history.mjs +34 -5
|
@@ -37,6 +37,7 @@ export declare function prepareProxyBodyForLogging(body: unknown): {
|
|
|
37
37
|
bytes?: number;
|
|
38
38
|
truncated: boolean;
|
|
39
39
|
originalBytes?: number;
|
|
40
|
+
unparseableFrames: number;
|
|
40
41
|
};
|
|
41
42
|
/** Cheap gate for response observers; avoid copying diagnostics on the serving path. */
|
|
42
43
|
export declare function isProxyBodyCaptureEnabled(): boolean;
|
|
@@ -10,6 +10,8 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { join } from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
|
+
import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
|
|
14
|
+
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
13
15
|
import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } from "fs";
|
|
14
16
|
import { writeFile } from "fs/promises";
|
|
15
17
|
import { setImmediate as yieldToRequests } from "node:timers/promises";
|
|
@@ -19,7 +21,7 @@ import { isBorrowedRequest } from "./shareContext.js";
|
|
|
19
21
|
import { OtelBridge } from "../observability/otelBridge.js";
|
|
20
22
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
21
23
|
import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
|
|
22
|
-
import { notifyProxyFinalLog, notifyProxyAttemptLog } from "./proxyActivity.js";
|
|
24
|
+
import { notifyProxyFinalLog, notifyProxyAttemptLog, getProxyRequestAccounting, } from "./proxyActivity.js";
|
|
23
25
|
import { withTimeout } from "../utils/async/withTimeout.js";
|
|
24
26
|
import { resolveProxyLogTraceContext, proxyLogContext, } from "./proxyTraceContext.js";
|
|
25
27
|
let logDir = null;
|
|
@@ -191,7 +193,64 @@ export function initRequestLogger(enabled = true, customLogsDir) {
|
|
|
191
193
|
logger.warn(`[proxy] Request logging disabled — failed to create log directory: ${err instanceof Error ? err.message : String(err)}`);
|
|
192
194
|
}
|
|
193
195
|
}
|
|
196
|
+
/** Keep price-table estimates distinct from actual subscription billing. */
|
|
197
|
+
function annotateRequestPricing(entry) {
|
|
198
|
+
entry.apiEquivalentCostUsd = null;
|
|
199
|
+
entry.apiEquivalentCacheSavingsUsd = null;
|
|
200
|
+
entry.pricingBasis = "api_price_table";
|
|
201
|
+
entry.pricingProvider = entry.provider ?? "openai-compatible";
|
|
202
|
+
if (entry.usageOwnerRequestId &&
|
|
203
|
+
entry.usageOwnerRequestId !== entry.requestId) {
|
|
204
|
+
entry.pricingStatus = "owned_by_child";
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const input = entry.inputTokens;
|
|
208
|
+
const output = entry.outputTokens;
|
|
209
|
+
const cacheRead = entry.cacheReadTokens ?? 0;
|
|
210
|
+
const cacheCreation = entry.cacheCreationTokens ?? 0;
|
|
211
|
+
if (input === undefined ||
|
|
212
|
+
output === undefined ||
|
|
213
|
+
!Number.isFinite(input) ||
|
|
214
|
+
input < 0 ||
|
|
215
|
+
!Number.isFinite(output) ||
|
|
216
|
+
output < 0 ||
|
|
217
|
+
!Number.isFinite(cacheRead) ||
|
|
218
|
+
cacheRead < 0 ||
|
|
219
|
+
!Number.isFinite(cacheCreation) ||
|
|
220
|
+
cacheCreation < 0 ||
|
|
221
|
+
(entry.inputIncludesCachedTokens === true &&
|
|
222
|
+
cacheRead + cacheCreation > input)) {
|
|
223
|
+
entry.pricingStatus = "usage_incomplete";
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!hasPricing(entry.pricingProvider, entry.model)) {
|
|
227
|
+
entry.pricingStatus = "unavailable";
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!isExactPricingMatch(entry.pricingProvider, entry.model)) {
|
|
231
|
+
entry.pricingStatus = "inferred";
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const usage = proxyTokenUsage({
|
|
235
|
+
inputTokens: input,
|
|
236
|
+
outputTokens: output,
|
|
237
|
+
cacheReadTokens: cacheRead,
|
|
238
|
+
cacheCreationTokens: cacheCreation,
|
|
239
|
+
inputIncludesCachedTokens: entry.inputIncludesCachedTokens,
|
|
240
|
+
});
|
|
241
|
+
entry.pricingStatus = "exact";
|
|
242
|
+
entry.apiEquivalentCostUsd = calculateCost(entry.pricingProvider, entry.model, usage);
|
|
243
|
+
const uncachedCost = calculateCost(entry.pricingProvider, entry.model, {
|
|
244
|
+
...usage,
|
|
245
|
+
input: usage.input + (usage.cacheReadTokens ?? 0),
|
|
246
|
+
cacheReadTokens: 0,
|
|
247
|
+
});
|
|
248
|
+
entry.apiEquivalentCacheSavingsUsd = Math.max(0, Math.round((uncachedCost - entry.apiEquivalentCostUsd) * 1_000_000) /
|
|
249
|
+
1_000_000);
|
|
250
|
+
}
|
|
194
251
|
export async function logRequest(entry) {
|
|
252
|
+
Object.assign(entry, getProxyRequestAccounting(entry.requestId));
|
|
253
|
+
annotateRequestPricing(entry);
|
|
195
254
|
if (!entry.traceId || entry.traceFlags === undefined) {
|
|
196
255
|
const traceCtx = resolveProxyLogTraceContext(entry);
|
|
197
256
|
if (traceCtx) {
|
|
@@ -206,7 +265,9 @@ export async function logRequest(entry) {
|
|
|
206
265
|
: entry.responseStatus >= 400 || entry.errorType
|
|
207
266
|
? "handler_error"
|
|
208
267
|
: "completed";
|
|
209
|
-
notifyProxyFinalLog(entry)
|
|
268
|
+
if (!notifyProxyFinalLog(entry)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
210
271
|
if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
|
|
211
272
|
return;
|
|
212
273
|
}
|
|
@@ -239,6 +300,7 @@ export async function logRequestAttempt(entry) {
|
|
|
239
300
|
entry.traceFlags = traceCtx.traceFlags;
|
|
240
301
|
}
|
|
241
302
|
}
|
|
303
|
+
Object.assign(entry, getProxyRequestAccounting(entry.requestId));
|
|
242
304
|
notifyProxyAttemptLog(entry);
|
|
243
305
|
if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
|
|
244
306
|
return;
|
|
@@ -306,8 +368,13 @@ function emitOtlpLogRecord(entry) {
|
|
|
306
368
|
return;
|
|
307
369
|
}
|
|
308
370
|
const otelLogger = provider.getLogger("neurolink-proxy", "1.0.0");
|
|
309
|
-
//
|
|
310
|
-
|
|
371
|
+
// A streaming response can fail after sending HTTP 200. Indexed fields
|
|
372
|
+
// and severity must agree with the final semantic outcome in the body.
|
|
373
|
+
const isError = (entry.responseStatus ?? 0) >= 400 ||
|
|
374
|
+
Boolean(entry.errorType) ||
|
|
375
|
+
entry.terminalOutcome === "stream_error" ||
|
|
376
|
+
entry.terminalOutcome === "handler_error" ||
|
|
377
|
+
entry.terminalOutcome === "client_cancelled";
|
|
311
378
|
const isRateLimit = entry.responseStatus === 429;
|
|
312
379
|
const severityNumber = isError
|
|
313
380
|
? isRateLimit
|
|
@@ -324,6 +391,18 @@ function emitOtlpLogRecord(entry) {
|
|
|
324
391
|
: `${entry.method} ${entry.path} → ${entry.responseStatus} (${entry.responseTimeMs}ms)`,
|
|
325
392
|
attributes: {
|
|
326
393
|
"proxy.record_kind": "request_final",
|
|
394
|
+
"proxy.accounting_scope": entry.accountingScope ?? "client",
|
|
395
|
+
...(entry.parentRequestId
|
|
396
|
+
? { "proxy.parent_request_id": entry.parentRequestId }
|
|
397
|
+
: {}),
|
|
398
|
+
...(entry.usageOwnerRequestId
|
|
399
|
+
? { "proxy.usage_owner_request_id": entry.usageOwnerRequestId }
|
|
400
|
+
: {}),
|
|
401
|
+
...(entry.inputIncludesCachedTokens !== undefined
|
|
402
|
+
? {
|
|
403
|
+
"ai.input_includes_cached_tokens": entry.inputIncludesCachedTokens,
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
327
406
|
// Core request fields
|
|
328
407
|
"request.id": entry.requestId,
|
|
329
408
|
"http.method": entry.method,
|
|
@@ -334,6 +413,19 @@ function emitOtlpLogRecord(entry) {
|
|
|
334
413
|
"ai.model": entry.model,
|
|
335
414
|
"ai.stream": entry.stream,
|
|
336
415
|
"ai.tool_count": entry.toolCount,
|
|
416
|
+
"ai.pricing.status": entry.pricingStatus,
|
|
417
|
+
"ai.pricing.basis": entry.pricingBasis,
|
|
418
|
+
"ai.pricing.provider": entry.pricingProvider,
|
|
419
|
+
...(entry.apiEquivalentCostUsd !== null &&
|
|
420
|
+
entry.apiEquivalentCostUsd !== undefined
|
|
421
|
+
? { "ai.cost.api_equivalent_usd": entry.apiEquivalentCostUsd }
|
|
422
|
+
: {}),
|
|
423
|
+
...(entry.apiEquivalentCacheSavingsUsd !== null &&
|
|
424
|
+
entry.apiEquivalentCacheSavingsUsd !== undefined
|
|
425
|
+
? {
|
|
426
|
+
"ai.cost.cache_savings_usd": entry.apiEquivalentCacheSavingsUsd,
|
|
427
|
+
}
|
|
428
|
+
: {}),
|
|
337
429
|
// Account info
|
|
338
430
|
"account.name": entry.account,
|
|
339
431
|
"account.type": entry.accountType,
|
|
@@ -368,7 +460,9 @@ function emitOtlpLogRecord(entry) {
|
|
|
368
460
|
...(entry.traceId && { "trace.id": entry.traceId }),
|
|
369
461
|
...(entry.spanId && { "span.id": entry.spanId }),
|
|
370
462
|
// Derived fields for dashboards (matches backfill script)
|
|
371
|
-
is_success:
|
|
463
|
+
is_success: !isError &&
|
|
464
|
+
entry.responseStatus >= 200 &&
|
|
465
|
+
entry.responseStatus < 400,
|
|
372
466
|
is_rate_limited: entry.responseStatus === 429,
|
|
373
467
|
is_overloaded: entry.responseStatus === 529,
|
|
374
468
|
is_error: isError,
|
|
@@ -584,11 +678,19 @@ export async function logBodyCapture(entry) {
|
|
|
584
678
|
bodyTruncated: stored.bodyTruncated,
|
|
585
679
|
bodyCaptureLimitBytes: stored.bodyCaptureLimitBytes,
|
|
586
680
|
originalRedactedBodyBytes: stored.originalRedactedBodyBytes,
|
|
681
|
+
inputRetainedBytes: stored.inputRetainedBytes,
|
|
682
|
+
inputEncoding: stored.inputEncoding,
|
|
683
|
+
sourceTruncated: stored.sourceTruncated,
|
|
684
|
+
processingTruncated: stored.processingTruncated,
|
|
685
|
+
redactionLossy: stored.redactionLossy,
|
|
686
|
+
unparseableRedactedFrames: stored.unparseableRedactedFrames,
|
|
587
687
|
bodyWriteFailed: stored.bodyWriteFailed,
|
|
588
688
|
captureError: processed.error,
|
|
589
689
|
captureAdmission: processed.admission,
|
|
590
690
|
captureQueueWaitMs: processed.queueWaitMs,
|
|
591
691
|
captureProcessingMs: processed.processingMs,
|
|
692
|
+
captureAdmissionWaitMs: processed.admissionWaitMs,
|
|
693
|
+
captureWorkerQueueWaitMs: processed.workerQueueWaitMs,
|
|
592
694
|
metadata: processed.error ? undefined : metadata.metadata,
|
|
593
695
|
};
|
|
594
696
|
if (traceCtx) {
|
|
@@ -87,7 +87,7 @@ export async function startProxyRestartControl(options) {
|
|
|
87
87
|
drainingWorkers: snapshot.draining.length,
|
|
88
88
|
};
|
|
89
89
|
};
|
|
90
|
-
const
|
|
90
|
+
const assertReplacementAvailable = () => {
|
|
91
91
|
const snapshot = options.server.snapshot();
|
|
92
92
|
if (closing || !snapshot.active) {
|
|
93
93
|
throw new Error("No serving worker is available for a rolling restart.");
|
|
@@ -96,8 +96,13 @@ export async function startProxyRestartControl(options) {
|
|
|
96
96
|
throw new Error("An update or worker replacement is already in progress.");
|
|
97
97
|
}
|
|
98
98
|
if (snapshot.draining.length) {
|
|
99
|
-
throw new Error("
|
|
99
|
+
throw new Error("Previous workers are still draining; another restart is deferred.");
|
|
100
100
|
}
|
|
101
|
+
// This replaces a worker, not the listener/supervisor. Normal admissions
|
|
102
|
+
// can overlap preflight and activation: the rolling supervisor retains
|
|
103
|
+
// each previous generation until its pending transfers commit, then drains
|
|
104
|
+
// its existing streams. Full quiescence is only required before replacing
|
|
105
|
+
// the supervisor itself (updateCoordinator.isProxyRuntimeSettled).
|
|
101
106
|
};
|
|
102
107
|
const operate = async (restart) => {
|
|
103
108
|
if (busy || closing) {
|
|
@@ -118,7 +123,7 @@ export async function startProxyRestartControl(options) {
|
|
|
118
123
|
};
|
|
119
124
|
};
|
|
120
125
|
try {
|
|
121
|
-
|
|
126
|
+
assertReplacementAvailable();
|
|
122
127
|
const before = options.server.snapshot();
|
|
123
128
|
const active = before.active;
|
|
124
129
|
if (!active) {
|
|
@@ -135,7 +140,7 @@ export async function startProxyRestartControl(options) {
|
|
|
135
140
|
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
136
141
|
throw new Error("The configured worker executable did not report a valid installed version.");
|
|
137
142
|
}
|
|
138
|
-
|
|
143
|
+
assertReplacementAvailable();
|
|
139
144
|
if (options.server.snapshot().active?.pid !== previousWorkerPid) {
|
|
140
145
|
throw new Error("Serving worker changed during preflight; retry the check.");
|
|
141
146
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { handleProxyTokenBudgetMessage, releaseProxyTokenBudgetOwner, } from "./proxyTokenBudget.js";
|
|
2
3
|
import { ErrorFactory } from "../utils/errorHandling.js";
|
|
3
4
|
import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, PROXY_SOCKET_OFFER_TIMEOUT, PROXY_SOCKET_COMMIT_TIMEOUT, } from "./rollingWorkerProtocol.js";
|
|
4
5
|
/**
|
|
@@ -12,6 +13,8 @@ export function spawnProxySocketWorker(options) {
|
|
|
12
13
|
const statusListeners = new Set();
|
|
13
14
|
const pendingStatusMessages = [];
|
|
14
15
|
let spawnError;
|
|
16
|
+
let budgetResponseFailure;
|
|
17
|
+
let budgetTerminationTimeout;
|
|
15
18
|
const child = (options.spawn ?? spawn)(options.command, options.args, {
|
|
16
19
|
env: {
|
|
17
20
|
...process.env,
|
|
@@ -84,6 +87,38 @@ export function spawnProxySocketWorker(options) {
|
|
|
84
87
|
}
|
|
85
88
|
pending.callback(error);
|
|
86
89
|
};
|
|
90
|
+
/** Lost budget replies leave unknown leases; release them only after exit. */
|
|
91
|
+
const quarantineBudgetDeliveryFailure = () => {
|
|
92
|
+
if (budgetResponseFailure) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const error = ErrorFactory.proxyWorkerLifecycle(`proxy worker ${childPid} token budget response delivery failed`);
|
|
96
|
+
budgetResponseFailure = error;
|
|
97
|
+
// An IPC-disconnected worker may ignore graceful shutdown. Retain its
|
|
98
|
+
// reservation occupancy until exit, but bound how long it can survive.
|
|
99
|
+
budgetTerminationTimeout = setTimeout(() => {
|
|
100
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
101
|
+
child.kill("SIGKILL");
|
|
102
|
+
}
|
|
103
|
+
}, 1_000);
|
|
104
|
+
budgetTerminationTimeout.unref?.();
|
|
105
|
+
try {
|
|
106
|
+
publishStatus({
|
|
107
|
+
type: "proxy-worker:fatal",
|
|
108
|
+
generation: options.generation,
|
|
109
|
+
pid: childPid,
|
|
110
|
+
message: error.message,
|
|
111
|
+
});
|
|
112
|
+
for (const socketId of [...pendingSockets.keys()]) {
|
|
113
|
+
settleSocket(socketId, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
118
|
+
child.kill("SIGTERM");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
87
122
|
/**
|
|
88
123
|
* Accept messages only from this worker and start a fresh deadline for
|
|
89
124
|
* the commit phase.
|
|
@@ -120,12 +155,35 @@ export function spawnProxySocketWorker(options) {
|
|
|
120
155
|
}
|
|
121
156
|
};
|
|
122
157
|
child.on("message", (message) => {
|
|
158
|
+
if (budgetResponseFailure) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (handleProxyTokenBudgetMessage(message, { pid: childPid, generation: options.generation }, (response) => {
|
|
162
|
+
if (!child.connected) {
|
|
163
|
+
quarantineBudgetDeliveryFailure();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
child.send(response, (error) => {
|
|
168
|
+
if (error) {
|
|
169
|
+
quarantineBudgetDeliveryFailure();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
quarantineBudgetDeliveryFailure();
|
|
175
|
+
}
|
|
176
|
+
})) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
123
179
|
onInternalMessage(message);
|
|
124
180
|
if (isProxyWorkerStatusMessage(message)) {
|
|
125
181
|
publishStatus(message);
|
|
126
182
|
}
|
|
127
183
|
});
|
|
128
184
|
child.once("exit", (code, signal) => {
|
|
185
|
+
clearTimeout(budgetTerminationTimeout);
|
|
186
|
+
releaseProxyTokenBudgetOwner(childPid, options.generation);
|
|
129
187
|
for (const socketId of [...pendingSockets.keys()]) {
|
|
130
188
|
settleSocket(socketId, ErrorFactory.proxyWorkerLifecycle(`proxy worker ${childPid} exited before socket transfer committed (code=${code ?? "none"}, signal=${signal ?? "none"})`, {
|
|
131
189
|
workerPid: childPid,
|
|
@@ -136,6 +194,9 @@ export function spawnProxySocketWorker(options) {
|
|
|
136
194
|
}
|
|
137
195
|
});
|
|
138
196
|
const sendControl = (message) => {
|
|
197
|
+
if (budgetResponseFailure) {
|
|
198
|
+
throw budgetResponseFailure;
|
|
199
|
+
}
|
|
139
200
|
if (!child.connected) {
|
|
140
201
|
throw new Error(`proxy worker ${childPid} IPC channel is closed`);
|
|
141
202
|
}
|
|
@@ -149,6 +210,10 @@ export function spawnProxySocketWorker(options) {
|
|
|
149
210
|
pid: childPid,
|
|
150
211
|
sendControl,
|
|
151
212
|
sendSocket: (generation, socket, callback) => {
|
|
213
|
+
if (budgetResponseFailure) {
|
|
214
|
+
callback(budgetResponseFailure);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
152
217
|
if (!child.connected) {
|
|
153
218
|
callback(new Error(`proxy worker ${childPid} IPC channel is closed`));
|
|
154
219
|
return;
|
|
@@ -287,6 +287,13 @@ export class RollingWorkerSupervisor {
|
|
|
287
287
|
this.publishState();
|
|
288
288
|
this.notifyShutdownWaiters();
|
|
289
289
|
}
|
|
290
|
+
else if (this.draining.has(generation)) {
|
|
291
|
+
// A retiring generation still owns requests and budget leases.
|
|
292
|
+
// Preserve its fatal cause without dropping actual-exit tracking.
|
|
293
|
+
this.recordFailure(generation, expectedVersion, "runtime", message.message, { workerPid: handle.pid });
|
|
294
|
+
this.options.log?.(`[proxy-supervisor] draining worker reported fatal generation=${generation} pid=${handle.pid}: ${message.message}`);
|
|
295
|
+
this.publishState();
|
|
296
|
+
}
|
|
290
297
|
return;
|
|
291
298
|
}
|
|
292
299
|
if (message.type === "proxy-worker:replacement-requested") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
|
|
1
|
+
import type { ProxyRuntimeActivity, ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* A replacement updater starts from the newly installed package. The registry
|
|
4
4
|
* then reports no newer version even when its launchd parent still runs the
|
|
@@ -12,9 +12,22 @@ export declare function shouldRefreshStaleSupervisor(options: {
|
|
|
12
12
|
workerVersion?: string;
|
|
13
13
|
supervisorVersion?: string;
|
|
14
14
|
}): boolean;
|
|
15
|
+
/** An idle active worker alone does not make its supervisor safe to stop. */
|
|
16
|
+
export declare function isProxyRuntimeSettled(activity: ProxyRuntimeActivity): boolean;
|
|
17
|
+
/** Parse untrusted status conservatively; incomplete rolling evidence is busy. */
|
|
18
|
+
export declare function parseProxyRuntimeActivity(payload: unknown): ProxyRuntimeActivity | null;
|
|
15
19
|
/**
|
|
16
20
|
* Prefer a naturally quiet window, then briefly drain new inference traffic.
|
|
17
21
|
* Existing requests are never interrupted; a bounded drain failure is returned
|
|
18
22
|
* to the caller so admission can be reopened and retried later.
|
|
19
23
|
*/
|
|
20
24
|
export declare function waitForProxyUpdateWindow(options: ProxyUpdateWindowOptions): Promise<ProxyUpdateWindowResult>;
|
|
25
|
+
/** An asynchronous staging job must not publish after its service owner changes. */
|
|
26
|
+
export declare function isProxyUpdateOwnerCurrent(options: {
|
|
27
|
+
stopping: boolean;
|
|
28
|
+
parentPid: number;
|
|
29
|
+
updaterPid: number;
|
|
30
|
+
parentStatus: "running" | "not_running" | "unknown";
|
|
31
|
+
runtimePid?: number;
|
|
32
|
+
runtimeUpdaterPid?: number;
|
|
33
|
+
}): boolean;
|
|
@@ -11,8 +11,56 @@ export function shouldRefreshStaleSupervisor(options) {
|
|
|
11
11
|
typeof options.supervisorVersion === "string" &&
|
|
12
12
|
options.supervisorVersion !== options.runningVersion);
|
|
13
13
|
}
|
|
14
|
+
/** An idle active worker alone does not make its supervisor safe to stop. */
|
|
15
|
+
export function isProxyRuntimeSettled(activity) {
|
|
16
|
+
return (activity.activeRequests === 0 &&
|
|
17
|
+
(activity.drainingWorkers ?? 0) === 0 &&
|
|
18
|
+
(activity.queuedSockets ?? 0) === 0 &&
|
|
19
|
+
(activity.pendingTransfers ?? 0) === 0 &&
|
|
20
|
+
(activity.candidateWorkers ?? 0) === 0);
|
|
21
|
+
}
|
|
22
|
+
/** Parse untrusted status conservatively; incomplete rolling evidence is busy. */
|
|
23
|
+
export function parseProxyRuntimeActivity(payload) {
|
|
24
|
+
if (!payload || typeof payload !== "object") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const value = payload;
|
|
28
|
+
const active = value.activity?.activeRequests;
|
|
29
|
+
if (typeof active !== "number" ||
|
|
30
|
+
!Number.isSafeInteger(active) ||
|
|
31
|
+
active < 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const result = {
|
|
35
|
+
activeRequests: active,
|
|
36
|
+
lastActivityAt: typeof value.activity?.lastActivityAt === "string"
|
|
37
|
+
? value.activity.lastActivityAt
|
|
38
|
+
: null,
|
|
39
|
+
};
|
|
40
|
+
const rolling = value.autoUpdate?.rolling;
|
|
41
|
+
if (value.autoUpdate?.supervisorPid && !rolling) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (rolling) {
|
|
45
|
+
if (!Array.isArray(rolling.draining) ||
|
|
46
|
+
typeof rolling.queuedSockets !== "number" ||
|
|
47
|
+
!Number.isSafeInteger(rolling.queuedSockets) ||
|
|
48
|
+
rolling.queuedSockets < 0 ||
|
|
49
|
+
typeof rolling.pendingTransfers !== "number" ||
|
|
50
|
+
!Number.isSafeInteger(rolling.pendingTransfers) ||
|
|
51
|
+
rolling.pendingTransfers < 0 ||
|
|
52
|
+
rolling.candidate === undefined) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
result.drainingWorkers = rolling.draining.length;
|
|
56
|
+
result.queuedSockets = rolling.queuedSockets;
|
|
57
|
+
result.pendingTransfers = rolling.pendingTransfers;
|
|
58
|
+
result.candidateWorkers = rolling.candidate === null ? 0 : 1;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
14
62
|
function isQuiet(activity, quietThresholdMs, nowMs) {
|
|
15
|
-
if (activity
|
|
63
|
+
if (!isProxyRuntimeSettled(activity)) {
|
|
16
64
|
return false;
|
|
17
65
|
}
|
|
18
66
|
if (!activity.lastActivityAt) {
|
|
@@ -61,7 +109,7 @@ export async function waitForProxyUpdateWindow(options) {
|
|
|
61
109
|
}
|
|
62
110
|
const activity = await options.getActivity();
|
|
63
111
|
options.onPhase?.("draining", activity);
|
|
64
|
-
if (activity
|
|
112
|
+
if (activity && isProxyRuntimeSettled(activity)) {
|
|
65
113
|
return { ready: true, draining: true };
|
|
66
114
|
}
|
|
67
115
|
await wait(options.pollIntervalMs);
|
|
@@ -70,3 +118,10 @@ export async function waitForProxyUpdateWindow(options) {
|
|
|
70
118
|
options.onPhase?.("drain_timeout", activity);
|
|
71
119
|
return { ready: false, draining: true, reason: "drain_timeout" };
|
|
72
120
|
}
|
|
121
|
+
/** An asynchronous staging job must not publish after its service owner changes. */
|
|
122
|
+
export function isProxyUpdateOwnerCurrent(options) {
|
|
123
|
+
return (!options.stopping &&
|
|
124
|
+
options.parentStatus === "running" &&
|
|
125
|
+
options.runtimePid === options.parentPid &&
|
|
126
|
+
options.runtimeUpdaterPid === options.updaterPid);
|
|
127
|
+
}
|
|
@@ -2,7 +2,7 @@ import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
|
|
|
2
2
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
3
3
|
import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
|
|
4
4
|
import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
|
|
5
|
-
import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
5
|
+
import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamBody, AnthropicUpstreamBodyBuilder, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
6
6
|
declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
|
|
7
7
|
declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
|
|
8
8
|
declare function acquireAccountAdmission(accountKey: string, capacity: number, abortSignal?: AbortSignal, timeoutMs?: number): Promise<AccountAdmissionLease | undefined>;
|
|
@@ -269,10 +269,7 @@ declare function handleAnthropicAuthRetry(args: {
|
|
|
269
269
|
account: ProxyPassthroughAccount;
|
|
270
270
|
accountState: RuntimeAccountState;
|
|
271
271
|
headers: Record<string, string>;
|
|
272
|
-
buildUpstreamBody:
|
|
273
|
-
bodyStr: string;
|
|
274
|
-
sessionId?: string;
|
|
275
|
-
};
|
|
272
|
+
buildUpstreamBody: AnthropicUpstreamBodyBuilder;
|
|
276
273
|
url: string;
|
|
277
274
|
enabledAccounts: ProxyPassthroughAccount[];
|
|
278
275
|
orderedAccounts: ProxyPassthroughAccount[];
|
|
@@ -346,9 +343,11 @@ declare function handleAnthropicNonOkResponse(args: {
|
|
|
346
343
|
*/
|
|
347
344
|
declare function isAntiAbuseConstruction429(headers: Record<string, string>, body: string): boolean;
|
|
348
345
|
declare function fetchAnthropicAccountResponse(args: {
|
|
346
|
+
ctx: ServerContext;
|
|
349
347
|
url: string;
|
|
350
348
|
headers: Record<string, string>;
|
|
351
349
|
finalBodyStr: string;
|
|
350
|
+
preparedContext: AnthropicUpstreamBody["preparedContext"];
|
|
352
351
|
requestedModel?: string;
|
|
353
352
|
account: ProxyPassthroughAccount;
|
|
354
353
|
accountState: RuntimeAccountState;
|