@juspay/neurolink 12.17.0 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +2 -7
  2. package/dist/browser/neurolink.min.js +422 -413
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/providers/anthropic/client.js +15 -5
  7. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  8. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  9. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  10. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  11. package/dist/proxy/bodyCaptureWorker.js +20 -2
  12. package/dist/proxy/codexFallback.d.ts +10 -0
  13. package/dist/proxy/codexFallback.js +108 -24
  14. package/dist/proxy/codexUsage.js +2 -0
  15. package/dist/proxy/globalInstaller.d.ts +32 -1
  16. package/dist/proxy/globalInstaller.js +386 -3
  17. package/dist/proxy/otelLogSink.d.ts +3 -3
  18. package/dist/proxy/otelLogSink.js +137 -144
  19. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  20. package/dist/proxy/otlpLogTransport.js +334 -0
  21. package/dist/proxy/proxyActivity.d.ts +22 -2
  22. package/dist/proxy/proxyActivity.js +114 -9
  23. package/dist/proxy/proxyAnalysis.js +82 -52
  24. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  25. package/dist/proxy/proxyClientCancellation.js +23 -0
  26. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  27. package/dist/proxy/proxyContextPreflight.js +337 -0
  28. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  29. package/dist/proxy/proxyFailureDetails.js +96 -0
  30. package/dist/proxy/proxyLifecycle.js +7 -0
  31. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  32. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  33. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  34. package/dist/proxy/proxyTokenBudget.js +523 -0
  35. package/dist/proxy/proxyTracer.d.ts +3 -2
  36. package/dist/proxy/proxyTracer.js +46 -20
  37. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  38. package/dist/proxy/proxyTranslationEngine.js +736 -376
  39. package/dist/proxy/requestLogger.d.ts +1 -0
  40. package/dist/proxy/requestLogger.js +107 -5
  41. package/dist/proxy/restartControl.js +9 -4
  42. package/dist/proxy/rollingWorkerProcess.js +65 -0
  43. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  44. package/dist/proxy/updateCoordinator.d.ts +14 -1
  45. package/dist/proxy/updateCoordinator.js +57 -2
  46. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  47. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  48. package/dist/server/routes/codexProxyRoutes.js +191 -9
  49. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  50. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  51. package/dist/types/cli.d.ts +3 -0
  52. package/dist/types/codex.d.ts +5 -1
  53. package/dist/types/index.d.ts +2 -0
  54. package/dist/types/index.js +2 -0
  55. package/dist/types/proxy.d.ts +148 -3
  56. package/dist/types/proxy.js +0 -15
  57. package/dist/types/proxyBudget.d.ts +74 -0
  58. package/dist/types/proxyBudget.js +1 -0
  59. package/dist/types/proxyContext.d.ts +35 -0
  60. package/dist/types/proxyContext.js +1 -0
  61. package/dist/utils/toolChoice.d.ts +19 -0
  62. package/dist/utils/toolChoice.js +39 -0
  63. package/docs/assets/dashboards/README.md +37 -0
  64. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  65. package/docs-site/static/search-index.json +1 -1
  66. package/package.json +4 -1
  67. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  68. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  69. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  70. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  71. 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
- const failed = request.status >= 400 ||
380
- !!request.errorType ||
381
- terminalStreamErrors.has(requestId);
382
- if (failed) {
383
- errors += 1;
381
+ if (request.accountingScope === "internal") {
382
+ internalFinals++;
384
383
  }
385
384
  else {
386
- success += 1;
387
- }
388
- finalRateLimits += request.status === 429 ? 1 : 0;
389
- const accountStats = accountEntry(accounts, request.account, request.accountType);
390
- accountStats.finalRequests += 1;
391
- accountStats.finalErrors += failed ? 1 : 0;
392
- if (failed) {
393
- increment(errorTypes, terminalStreamErrors.has(requestId)
394
- ? "stream_error"
395
- : (request.errorType ?? `http_${request.status}`));
396
- if (request.errorCode) {
397
- increment(errorCodes, request.errorCode);
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
- if (request.durationMs !== null && request.durationMs >= 0) {
401
- finalRequestLatency.push(request.durationMs);
402
- }
403
- const requestAttempts = attemptsByRequest.get(requestId);
404
- recoveredAfterRetry += !failed && requestAttempts?.hadError ? 1 : 0;
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: finalRequests.size,
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
- increment(terminalOutcomes, resolved);
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.size,
1214
- auxiliaryRequests: [...accepted].filter((id) => auxiliaryRequests.has(id))
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
- headers: headers.size,
1217
- firstChunks: firstChunks.size,
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(headersLatency),
1241
- firstChunk: summarizeLatency(firstChunkLatency),
1242
- firstUsefulOutput: summarizeLatency([...finalRequests.values()].flatMap((record) => record.firstUsefulOutputMs === null
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(terminalLatency),
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;