@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
|
@@ -355,6 +355,8 @@ function accountEntry(accounts, account, accountType) {
|
|
|
355
355
|
return created;
|
|
356
356
|
}
|
|
357
357
|
function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByRequest, accounts) {
|
|
358
|
+
let clientFinals = 0;
|
|
359
|
+
let internalFinals = 0;
|
|
358
360
|
let success = 0;
|
|
359
361
|
let errors = 0;
|
|
360
362
|
let finalRateLimits = 0;
|
|
@@ -376,37 +378,49 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
|
|
|
376
378
|
const errorTypes = {};
|
|
377
379
|
const errorCodes = {};
|
|
378
380
|
for (const [requestId, request] of finalRequests) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
terminalStreamErrors.has(requestId);
|
|
382
|
-
if (failed) {
|
|
383
|
-
errors += 1;
|
|
381
|
+
if (request.accountingScope === "internal") {
|
|
382
|
+
internalFinals++;
|
|
384
383
|
}
|
|
385
384
|
else {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
385
|
+
clientFinals++;
|
|
386
|
+
const failed = request.status >= 400 ||
|
|
387
|
+
!!request.errorType ||
|
|
388
|
+
terminalStreamErrors.has(requestId);
|
|
389
|
+
if (failed) {
|
|
390
|
+
errors += 1;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
success += 1;
|
|
394
|
+
}
|
|
395
|
+
finalRateLimits += request.status === 429 ? 1 : 0;
|
|
396
|
+
const accountStats = accountEntry(accounts, request.account, request.accountType);
|
|
397
|
+
accountStats.finalRequests += 1;
|
|
398
|
+
accountStats.finalErrors += failed ? 1 : 0;
|
|
399
|
+
if (failed) {
|
|
400
|
+
increment(errorTypes, terminalStreamErrors.has(requestId)
|
|
401
|
+
? "stream_error"
|
|
402
|
+
: (request.errorType ?? `http_${request.status}`));
|
|
403
|
+
if (request.errorCode) {
|
|
404
|
+
increment(errorCodes, request.errorCode);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (request.durationMs !== null && request.durationMs >= 0) {
|
|
408
|
+
finalRequestLatency.push(request.durationMs);
|
|
409
|
+
}
|
|
410
|
+
const requestAttempts = attemptsByRequest.get(requestId);
|
|
411
|
+
recoveredAfterRetry += !failed && requestAttempts?.hadError ? 1 : 0;
|
|
412
|
+
if (requestAttempts?.count === 1 &&
|
|
413
|
+
requestAttempts.durationCount === 1 &&
|
|
414
|
+
request.durationMs !== null &&
|
|
415
|
+
Number.isFinite(requestAttempts.totalDurationMs)) {
|
|
416
|
+
singleAttemptDelta.push(request.durationMs - requestAttempts.totalDurationMs);
|
|
398
417
|
}
|
|
399
418
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (requestAttempts?.count === 1 &&
|
|
406
|
-
requestAttempts.durationCount === 1 &&
|
|
407
|
-
request.durationMs !== null &&
|
|
408
|
-
Number.isFinite(requestAttempts.totalDurationMs)) {
|
|
409
|
-
singleAttemptDelta.push(request.durationMs - requestAttempts.totalDurationMs);
|
|
419
|
+
// Only the designated owner contributes provider usage. Client bridge
|
|
420
|
+
// records retain outcome/latency while their linked child owns token cost.
|
|
421
|
+
if (request.usageOwnerRequestId &&
|
|
422
|
+
request.usageOwnerRequestId !== requestId) {
|
|
423
|
+
continue;
|
|
410
424
|
}
|
|
411
425
|
if (request.inputTokens !== null ||
|
|
412
426
|
request.outputTokens !== null ||
|
|
@@ -455,7 +469,8 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
|
|
|
455
469
|
}
|
|
456
470
|
return {
|
|
457
471
|
requestTotals: {
|
|
458
|
-
completed:
|
|
472
|
+
completed: clientFinals,
|
|
473
|
+
internalCompleted: internalFinals,
|
|
459
474
|
success,
|
|
460
475
|
errors,
|
|
461
476
|
finalRateLimits,
|
|
@@ -587,6 +602,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
587
602
|
let malformedLines = 0;
|
|
588
603
|
let unsupportedLifecycleLines = 0;
|
|
589
604
|
const accepted = new Set();
|
|
605
|
+
const internalRequests = new Set();
|
|
590
606
|
const auxiliaryRequests = new Set();
|
|
591
607
|
const headers = new Set();
|
|
592
608
|
const firstChunks = new Set();
|
|
@@ -630,6 +646,9 @@ export async function analyzeProxyLogs(options) {
|
|
|
630
646
|
if (timestamp === null || !requestId) {
|
|
631
647
|
return;
|
|
632
648
|
}
|
|
649
|
+
if (record.accountingScope === "internal") {
|
|
650
|
+
internalRequests.add(requestId);
|
|
651
|
+
}
|
|
633
652
|
const event = stringValue(record.event);
|
|
634
653
|
if (record.schemaVersion !== 1 ||
|
|
635
654
|
!event ||
|
|
@@ -749,15 +768,6 @@ export async function analyzeProxyLogs(options) {
|
|
|
749
768
|
if (elapsed !== null && elapsed >= 0) {
|
|
750
769
|
terminalLatencyByRequest.set(requestId, elapsed);
|
|
751
770
|
}
|
|
752
|
-
increment(terminalOutcomes, stringValue(record.terminalOutcome) ?? "unknown");
|
|
753
|
-
const errorType = stringValue(record.errorType);
|
|
754
|
-
const errorCode = stringValue(record.errorCode);
|
|
755
|
-
if (errorType) {
|
|
756
|
-
increment(lifecycleErrorTypes, errorType);
|
|
757
|
-
}
|
|
758
|
-
if (errorCode) {
|
|
759
|
-
increment(lifecycleErrorCodes, errorCode);
|
|
760
|
-
}
|
|
761
771
|
}
|
|
762
772
|
}, () => {
|
|
763
773
|
malformedLines += 1;
|
|
@@ -785,11 +795,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
785
795
|
// Contradictory copies are not reliable latency samples. Keep their data
|
|
786
796
|
// quality count, but do not choose one timing arbitrarily.
|
|
787
797
|
const verifiedLatencies = (values) => [...values]
|
|
788
|
-
.filter(([id]) => !conflictedRequests.has(id))
|
|
798
|
+
.filter(([id]) => !conflictedRequests.has(id) && !internalRequests.has(id))
|
|
789
799
|
.map(([, ms]) => ms);
|
|
790
|
-
const headersLatency = verifiedLatencies(headersLatencyByRequest);
|
|
791
|
-
const firstChunkLatency = verifiedLatencies(firstChunkLatencyByRequest);
|
|
792
|
-
const terminalLatency = verifiedLatencies(terminalLatencyByRequest);
|
|
793
800
|
let lifecycleSequenceGaps = 0;
|
|
794
801
|
let lifecycleSequenceDuplicates = 0;
|
|
795
802
|
for (const [processId, allValues] of sequences) {
|
|
@@ -972,7 +979,15 @@ export async function analyzeProxyLogs(options) {
|
|
|
972
979
|
else {
|
|
973
980
|
absentRoutingDecisions += 1;
|
|
974
981
|
}
|
|
982
|
+
if (record.accountingScope === "internal") {
|
|
983
|
+
internalRequests.add(requestId);
|
|
984
|
+
}
|
|
975
985
|
const parsed = {
|
|
986
|
+
accountingScope: internalRequests.has(requestId)
|
|
987
|
+
? "internal"
|
|
988
|
+
: "client",
|
|
989
|
+
parentRequestId: stringValue(record.parentRequestId) ?? undefined,
|
|
990
|
+
usageOwnerRequestId: stringValue(record.usageOwnerRequestId) ?? undefined,
|
|
976
991
|
firstUsefulOutputMs: finiteNumber(record.firstUsefulOutputMs),
|
|
977
992
|
timestamp: new Date(timestamp).toISOString(),
|
|
978
993
|
status,
|
|
@@ -1057,7 +1072,17 @@ export async function analyzeProxyLogs(options) {
|
|
|
1057
1072
|
!(recorded === "bodyless" && resolved === "completed")) {
|
|
1058
1073
|
finalOutcomeConflicts += 1;
|
|
1059
1074
|
}
|
|
1060
|
-
|
|
1075
|
+
if (!internalRequests.has(requestId)) {
|
|
1076
|
+
increment(terminalOutcomes, resolved);
|
|
1077
|
+
const errorType = stringValue(record.errorType);
|
|
1078
|
+
const errorCode = stringValue(record.errorCode);
|
|
1079
|
+
if (errorType) {
|
|
1080
|
+
increment(lifecycleErrorTypes, errorType);
|
|
1081
|
+
}
|
|
1082
|
+
if (errorCode) {
|
|
1083
|
+
increment(lifecycleErrorCodes, errorCode);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1061
1086
|
}
|
|
1062
1087
|
let capturesIndexed = 0;
|
|
1063
1088
|
let truncatedCaptures = 0;
|
|
@@ -1210,14 +1235,18 @@ export async function analyzeProxyLogs(options) {
|
|
|
1210
1235
|
},
|
|
1211
1236
|
];
|
|
1212
1237
|
}),
|
|
1213
|
-
accepted: accepted.
|
|
1214
|
-
|
|
1238
|
+
accepted: [...accepted].filter((id) => !internalRequests.has(id)).length,
|
|
1239
|
+
internalAccepted: [...accepted].filter((id) => internalRequests.has(id))
|
|
1240
|
+
.length,
|
|
1241
|
+
auxiliaryRequests: [...accepted].filter((id) => auxiliaryRequests.has(id) && !internalRequests.has(id)).length,
|
|
1242
|
+
headers: [...headers].filter((id) => !internalRequests.has(id)).length,
|
|
1243
|
+
firstChunks: [...firstChunks].filter((id) => !internalRequests.has(id))
|
|
1215
1244
|
.length,
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
terminal: terminal.size,
|
|
1219
|
-
unsettled: [...accepted].filter((requestId) => !terminal.has(requestId))
|
|
1245
|
+
terminal: [...terminal].filter((id) => !internalRequests.has(id)).length,
|
|
1246
|
+
internalTerminal: [...terminal].filter((id) => internalRequests.has(id))
|
|
1220
1247
|
.length,
|
|
1248
|
+
internalUnsettled: [...accepted].filter((id) => internalRequests.has(id) && !terminal.has(id)).length,
|
|
1249
|
+
unsettled: [...accepted].filter((requestId) => !internalRequests.has(requestId) && !terminal.has(requestId)).length,
|
|
1221
1250
|
terminalOutcomes,
|
|
1222
1251
|
errorTypes: lifecycleErrorTypes,
|
|
1223
1252
|
errorCodes: lifecycleErrorCodes,
|
|
@@ -1237,12 +1266,13 @@ export async function analyzeProxyLogs(options) {
|
|
|
1237
1266
|
unclassified: unclassifiedRateLimits,
|
|
1238
1267
|
},
|
|
1239
1268
|
latencyMs: {
|
|
1240
|
-
headers: summarizeLatency(
|
|
1241
|
-
firstChunk: summarizeLatency(
|
|
1242
|
-
firstUsefulOutput: summarizeLatency([...finalRequests.values()].flatMap((record) => record.
|
|
1269
|
+
headers: summarizeLatency(verifiedLatencies(headersLatencyByRequest)),
|
|
1270
|
+
firstChunk: summarizeLatency(verifiedLatencies(firstChunkLatencyByRequest)),
|
|
1271
|
+
firstUsefulOutput: summarizeLatency([...finalRequests.values()].flatMap((record) => record.accountingScope === "internal" ||
|
|
1272
|
+
record.firstUsefulOutputMs === null
|
|
1243
1273
|
? []
|
|
1244
1274
|
: [record.firstUsefulOutputMs])),
|
|
1245
|
-
terminal: summarizeLatency(
|
|
1275
|
+
terminal: summarizeLatency(verifiedLatencies(terminalLatencyByRequest)),
|
|
1246
1276
|
finalRequest: summarizeLatency(finalSummary.finalRequestLatency),
|
|
1247
1277
|
attempt: summarizeLatency(attemptLatency),
|
|
1248
1278
|
singleAttemptDelta: summarizeLatency(finalSummary.singleAttemptDelta),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/** Node's fetch Request does not always abort when a completed upload's peer
|
|
3
|
+
* disconnects before response headers. Observe the response socket as well. */
|
|
4
|
+
export declare function bindProxyClientCancellation(request: Request, bindings: {
|
|
5
|
+
incoming?: IncomingMessage;
|
|
6
|
+
outgoing?: ServerResponse;
|
|
7
|
+
} | undefined, controller: AbortController): () => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Node's fetch Request does not always abort when a completed upload's peer
|
|
2
|
+
* disconnects before response headers. Observe the response socket as well. */
|
|
3
|
+
export function bindProxyClientCancellation(request, bindings, controller) {
|
|
4
|
+
const abort = () => controller.abort(new DOMException("Client disconnected", "AbortError"));
|
|
5
|
+
const close = () => {
|
|
6
|
+
if (!bindings?.outgoing?.writableFinished) {
|
|
7
|
+
abort();
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
11
|
+
bindings?.incoming?.once("aborted", abort);
|
|
12
|
+
bindings?.outgoing?.once("close", close);
|
|
13
|
+
if (request.signal.aborted ||
|
|
14
|
+
bindings?.incoming?.aborted ||
|
|
15
|
+
bindings?.outgoing?.destroyed) {
|
|
16
|
+
abort();
|
|
17
|
+
}
|
|
18
|
+
return () => {
|
|
19
|
+
request.signal.removeEventListener("abort", abort);
|
|
20
|
+
bindings?.incoming?.removeListener("aborted", abort);
|
|
21
|
+
bindings?.outgoing?.removeListener("close", close);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProxyContextPolicy, ProxyContextEvidence, ProxyPreparedContext } from "../types/index.js";
|
|
2
|
+
/** Local refusal before spending a provider request; never silently truncates. */
|
|
3
|
+
export declare class ProxyContextPreflightError extends Error {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly evidence?: ProxyContextEvidence | undefined;
|
|
6
|
+
readonly status = 400;
|
|
7
|
+
readonly retryable = false;
|
|
8
|
+
constructor(code: string, message: string, evidence?: ProxyContextEvidence | undefined);
|
|
9
|
+
}
|
|
10
|
+
/** An invalid configured budget must not silently disable protection. */
|
|
11
|
+
export declare function parseProxyContextPolicy(raw: string | undefined): ProxyContextPolicy;
|
|
12
|
+
export declare function getProxyContextPolicy(): ProxyContextPolicy;
|
|
13
|
+
/**
|
|
14
|
+
* Preserve history, instructions and tool-call pairs. Tool reduction is explicit
|
|
15
|
+
* opt-in; discovered/configured context limits never use a guessed model default.
|
|
16
|
+
*/
|
|
17
|
+
export declare function prepareProxyRequestContext<T extends object>(args: {
|
|
18
|
+
provider: string;
|
|
19
|
+
model: string;
|
|
20
|
+
body: T;
|
|
21
|
+
/** Original protocol body before an adapter flattens historical tool calls. */
|
|
22
|
+
toolReferenceBody?: unknown;
|
|
23
|
+
maxOutputTokens?: number;
|
|
24
|
+
policy?: ProxyContextPolicy;
|
|
25
|
+
}): ProxyPreparedContext<T>;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { getRuntimeContextWindow, getRuntimeOutputCeiling, } from "../constants/contextWindows.js";
|
|
2
|
+
import { estimateTokens, IMAGE_TOKEN_ESTIMATE, } from "../utils/tokenEstimation.js";
|
|
3
|
+
/** Local refusal before spending a provider request; never silently truncates. */
|
|
4
|
+
export class ProxyContextPreflightError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
evidence;
|
|
7
|
+
status = 400;
|
|
8
|
+
retryable = false;
|
|
9
|
+
constructor(code, message, evidence) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.evidence = evidence;
|
|
13
|
+
this.name = "ProxyContextPreflightError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function record(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function positive(value) {
|
|
20
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
21
|
+
}
|
|
22
|
+
/** An invalid configured budget must not silently disable protection. */
|
|
23
|
+
export function parseProxyContextPolicy(raw) {
|
|
24
|
+
if (!raw?.trim()) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "NEUROLINK_PROXY_CONTEXT_POLICY must contain a JSON object");
|
|
33
|
+
}
|
|
34
|
+
if (!record(value)) {
|
|
35
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "Proxy context policy must be an object");
|
|
36
|
+
}
|
|
37
|
+
for (const key of Object.keys(value)) {
|
|
38
|
+
if (![
|
|
39
|
+
"maxInputTokens",
|
|
40
|
+
"outputReserveTokens",
|
|
41
|
+
"enforceDiscoveredLimits",
|
|
42
|
+
"toolAllowlist",
|
|
43
|
+
"models",
|
|
44
|
+
].includes(key)) {
|
|
45
|
+
throw new ProxyContextPreflightError("invalid_context_policy", `Unknown context policy field: ${key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const key of ["maxInputTokens", "outputReserveTokens"]) {
|
|
49
|
+
if (value[key] !== undefined && !positive(value[key])) {
|
|
50
|
+
throw new ProxyContextPreflightError("invalid_context_policy", `${key} must be a positive safe integer`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (value.enforceDiscoveredLimits !== undefined &&
|
|
54
|
+
typeof value.enforceDiscoveredLimits !== "boolean") {
|
|
55
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "enforceDiscoveredLimits must be boolean");
|
|
56
|
+
}
|
|
57
|
+
if (value.toolAllowlist !== undefined &&
|
|
58
|
+
(!Array.isArray(value.toolAllowlist) ||
|
|
59
|
+
value.toolAllowlist.length > 4096 ||
|
|
60
|
+
!value.toolAllowlist.every((name) => typeof name === "string" && name.length > 0 && name.length <= 256))) {
|
|
61
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "toolAllowlist must be a bounded array of tool names");
|
|
62
|
+
}
|
|
63
|
+
if (value.models !== undefined) {
|
|
64
|
+
if (!record(value.models) || Object.keys(value.models).length > 1024) {
|
|
65
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "models must be a bounded map of provider/model limits");
|
|
66
|
+
}
|
|
67
|
+
for (const [key, limit] of Object.entries(value.models)) {
|
|
68
|
+
if (!key.includes("/") ||
|
|
69
|
+
!record(limit) ||
|
|
70
|
+
!positive(limit.contextWindow) ||
|
|
71
|
+
(limit.maxOutputTokens !== undefined &&
|
|
72
|
+
!positive(limit.maxOutputTokens)) ||
|
|
73
|
+
Object.keys(limit).some((k) => !["contextWindow", "maxOutputTokens"].includes(k))) {
|
|
74
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "Each model needs an explicit provider/model key and positive contextWindow/output limit");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
let lastPolicyText;
|
|
81
|
+
let lastPolicy;
|
|
82
|
+
export function getProxyContextPolicy() {
|
|
83
|
+
const raw = process.env.NEUROLINK_PROXY_CONTEXT_POLICY;
|
|
84
|
+
if (!lastPolicy || raw !== lastPolicyText) {
|
|
85
|
+
lastPolicy = parseProxyContextPolicy(raw);
|
|
86
|
+
lastPolicyText = raw;
|
|
87
|
+
}
|
|
88
|
+
return lastPolicy;
|
|
89
|
+
}
|
|
90
|
+
/** Collect referenced tools so selection never strands a historical call/result. */
|
|
91
|
+
function referencedTools(value, names, seen = new WeakSet(), depth = 0) {
|
|
92
|
+
if (!value || typeof value !== "object" || depth > 128 || seen.has(value)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
seen.add(value);
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
for (const item of value) {
|
|
98
|
+
referencedTools(item, names, seen, depth + 1);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const item = value;
|
|
103
|
+
if (["tool_use", "function_call", "tool_call", "function"].includes(String(item.type))) {
|
|
104
|
+
const name = item.name ?? item.toolName;
|
|
105
|
+
if (typeof name === "string") {
|
|
106
|
+
names.add(name);
|
|
107
|
+
}
|
|
108
|
+
if (record(item.function) && typeof item.function.name === "string") {
|
|
109
|
+
names.add(item.function.name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (typeof item.toolName === "string") {
|
|
113
|
+
names.add(item.toolName);
|
|
114
|
+
}
|
|
115
|
+
if (record(item.functionCall) && typeof item.functionCall.name === "string") {
|
|
116
|
+
names.add(item.functionCall.name);
|
|
117
|
+
}
|
|
118
|
+
for (const [key, child] of Object.entries(item)) {
|
|
119
|
+
if (key !== "tools") {
|
|
120
|
+
referencedTools(child, names, seen, depth + 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function toolName(tool) {
|
|
125
|
+
if (!record(tool)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (typeof tool.name === "string") {
|
|
129
|
+
return tool.name;
|
|
130
|
+
}
|
|
131
|
+
return record(tool.function) && typeof tool.function.name === "string"
|
|
132
|
+
? tool.function.name
|
|
133
|
+
: undefined;
|
|
134
|
+
}
|
|
135
|
+
function toolCount(value) {
|
|
136
|
+
return Array.isArray(value)
|
|
137
|
+
? value.length
|
|
138
|
+
: record(value)
|
|
139
|
+
? Object.keys(value).length
|
|
140
|
+
: 0;
|
|
141
|
+
}
|
|
142
|
+
/** Estimate without copying binary payloads or building another giant JSON string. */
|
|
143
|
+
function estimateValue(value, provider, state, seen = new WeakSet(), depth = 0) {
|
|
144
|
+
if (typeof value === "string") {
|
|
145
|
+
return estimateTokens(value, provider) + 1;
|
|
146
|
+
}
|
|
147
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
if (!value || typeof value !== "object") {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
if (depth > 128) {
|
|
154
|
+
throw new ProxyContextPreflightError("context_structure_too_deep", "Request context nesting exceeds the supported limit");
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(value)) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
seen.add(value);
|
|
160
|
+
try {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.reduce((sum, item) => sum + estimateValue(item, provider, state, seen, depth + 1) + 4, 0);
|
|
163
|
+
}
|
|
164
|
+
const item = value;
|
|
165
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
166
|
+
state.multimodal = true;
|
|
167
|
+
return IMAGE_TOKEN_ESTIMATE;
|
|
168
|
+
}
|
|
169
|
+
if (typeof item.text === "string" && Array.isArray(item.images)) {
|
|
170
|
+
state.multimodal = item.images.length > 0 || state.multimodal;
|
|
171
|
+
return (estimateTokens(item.text, provider) +
|
|
172
|
+
item.images.length * IMAGE_TOKEN_ESTIMATE);
|
|
173
|
+
}
|
|
174
|
+
if ([
|
|
175
|
+
"input_image",
|
|
176
|
+
"image",
|
|
177
|
+
"image_url",
|
|
178
|
+
"input_audio",
|
|
179
|
+
"audio",
|
|
180
|
+
"file",
|
|
181
|
+
"input_file",
|
|
182
|
+
"document",
|
|
183
|
+
].includes(String(item.type)) ||
|
|
184
|
+
item.inlineData !== undefined ||
|
|
185
|
+
item.fileData !== undefined) {
|
|
186
|
+
state.multimodal = true;
|
|
187
|
+
return IMAGE_TOKEN_ESTIMATE;
|
|
188
|
+
}
|
|
189
|
+
return Object.entries(item).reduce((sum, [key, child]) => sum +
|
|
190
|
+
estimateTokens(key, provider) +
|
|
191
|
+
estimateValue(child, provider, state, seen, depth + 1), 0);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
seen.delete(value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Preserve history, instructions and tool-call pairs. Tool reduction is explicit
|
|
199
|
+
* opt-in; discovered/configured context limits never use a guessed model default.
|
|
200
|
+
*/
|
|
201
|
+
export function prepareProxyRequestContext(args) {
|
|
202
|
+
const policy = args.policy ?? getProxyContextPolicy();
|
|
203
|
+
const original = args.body;
|
|
204
|
+
let body = args.body;
|
|
205
|
+
const tools = original.tools;
|
|
206
|
+
if (policy.toolAllowlist) {
|
|
207
|
+
const keep = new Set(policy.toolAllowlist);
|
|
208
|
+
referencedTools(original, keep);
|
|
209
|
+
referencedTools(args.toolReferenceBody, keep);
|
|
210
|
+
const choice = original.tool_choice;
|
|
211
|
+
if (record(choice)) {
|
|
212
|
+
if (typeof choice.name === "string") {
|
|
213
|
+
keep.add(choice.name);
|
|
214
|
+
}
|
|
215
|
+
if (record(choice.function) && typeof choice.function.name === "string") {
|
|
216
|
+
keep.add(choice.function.name);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const selected = Array.isArray(tools)
|
|
220
|
+
? tools.filter((tool) => {
|
|
221
|
+
const name = toolName(tool);
|
|
222
|
+
return name === undefined || keep.has(name);
|
|
223
|
+
})
|
|
224
|
+
: record(tools)
|
|
225
|
+
? Object.fromEntries(Object.entries(tools).filter(([name]) => keep.has(name)))
|
|
226
|
+
: tools;
|
|
227
|
+
body = {
|
|
228
|
+
...args.body,
|
|
229
|
+
tools: selected,
|
|
230
|
+
...(Array.isArray(original.toolFilter)
|
|
231
|
+
? {
|
|
232
|
+
toolFilter: original.toolFilter.filter((name) => typeof name !== "string" || keep.has(name)),
|
|
233
|
+
}
|
|
234
|
+
: {}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const wire = body;
|
|
238
|
+
const state = { multimodal: false };
|
|
239
|
+
const provider = args.provider === "codex" ? "openai" : args.provider;
|
|
240
|
+
const toolsTokensEstimate = estimateValue(wire.tools, provider, state);
|
|
241
|
+
const instructionsTokensEstimate = [
|
|
242
|
+
wire.instructions,
|
|
243
|
+
wire.system,
|
|
244
|
+
wire.systemPrompt,
|
|
245
|
+
wire.systemInstruction,
|
|
246
|
+
].reduce((sum, value) => sum + estimateValue(value, provider, state), 0);
|
|
247
|
+
const schemaTokensEstimate = [
|
|
248
|
+
wire.response_format,
|
|
249
|
+
wire.output_config,
|
|
250
|
+
wire.structuredOutput,
|
|
251
|
+
wire.schema,
|
|
252
|
+
record(wire.text) ? wire.text.format : undefined,
|
|
253
|
+
record(wire.generationConfig)
|
|
254
|
+
? wire.generationConfig.responseSchema
|
|
255
|
+
: undefined,
|
|
256
|
+
record(wire.generationConfig)
|
|
257
|
+
? wire.generationConfig.responseJsonSchema
|
|
258
|
+
: undefined,
|
|
259
|
+
].reduce((sum, value) => sum + estimateValue(value, provider, state), 0);
|
|
260
|
+
const inputTokensEstimate = 24 +
|
|
261
|
+
schemaTokensEstimate +
|
|
262
|
+
toolsTokensEstimate +
|
|
263
|
+
instructionsTokensEstimate +
|
|
264
|
+
[
|
|
265
|
+
wire.messages,
|
|
266
|
+
wire.input,
|
|
267
|
+
wire.contents,
|
|
268
|
+
wire.conversationMessages,
|
|
269
|
+
wire.prompt,
|
|
270
|
+
].reduce((sum, value) => sum + estimateValue(value, provider, state), 0);
|
|
271
|
+
const configured = policy.models?.[`${args.provider}/${args.model}`];
|
|
272
|
+
const discovered = getRuntimeContextWindow(args.provider, args.model);
|
|
273
|
+
const contextWindow = configured?.contextWindow ?? discovered;
|
|
274
|
+
const outputCeiling = configured?.maxOutputTokens ??
|
|
275
|
+
getRuntimeOutputCeiling(args.provider, args.model);
|
|
276
|
+
const requestedOutput = args.maxOutputTokens ??
|
|
277
|
+
wire.max_tokens ??
|
|
278
|
+
wire.max_output_tokens ??
|
|
279
|
+
wire.maxTokens ??
|
|
280
|
+
(record(wire.generationConfig)
|
|
281
|
+
? wire.generationConfig.maxOutputTokens
|
|
282
|
+
: undefined);
|
|
283
|
+
if (requestedOutput !== undefined && !positive(requestedOutput)) {
|
|
284
|
+
throw new ProxyContextPreflightError("invalid_output_budget", "Requested output tokens must be a positive safe integer");
|
|
285
|
+
}
|
|
286
|
+
// Codex rejects max_output_tokens; its model-owned ceiling, not Claude's
|
|
287
|
+
// requested max_tokens, must be reserved when bridging to that transport.
|
|
288
|
+
const outputTokensReserve = args.provider === "codex"
|
|
289
|
+
? (outputCeiling ?? policy.outputReserveTokens ?? 32768)
|
|
290
|
+
: positive(requestedOutput)
|
|
291
|
+
? requestedOutput
|
|
292
|
+
: (outputCeiling ?? policy.outputReserveTokens ?? 32768);
|
|
293
|
+
const evidence = {
|
|
294
|
+
provider: args.provider,
|
|
295
|
+
model: args.model,
|
|
296
|
+
inputTokensEstimate,
|
|
297
|
+
toolsTokensEstimate,
|
|
298
|
+
instructionsTokensEstimate,
|
|
299
|
+
schemaTokensEstimate,
|
|
300
|
+
outputTokensReserve,
|
|
301
|
+
reasoningIncludedInOutputReserve: true,
|
|
302
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
303
|
+
contextLimitSource: configured
|
|
304
|
+
? "configured"
|
|
305
|
+
: discovered
|
|
306
|
+
? "discovered"
|
|
307
|
+
: "unknown",
|
|
308
|
+
tokenCountSource: "estimated",
|
|
309
|
+
multimodalEstimate: state.multimodal,
|
|
310
|
+
originalToolCount: toolCount(tools),
|
|
311
|
+
retainedToolCount: toolCount(wire.tools),
|
|
312
|
+
historyModified: false,
|
|
313
|
+
};
|
|
314
|
+
if (policy.maxInputTokens !== undefined &&
|
|
315
|
+
inputTokensEstimate > policy.maxInputTokens) {
|
|
316
|
+
throw new ProxyContextPreflightError("proxy_input_budget_exceeded", "Estimated input exceeds the configured proxy token budget; reduce context or tools before retrying", evidence);
|
|
317
|
+
}
|
|
318
|
+
if (outputCeiling !== undefined &&
|
|
319
|
+
positive(requestedOutput) &&
|
|
320
|
+
args.provider !== "codex" &&
|
|
321
|
+
requestedOutput > outputCeiling) {
|
|
322
|
+
throw new ProxyContextPreflightError("proxy_output_budget_exceeded", "Requested output exceeds the serving model's known output limit", evidence);
|
|
323
|
+
}
|
|
324
|
+
if (contextWindow !== undefined &&
|
|
325
|
+
policy.enforceDiscoveredLimits !== false &&
|
|
326
|
+
!state.multimodal &&
|
|
327
|
+
inputTokensEstimate + outputTokensReserve > contextWindow) {
|
|
328
|
+
throw new ProxyContextPreflightError("proxy_context_window_exceeded", "Estimated input plus output/reasoning reserve exceeds the serving model context window; no history was truncated", evidence);
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
body,
|
|
332
|
+
inputTokensEstimate,
|
|
333
|
+
outputTokensReserve,
|
|
334
|
+
totalTokensReservation: inputTokensEstimate + outputTokensReserve,
|
|
335
|
+
evidence,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Shared provider terminal codes across native and SDK transports. */
|
|
2
|
+
export declare function classifyProxyFailureCode(code: string | undefined): {
|
|
3
|
+
status: number;
|
|
4
|
+
retryable: boolean;
|
|
5
|
+
} | {
|
|
6
|
+
status: undefined;
|
|
7
|
+
retryable: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
status: number | undefined;
|
|
10
|
+
retryable: undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Read explicit SDK/API failure evidence without guessing from free-form text.
|
|
13
|
+
* Bounded traversal covers common error/cause/data and JSON response envelopes. */
|
|
14
|
+
export declare function getProxyUpstreamFailure(error: unknown): {
|
|
15
|
+
status: number;
|
|
16
|
+
code: string | undefined;
|
|
17
|
+
retryable: boolean | undefined;
|
|
18
|
+
message: string | undefined;
|
|
19
|
+
} | undefined;
|