@juspay/neurolink 12.12.3 → 12.12.5

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.
@@ -66,7 +66,6 @@ const gatedShareRequests = new WeakSet();
66
66
  const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
67
67
  /** How long shutdown waits on the share listener before moving on. */
68
68
  const SHARE_LISTENER_CLOSE_TIMEOUT_MS = 10_000;
69
- const PROXY_STATUS_TOKEN_READ_TIMEOUT_MS = 2_000;
70
69
  const PROXY_STATUS_RECONCILE_TIMEOUT_MS = 750;
71
70
  const PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS = 750;
72
71
  // Allowed drift between a pid's OS-reported start time and the persisted
@@ -1164,6 +1163,10 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
1164
1163
  });
1165
1164
  },
1166
1165
  onTerminal: ({ outcome, observedBodyBytes, responseChunks }) => {
1166
+ if (outcome === "completed" &&
1167
+ metadata.terminalErrorType === "stream_error") {
1168
+ outcome = "stream_error";
1169
+ }
1167
1170
  logProxyLifecycleEvent({
1168
1171
  event: "request_terminal",
1169
1172
  requestId: metadata.requestId,
@@ -1500,6 +1503,7 @@ export async function createProxyStartApp(params) {
1500
1503
  shareOutcome.release();
1501
1504
  }
1502
1505
  }
1506
+ const requestAbortController = new AbortController();
1503
1507
  const ctx = {
1504
1508
  requestId: metadata?.requestId ?? crypto.randomUUID(),
1505
1509
  method: c.req.method,
@@ -1509,6 +1513,10 @@ export async function createProxyStartApp(params) {
1509
1513
  params: c.req.param(),
1510
1514
  body,
1511
1515
  rawBody,
1516
+ abortSignal: AbortSignal.any([
1517
+ c.req.raw.signal,
1518
+ requestAbortController.signal,
1519
+ ]),
1512
1520
  // The proxy runtime exposes only the structural slice of NeuroLink the
1513
1521
  // routes use; narrow (overlap-checked) to the full class for ServerContext.
1514
1522
  neurolink: params.neurolink,
@@ -1555,39 +1563,42 @@ export async function createProxyStartApp(params) {
1555
1563
  Symbol.asyncIterator in Object(result)) {
1556
1564
  const iterator = result[Symbol.asyncIterator]();
1557
1565
  let cancelled = false;
1566
+ const encoder = new TextEncoder();
1558
1567
  const responseStream = new ReadableStream({
1559
- async start(controller) {
1568
+ async pull(controller) {
1569
+ if (cancelled) {
1570
+ return;
1571
+ }
1560
1572
  try {
1561
- while (!cancelled) {
1562
- const { value, done } = await iterator.next();
1563
- if (done) {
1564
- break;
1565
- }
1566
- controller.enqueue(new TextEncoder().encode(value));
1573
+ const next = await iterator.next();
1574
+ if (cancelled) {
1575
+ return;
1576
+ }
1577
+ if (next.done) {
1578
+ controller.close();
1579
+ }
1580
+ else {
1581
+ controller.enqueue(encoder.encode(next.value));
1567
1582
  }
1568
- controller.close();
1569
1583
  }
1570
1584
  catch (streamErr) {
1571
1585
  if (cancelled) {
1572
- controller.close();
1573
1586
  return;
1574
1587
  }
1575
- const errMsg = streamErr instanceof Error
1576
- ? streamErr.message
1577
- : String(streamErr);
1578
- const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Stream interrupted: ${errMsg}` } })}\n\n`;
1579
- try {
1580
- controller.enqueue(new TextEncoder().encode(errorEvent));
1581
- }
1582
- catch {
1583
- // Controller already errored — ignore
1588
+ if (metadata) {
1589
+ metadata.terminalErrorType = "stream_error";
1584
1590
  }
1591
+ logger.debug("[proxy] response stream interrupted", {
1592
+ error: streamErr,
1593
+ });
1594
+ controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: "Stream interrupted" } })}\n\n`));
1585
1595
  controller.close();
1586
1596
  }
1587
1597
  },
1588
- async cancel() {
1598
+ async cancel(reason) {
1589
1599
  cancelled = true;
1590
- await iterator.return?.();
1600
+ requestAbortController.abort(reason);
1601
+ await withTimeout(Promise.resolve(iterator.return?.()), 1000, "[proxy] response cancellation timed out").catch(() => undefined);
1591
1602
  },
1592
1603
  });
1593
1604
  return new Response(responseStream, {
@@ -1697,48 +1708,24 @@ export async function createProxyStartApp(params) {
1697
1708
  let accountInventoryLoaded = false;
1698
1709
  try {
1699
1710
  const { tokenStore } = await import("../../auth/tokenStore.js");
1700
- const [anthropicKeys, codexKeys] = await withTimeout(Promise.all([
1701
- tokenStore.listByPrefix("anthropic:"),
1702
- tokenStore.listByPrefix("codex:"),
1703
- ]), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account enumeration timed out");
1704
- for (const key of anthropicKeys) {
1705
- storedAnthropicAccountKeys.add(normalizeAnthropicAccountKey(key));
1706
- }
1707
- for (const key of codexKeys) {
1708
- storedCodexAccountKeys.add(key);
1709
- }
1710
- // Once account names are known, preserve them even when optional token
1711
- // metadata is slow. That keeps the status table useful and avoids
1712
- // incorrectly presenting known accounts as removed.
1713
- accountInventoryLoaded = true;
1714
- const storedKeys = [...anthropicKeys, ...codexKeys];
1715
- const inventory = await withTimeout((async () => {
1716
- const tokenExpirations = await Promise.all(storedKeys.map(async (key) => {
1717
- try {
1718
- const tokens = await withTimeout(tokenStore.peekTokens(key), PROXY_STATUS_TOKEN_READ_TIMEOUT_MS, "[proxy] /status token inspection timed out");
1719
- return tokens ? [key, tokens.expiresAt] : undefined;
1720
- }
1721
- catch (error) {
1722
- logger.debug(`[proxy] /status: failed to inspect token metadata for ${normalizeAnthropicAccountKey(key)}: ${error instanceof Error ? error.message : String(error)}`);
1723
- return undefined;
1724
- }
1725
- }));
1726
- const disabledKeys = await tokenStore.listDisabled();
1727
- return { tokenExpirations, disabledKeys };
1728
- })(), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account metadata timed out");
1729
- for (const expiration of inventory.tokenExpirations) {
1730
- if (expiration) {
1731
- const key = expiration[0].startsWith("anthropic:")
1732
- ? normalizeAnthropicAccountKey(expiration[0])
1733
- : expiration[0];
1734
- storedAccountExpirations.set(key, expiration[1]);
1711
+ const inventory = await withTimeout(tokenStore.getProviderSnapshot(), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account inspection timed out");
1712
+ for (const [storedKey, entry] of Object.entries(inventory)) {
1713
+ if (!storedKey.startsWith("anthropic:") &&
1714
+ !storedKey.startsWith("codex:")) {
1715
+ continue;
1716
+ }
1717
+ const key = storedKey.startsWith("anthropic:")
1718
+ ? normalizeAnthropicAccountKey(storedKey)
1719
+ : storedKey;
1720
+ (key.startsWith("anthropic:")
1721
+ ? storedAnthropicAccountKeys
1722
+ : storedCodexAccountKeys).add(key);
1723
+ storedAccountExpirations.set(key, entry.tokens.expiresAt);
1724
+ if (entry.disabled) {
1725
+ disabledProviderAccountKeys.add(key);
1735
1726
  }
1736
1727
  }
1737
- for (const key of inventory.disabledKeys) {
1738
- disabledProviderAccountKeys.add(key.startsWith("anthropic:")
1739
- ? normalizeAnthropicAccountKey(key)
1740
- : key);
1741
- }
1728
+ accountInventoryLoaded = true;
1742
1729
  }
1743
1730
  catch (err) {
1744
1731
  logger.debug(`[proxy] /status: failed to resolve account cooldown labels: ${err instanceof Error ? err.message : String(err)}`);
@@ -3090,7 +3077,15 @@ function printStatusStats(stats) {
3090
3077
  }
3091
3078
  }
3092
3079
  }
3093
- if (stats.accounts?.length) {
3080
+ const historical = (stats.accounts ?? []).filter((account) => account.status === "unattributed");
3081
+ const accounts = (stats.accounts ?? []).filter((account) => account.status !== "unattributed");
3082
+ if (historical.length > 0) {
3083
+ const sum = (field) => historical.reduce((total, account) => total + (account[field] ?? 0), 0);
3084
+ console.info(`\n Historical usage (provider unknown; included in totals):`);
3085
+ console.info(` ${historical.length} legacy records: ${sum("attempts")} attempts, ${sum("success")} success, ${sum("errors")} errors, ${sum("rateLimits")} rate-limited attempts`);
3086
+ console.info(" Per-record history remains available with --format json.");
3087
+ }
3088
+ if (accounts.length > 0) {
3094
3089
  console.info(`\n Accounts:`);
3095
3090
  const headers = [
3096
3091
  "ACCOUNT",
@@ -3101,8 +3096,8 @@ function printStatusStats(stats) {
3101
3096
  "RL",
3102
3097
  "STATUS",
3103
3098
  ];
3104
- const rows = stats.accounts.map((account) => [
3105
- account.label,
3099
+ const rows = accounts.map((account) => [
3100
+ account.key ?? account.label,
3106
3101
  account.type,
3107
3102
  String(account.attempts ?? account.requests ?? 0),
3108
3103
  String(account.success ?? 0),
@@ -3267,7 +3262,7 @@ export const proxyStatusCommand = {
3267
3262
  });
3268
3263
  if (statusResp.ok) {
3269
3264
  const statusData = (await statusResp.json());
3270
- liveStats = statusData.stats;
3265
+ liveStats = statusData.stats ?? null;
3271
3266
  liveConfig = statusData.config;
3272
3267
  status.workerVersion =
3273
3268
  typeof statusData.version === "string"
@@ -3342,6 +3337,9 @@ export const proxyStatusCommand = {
3342
3337
  else if (status.rolling?.draining.length) {
3343
3338
  logger.always(` ${chalk.bold("Handoff:")} ${chalk.cyan(`${status.rolling.draining.length} previous worker(s) draining`)}`);
3344
3339
  }
3340
+ if (status.rolling) {
3341
+ logger.always(` ${chalk.bold("Socket handoff:")} ${status.rolling.pendingTransfers ?? "unknown"} pending, ${status.rolling.queuedSockets} queued; ${status.rolling.rejectedSockets} rejected, ${status.rolling.failedTransfers} failed transfers since supervisor start`);
3342
+ }
3345
3343
  if (status.deferredUpdate) {
3346
3344
  const active = status.deferredUpdate.activeRequests === null
3347
3345
  ? "unknown activity"
@@ -3398,21 +3396,8 @@ export const proxyStatusCommand = {
3398
3396
  logger.always("");
3399
3397
  logger.always(chalk.gray(" (Could not reach proxy for live status)"));
3400
3398
  }
3401
- // Try to get detailed stats
3402
- try {
3403
- const liveUrl = status.url;
3404
- const statusResp = await fetch(`${liveUrl}/status`, {
3405
- signal: AbortSignal.timeout(2_000),
3406
- });
3407
- if (statusResp.ok) {
3408
- const statusData = (await statusResp.json());
3409
- if (statusData.stats) {
3410
- printStatusStats(statusData.stats);
3411
- }
3412
- }
3413
- }
3414
- catch {
3415
- /* non-fatal */
3399
+ if (liveStats) {
3400
+ printStatusStats(liveStats);
3416
3401
  }
3417
3402
  }
3418
3403
  else {
@@ -1,4 +1,4 @@
1
- import type { ClaudeErrorResponse, ClaudeRequest, ClaudeResponse, ContentBlockType, InternalResult, ParsedClaudeRequest, StreamLifecycleState } from "../types/index.js";
1
+ import type { ClaudeErrorResponse, ClaudeRequest, ClaudeResponse, ContentBlockType, InternalResult, ParsedClaudeRequest, SSEMessageDelta, StreamLifecycleState } from "../types/index.js";
2
2
  /** Generate a unique message id in the Claude format. */
3
3
  export declare function generateMessageId(): string;
4
4
  /** Generate a Claude-format tool use ID (`toolu_` + 24 random chars). */
@@ -121,7 +121,7 @@ export declare class ClaudeStreamSerializer {
121
121
  /**
122
122
  * Finalize the stream: content_block_stop, message_delta, message_stop.
123
123
  */
124
- finish(outputTokens?: number, finishReason?: string): Generator<string>;
124
+ finish(outputTokens?: number, finishReason?: string, finalUsage?: Partial<SSEMessageDelta["usage"]>): Generator<string>;
125
125
  /**
126
126
  * Emit an error event. Transitions to terminal ERROR state.
127
127
  */
@@ -582,7 +582,7 @@ export class ClaudeStreamSerializer {
582
582
  /**
583
583
  * Finalize the stream: content_block_stop, message_delta, message_stop.
584
584
  */
585
- *finish(outputTokens, finishReason) {
585
+ *finish(outputTokens, finishReason, finalUsage) {
586
586
  // If we never started (empty response), start first
587
587
  if (this.state === "idle") {
588
588
  yield* this.ensureMessageStarted();
@@ -603,7 +603,7 @@ export class ClaudeStreamSerializer {
603
603
  stop_reason: mapStopReason(resolvedFinishReason),
604
604
  stop_sequence: null,
605
605
  },
606
- usage: { output_tokens: this.outputTokens },
606
+ usage: { ...finalUsage, output_tokens: this.outputTokens },
607
607
  };
608
608
  yield formatSSE("message_delta", messageDelta);
609
609
  // message_stop
@@ -6,14 +6,14 @@
6
6
  * in the native Codex proxy handler so fallback traffic follows the same pool
7
7
  * rules as a native Codex request.
8
8
  */
9
- import type { ClaudeRequest, CodexFallbackResult, CodexResponsesRequest } from "../types/index.js";
9
+ import type { ClaudeRequest, CodexFallbackResult, CodexFallbackStream, CodexReasoningEffort, CodexResponsesRequest } from "../types/index.js";
10
10
  export declare class CodexFallbackResponseError extends Error {
11
11
  readonly status: number;
12
12
  readonly responseBody: string;
13
13
  constructor(status: number, responseBody: string);
14
14
  }
15
15
  /** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
16
- export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string): CodexResponsesRequest;
16
+ export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string, reasoningEffort?: CodexReasoningEffort): CodexResponsesRequest;
17
17
  /**
18
18
  * Parse a complete Codex Responses SSE stream before emitting Claude output.
19
19
  *
@@ -24,3 +24,9 @@ export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model:
24
24
  export declare function parseCodexFallbackSSE(sse: string): CodexFallbackResult;
25
25
  /** Consume and validate a native Codex response before producing Claude output. */
26
26
  export declare function consumeCodexFallbackResponse(response: Response): Promise<CodexFallbackResult>;
27
+ /**
28
+ * Translate a Codex stream as events arrive. A completed tool call is emitted
29
+ * once its arguments validate; text does not wait for response.completed.
30
+ * The caller owns error framing and must never retry after emitting output.
31
+ */
32
+ export declare function createCodexFallbackStream(response: Response, model: string): Promise<CodexFallbackStream>;
@@ -6,6 +6,7 @@
6
6
  * in the native Codex proxy handler so fallback traffic follows the same pool
7
7
  * rules as a native Codex request.
8
8
  */
9
+ import { ClaudeStreamSerializer, generateToolUseId } from "./claudeFormat.js";
9
10
  import { extractCodexUsage } from "./codexUsage.js";
10
11
  export class CodexFallbackResponseError extends Error {
11
12
  status;
@@ -131,7 +132,7 @@ function convertClaudeMessage(role, content) {
131
132
  return input;
132
133
  }
133
134
  /** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
134
- export function convertClaudeRequestToCodex(body, model) {
135
+ export function convertClaudeRequestToCodex(body, model, reasoningEffort) {
135
136
  const input = body.messages.flatMap((message) => convertClaudeMessage(message.role, message.content));
136
137
  const request = {
137
138
  model,
@@ -139,6 +140,9 @@ export function convertClaudeRequestToCodex(body, model) {
139
140
  stream: true,
140
141
  // ChatGPT's backend rejects requests unless this is explicitly false.
141
142
  store: false,
143
+ ...(reasoningEffort !== undefined
144
+ ? { reasoning: { effort: reasoningEffort } }
145
+ : {}),
142
146
  };
143
147
  const instructions = buildSystemInstructions(body);
144
148
  if (instructions) {
@@ -369,3 +373,183 @@ export async function consumeCodexFallbackResponse(response) {
369
373
  }
370
374
  return parseCodexFallbackSSE(await response.text());
371
375
  }
376
+ /**
377
+ * Translate a Codex stream as events arrive. A completed tool call is emitted
378
+ * once its arguments validate; text does not wait for response.completed.
379
+ * The caller owns error framing and must never retry after emitting output.
380
+ */
381
+ export async function createCodexFallbackStream(response, model) {
382
+ if (!response.ok) {
383
+ throw new CodexFallbackResponseError(response.status, await response.text().catch(() => ""));
384
+ }
385
+ if (!response.body ||
386
+ !(response.headers.get("content-type") ?? "")
387
+ .toLowerCase()
388
+ .includes("text/event-stream")) {
389
+ await response.body?.cancel().catch(() => undefined);
390
+ throw new Error("Codex fallback returned a non-SSE or empty response");
391
+ }
392
+ const reader = response.body.getReader();
393
+ let cancellation;
394
+ const cancel = (reason) => {
395
+ cancellation ??= reader
396
+ .cancel(reason)
397
+ .catch(() => undefined)
398
+ .finally(() => reader.releaseLock());
399
+ return cancellation;
400
+ };
401
+ async function* frames() {
402
+ const serializer = new ClaudeStreamSerializer(model);
403
+ const decoder = new TextDecoder();
404
+ const toolCalls = new Map();
405
+ const emittedTools = new Set();
406
+ const emittedTextItems = new Set();
407
+ const textParts = [];
408
+ let textLength = 0;
409
+ let toolChars = 0;
410
+ let carry = "";
411
+ let searchFrom = 0;
412
+ let completed = false;
413
+ let usage;
414
+ const maxChars = 16 * 1024 * 1024;
415
+ function* text(value, index) {
416
+ if (!value) {
417
+ return;
418
+ }
419
+ textLength += value.length;
420
+ if (textLength > maxChars) {
421
+ throw new Error("Codex fallback output exceeded the stream limit");
422
+ }
423
+ textParts.push(value);
424
+ emittedTextItems.add(index);
425
+ yield* serializer.pushDelta(value);
426
+ }
427
+ function* item(value, index) {
428
+ if (!isRecord(value)) {
429
+ throw new Error("Codex fallback output item is malformed");
430
+ }
431
+ if (value.type === "function_call") {
432
+ const id = asNonEmptyString(value.call_id);
433
+ if (!id || !emittedTools.has(id)) {
434
+ toolChars += JSON.stringify(value).length;
435
+ if (toolChars > maxChars || toolCalls.size >= 4096) {
436
+ throw new Error("Codex fallback tools exceeded the stream limit");
437
+ }
438
+ addFunctionCall(value, toolCalls);
439
+ const call = id ? toolCalls.get(id) : undefined;
440
+ if (id && call) {
441
+ emittedTools.add(id);
442
+ yield* serializer.pushToolUse(generateToolUseId(), call.toolName, call.args);
443
+ }
444
+ }
445
+ }
446
+ if (!emittedTextItems.has(index)) {
447
+ yield* text(outputTextFromItem(value), index);
448
+ }
449
+ }
450
+ function* event(frame) {
451
+ for (const { event: eventName, payload } of parseSSEPayloads(frame)) {
452
+ const type = asNonEmptyString(payload.type) ?? eventName;
453
+ if (!type) {
454
+ throw new Error("Codex fallback stream event is missing a type");
455
+ }
456
+ if (completed) {
457
+ throw new Error("Codex fallback stream emitted events after completion");
458
+ }
459
+ if (type === "error" ||
460
+ type === "response.failed" ||
461
+ type === "response.incomplete") {
462
+ throw new Error(`Codex fallback stream terminated with ${type}`);
463
+ }
464
+ const index = typeof payload.output_index === "number" ? payload.output_index : 0;
465
+ if (type === "response.output_text.delta") {
466
+ if (typeof payload.delta !== "string") {
467
+ throw new Error("Codex fallback text delta is malformed");
468
+ }
469
+ yield* text(payload.delta, index);
470
+ }
471
+ else if (type === "response.output_item.done") {
472
+ if (!isRecord(payload.item)) {
473
+ throw new Error("Codex fallback output item is malformed");
474
+ }
475
+ yield* item(payload.item, index);
476
+ }
477
+ else if (type === "response.completed") {
478
+ if (responseStatus(payload) !== "completed") {
479
+ throw new Error("Codex fallback response did not complete");
480
+ }
481
+ completed = true;
482
+ const parsedUsage = extractCodexUsage(payload);
483
+ if (parsedUsage) {
484
+ usage = {
485
+ input: parsedUsage.inputTokens,
486
+ output: parsedUsage.outputTokens,
487
+ total: parsedUsage.inputTokens + parsedUsage.outputTokens,
488
+ cacheReadTokens: parsedUsage.cacheReadTokens,
489
+ cacheCreationTokens: parsedUsage.cacheCreationTokens,
490
+ };
491
+ }
492
+ const responseBody = payload.response;
493
+ if (isRecord(responseBody) && Array.isArray(responseBody.output)) {
494
+ for (const [i, output] of responseBody.output.entries()) {
495
+ yield* item(output, i);
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
501
+ try {
502
+ yield* serializer.start();
503
+ while (true) {
504
+ const chunk = await reader.read();
505
+ carry += decoder.decode(chunk.value, { stream: !chunk.done });
506
+ // Search only new bytes plus the boundary overlap. Long tool payloads
507
+ // split over many chunks must not rescan their accumulated prefix.
508
+ const boundary = /\r?\n\r?\n/g;
509
+ boundary.lastIndex = searchFrom;
510
+ let match;
511
+ while ((match = boundary.exec(carry)) !== null) {
512
+ if (match.index > maxChars) {
513
+ throw new Error("Codex fallback event exceeded the stream limit");
514
+ }
515
+ yield* event(carry.slice(0, match.index));
516
+ carry = carry.slice(match.index + match[0].length);
517
+ boundary.lastIndex = 0;
518
+ }
519
+ if (carry.length > maxChars) {
520
+ throw new Error("Codex fallback event exceeded the stream limit");
521
+ }
522
+ searchFrom = Math.max(0, carry.length - 3);
523
+ if (chunk.done) {
524
+ break;
525
+ }
526
+ }
527
+ if (carry.trim()) {
528
+ yield* event(carry);
529
+ }
530
+ if (!completed) {
531
+ throw new Error("Codex fallback stream ended before response.completed");
532
+ }
533
+ if (textLength === 0 && toolCalls.size === 0) {
534
+ throw new Error("Codex fallback returned no content or tool calls");
535
+ }
536
+ const finishReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
537
+ yield* serializer.finish(usage?.output, finishReason, {
538
+ input_tokens: usage?.input,
539
+ cache_read_input_tokens: usage?.cacheReadTokens,
540
+ cache_creation_input_tokens: usage?.cacheCreationTokens,
541
+ });
542
+ return {
543
+ text: textParts.join(""),
544
+ toolCalls: [...toolCalls.values()],
545
+ finishReason,
546
+ ...(usage ? { usage } : {}),
547
+ };
548
+ }
549
+ finally {
550
+ await cancel();
551
+ reader.releaseLock();
552
+ }
553
+ }
554
+ return { frames: frames(), cancel };
555
+ }
@@ -189,6 +189,18 @@ function applyAccountDefaults(account) {
189
189
  metadata: account.metadata,
190
190
  };
191
191
  }
192
+ const CODEX_REASONING_EFFORTS = [
193
+ "none",
194
+ "minimal",
195
+ "low",
196
+ "medium",
197
+ "high",
198
+ "xhigh",
199
+ "max",
200
+ ];
201
+ function isCodexReasoningEffort(value) {
202
+ return CODEX_REASONING_EFFORTS.some((effort) => effort === value);
203
+ }
192
204
  /**
193
205
  * Validate the shape of a parsed proxy config.
194
206
  * Returns an array of human-readable error strings (empty = valid).
@@ -215,6 +227,28 @@ export function validateProxyConfig(config) {
215
227
  }
216
228
  if (hasRouting) {
217
229
  const routing = cfg.routing;
230
+ const rawFallback = routing["fallback-chain"] ?? routing.fallbackChain;
231
+ if (Array.isArray(rawFallback)) {
232
+ rawFallback.forEach((entry, index) => {
233
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
234
+ return;
235
+ }
236
+ const fallback = entry;
237
+ const effort = fallback["reasoning-effort"] !== undefined
238
+ ? fallback["reasoning-effort"]
239
+ : fallback.reasoningEffort;
240
+ if (effort === undefined) {
241
+ return;
242
+ }
243
+ const field = `routing.fallback-chain[${index}].reasoning-effort`;
244
+ if (!isCodexReasoningEffort(effort)) {
245
+ errors.push(`${field} must be one of: ${CODEX_REASONING_EFFORTS.join(", ")}`);
246
+ }
247
+ if (String(fallback.provider ?? "").trim() !== "codex") {
248
+ errors.push(`${field} is only supported for provider codex`);
249
+ }
250
+ });
251
+ }
218
252
  const rawAccountAllowlist = routing["account-allowlist"] ?? routing.accountAllowlist;
219
253
  if (rawAccountAllowlist !== undefined) {
220
254
  if (!Array.isArray(rawAccountAllowlist)) {
@@ -350,7 +384,7 @@ function warnPlaintextApiKeys(accounts) {
350
384
  * Extracts:
351
385
  * - `strategy` ("round-robin" | "fill-first")
352
386
  * - `model-mappings` / `modelMappings` — array of {from, to, provider}
353
- * - `fallback-chain` / `fallbackChain` — array of {provider, model}
387
+ * - `fallback-chain` / `fallbackChain` — array of {provider, model, reasoningEffort?}
354
388
  * - `auto-fallback` / `autoFallback` — opt in to an unspecified provider
355
389
  * - `max-inflight-per-account` / `maxInflightPerAccount` — concurrency cap
356
390
  * - `passthroughModels` / `passthrough-models` — array of model IDs
@@ -404,7 +438,16 @@ function parseRoutingConfig(raw) {
404
438
  logger.warn(`[proxy-config] Skipping fallback entry with empty "provider" or "model": ${JSON.stringify(e)}`);
405
439
  return null;
406
440
  }
407
- return { provider, model };
441
+ const effort = e["reasoning-effort"] !== undefined
442
+ ? e["reasoning-effort"]
443
+ : e.reasoningEffort;
444
+ return {
445
+ provider,
446
+ model,
447
+ ...(isCodexReasoningEffort(effort)
448
+ ? { reasoningEffort: effort }
449
+ : {}),
450
+ };
408
451
  })
409
452
  .filter((e) => e !== null);
410
453
  }
@@ -13,6 +13,7 @@ export async function startRollingProxyServer(options) {
13
13
  let requestedReplacementTimer;
14
14
  let requestedReplacementSchedule = 0;
15
15
  let requestedReplacementPending = false;
16
+ let requestedReplacementReason = "environment";
16
17
  let replacementQueueTail = null;
17
18
  const recoveryDelayMs = Math.max(1, options.recoveryDelayMs ?? DEFAULT_RECOVERY_DELAY_MS);
18
19
  const maxRecoveryDelayMs = Math.max(recoveryDelayMs, options.maxRecoveryDelayMs ?? DEFAULT_MAX_RECOVERY_DELAY_MS);
@@ -70,11 +71,14 @@ export async function startRollingProxyServer(options) {
70
71
  scheduleRecovery();
71
72
  }
72
73
  };
73
- function scheduleRequestedReplacement() {
74
+ function scheduleRequestedReplacement(request) {
74
75
  if (closing) {
75
76
  return;
76
77
  }
77
78
  requestedReplacementPending = true;
79
+ if (request) {
80
+ requestedReplacementReason = request.reason;
81
+ }
78
82
  if (requestedReplacementTimer || replacementQueueTail) {
79
83
  return;
80
84
  }
@@ -89,15 +93,16 @@ export async function startRollingProxyServer(options) {
89
93
  }
90
94
  requestedReplacementPending = false;
91
95
  const replacementVersion = desiredVersion;
96
+ const replacementReason = requestedReplacementReason;
92
97
  void queueReplacement(async () => {
93
98
  if (closing || !supervisor.snapshot().active) {
94
99
  return;
95
100
  }
96
- options.log?.(`[proxy-supervisor] preparing same-version worker replacement version=${replacementVersion} reason=environment`);
101
+ options.log?.(`[proxy-supervisor] preparing same-version worker replacement version=${replacementVersion} reason=${replacementReason}`);
97
102
  await supervisor.replace(replacementVersion);
98
- options.log?.(`[proxy-supervisor] same-version worker replacement complete version=${replacementVersion} reason=environment`);
103
+ options.log?.(`[proxy-supervisor] same-version worker replacement complete version=${replacementVersion} reason=${replacementReason}`);
99
104
  }).catch((error) => {
100
- options.log?.(`[proxy-supervisor] same-version worker replacement failed version=${replacementVersion} reason=environment: ${error instanceof Error ? error.message : String(error)}`);
105
+ options.log?.(`[proxy-supervisor] same-version worker replacement failed version=${replacementVersion} reason=${replacementReason}: ${error instanceof Error ? error.message : String(error)}`);
101
106
  });
102
107
  }, 50);
103
108
  requestedReplacementTimer.unref?.();
@@ -106,6 +111,7 @@ export async function startRollingProxyServer(options) {
106
111
  spawnWorker: options.spawnWorker,
107
112
  readyTimeoutMs: options.readyTimeoutMs,
108
113
  socketQueueLimit: options.socketQueueLimit,
114
+ maxPendingTransfers: options.maxPendingTransfers,
109
115
  socketQueueTimeoutMs: options.socketQueueTimeoutMs,
110
116
  shutdownTimeoutMs: options.shutdownTimeoutMs,
111
117
  onStateChange: stateChanged,
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { ErrorFactory } from "../utils/errorHandling.js";
3
- import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, } from "./rollingWorkerProtocol.js";
3
+ import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, PROXY_SOCKET_OFFER_TIMEOUT, } from "./rollingWorkerProtocol.js";
4
4
  export function spawnProxySocketWorker(options) {
5
5
  const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 30_000);
6
6
  let nextSocketId = 0;
@@ -134,7 +134,13 @@ export function spawnProxySocketWorker(options) {
134
134
  }
135
135
  const socketId = `${generation}:${++nextSocketId}`;
136
136
  const timeout = setTimeout(() => {
137
- settleSocket(socketId, new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`));
137
+ const error = new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`);
138
+ if (!pendingSockets.get(socketId)?.accepted) {
139
+ // No commit was sent. The cancel message settles this offer without
140
+ // terminating unrelated requests already owned by the worker.
141
+ error.code = PROXY_SOCKET_OFFER_TIMEOUT;
142
+ }
143
+ settleSocket(socketId, error);
138
144
  }, socketAckTimeoutMs);
139
145
  timeout.unref?.();
140
146
  pendingSockets.set(socketId, {