@juspay/neurolink 12.12.4 → 12.12.6

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 {
@@ -468,6 +468,16 @@ export declare abstract class BaseProvider implements AIProvider {
468
468
  * TODO(#1576): Implement global level middlewares that can be used
469
469
  */
470
470
  protected getAISDKModelWithMiddleware(options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
471
+ /**
472
+ * Apply the configured middleware chain to a caller-supplied base model.
473
+ *
474
+ * `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
475
+ * the model the non-streaming path drives. Streaming paths build a
476
+ * different base — one whose `doStream` starts the provider's own stream
477
+ * loop — and need the same chain applied to it, so the wrapping is split
478
+ * out here rather than duplicated per provider.
479
+ */
480
+ protected applyMiddlewareToModel(baseModel: LanguageModel, options?: TextGenerationOptions | StreamOptions): Promise<LanguageModel>;
471
481
  /**
472
482
  * Extract middleware options - delegated to Utilities
473
483
  */
@@ -2014,8 +2014,18 @@ export class BaseProvider {
2014
2014
  * TODO(#1576): Implement global level middlewares that can be used
2015
2015
  */
2016
2016
  async getAISDKModelWithMiddleware(options = {}) {
2017
- // Get the base model
2018
- const baseModel = await this.getAISDKModel();
2017
+ return this.applyMiddlewareToModel(await this.getAISDKModel(), options);
2018
+ }
2019
+ /**
2020
+ * Apply the configured middleware chain to a caller-supplied base model.
2021
+ *
2022
+ * `getAISDKModelWithMiddleware()` always wraps `getAISDKModel()`, which is
2023
+ * the model the non-streaming path drives. Streaming paths build a
2024
+ * different base — one whose `doStream` starts the provider's own stream
2025
+ * loop — and need the same chain applied to it, so the wrapping is split
2026
+ * out here rather than duplicated per provider.
2027
+ */
2028
+ async applyMiddlewareToModel(baseModel, options = {}) {
2019
2029
  logger.debug(`Retrieved base model for ${this.providerName}`, {
2020
2030
  provider: this.providerName,
2021
2031
  model: this.modelName,
@@ -43,7 +43,6 @@ export function createGuardrailsMiddleware(config = {}) {
43
43
  const blockingState = new WeakMap();
44
44
  const middleware = {
45
45
  specificationVersion: "v3",
46
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
46
  transformParams: async ({ params }) => {
48
47
  if (config.precallEvaluation?.enabled) {
49
48
  const { shouldBlock, transformedParams } = await handlePrecallGuardrails(params, config.precallEvaluation);
@@ -53,7 +52,6 @@ export function createGuardrailsMiddleware(config = {}) {
53
52
  }
54
53
  return params;
55
54
  },
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
55
  wrapGenerate: async ({ doGenerate, params }) => {
58
56
  logger.debug(`[GuardrailsMiddleware] Applying to generate call.`);
59
57
  // Check if this request should be blocked (set by transformParams)
@@ -61,14 +59,23 @@ export function createGuardrailsMiddleware(config = {}) {
61
59
  return createBlockedResponse();
62
60
  }
63
61
  let result = await doGenerate();
64
- const filterResult = applyContentFiltering(result.text || "", config.badWords, "generate");
65
- if (filterResult.hasChanges) {
66
- result = { ...result, text: filterResult.filteredText };
67
- }
62
+ result = {
63
+ ...result,
64
+ content: result.content.map((part) => part.type === "text"
65
+ ? {
66
+ ...part,
67
+ text: applyContentFiltering(part.text, config.badWords, "generate").filteredText,
68
+ }
69
+ : part),
70
+ };
68
71
  if (config.modelFilter?.enabled && config.modelFilter.filterModel) {
69
72
  logger.debug(`[GuardrailsMiddleware] Invoking model-based filter.`);
70
73
  try {
71
- const filterPrompt = `Is the following text safe? Respond with only "safe" or "unsafe".\n\nText: "${result.text}"`;
74
+ const text = result.content
75
+ .filter((part) => part.type === "text")
76
+ .map((part) => part.text)
77
+ .join("");
78
+ const filterPrompt = `Is the following text safe? Respond with only "safe" or "unsafe".\n\nText: "${text}"`;
72
79
  // `ModelFilterConfig.filterModel` is typed `LanguageModel`, which
73
80
  // admits a bare model id, and the documented examples used one.
74
81
  // `generateOnceNative` needs a handle exposing doGenerate, so a
@@ -78,7 +85,12 @@ export function createGuardrailsMiddleware(config = {}) {
78
85
  const { text: filterResponse } = await generateOnceNative(filterModel, { prompt: filterPrompt });
79
86
  if (filterResponse.toLowerCase().trim() === "unsafe") {
80
87
  logger.warn(`[GuardrailsMiddleware] Model-based filter flagged content as unsafe.`);
81
- result = { ...result, text: "<REDACTED BY AI GUARDRAIL>" };
88
+ result = {
89
+ ...result,
90
+ content: result.content.map((part) => part.type === "text"
91
+ ? { ...part, text: "<REDACTED BY AI GUARDRAIL>" }
92
+ : part),
93
+ };
82
94
  }
83
95
  }
84
96
  catch (error) {
@@ -89,7 +101,6 @@ export function createGuardrailsMiddleware(config = {}) {
89
101
  }
90
102
  return result;
91
103
  },
92
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
104
  wrapStream: async ({ doStream, params }) => {
94
105
  logger.debug(`[GuardrailsMiddleware] Applying to stream call.`);
95
106
  // Check if this request should be blocked (set by transformParams)
@@ -106,13 +117,12 @@ export function createGuardrailsMiddleware(config = {}) {
106
117
  transform(chunk, controller) {
107
118
  hasYieldedChunks = true;
108
119
  let filteredChunk = chunk;
109
- if (typeof filteredChunk === "object" &&
110
- "textDelta" in filteredChunk) {
111
- const filterResult = applyContentFiltering(filteredChunk.textDelta, config.badWords, "stream");
120
+ if (filteredChunk.type === "text-delta") {
121
+ const filterResult = applyContentFiltering(filteredChunk.delta, config.badWords, "stream");
112
122
  if (filterResult.hasChanges) {
113
123
  filteredChunk = {
114
124
  ...filteredChunk,
115
- textDelta: filterResult.filteredText,
125
+ delta: filterResult.filteredText,
116
126
  };
117
127
  }
118
128
  }
@@ -1,4 +1,4 @@
1
- import type { BadWordsConfig, ContentFilteringResult, EvaluationActionResult, PrecallEvaluationConfig, PrecallEvaluationResult } from "../../types/index.js";
1
+ import type { BadWordsConfig, LanguageModelV3GenerateResult, LanguageModelV3StreamPart, ContentFilteringResult, EvaluationActionResult, PrecallEvaluationConfig, PrecallEvaluationResult } from "../../types/index.js";
2
2
  export declare const DEFAULT_EVALUATION_PROMPT = "\nEvaluate the following user input for safety and appropriateness. Respond ONLY with valid JSON:\n\n{\n \"overall\": \"safe|unsafe|suspicious|inappropriate\",\n \"safetyScore\": <1-10>,\n \"appropriatenessScore\": <1-10>,\n \"confidenceLevel\": <1-10>,\n \"issues\": [\n {\n \"category\": \"explicit_content|hate_speech|violence|personal_info|spam|other\",\n \"severity\": \"low|medium|high|critical\",\n \"description\": \"Brief description\"\n }\n ],\n \"suggestedAction\": \"allow|block|sanitize|warn\",\n \"reasoning\": \"Brief explanation\"\n}\n\nUser Input: \"{USER_INPUT}\"\n";
3
3
  /**
4
4
  * Extract user input from middleware params
@@ -25,20 +25,8 @@ export declare function applyEvaluationActions(evaluation: PrecallEvaluationResu
25
25
  */
26
26
  export declare function applySanitization(params: any, sanitizedInput: string): any;
27
27
  export declare function escapeRegExp(string: string): string;
28
- export declare function createBlockedResponse(): {
29
- text: string;
30
- usage: {
31
- promptTokens: number;
32
- completionTokens: number;
33
- };
34
- finishReason: "stop";
35
- warnings: never[];
36
- rawCall: {
37
- rawPrompt: null;
38
- rawSettings: {};
39
- };
40
- };
41
- export declare function createBlockedStream(): ReadableStream<any>;
28
+ export declare function createBlockedResponse(): LanguageModelV3GenerateResult;
29
+ export declare function createBlockedStream(): ReadableStream<LanguageModelV3StreamPart>;
42
30
  /**
43
31
  * Apply content filtering using bad words configuration
44
32
  * Handles both regex patterns and string lists with proper priority
@@ -258,24 +258,31 @@ export function escapeRegExp(string) {
258
258
  }
259
259
  export function createBlockedResponse() {
260
260
  return {
261
- text: "Request contains inappropriate content and has been blocked.",
262
- usage: { promptTokens: 0, completionTokens: 0 },
263
- finishReason: "stop",
261
+ content: [
262
+ {
263
+ type: "text",
264
+ text: "Request contains inappropriate content and has been blocked.",
265
+ },
266
+ ],
267
+ usage: { inputTokens: { total: 0 }, outputTokens: { total: 0 } },
268
+ finishReason: { unified: "stop" },
264
269
  warnings: [],
265
- rawCall: { rawPrompt: null, rawSettings: {} },
266
270
  };
267
271
  }
268
272
  export function createBlockedStream() {
269
273
  return new ReadableStream({
270
274
  start(controller) {
275
+ controller.enqueue({ type: "text-start", id: "blocked" });
271
276
  controller.enqueue({
272
277
  type: "text-delta",
273
- textDelta: "Request contains inappropriate content and has been blocked.",
278
+ id: "blocked",
279
+ delta: "Request contains inappropriate content and has been blocked.",
274
280
  });
281
+ controller.enqueue({ type: "text-end", id: "blocked" });
275
282
  controller.enqueue({
276
283
  type: "finish",
277
- finishReason: "stop",
278
- usage: { promptTokens: 0, completionTokens: 0 },
284
+ finishReason: { unified: "stop" },
285
+ usage: { inputTokens: { total: 0 }, outputTokens: { total: 0 } },
279
286
  });
280
287
  controller.close();
281
288
  },
@@ -6,10 +6,9 @@
6
6
  * `wrapGenerate` / `wrapStream` hooks. Reproduced here so the middleware
7
7
  * factory no longer needs the ai package.
8
8
  *
9
- * Worth recording: `wrapStream` does not currently run in this codebase. Every
10
- * streaming path is native and bypasses the wrapped model entirely, so only
11
- * `wrapGenerate` is reachable. That is a pre-existing gap, not one this
12
- * introduced.
9
+ * The OpenAI-compatible streaming path also uses this wrapper. Other native
10
+ * streaming implementations must opt in explicitly; exposing a middleware
11
+ * option or a model-shaped handle alone does not apply the chain.
13
12
  */
14
13
  import type { LanguageModelV3, LanguageModelV3Middleware } from "../types/index.js";
15
14
  export declare const wrapLanguageModel: ({ model, middleware, }: {
@@ -6,10 +6,9 @@
6
6
  * `wrapGenerate` / `wrapStream` hooks. Reproduced here so the middleware
7
7
  * factory no longer needs the ai package.
8
8
  *
9
- * Worth recording: `wrapStream` does not currently run in this codebase. Every
10
- * streaming path is native and bypasses the wrapped model entirely, so only
11
- * `wrapGenerate` is reachable. That is a pre-existing gap, not one this
12
- * introduced.
9
+ * The OpenAI-compatible streaming path also uses this wrapper. Other native
10
+ * streaming implementations must opt in explicitly; exposing a middleware
11
+ * option or a model-shaped handle alone does not apply the chain.
13
12
  */
14
13
  const doWrap = (model, middleware) => {
15
14
  const transform = async (params, type) => middleware.transformParams