@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
@@ -52,6 +52,8 @@ export declare function registerRuntimeOutputCeiling(provider: string, model: st
52
52
  * undefined when the serving infrastructure has not advertised one.
53
53
  */
54
54
  export declare function getRuntimeOutputCeiling(provider: string, model?: string): number | undefined;
55
+ /** Remove a model ceiling that discovery no longer advertises as valid. */
56
+ export declare function clearRuntimeOutputCeiling(provider: string, model: string): void;
55
57
  /** Test hook: clear runtime-discovered output ceilings (state is module-global). */
56
58
  export declare function clearRuntimeOutputCeilings(): void;
57
59
  /**
@@ -425,6 +425,10 @@ export function getRuntimeOutputCeiling(provider, model) {
425
425
  }
426
426
  return RUNTIME_OUTPUT_CEILINGS.get(`${provider}:${model}`);
427
427
  }
428
+ /** Remove a model ceiling that discovery no longer advertises as valid. */
429
+ export function clearRuntimeOutputCeiling(provider, model) {
430
+ RUNTIME_OUTPUT_CEILINGS.delete(`${provider}:${model}`);
431
+ }
428
432
  /** Test hook: clear runtime-discovered output ceilings (state is module-global). */
429
433
  export function clearRuntimeOutputCeilings() {
430
434
  RUNTIME_OUTPUT_CEILINGS.clear();
@@ -34,7 +34,7 @@ import { transformToolExecutions } from "../../utils/transformationUtils.js";
34
34
  import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
35
35
  import { composeAbortSignals, composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../../utils/timeout.js";
36
36
  import { raceWithAbort } from "../../utils/async/index.js";
37
- import { resolveToolChoice } from "../../utils/toolChoice.js";
37
+ import { normalizeResolvedToolChoice, resolveToolChoice, } from "../../utils/toolChoice.js";
38
38
  import { emitToolEndFromStepFinish } from "../../utils/toolEndEmitter.js";
39
39
  import { NoOutputGeneratedError } from "../../utils/generationErrors.js";
40
40
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../../utils/noOutputSentinel.js";
@@ -1074,10 +1074,20 @@ export class AnthropicProvider extends BaseProvider {
1074
1074
  if (tools && tools.length === 0) {
1075
1075
  tools = undefined;
1076
1076
  }
1077
- let toolChoice = options.toolChoice
1078
- ? toolChoiceToAnthropic(options.toolChoice.type === "tool"
1079
- ? { type: "tool", toolName: options.toolChoice.toolName }
1080
- : options.toolChoice.type)
1077
+ // The value here has already been through `resolveToolChoice`
1078
+ // upstream in `executeNativeGenerate`, so this layer translates
1079
+ // rather than re-resolves — re-resolving would apply the auto/none
1080
+ // defaulting twice, and `toolsRecord`/`shouldUseTools` are not in
1081
+ // scope inside `getAISDKModel` in any case.
1082
+ //
1083
+ // Only the SHAPE varies, and reading `.type` off it was the bug:
1084
+ // a bare `"required"` has no `.type`, so the translator received
1085
+ // `undefined` and the field was dropped from the request entirely.
1086
+ // Normalisation now lives in the shared util rather than being
1087
+ // re-derived here, so this site cannot drift from its siblings again.
1088
+ const normalisedToolChoice = normalizeResolvedToolChoice(options.toolChoice);
1089
+ let toolChoice = normalisedToolChoice
1090
+ ? toolChoiceToAnthropic(normalisedToolChoice)
1081
1091
  : undefined;
1082
1092
  // JSON/structured output: Anthropic has no response_format — emulate
1083
1093
  // with a forced synthetic tool whose input IS the JSON payload (the
@@ -0,0 +1,3 @@
1
+ import type { ProxyTokenBudgetLease } from "../types/index.js";
2
+ /** Observe consumed bytes only: no tee, eager draining, or unbounded SSE frame. */
3
+ export declare function observeAnthropicBudgetResponse(response: Response, lease: ProxyTokenBudgetLease, onSettlement: () => void, signal?: AbortSignal): Response;
@@ -0,0 +1,153 @@
1
+ import { settleProxyTokenBudget } from "./proxyTokenBudget.js";
2
+ /** Observe consumed bytes only: no tee, eager draining, or unbounded SSE frame. */
3
+ export function observeAnthropicBudgetResponse(response, lease, onSettlement, signal) {
4
+ if (lease.snapshot.scope === "disabled") {
5
+ return response;
6
+ }
7
+ let finishing;
8
+ let terminalSeen = false;
9
+ let finalOutputUsageSeen = false;
10
+ let logicalFailureSeen = false;
11
+ let input;
12
+ let output;
13
+ let cachedRead = 0;
14
+ let cachedWrite = 0;
15
+ let pending = "";
16
+ let oversized = false;
17
+ const isSse = response.headers
18
+ .get("content-type")
19
+ ?.includes("text/event-stream");
20
+ const decoder = new TextDecoder();
21
+ const readUsage = (text) => {
22
+ try {
23
+ const event = JSON.parse(text);
24
+ if (event?.type === "message_stop") {
25
+ terminalSeen = true;
26
+ }
27
+ if (event?.type === "error") {
28
+ logicalFailureSeen = true;
29
+ }
30
+ const usage = event?.usage ?? event?.message?.usage;
31
+ if (!usage || typeof usage !== "object") {
32
+ return;
33
+ }
34
+ const valid = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
35
+ if (valid(usage.input_tokens)) {
36
+ input = Math.max(input ?? 0, usage.input_tokens);
37
+ }
38
+ if (valid(usage.output_tokens)) {
39
+ if (event?.type === "message_delta") {
40
+ finalOutputUsageSeen = true;
41
+ }
42
+ output = Math.max(output ?? 0, usage.output_tokens);
43
+ }
44
+ if (valid(usage.cache_read_input_tokens)) {
45
+ cachedRead = Math.max(cachedRead, usage.cache_read_input_tokens);
46
+ }
47
+ if (valid(usage.cache_creation_input_tokens)) {
48
+ cachedWrite = Math.max(cachedWrite, usage.cache_creation_input_tokens);
49
+ }
50
+ }
51
+ catch {
52
+ /* Incomplete/oversized frames keep the conservative reservation. */
53
+ }
54
+ };
55
+ const observe = (text) => {
56
+ if (!isSse) {
57
+ if (!oversized && pending.length + text.length <= 1024 * 1024) {
58
+ pending += text;
59
+ }
60
+ else {
61
+ oversized = true;
62
+ pending = "";
63
+ }
64
+ return;
65
+ }
66
+ // Split large chunks without retaining a large frame between pulls.
67
+ for (const part of text.split(/(?<=\n)/u)) {
68
+ if (!oversized && pending.length + part.length <= 64 * 1024) {
69
+ pending += part;
70
+ }
71
+ else {
72
+ oversized = true;
73
+ pending = "";
74
+ }
75
+ if (part.endsWith("\n")) {
76
+ if (!oversized && pending.startsWith("data:")) {
77
+ readUsage(pending.slice(5).trim());
78
+ }
79
+ pending = "";
80
+ oversized = false;
81
+ }
82
+ }
83
+ };
84
+ const finish = (normal) => {
85
+ if (finishing) {
86
+ return finishing;
87
+ }
88
+ finishing = (async () => {
89
+ signal?.removeEventListener("abort", onAbort);
90
+ if (normal) {
91
+ observe(decoder.decode());
92
+ if (!oversized && pending) {
93
+ readUsage(isSse && pending.startsWith("data:")
94
+ ? pending.slice(5).trim()
95
+ : pending);
96
+ }
97
+ }
98
+ const actual = normal &&
99
+ !logicalFailureSeen &&
100
+ (!isSse || (terminalSeen && finalOutputUsageSeen)) &&
101
+ input !== undefined &&
102
+ output !== undefined
103
+ ? input + output + cachedRead + cachedWrite
104
+ : undefined;
105
+ try {
106
+ await settleProxyTokenBudget(lease, actual);
107
+ }
108
+ finally {
109
+ onSettlement();
110
+ }
111
+ })();
112
+ return finishing;
113
+ };
114
+ const onAbort = () => {
115
+ void finish(false).catch(() => undefined);
116
+ };
117
+ if (signal?.aborted) {
118
+ onAbort();
119
+ }
120
+ else {
121
+ signal?.addEventListener("abort", onAbort, { once: true });
122
+ }
123
+ if (!response.body) {
124
+ void finish(false).catch(() => undefined);
125
+ return response;
126
+ }
127
+ const reader = response.body.getReader();
128
+ return new Response(new ReadableStream({
129
+ async pull(controller) {
130
+ try {
131
+ const next = await reader.read();
132
+ if (next.done) {
133
+ await finish(true);
134
+ controller.close();
135
+ return;
136
+ }
137
+ observe(decoder.decode(next.value, { stream: true }));
138
+ controller.enqueue(next.value);
139
+ }
140
+ catch (error) {
141
+ await finish(false).catch(() => undefined);
142
+ controller.error(error);
143
+ }
144
+ },
145
+ async cancel(reason) {
146
+ await Promise.allSettled([reader.cancel(reason), finish(false)]);
147
+ },
148
+ }, { highWaterMark: 0 }), {
149
+ status: response.status,
150
+ statusText: response.statusText,
151
+ headers: response.headers,
152
+ });
153
+ }
@@ -10,6 +10,7 @@ export declare function prepareProxyBodyForLogging(body: unknown): {
10
10
  bytes?: number;
11
11
  truncated: boolean;
12
12
  originalBytes?: number;
13
+ unparseableFrames: number;
13
14
  };
14
15
  /**
15
16
  * Expose the same header-redaction policy to replay and metadata
@@ -54,7 +54,7 @@ function redactNestedValue(key, value) {
54
54
  : value;
55
55
  }
56
56
  /** Redact every value under a sensitive key, including objects and arrays. */
57
- function redactBody(body) {
57
+ function redactBody(body, redaction) {
58
58
  if (body === undefined || body === null) {
59
59
  return undefined;
60
60
  }
@@ -83,6 +83,7 @@ function redactBody(body) {
83
83
  redacted = JSON.stringify(JSON.parse(data), redactNestedValue);
84
84
  }
85
85
  catch {
86
+ redaction.unparseableFrames++;
86
87
  redacted = "[UNPARSEABLE DATA REDACTED]";
87
88
  }
88
89
  return [
@@ -95,6 +96,7 @@ function redactBody(body) {
95
96
  // Invalid or truncated structured JSON must not expose an unterminated
96
97
  // credential value that a quoted-string regex cannot safely redact.
97
98
  if (/^\s*[[{]/.test(body)) {
99
+ redaction.unparseableFrames++;
98
100
  return "[UNPARSEABLE DATA REDACTED]";
99
101
  }
100
102
  // Plain-text bodies retain the existing credential-field redaction.
@@ -175,13 +177,15 @@ export function splitUtf8StringByBytes(input, maxBytes) {
175
177
  * ceiling.
176
178
  */
177
179
  function prepareRedactedBody(body, maxBytes = MAX_CAPTURED_BODY_BYTES) {
178
- const redacted = redactBody(body);
180
+ const redaction = { unparseableFrames: 0 };
181
+ const redacted = redactBody(body, redaction);
179
182
  if (redacted === undefined) {
180
- return { truncated: false };
183
+ return { truncated: false, ...redaction };
181
184
  }
182
185
  return {
183
186
  ...truncateUtf8String(redacted, maxBytes),
184
187
  originalBytes: utf8ByteLength(redacted),
188
+ ...redaction,
185
189
  };
186
190
  }
187
191
  /**
@@ -251,11 +255,25 @@ export async function processProxyBodyCapture(entry, logDir) {
251
255
  const headers = redactHeaders(entry.headers);
252
256
  const limit = logDir === null ? MAX_OTEL_CAPTURED_BODY_BYTES : MAX_CAPTURED_BODY_BYTES;
253
257
  const prepared = prepareRedactedBody(entry.body, limit);
254
- prepared.truncated ||= entry.sourceTruncated === true;
258
+ // Structured objects no longer carry their wire byte representation. Do not
259
+ // serialize a second unredacted copy merely to manufacture a byte count.
260
+ const inputRetainedBytes = typeof entry.body === "string" ? utf8ByteLength(entry.body) : undefined;
261
+ const captureMetadata = {
262
+ sourceTruncated: entry.sourceTruncated === true,
263
+ processingTruncated: prepared.truncated,
264
+ inputRetainedBytes,
265
+ inputEncoding: typeof entry.body === "string"
266
+ ? "utf8_text"
267
+ : "structured_object",
268
+ redactionLossy: prepared.unparseableFrames > 0,
269
+ unparseableRedactedFrames: prepared.unparseableFrames,
270
+ };
271
+ prepared.truncated ||= captureMetadata.sourceTruncated;
255
272
  if (logDir === null) {
256
273
  return {
257
274
  headers,
258
275
  stored: {
276
+ ...captureMetadata,
259
277
  redactedBody: prepared.value,
260
278
  redactedBodyBytes: prepared.bytes,
261
279
  bodyTruncated: prepared.truncated,
@@ -281,6 +299,7 @@ export async function processProxyBodyCapture(entry, logDir) {
281
299
  headers,
282
300
  stored: {
283
301
  ...stored,
302
+ ...captureMetadata,
284
303
  bodyCaptureLimitBytes: limit,
285
304
  originalRedactedBodyBytes: prepared.originalBytes,
286
305
  },
@@ -37,6 +37,7 @@ const snapshot = {
37
37
  rejectionReasons: {},
38
38
  };
39
39
  const pending = new Map();
40
+ const publishingSince = new Map();
40
41
  const otelAdmissionWaiters = [];
41
42
  let admissionRetryTimer;
42
43
  /** Atomically reserve the active clone/publication lease. */
@@ -111,7 +112,7 @@ function waitForOtelAdmission(bytes) {
111
112
  drainOtelAdmissionWaiters();
112
113
  }, OTEL_ADMISSION_WAIT_MS);
113
114
  timer.unref?.();
114
- otelAdmissionWaiters.push({ bytes, resolve, timer });
115
+ otelAdmissionWaiters.push({ bytes, queuedAt: Date.now(), resolve, timer });
115
116
  });
116
117
  }
117
118
  function releaseCapture(bytes) {
@@ -276,6 +277,7 @@ export async function captureProxyBody(entry, logDir, consume) {
276
277
  return consume({
277
278
  error,
278
279
  stored: { bodyWriteFailed: true },
280
+ admissionWaitMs: Math.max(0, Date.now() - queuedAt),
279
281
  admission: {
280
282
  limitingResource: bytes > maxEntryBytes
281
283
  ? "entry"
@@ -293,6 +295,7 @@ export async function captureProxyBody(entry, logDir, consume) {
293
295
  },
294
296
  });
295
297
  }
298
+ const admittedAt = Date.now();
296
299
  let current;
297
300
  try {
298
301
  current = getWorker();
@@ -312,14 +315,19 @@ export async function captureProxyBody(entry, logDir, consume) {
312
315
  timer.unref();
313
316
  pending.set(id, {
314
317
  timer,
318
+ startedAt: Date.now(),
315
319
  resolve: (result) => {
316
320
  // Keep the byte/count lease through index writes and OTLP publication,
317
321
  // so completed worker results cannot form an unbounded parent backlog.
322
+ publishingSince.set(id, Date.now());
323
+ result.admissionWaitMs = Math.max(0, admittedAt - queuedAt);
324
+ result.workerQueueWaitMs = Math.max(0, (result.queueWaitMs ?? 0) - result.admissionWaitMs);
318
325
  void consume(result)
319
326
  .catch(() => {
320
327
  result.error ??= "body_capture_publication_failed";
321
328
  })
322
329
  .finally(() => {
330
+ publishingSince.delete(id);
323
331
  releaseCapture(bytes);
324
332
  if (result.error || result.stored.bodyWriteFailed) {
325
333
  snapshot.failed += 1;
@@ -351,7 +359,17 @@ export async function captureProxyBody(entry, logDir, consume) {
351
359
  * retained publication work.
352
360
  */
353
361
  export function getBodyCaptureWorkerSnapshot() {
354
- return { ...snapshot, rejectionReasons: { ...snapshot.rejectionReasons } };
362
+ const now = Date.now();
363
+ const oldestAge = (times) => times.length ? Math.max(0, now - Math.min(...times)) : 0;
364
+ return {
365
+ ...snapshot,
366
+ rejectionReasons: { ...snapshot.rejectionReasons },
367
+ processing: pending.size,
368
+ publishing: publishingSince.size,
369
+ oldestAdmissionWaitMs: oldestAge(otelAdmissionWaiters.map((waiter) => waiter.queuedAt)),
370
+ oldestProcessingMs: oldestAge([...pending.values()].map((task) => task.startedAt)),
371
+ oldestPublicationMs: oldestAge([...publishingSince.values()]),
372
+ };
355
373
  }
356
374
  /** Isolated tests point at a separately executed built worker. */
357
375
  export const __bodyCaptureWorkerTestHooks = {
@@ -10,8 +10,18 @@ import type { ClaudeRequest, CodexFallbackResult, CodexFallbackStream, CodexReas
10
10
  export declare class CodexFallbackResponseError extends Error {
11
11
  readonly status: number;
12
12
  readonly responseBody: string;
13
+ readonly code?: string;
14
+ readonly retryable?: boolean;
13
15
  constructor(status: number, responseBody: string);
14
16
  }
17
+ /** A provider terminal event, preserved through the format adapter. */
18
+ export declare class CodexFallbackStreamError extends Error {
19
+ readonly status: number;
20
+ readonly code: string;
21
+ readonly retryable?: boolean;
22
+ readonly usage?: CodexFallbackResult["usage"];
23
+ constructor(payload: Record<string, unknown>, eventType: string, usage?: CodexFallbackResult["usage"]);
24
+ }
15
25
  /** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
16
26
  export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string, reasoningEffort?: CodexReasoningEffort): CodexResponsesRequest;
17
27
  /**
@@ -7,18 +7,88 @@
7
7
  * rules as a native Codex request.
8
8
  */
9
9
  import { ClaudeStreamSerializer, generateToolUseId } from "./claudeFormat.js";
10
+ import { classifyProxyFailureCode } from "./proxyFailureDetails.js";
10
11
  import { extractCodexUsage } from "./codexUsage.js";
11
12
  import { proxyTokenUsage } from "./proxyTokenUsage.js";
13
+ import { sanitizeForLog } from "../utils/logSanitize.js";
12
14
  export class CodexFallbackResponseError extends Error {
13
15
  status;
14
16
  responseBody;
17
+ code;
18
+ retryable;
15
19
  constructor(status, responseBody) {
16
20
  super(`Codex fallback request returned HTTP ${status}`);
17
21
  this.name = "CodexFallbackResponseError";
18
22
  this.status = status;
19
23
  this.responseBody = responseBody;
24
+ try {
25
+ const payload = JSON.parse(responseBody);
26
+ if (isRecord(payload)) {
27
+ const detail = streamFailureDetails(payload, "http_error");
28
+ this.code = detail.code;
29
+ this.retryable = detail.retryable;
30
+ if (detail.message) {
31
+ this.message = detail.message;
32
+ }
33
+ }
34
+ }
35
+ catch {
36
+ // Non-JSON upstream responses retain the HTTP status without exposing HTML.
37
+ }
20
38
  }
21
39
  }
40
+ /** A provider terminal event, preserved through the format adapter. */
41
+ export class CodexFallbackStreamError extends Error {
42
+ status;
43
+ code;
44
+ retryable;
45
+ usage;
46
+ constructor(payload, eventType, usage) {
47
+ const detail = streamFailureDetails(payload, eventType);
48
+ super(detail.message ?? `Codex fallback stream terminated with ${eventType}`);
49
+ this.name = "CodexFallbackStreamError";
50
+ this.code = detail.code;
51
+ this.status = detail.status;
52
+ this.retryable = detail.retryable;
53
+ const observed = extractCodexUsage(payload);
54
+ this.usage = observed
55
+ ? {
56
+ ...proxyTokenUsage({
57
+ inputTokens: observed.inputTokens,
58
+ outputTokens: observed.outputTokens,
59
+ reasoningTokens: observed.reasoningTokensObserved
60
+ ? observed.reasoningTokens
61
+ : undefined,
62
+ cacheReadTokens: observed.cacheReadTokens,
63
+ cacheCreationTokens: observed.cacheCreationTokens,
64
+ inputIncludesCachedTokens: true,
65
+ }),
66
+ inputTokensObserved: observed.inputTokensObserved,
67
+ outputTokensObserved: observed.outputTokensObserved,
68
+ }
69
+ : usage;
70
+ }
71
+ }
72
+ function streamFailureDetails(payload, eventType) {
73
+ const response = isRecord(payload.response) ? payload.response : payload;
74
+ const error = isRecord(response.error) ? response.error : response;
75
+ const incomplete = isRecord(response.incomplete_details)
76
+ ? response.incomplete_details
77
+ : {};
78
+ const code = sanitizeForLog(asNonEmptyString(error.code) ??
79
+ asNonEmptyString(incomplete.reason) ??
80
+ eventType, 200);
81
+ const known = classifyProxyFailureCode(code);
82
+ return {
83
+ code,
84
+ status: known.status ?? 502,
85
+ retryable: known.retryable ??
86
+ (typeof error.retryable === "boolean" ? error.retryable : undefined),
87
+ message: typeof error.message === "string"
88
+ ? sanitizeForLog(error.message, 500)
89
+ : undefined,
90
+ };
91
+ }
22
92
  function isRecord(value) {
23
93
  return value !== null && typeof value === "object" && !Array.isArray(value);
24
94
  }
@@ -338,7 +408,7 @@ export function parseCodexFallbackSSE(sse) {
338
408
  if (type === "error" ||
339
409
  type === "response.failed" ||
340
410
  type === "response.incomplete") {
341
- throw new Error(`Codex fallback stream terminated with ${type}`);
411
+ throw new CodexFallbackStreamError(payload, type, usage);
342
412
  }
343
413
  if (type === "response.output_text.delta") {
344
414
  if (typeof payload.delta !== "string") {
@@ -368,16 +438,20 @@ export function parseCodexFallbackSSE(sse) {
368
438
  }
369
439
  const parsedUsage = extractCodexUsage(payload);
370
440
  if (parsedUsage) {
371
- usage = proxyTokenUsage({
372
- inputTokens: parsedUsage.inputTokens,
373
- outputTokens: parsedUsage.outputTokens,
374
- reasoningTokens: parsedUsage.reasoningTokensObserved
375
- ? parsedUsage.reasoningTokens
376
- : undefined,
377
- cacheReadTokens: parsedUsage.cacheReadTokens,
378
- cacheCreationTokens: parsedUsage.cacheCreationTokens,
379
- inputIncludesCachedTokens: true,
380
- });
441
+ usage = {
442
+ ...proxyTokenUsage({
443
+ inputTokens: parsedUsage.inputTokens,
444
+ outputTokens: parsedUsage.outputTokens,
445
+ reasoningTokens: parsedUsage.reasoningTokensObserved
446
+ ? parsedUsage.reasoningTokens
447
+ : undefined,
448
+ cacheReadTokens: parsedUsage.cacheReadTokens,
449
+ cacheCreationTokens: parsedUsage.cacheCreationTokens,
450
+ inputIncludesCachedTokens: true,
451
+ }),
452
+ inputTokensObserved: parsedUsage.inputTokensObserved,
453
+ outputTokensObserved: parsedUsage.outputTokensObserved,
454
+ };
381
455
  }
382
456
  textFromResponse = outputTextFromResponse(payload);
383
457
  }
@@ -387,7 +461,10 @@ export function parseCodexFallbackSSE(sse) {
387
461
  const text = textFromDeltas || textFromCompletedItems || textFromResponse;
388
462
  const resolvedToolCalls = [...toolCalls.values()];
389
463
  if (!text && resolvedToolCalls.length === 0) {
390
- throw new Error("Codex fallback returned no content or tool calls");
464
+ throw new CodexFallbackStreamError({
465
+ code: "empty_response",
466
+ message: "Codex fallback returned no content or tool calls",
467
+ }, "empty_response", usage);
391
468
  }
392
469
  return {
393
470
  text,
@@ -498,7 +575,7 @@ export async function createCodexFallbackStream(response, model) {
498
575
  if (type === "error" ||
499
576
  type === "response.failed" ||
500
577
  type === "response.incomplete") {
501
- throw new Error(`Codex fallback stream terminated with ${type}`);
578
+ throw new CodexFallbackStreamError(payload, type, usage);
502
579
  }
503
580
  const index = typeof payload.output_index === "number" ? payload.output_index : 0;
504
581
  if (type === "response.output_text.delta") {
@@ -520,16 +597,20 @@ export async function createCodexFallbackStream(response, model) {
520
597
  completed = true;
521
598
  const parsedUsage = extractCodexUsage(payload);
522
599
  if (parsedUsage) {
523
- usage = proxyTokenUsage({
524
- inputTokens: parsedUsage.inputTokens,
525
- outputTokens: parsedUsage.outputTokens,
526
- reasoningTokens: parsedUsage.reasoningTokensObserved
527
- ? parsedUsage.reasoningTokens
528
- : undefined,
529
- cacheReadTokens: parsedUsage.cacheReadTokens,
530
- cacheCreationTokens: parsedUsage.cacheCreationTokens,
531
- inputIncludesCachedTokens: true,
532
- });
600
+ usage = {
601
+ ...proxyTokenUsage({
602
+ inputTokens: parsedUsage.inputTokens,
603
+ outputTokens: parsedUsage.outputTokens,
604
+ reasoningTokens: parsedUsage.reasoningTokensObserved
605
+ ? parsedUsage.reasoningTokens
606
+ : undefined,
607
+ cacheReadTokens: parsedUsage.cacheReadTokens,
608
+ cacheCreationTokens: parsedUsage.cacheCreationTokens,
609
+ inputIncludesCachedTokens: true,
610
+ }),
611
+ inputTokensObserved: parsedUsage.inputTokensObserved,
612
+ outputTokensObserved: parsedUsage.outputTokensObserved,
613
+ };
533
614
  }
534
615
  const responseBody = payload.response;
535
616
  if (isRecord(responseBody) && Array.isArray(responseBody.output)) {
@@ -573,7 +654,10 @@ export async function createCodexFallbackStream(response, model) {
573
654
  throw new Error("Codex fallback stream ended before response.completed");
574
655
  }
575
656
  if (textLength === 0 && toolCalls.size === 0) {
576
- throw new Error("Codex fallback returned no content or tool calls");
657
+ throw new CodexFallbackStreamError({
658
+ code: "empty_response",
659
+ message: "Codex fallback returned no content or tool calls",
660
+ }, "empty_response", usage);
577
661
  }
578
662
  const finishReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
579
663
  yield* serializer.finish(usage?.output, finishReason, {
@@ -104,6 +104,8 @@ export function extractCodexUsage(payload) {
104
104
  return {
105
105
  inputTokens: nonNegativeInt(input),
106
106
  outputTokens: nonNegativeInt(output),
107
+ inputTokensObserved: typeof input === "number" && Number.isFinite(input) && input >= 0,
108
+ outputTokensObserved: typeof output === "number" && Number.isFinite(output) && output >= 0,
107
109
  cacheReadTokens: nonNegativeInt(inputDetails?.cached_tokens),
108
110
  cacheCreationTokens: nonNegativeInt(inputDetails?.cache_write_tokens),
109
111
  reasoningTokens: nonNegativeInt(outputDetails?.reasoning_tokens),
@@ -1,4 +1,4 @@
1
- import type { GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
1
+ import type { ProxyServiceInstallSettings, ProxyPackageSelection, ProxyStagedInstallOptions, GlobalInstallerExecFile, GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
2
2
  /** Resolve a package manager that can update the installation currently running. */
3
3
  export declare function resolveGlobalInstaller(options?: ResolveGlobalInstallerOptions): GlobalInstallerResolution;
4
4
  export declare function getGlobalInstallArgs(kind: GlobalInstallerKind, packageSpec: string): string[];
@@ -17,3 +17,34 @@ export declare function isTransientInstallFailure(error: unknown): boolean;
17
17
  */
18
18
  export declare function validateInstalledVersion(options: ValidateInstalledVersionOptions): Promise<InstalledVersionValidation>;
19
19
  export declare function describeInstallFailure(error: unknown): string;
20
+ /** Read package metadata without importing the CLI/provider/telemetry graph. */
21
+ export declare function inspectProxyPackage(entryScript: string, nodePath?: string): ProxyPackageSelection;
22
+ export declare function readProxyPackageSelection(packagesDir: string, previous?: boolean): ProxyPackageSelection | null;
23
+ /** Resolve immutable selections before consulting an older mutable global entry. */
24
+ export declare function resolveProxyWorkerPackage(options: {
25
+ packagesDir: string;
26
+ entryScript: string;
27
+ expectedVersion: string;
28
+ }): ProxyPackageSelection;
29
+ /** Persist only package selection; runtime env, config and launchd settings are untouched. */
30
+ export declare function selectProxyPackage(packagesDir: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
31
+ /** No startup probes: IPC descriptors and OTel initialization belong to the real process. */
32
+ export declare function writeProxyPackageLauncher(path: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
33
+ /** Install in a private tree, then publish the immutable directory after validation. */
34
+ export declare function installStagedProxyPackage(options: ProxyStagedInstallOptions): Promise<ProxyPackageSelection>;
35
+ /** Only operator runtime settings survive reinstall, never worker IPC identity. */
36
+ export declare function isProxyServiceEnvironmentKey(name: string): boolean;
37
+ /** Parse plutil JSON without exposing credentials or invoking the saved launcher. */
38
+ export declare function parseProxyServiceInstallSettings(value: unknown): ProxyServiceInstallSettings;
39
+ /** Distinguish an absent launchd job from permission failures without logging plist/env data. */
40
+ export declare function probeProxyLaunchdPresence(options: {
41
+ label: string;
42
+ uid: number;
43
+ execFileSync?: GlobalInstallerExecFile;
44
+ }): "loaded" | "not_loaded" | "unknown";
45
+ /** Read-only guard: callers must run this before every reinstall mutation. */
46
+ export declare function assertProxyServiceInstallIdle(options: {
47
+ pids: number[];
48
+ getProcessStatus: (pid: number) => "running" | "not_running" | "unknown";
49
+ launchdPresence: "loaded" | "not_loaded" | "unknown";
50
+ }): void;