@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
@@ -12,12 +12,16 @@
12
12
  * provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
13
13
  */
14
14
  import { buildOpenAIError, convertClaudeToOpenAIResponse, convertOpenAIToClaudeRequest, createClaudeToOpenAIStreamTransform, parseOpenAIRequest, } from "../../proxy/openaiFormat.js";
15
+ import { createProxyRouteBodyCapture } from "../../proxy/proxyRouteBodyCapture.js";
15
16
  import { ProxyTracer } from "../../proxy/proxyTracer.js";
16
17
  import { buildModelsListResponse, handleTranslatedJsonRequest, handleTranslatedStreamRequest, } from "../../proxy/proxyTranslationEngine.js";
17
18
  import { buildClientAttribution } from "../../proxy/clientAttribution.js";
18
19
  import { logRequest } from "../../proxy/requestLogger.js";
20
+ import { registerInternalProxyRequest, isProxyRequestFinalized, getProxyBridgeResult, releaseProxyRequestAccounting, } from "../../proxy/proxyActivity.js";
21
+ import { recordFinalSuccess, recordFinalError, } from "../../proxy/usageStats.js";
22
+ import { getProxyRequestTraceContext } from "../../proxy/proxyTraceContext.js";
19
23
  import { buildProxyTranslationPlan } from "../../proxy/routingPolicy.js";
20
- import { withTimeout } from "../../utils/async/withTimeout.js";
24
+ import { raceWithAbort, withTimeout } from "../../utils/async/withTimeout.js";
21
25
  import { sanitizeForLog } from "../../utils/logSanitize.js";
22
26
  import { logger } from "../../utils/logger.js";
23
27
  // Maximum time the internal loopback fetch is allowed to take before we
@@ -83,9 +87,102 @@ function adaptForTranslationPlan(parsed) {
83
87
  // ---------------------------------------------------------------------------
84
88
  // OpenAI -> Anthropic loopback bridge
85
89
  // ---------------------------------------------------------------------------
90
+ function startBridgeTracer(ctx, body) {
91
+ try {
92
+ const tracer = ProxyTracer.startRequest({
93
+ requestId: ctx.requestId,
94
+ method: ctx.method,
95
+ path: ctx.path,
96
+ model: body.model,
97
+ stream: body.stream === true,
98
+ toolCount: body.tools?.length ?? 0,
99
+ provider: "openai-bridge",
100
+ recordRequestMetrics: true,
101
+ recordUsageMetrics: false,
102
+ ...buildClientAttribution(ctx.headers),
103
+ }, ctx.headers);
104
+ tracer.setMode("full");
105
+ return tracer;
106
+ }
107
+ catch {
108
+ // Trace setup must not fail the request.
109
+ return undefined;
110
+ }
111
+ }
112
+ function buildBridgeHeaders(ctx, stream, token) {
113
+ // Forward a minimal set of headers. The proxy's own /v1/messages handler
114
+ // will attach OAuth credentials from its account pool.
115
+ const forwardHeaders = {
116
+ "content-type": "application/json",
117
+ accept: stream ? "text/event-stream" : "application/json",
118
+ "x-neurolink-internal-request": token,
119
+ };
120
+ const traceContext = getProxyRequestTraceContext(ctx.requestId);
121
+ if (traceContext) {
122
+ forwardHeaders.traceparent = `00-${traceContext.traceId}-${traceContext.spanId}-${traceContext.traceFlags.toString(16).padStart(2, "0")}`;
123
+ }
124
+ for (const [k, v] of Object.entries(ctx.headers)) {
125
+ if (typeof v !== "string") {
126
+ continue;
127
+ }
128
+ const lower = k.toLowerCase();
129
+ if (lower.startsWith("anthropic-") ||
130
+ lower === "x-api-key" ||
131
+ [
132
+ "user-agent",
133
+ "x-neurolink-session-id",
134
+ "x-claude-code-session-id",
135
+ "session_id",
136
+ "session-id",
137
+ "tracestate",
138
+ "baggage",
139
+ ].includes(lower) ||
140
+ (lower === "traceparent" && !forwardHeaders.traceparent)) {
141
+ forwardHeaders[lower] = v;
142
+ }
143
+ }
144
+ return forwardHeaders;
145
+ }
146
+ function bridgeAttribution(child) {
147
+ const servingModelStatus = child?.servingModelStatus ?? "unavailable";
148
+ const accountIdentityStatus = child?.accountIdentityStatus ??
149
+ (child?.accountKey &&
150
+ child.accountType !== "translation" &&
151
+ child.account !== "unknown"
152
+ ? "observed"
153
+ : "unavailable");
154
+ return { servingModelStatus, accountIdentityStatus };
155
+ }
156
+ async function bridgeJsonResponse(upstream, signal, model, writeLifecycle, errorResponse, capture) {
157
+ try {
158
+ const claudeJson = (await raceWithAbort(upstream.json(), signal));
159
+ // Child owns billed usage; parent owns client delivery and its serialized body.
160
+ const response = convertClaudeToOpenAIResponse(claudeJson, model);
161
+ capture.json(response, 200);
162
+ await writeLifecycle(200);
163
+ return response;
164
+ }
165
+ catch (error) {
166
+ const status = signal.aborted && signal.reason?.name !== "TimeoutError"
167
+ ? 499
168
+ : signal.reason?.name === "TimeoutError"
169
+ ? 504
170
+ : 502;
171
+ await writeLifecycle(status, {
172
+ errorType: status === 499
173
+ ? "client_cancelled"
174
+ : status === 504
175
+ ? "loopback_timeout"
176
+ : "loopback_response_error",
177
+ errorMessage: error instanceof Error ? error.message : String(error),
178
+ });
179
+ return errorResponse(status, "Anthropic loopback failed");
180
+ }
181
+ }
86
182
  /**
87
183
  * Forward an OpenAI-format request targeting a Claude model through the
88
- * proxy's own /v1/messages endpoint via a loopback fetch().
184
+ * proxy's own /v1/messages endpoint. The CLI injects its same-worker HTTP
185
+ * application, so a rolling supervisor cannot move the child to another worker.
89
186
  *
90
187
  * This reuses the full Claude passthrough path (OAuth account rotation, retry,
91
188
  * SSE interception, etc.) and only adds format conversion at the edges.
@@ -96,62 +193,131 @@ function adaptForTranslationPlan(parsed) {
96
193
  * port via `createOpenAIProxyRoutes(modelRouter, basePath, loopbackPort)`.
97
194
  */
98
195
  async function handleOpenAIToAnthropicBridge(args) {
99
- const { ctx, body, targetModel, requestStartTime, loopbackPort } = args;
196
+ const { ctx, body, targetModel, requestStartTime, loopbackPort, internalDispatch, } = args;
100
197
  const stream = body.stream === true;
101
198
  const toolCount = body.tools?.length ?? 0;
102
- const writeLifecycle = (responseStatus, extra = {}) => logRequest({
103
- timestamp: new Date().toISOString(),
104
- requestId: ctx.requestId,
105
- method: ctx.method,
106
- path: ctx.path,
107
- model: body.model,
108
- stream,
109
- toolCount,
110
- account: "",
111
- accountType: "openai-bridge",
112
- ...buildClientAttribution(ctx.headers),
113
- responseStatus,
114
- responseTimeMs: Date.now() - requestStartTime,
115
- ...extra,
116
- });
117
- // Convert to Claude format and remap the model to the router's choice.
118
- const claudeBody = convertOpenAIToClaudeRequest(body);
119
- claudeBody.model = targetModel;
199
+ const capture = createProxyRouteBodyCapture(ctx, body.model, stream, requestStartTime);
200
+ capture.request();
201
+ const errorResponse = (status, message) => {
202
+ capture.json(buildOpenAIError(status, message), status);
203
+ return buildOpenAIErrorResponse(status, message);
204
+ };
205
+ const internal = registerInternalProxyRequest(ctx.requestId);
206
+ const bridgeTracer = startBridgeTracer(ctx, body);
207
+ const cancellation = new AbortController();
208
+ const signal = AbortSignal.any([
209
+ cancellation.signal,
210
+ AbortSignal.timeout(LOOPBACK_TIMEOUT_MS),
211
+ ...(ctx.abortSignal ? [ctx.abortSignal] : []),
212
+ ]);
213
+ let lifecycleWritten = false;
214
+ const writeLifecycle = (responseStatus, extra = {}) => {
215
+ if (lifecycleWritten) {
216
+ return Promise.resolve();
217
+ }
218
+ lifecycleWritten = true;
219
+ internal.dispose();
220
+ const child = getProxyBridgeResult(ctx.requestId);
221
+ const { servingModelStatus, accountIdentityStatus } = bridgeAttribution(child);
222
+ if (servingModelStatus === "observed" &&
223
+ child?.model &&
224
+ child.model !== body.model) {
225
+ bridgeTracer?.setModelSubstitution(body.model, child.model, child.provider);
226
+ }
227
+ if (extra.errorType) {
228
+ bridgeTracer?.setError(extra.errorType, extra.errorMessage ?? extra.errorType);
229
+ }
230
+ bridgeTracer?.end(responseStatus, Date.now() - requestStartTime);
231
+ if (!isProxyRequestFinalized(ctx.requestId)) {
232
+ if (responseStatus >= 400) {
233
+ recordFinalError(responseStatus, child?.account, child?.accountType, {
234
+ requestId: ctx.requestId,
235
+ errorType: extra.errorType,
236
+ message: extra.errorMessage,
237
+ terminalOutcome: responseStatus === 499
238
+ ? "client_cancelled"
239
+ : extra.errorType?.includes("stream")
240
+ ? "stream_error"
241
+ : "handler_error",
242
+ });
243
+ }
244
+ else {
245
+ recordFinalSuccess(child?.account, child?.accountType);
246
+ }
247
+ }
248
+ return logRequest({
249
+ timestamp: new Date().toISOString(),
250
+ requestId: ctx.requestId,
251
+ method: ctx.method,
252
+ path: ctx.path,
253
+ model: child?.model ?? body.model,
254
+ servingModelStatus,
255
+ stream,
256
+ toolCount,
257
+ account: child?.account ?? "",
258
+ accountKey: child?.accountKey,
259
+ accountType: child?.accountType ?? "openai-bridge",
260
+ accountIdentityStatus,
261
+ requestedModel: body.model,
262
+ provider: child?.provider,
263
+ errorCode: child?.errorCode,
264
+ transportScope: child?.transportScope,
265
+ retryable: child?.retryable,
266
+ accountingScope: "client",
267
+ usageOwnerRequestId: internal.requestId,
268
+ ...buildClientAttribution(ctx.headers),
269
+ responseStatus,
270
+ responseTimeMs: Date.now() - requestStartTime,
271
+ ...extra,
272
+ }).finally(() => releaseProxyRequestAccounting(ctx.requestId));
273
+ };
120
274
  // SECURITY: Never derive the loopback target from the client-controlled
121
275
  // `Host` header. The bridge always fetches from 127.0.0.1 on the listener's
122
276
  // configured port — anything else would be an SSRF vector.
123
277
  const internalUrl = `http://127.0.0.1:${loopbackPort}/v1/messages`;
124
- // Forward a minimal set of headers. The proxy's own /v1/messages handler
125
- // will attach OAuth credentials from its account pool.
126
- const forwardHeaders = {
127
- "content-type": "application/json",
128
- accept: stream ? "text/event-stream" : "application/json",
129
- };
130
- for (const [k, v] of Object.entries(ctx.headers)) {
131
- if (typeof v !== "string") {
132
- continue;
133
- }
134
- const lower = k.toLowerCase();
135
- if (lower.startsWith("anthropic-") || lower === "x-api-key") {
136
- forwardHeaders[lower] = v;
137
- }
138
- }
139
278
  // Bound the self-call with a timeout so a stuck inner handler can't hang
140
279
  // the outer /v1/chat/completions request indefinitely.
141
- const upstream = await withTimeout(fetch(internalUrl, {
142
- method: "POST",
143
- headers: forwardHeaders,
144
- body: JSON.stringify({ ...claudeBody, stream }),
145
- }), LOOPBACK_TIMEOUT_MS, `Anthropic loopback timed out after ${LOOPBACK_TIMEOUT_MS}ms`);
280
+ let upstream;
281
+ try {
282
+ signal.throwIfAborted();
283
+ // Preparation can reject malformed inputs before dispatch. Keep it within
284
+ // lifecycle cleanup so the internal capability and parent maps are released.
285
+ const claudeBody = convertOpenAIToClaudeRequest(body);
286
+ claudeBody.model = targetModel;
287
+ const forwardHeaders = buildBridgeHeaders(ctx, stream, internal.token);
288
+ const requestOptions = {
289
+ method: "POST",
290
+ headers: forwardHeaders,
291
+ body: JSON.stringify({ ...claudeBody, stream }),
292
+ signal,
293
+ };
294
+ upstream = await raceWithAbort(Promise.resolve(internalDispatch
295
+ ? internalDispatch(new Request(internalUrl, requestOptions))
296
+ : fetch(internalUrl, requestOptions)), signal);
297
+ }
298
+ catch (error) {
299
+ internal.dispose();
300
+ const status = ctx.abortSignal?.aborted ? 499 : signal.aborted ? 504 : 502;
301
+ cancellation.abort(error);
302
+ await writeLifecycle(status, {
303
+ errorType: ctx.abortSignal?.aborted
304
+ ? "client_cancelled"
305
+ : status === 504
306
+ ? "loopback_timeout"
307
+ : "loopback_exception",
308
+ errorMessage: error instanceof Error ? error.message : String(error),
309
+ });
310
+ return errorResponse(status, "Anthropic loopback failed");
311
+ }
146
312
  if (!upstream.ok) {
147
- const errText = await upstream.text().catch(() => "");
313
+ const errText = await raceWithAbort(upstream.text(), signal).catch(() => "");
148
314
  const safeErrText = sanitizeForLog(errText);
149
315
  logger.always(`[proxy:openai] anthropic loopback error ${upstream.status}: ${safeErrText}`);
150
316
  await writeLifecycle(upstream.status, {
151
317
  errorType: "loopback_upstream_error",
152
318
  errorMessage: safeErrText,
153
319
  });
154
- return buildOpenAIErrorResponse(upstream.status, safeErrText || `Anthropic loopback failed with status ${upstream.status}`);
320
+ return errorResponse(upstream.status, safeErrText || `Anthropic loopback failed with status ${upstream.status}`);
155
321
  }
156
322
  if (stream) {
157
323
  if (!upstream.body) {
@@ -159,8 +325,9 @@ async function handleOpenAIToAnthropicBridge(args) {
159
325
  errorType: "loopback_empty_stream",
160
326
  errorMessage: "Anthropic loopback returned empty stream body",
161
327
  });
162
- return buildOpenAIErrorResponse(502, "Anthropic loopback returned empty stream body");
328
+ return errorResponse(502, "Anthropic loopback returned empty stream body");
163
329
  }
330
+ const clientCapture = capture.accumulator("client_response", "text/event-stream");
164
331
  let terminalStreamError;
165
332
  const transformed = upstream.body.pipeThrough(createClaudeToOpenAIStreamTransform(body.model, {
166
333
  onError: (message) => {
@@ -168,14 +335,28 @@ async function handleOpenAIToAnthropicBridge(args) {
168
335
  },
169
336
  }));
170
337
  const reader = transformed.getReader();
171
- let lifecycleWritten = false;
172
338
  const finishLifecycle = async (status, errorType, errorMessage) => {
173
- if (lifecycleWritten) {
174
- return;
175
- }
176
- lifecycleWritten = true;
339
+ signal.removeEventListener("abort", onAbort);
340
+ clientCapture.finish(status);
177
341
  await writeLifecycle(status, { errorType, errorMessage });
178
342
  };
343
+ const onAbort = () => {
344
+ const outcome = terminalStreamError
345
+ ? resolveStreamCancellationLifecycle(terminalStreamError)
346
+ : {
347
+ status: signal.reason?.name === "TimeoutError" ? 504 : 499,
348
+ errorType: signal.reason?.name === "TimeoutError"
349
+ ? "loopback_timeout"
350
+ : "client_cancelled",
351
+ errorMessage: "Anthropic loopback cancelled",
352
+ };
353
+ void finishLifecycle(outcome.status, outcome.errorType, outcome.errorMessage);
354
+ void withTimeout(reader.cancel(signal.reason), 1000, "Loopback cancellation timed out").catch(() => undefined);
355
+ };
356
+ signal.addEventListener("abort", onAbort, { once: true });
357
+ if (signal.aborted) {
358
+ onAbort();
359
+ }
179
360
  const trackedStream = new ReadableStream({
180
361
  async pull(controller) {
181
362
  try {
@@ -190,6 +371,7 @@ async function handleOpenAIToAnthropicBridge(args) {
190
371
  controller.close();
191
372
  return;
192
373
  }
374
+ clientCapture.append(value);
193
375
  controller.enqueue(value);
194
376
  }
195
377
  catch (error) {
@@ -199,13 +381,15 @@ async function handleOpenAIToAnthropicBridge(args) {
199
381
  }
200
382
  },
201
383
  async cancel(reason) {
202
- try {
203
- await reader.cancel(reason);
204
- }
205
- finally {
206
- const cancellation = resolveStreamCancellationLifecycle(terminalStreamError);
207
- await finishLifecycle(cancellation.status, cancellation.errorType, cancellation.errorMessage);
208
- }
384
+ const outcome = resolveStreamCancellationLifecycle(terminalStreamError);
385
+ // Claim the observed outcome before abort dispatch, but never keep
386
+ // provider work alive while a metadata sink waits for publication.
387
+ const finalizing = finishLifecycle(outcome.status, outcome.errorType, outcome.errorMessage);
388
+ cancellation.abort(reason);
389
+ await Promise.all([
390
+ finalizing,
391
+ withTimeout(reader.cancel(reason), 1000, "Loopback cancellation timed out").catch(() => undefined),
392
+ ]);
209
393
  },
210
394
  });
211
395
  return new Response(trackedStream, {
@@ -217,14 +401,7 @@ async function handleOpenAIToAnthropicBridge(args) {
217
401
  },
218
402
  });
219
403
  }
220
- const claudeJson = (await upstream.json());
221
- await writeLifecycle(200, {
222
- inputTokens: claudeJson.usage?.input_tokens,
223
- outputTokens: claudeJson.usage?.output_tokens,
224
- cacheCreationTokens: claudeJson.usage?.cache_creation_input_tokens,
225
- cacheReadTokens: claudeJson.usage?.cache_read_input_tokens,
226
- });
227
- return convertClaudeToOpenAIResponse(claudeJson, body.model);
404
+ return bridgeJsonResponse(upstream, signal, body.model, writeLifecycle, errorResponse, capture);
228
405
  }
229
406
  // ---------------------------------------------------------------------------
230
407
  // Route factory
@@ -243,7 +420,7 @@ async function handleOpenAIToAnthropicBridge(args) {
243
420
  * headers — to avoid SSRF.
244
421
  * @returns RouteGroup with OpenAI-compatible endpoints.
245
422
  */
246
- export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort = DEFAULT_LOOPBACK_PORT, runtimeConfigProvider) {
423
+ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort = DEFAULT_LOOPBACK_PORT, runtimeConfigProvider, internalDispatch) {
247
424
  return {
248
425
  prefix: `${basePath}/v1`,
249
426
  routes: [
@@ -285,6 +462,7 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
285
462
  targetModel,
286
463
  requestStartTime,
287
464
  loopbackPort,
465
+ internalDispatch,
288
466
  });
289
467
  }
290
468
  catch (err) {
@@ -296,6 +474,20 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
296
474
  const rawMessage = err instanceof Error ? err.message : String(err);
297
475
  const internalDetail = sanitizeForLog(rawMessage);
298
476
  logger.always(`[proxy:openai] anthropic loopback failed: ${internalDetail}`);
477
+ const status = ctx.abortSignal?.aborted
478
+ ? 499
479
+ : err instanceof Error && "status" in err && err.status === 503
480
+ ? 503
481
+ : err instanceof Error && err.name === "TimeoutError"
482
+ ? 504
483
+ : 502;
484
+ if (!isProxyRequestFinalized(ctx.requestId)) {
485
+ recordFinalError(status, undefined, undefined, {
486
+ requestId: ctx.requestId,
487
+ errorType: status === 499 ? "client_cancelled" : "loopback_exception",
488
+ message: internalDetail,
489
+ });
490
+ }
299
491
  await logRequest({
300
492
  timestamp: new Date().toISOString(),
301
493
  requestId: ctx.requestId,
@@ -306,12 +498,12 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
306
498
  toolCount: body.tools?.length ?? 0,
307
499
  account: "",
308
500
  accountType: "openai-bridge",
309
- responseStatus: 502,
501
+ responseStatus: status,
310
502
  responseTimeMs: Date.now() - requestStartTime,
311
- errorType: "loopback_exception",
503
+ errorType: status === 499 ? "client_cancelled" : "loopback_exception",
312
504
  errorMessage: internalDetail,
313
- });
314
- return buildOpenAIErrorResponse(502, "Anthropic loopback failed");
505
+ }).finally(() => releaseProxyRequestAccounting(ctx.requestId));
506
+ return buildOpenAIErrorResponse(status, "Anthropic loopback failed");
315
507
  }
316
508
  }
317
509
  // --- Parse request ---
@@ -842,6 +842,9 @@ export type ProxyTelemetryArgs = {
842
842
  format?: "json" | "text";
843
843
  maxRows?: number;
844
844
  proxyUrl?: string;
845
+ admissionLookbackMinutes?: number;
846
+ requestTimeoutMs?: number;
847
+ ingestionGraceMs?: number;
845
848
  };
846
849
  /** A fallback chain entry (serialisable subset of FallbackEntry) */
847
850
  export type FallbackInfo = {
@@ -175,7 +175,11 @@ export type CodexResponsesRequest = {
175
175
  export type CodexFallbackResult = {
176
176
  text: string;
177
177
  toolCalls: NonNullable<InternalResult["toolCalls"]>;
178
- usage?: NonNullable<InternalResult["usage"]>;
178
+ usage?: NonNullable<InternalResult["usage"]> & {
179
+ /** Numeric serializer compatibility must not imply provider observation. */
180
+ inputTokensObserved?: boolean;
181
+ outputTokensObserved?: boolean;
182
+ };
179
183
  finishReason: "end_turn" | "tool_use";
180
184
  };
181
185
  /** Incremental Claude frames and explicit upstream cancellation. */
@@ -54,6 +54,7 @@ export * from "./providerCatalog.generated.js";
54
54
  export * from "./providers.js";
55
55
  export * from "./proxy.js";
56
56
  export * from "./proxyClient.js";
57
+ export * from "./proxyContext.js";
57
58
  export * from "./rag.js";
58
59
  export * from "./scorer.js";
59
60
  export * from "./sdk.js";
@@ -96,3 +97,4 @@ export * from "./agentNetwork.js";
96
97
  export * from "./localUsage.js";
97
98
  export * from "./dispatch.js";
98
99
  export * from "./proxyRestart.js";
100
+ export * from "./proxyBudget.js";
@@ -58,6 +58,7 @@ export * from "./providerCatalog.generated.js";
58
58
  export * from "./providers.js";
59
59
  export * from "./proxy.js";
60
60
  export * from "./proxyClient.js";
61
+ export * from "./proxyContext.js";
61
62
  export * from "./rag.js";
62
63
  export * from "./scorer.js";
63
64
  export * from "./sdk.js";
@@ -111,3 +112,4 @@ export * from "./localUsage.js";
111
112
  // resolveRequestKind() dispatch-decision types
112
113
  export * from "./dispatch.js";
113
114
  export * from "./proxyRestart.js";
115
+ export * from "./proxyBudget.js";