@juspay/neurolink 11.22.5 → 11.23.1

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.
@@ -52,6 +52,18 @@ export declare class LiteLLMProvider extends OpenAIChatCompletionsProvider {
52
52
  protected getDefaultModel(): string;
53
53
  protected getFallbackModelName(): string;
54
54
  protected getFallbackModels(): string[];
55
+ /**
56
+ * generate() rides the SSE wire for LiteLLM: deployments commonly sit
57
+ * behind proxies/tunnels (e.g. Cloudflare, which 524s an origin that is
58
+ * silent for ~100s), and a non-streaming completion from a slow model
59
+ * sends nothing until it is fully done. Streaming keeps bytes flowing for
60
+ * the whole generation while the base class aggregates into the same
61
+ * complete result — structuredData coercion, tool calls, stopReason,
62
+ * usage and the JSON damage flags are unchanged for callers.
63
+ * Escape hatch: NEUROLINK_LITELLM_SSE_GENERATE=false restores the plain
64
+ * JSON wire.
65
+ */
66
+ protected useStreamingWireForGenerate(): boolean;
55
67
  /**
56
68
  * Gemini 2.5 models on LiteLLM have a known compatibility issue with
57
69
  * `max_tokens` — strip it before the wire body is built. Applies to
@@ -248,6 +248,20 @@ export class LiteLLMProvider extends OpenAIChatCompletionsProvider {
248
248
  "google/gemini-2.5-flash",
249
249
  ]);
250
250
  }
251
+ /**
252
+ * generate() rides the SSE wire for LiteLLM: deployments commonly sit
253
+ * behind proxies/tunnels (e.g. Cloudflare, which 524s an origin that is
254
+ * silent for ~100s), and a non-streaming completion from a slow model
255
+ * sends nothing until it is fully done. Streaming keeps bytes flowing for
256
+ * the whole generation while the base class aggregates into the same
257
+ * complete result — structuredData coercion, tool calls, stopReason,
258
+ * usage and the JSON damage flags are unchanged for callers.
259
+ * Escape hatch: NEUROLINK_LITELLM_SSE_GENERATE=false restores the plain
260
+ * JSON wire.
261
+ */
262
+ useStreamingWireForGenerate() {
263
+ return process.env.NEUROLINK_LITELLM_SSE_GENERATE !== "false";
264
+ }
251
265
  /**
252
266
  * Gemini 2.5 models on LiteLLM have a known compatibility issue with
253
267
  * `max_tokens` — strip it before the wire body is built. Applies to
@@ -77,6 +77,19 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
77
77
  * (OpenAI, Azure OpenAI) override this to false.
78
78
  */
79
79
  protected suppressResponseFormatWithTools(): boolean;
80
+ /**
81
+ * When true, `doGenerate` puts `stream: true` on the wire and aggregates
82
+ * the SSE stream into the SAME complete result the JSON wire returns —
83
+ * callers still get one awaited result with structuredData coercion, tool
84
+ * calls, finish reason and usage intact. Bytes then flow continuously, so
85
+ * proxy/tunnel idle limits (e.g. Cloudflare's ~100s 524 on tunneled
86
+ * gateways) cannot kill a slow completion, and the request timeout is
87
+ * re-armed on every chunk (idle semantics) instead of capping total
88
+ * duration. Default false: some OpenAI-compatible backends mishandle
89
+ * `stream_options` or omit usage on streams, so each provider opts in
90
+ * deliberately. LiteLLM overrides this to true.
91
+ */
92
+ protected useStreamingWireForGenerate(): boolean;
80
93
  /**
81
94
  * Hook to adjust the fully-built wire request body before it is sent, on
82
95
  * both the streaming and non-streaming paths. Default identity. Override for
@@ -108,6 +108,21 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
108
108
  suppressResponseFormatWithTools() {
109
109
  return true;
110
110
  }
111
+ /**
112
+ * When true, `doGenerate` puts `stream: true` on the wire and aggregates
113
+ * the SSE stream into the SAME complete result the JSON wire returns —
114
+ * callers still get one awaited result with structuredData coercion, tool
115
+ * calls, finish reason and usage intact. Bytes then flow continuously, so
116
+ * proxy/tunnel idle limits (e.g. Cloudflare's ~100s 524 on tunneled
117
+ * gateways) cannot kill a slow completion, and the request timeout is
118
+ * re-armed on every chunk (idle semantics) instead of capping total
119
+ * duration. Default false: some OpenAI-compatible backends mishandle
120
+ * `stream_options` or omit usage on streams, so each provider opts in
121
+ * deliberately. LiteLLM overrides this to true.
122
+ */
123
+ useStreamingWireForGenerate() {
124
+ return false;
125
+ }
111
126
  /**
112
127
  * Hook to adjust the fully-built wire request body before it is sent, on
113
128
  * both the streaming and non-streaming paths. Default identity. Override for
@@ -387,6 +402,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
387
402
  const correctBodyAfterContextOverflow = this.correctBodyAfterContextOverflow.bind(this);
388
403
  const resolveWireMaxTokens = this.resolveWireMaxTokens.bind(this);
389
404
  const suppressResponseFormatWithTools = this.suppressResponseFormatWithTools.bind(this);
405
+ const useStreamingWireForGenerate = this.useStreamingWireForGenerate.bind(this);
390
406
  const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
391
407
  return {
392
408
  specificationVersion: "v3",
@@ -414,6 +430,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
414
430
  // Fit max_tokens to the runtime-discovered output ceiling and
415
431
  // context window (no-op when nothing was discovered).
416
432
  const wireMaxTokens = resolveWireMaxTokens(modelId, options.maxOutputTokens, baseMessages, wireTools);
433
+ // SSE wire for generate (opt-in per provider): stream on the wire,
434
+ // aggregate below into the same complete response the JSON wire
435
+ // yields. See useStreamingWireForGenerate.
436
+ const sseWire = useStreamingWireForGenerate();
417
437
  const body = ensureJsonWordInBody(adjustRequestBody(buildBody({
418
438
  modelId,
419
439
  messages: baseMessages,
@@ -432,7 +452,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
432
452
  toolChoice: v3ToolChoiceToOpenAI(options.toolChoice, wireNameMaps?.toWire),
433
453
  }
434
454
  : {}),
435
- streaming: false,
455
+ streaming: sseWire,
436
456
  ...(responseFormat ? { responseFormat } : {}),
437
457
  }), modelId));
438
458
  // Per-step timeout: the AI-SDK V3 call options never carry `timeout`,
@@ -451,6 +471,10 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
451
471
  // this step's listeners the moment the request settles.
452
472
  const { signal: composedSignal, dispose: disposeComposedSignal } = composeAbortSignalsScoped(options.abortSignal, timeoutController?.controller.signal);
453
473
  let json;
474
+ // Whether the response we end up consuming is SSE. Starts as the
475
+ // provider's wire preference; the 400 fallback below can flip it
476
+ // when a backend rejects `stream`/`stream_options` outright.
477
+ let wireIsStreaming = sseWire;
454
478
  try {
455
479
  let res = await fetchImpl(url, {
456
480
  method: "POST",
@@ -474,14 +498,26 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
474
498
  // attempt, so the configured timeout caps the overall call —
475
499
  // matching the streaming path, which reuses its composed signal
476
500
  // for the retry.
477
- const retryBody = res.status === 400
501
+ const typedErr = apiErr;
502
+ let retryBody = res.status === 400
478
503
  ? (() => {
479
- const typedErr = apiErr;
480
504
  const overflowCorrected = correctBodyAfterContextOverflow(body, typedErr);
481
505
  return (adjustBodyAfter400(overflowCorrected ?? body, typedErr) ??
482
506
  overflowCorrected);
483
507
  })()
484
508
  : undefined;
509
+ // SSE-wire net: a backend that rejects streaming itself (the 400
510
+ // names `stream`/`stream_options`) gets ONE retry on the plain
511
+ // JSON wire. Gated on the error text so a genuine bad request
512
+ // isn't replayed just to fail identically a second time.
513
+ if (!retryBody &&
514
+ sseWire &&
515
+ res.status === 400 &&
516
+ /stream/i.test(typedErr.responseBody ?? "")) {
517
+ const { stream: _stream, stream_options: _streamOptions, ...jsonWireBody } = body;
518
+ retryBody = jsonWireBody;
519
+ wireIsStreaming = false;
520
+ }
485
521
  if (!retryBody) {
486
522
  throw apiErr;
487
523
  }
@@ -502,7 +538,59 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
502
538
  // after cleanup(), so a response whose headers arrived but whose
503
539
  // body stalled mid-transfer was bounded by nothing but the caller's
504
540
  // outer wall-clock.
505
- json = (await res.json());
541
+ if (wireIsStreaming) {
542
+ if (!res.body) {
543
+ throw new Error(`${providerName}: streaming generate response had no body`);
544
+ }
545
+ // Idle-timeout semantics: every raw chunk re-arms the timeout,
546
+ // so the configured window bounds silence, not total duration —
547
+ // the whole point of the SSE wire is that a slow-but-alive
548
+ // completion keeps the connection (and the timer) fed.
549
+ const monitored = timeoutController
550
+ ? res.body.pipeThrough(new TransformStream({
551
+ transform(chunk, controller) {
552
+ timeoutController.reset();
553
+ controller.enqueue(chunk);
554
+ },
555
+ }))
556
+ : res.body;
557
+ const sse = await parseSSEStream(monitored, () => { });
558
+ // Re-shape the aggregate into the JSON-wire response so every
559
+ // line below this point (content parts, finish-reason mapping,
560
+ // usage clamping, response metadata) is shared verbatim between
561
+ // the two wires and cannot drift.
562
+ json = {
563
+ ...(sse.id ? { id: sse.id } : {}),
564
+ ...(sse.model ? { model: sse.model } : {}),
565
+ choices: [
566
+ {
567
+ index: 0,
568
+ message: {
569
+ role: "assistant",
570
+ content: sse.text.length > 0 ? sse.text : null,
571
+ ...(sse.reasoning ? { reasoning: sse.reasoning } : {}),
572
+ ...(sse.toolCalls.size > 0
573
+ ? {
574
+ tool_calls: [...sse.toolCalls.values()].map((tc) => ({
575
+ id: tc.id,
576
+ type: "function",
577
+ function: {
578
+ name: tc.name,
579
+ arguments: tc.argsBuffered,
580
+ },
581
+ })),
582
+ }
583
+ : {}),
584
+ },
585
+ finish_reason: sse.finishReason ?? "stop",
586
+ },
587
+ ],
588
+ ...(sse.usage ? { usage: sse.usage } : {}),
589
+ };
590
+ }
591
+ else {
592
+ json = (await res.json());
593
+ }
506
594
  }
507
595
  finally {
508
596
  timeoutController?.cleanup();
@@ -531,6 +531,12 @@ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
531
531
  if (chunk.usage) {
532
532
  result.usage = chunk.usage;
533
533
  }
534
+ if (chunk.id && !result.id) {
535
+ result.id = chunk.id;
536
+ }
537
+ if (chunk.model && !result.model) {
538
+ result.model = chunk.model;
539
+ }
534
540
  const choice = chunk.choices?.[0];
535
541
  if (!choice) {
536
542
  return;
@@ -37,13 +37,23 @@ export declare class ChunkerFactory extends BaseFactory<Chunker, ChunkerConfig>
37
37
  */
38
38
  createChunker(strategyOrAlias: string, config?: ChunkerConfig): Promise<Chunker>;
39
39
  /**
40
- * Get metadata for a chunker
40
+ * Get metadata for a chunker.
41
+ *
42
+ * Awaits registration first. `metadataMap` and the alias map are populated by
43
+ * `registerAll()`, which only runs via the async `ensureInitialized()` — and
44
+ * the only place that awaited it was `create()`. So on a fresh process this
45
+ * returned `undefined` for every strategy until something happened to build a
46
+ * chunker first, and returned real metadata after. Order-dependent, silent,
47
+ * and `undefined` rather than a throw.
48
+ *
49
+ * `createChunker()` and `getAvailableStrategies()` were already async for
50
+ * exactly this reason; these readers were the ones that had not caught up.
41
51
  */
42
- getChunkerMetadata(strategyOrAlias: string): ChunkerMetadata | undefined;
52
+ getChunkerMetadata(strategyOrAlias: string): Promise<ChunkerMetadata | undefined>;
43
53
  /**
44
54
  * Get default configuration for a chunker
45
55
  */
46
- getDefaultConfig(strategyOrAlias: string): ChunkerConfig | undefined;
56
+ getDefaultConfig(strategyOrAlias: string): Promise<ChunkerConfig | undefined>;
47
57
  /**
48
58
  * Get available chunking strategies (not including aliases)
49
59
  */
@@ -51,19 +61,19 @@ export declare class ChunkerFactory extends BaseFactory<Chunker, ChunkerConfig>
51
61
  /**
52
62
  * Get all aliases mapped to their strategies
53
63
  */
54
- getStrategyAliases(): Map<string, string>;
64
+ getStrategyAliases(): Promise<Map<string, string>>;
55
65
  /**
56
66
  * Check if a strategy exists
57
67
  */
58
- hasStrategy(strategyOrAlias: string): boolean;
68
+ hasStrategy(strategyOrAlias: string): Promise<boolean>;
59
69
  /**
60
70
  * Get chunkers suitable for a use case
61
71
  */
62
- getChunkersForUseCase(useCase: string): ChunkingStrategy[];
72
+ getChunkersForUseCase(useCase: string): Promise<ChunkingStrategy[]>;
63
73
  /**
64
74
  * Get all chunker metadata
65
75
  */
66
- getAllMetadata(): Map<string, ChunkerMetadata>;
76
+ getAllMetadata(): Promise<Map<string, ChunkerMetadata>>;
67
77
  /**
68
78
  * Clear factory and metadata
69
79
  */
@@ -84,8 +94,8 @@ export declare function getAvailableStrategies(): Promise<ChunkingStrategy[]>;
84
94
  /**
85
95
  * Convenience function to get chunker metadata
86
96
  */
87
- export declare function getChunkerMetadata(strategyOrAlias: string): ChunkerMetadata | undefined;
97
+ export declare function getChunkerMetadata(strategyOrAlias: string): Promise<ChunkerMetadata | undefined>;
88
98
  /**
89
99
  * Convenience function to get default config
90
100
  */
91
- export declare function getDefaultConfig(strategyOrAlias: string): ChunkerConfig | undefined;
101
+ export declare function getDefaultConfig(strategyOrAlias: string): Promise<ChunkerConfig | undefined>;
@@ -229,17 +229,28 @@ export class ChunkerFactory extends BaseFactory {
229
229
  }
230
230
  }
231
231
  /**
232
- * Get metadata for a chunker
232
+ * Get metadata for a chunker.
233
+ *
234
+ * Awaits registration first. `metadataMap` and the alias map are populated by
235
+ * `registerAll()`, which only runs via the async `ensureInitialized()` — and
236
+ * the only place that awaited it was `create()`. So on a fresh process this
237
+ * returned `undefined` for every strategy until something happened to build a
238
+ * chunker first, and returned real metadata after. Order-dependent, silent,
239
+ * and `undefined` rather than a throw.
240
+ *
241
+ * `createChunker()` and `getAvailableStrategies()` were already async for
242
+ * exactly this reason; these readers were the ones that had not caught up.
233
243
  */
234
- getChunkerMetadata(strategyOrAlias) {
244
+ async getChunkerMetadata(strategyOrAlias) {
245
+ await this.ensureInitialized();
235
246
  const resolvedName = this.resolveName(strategyOrAlias);
236
247
  return this.metadataMap.get(resolvedName);
237
248
  }
238
249
  /**
239
250
  * Get default configuration for a chunker
240
251
  */
241
- getDefaultConfig(strategyOrAlias) {
242
- const metadata = this.getChunkerMetadata(strategyOrAlias);
252
+ async getDefaultConfig(strategyOrAlias) {
253
+ const metadata = await this.getChunkerMetadata(strategyOrAlias);
243
254
  return metadata?.defaultConfig;
244
255
  }
245
256
  /**
@@ -252,20 +263,23 @@ export class ChunkerFactory extends BaseFactory {
252
263
  /**
253
264
  * Get all aliases mapped to their strategies
254
265
  */
255
- getStrategyAliases() {
266
+ async getStrategyAliases() {
267
+ await this.ensureInitialized();
256
268
  return this.getAliases();
257
269
  }
258
270
  /**
259
271
  * Check if a strategy exists
260
272
  */
261
- hasStrategy(strategyOrAlias) {
273
+ async hasStrategy(strategyOrAlias) {
274
+ await this.ensureInitialized();
262
275
  const resolved = this.resolveName(strategyOrAlias);
263
276
  return this.has(resolved);
264
277
  }
265
278
  /**
266
279
  * Get chunkers suitable for a use case
267
280
  */
268
- getChunkersForUseCase(useCase) {
281
+ async getChunkersForUseCase(useCase) {
282
+ await this.ensureInitialized();
269
283
  const matches = [];
270
284
  const useCaseLower = useCase.toLowerCase();
271
285
  for (const [strategy, metadata] of this.metadataMap) {
@@ -279,7 +293,8 @@ export class ChunkerFactory extends BaseFactory {
279
293
  /**
280
294
  * Get all chunker metadata
281
295
  */
282
- getAllMetadata() {
296
+ async getAllMetadata() {
297
+ await this.ensureInitialized();
283
298
  return new Map(this.metadataMap);
284
299
  }
285
300
  /**
@@ -64,11 +64,11 @@ export declare class RerankerFactory extends BaseFactory<Reranker, RerankerConfi
64
64
  /**
65
65
  * Get metadata for a reranker
66
66
  */
67
- getRerankerMetadata(typeOrAlias: string): RerankerMetadata | undefined;
67
+ getRerankerMetadata(typeOrAlias: string): Promise<RerankerMetadata | undefined>;
68
68
  /**
69
69
  * Get default configuration for a reranker
70
70
  */
71
- getDefaultConfig(typeOrAlias: string): Partial<RerankerConfig> | undefined;
71
+ getDefaultConfig(typeOrAlias: string): Promise<Partial<RerankerConfig> | undefined>;
72
72
  /**
73
73
  * Get available reranker types (not including aliases)
74
74
  */
@@ -76,27 +76,27 @@ export declare class RerankerFactory extends BaseFactory<Reranker, RerankerConfi
76
76
  /**
77
77
  * Get all aliases mapped to their types
78
78
  */
79
- getTypeAliases(): Map<string, string>;
79
+ getTypeAliases(): Promise<Map<string, string>>;
80
80
  /**
81
81
  * Check if a type exists
82
82
  */
83
- hasType(typeOrAlias: string): boolean;
83
+ hasType(typeOrAlias: string): Promise<boolean>;
84
84
  /**
85
85
  * Get rerankers suitable for a use case
86
86
  */
87
- getRerankersForUseCase(useCase: string): RerankerType[];
87
+ getRerankersForUseCase(useCase: string): Promise<RerankerType[]>;
88
88
  /**
89
89
  * Get rerankers that don't require external APIs
90
90
  */
91
- getLocalRerankers(): RerankerType[];
91
+ getLocalRerankers(): Promise<RerankerType[]>;
92
92
  /**
93
93
  * Get rerankers that don't require AI models
94
94
  */
95
- getModelFreeRerankers(): RerankerType[];
95
+ getModelFreeRerankers(): Promise<RerankerType[]>;
96
96
  /**
97
97
  * Get all reranker metadata
98
98
  */
99
- getAllMetadata(): Map<RerankerType, RerankerMetadata>;
99
+ getAllMetadata(): Promise<Map<RerankerType, RerankerMetadata>>;
100
100
  /**
101
101
  * Clear factory and metadata
102
102
  */
@@ -117,8 +117,8 @@ export declare function getAvailableRerankerTypes(): Promise<RerankerType[]>;
117
117
  /**
118
118
  * Convenience function to get reranker metadata
119
119
  */
120
- export declare function getRerankerMetadata(typeOrAlias: string): RerankerMetadata | undefined;
120
+ export declare function getRerankerMetadata(typeOrAlias: string): Promise<RerankerMetadata | undefined>;
121
121
  /**
122
122
  * Convenience function to get default config
123
123
  */
124
- export declare function getRerankerDefaultConfig(typeOrAlias: string): Partial<RerankerConfig> | undefined;
124
+ export declare function getRerankerDefaultConfig(typeOrAlias: string): Promise<Partial<RerankerConfig> | undefined>;
@@ -316,15 +316,16 @@ export class RerankerFactory extends BaseFactory {
316
316
  /**
317
317
  * Get metadata for a reranker
318
318
  */
319
- getRerankerMetadata(typeOrAlias) {
319
+ async getRerankerMetadata(typeOrAlias) {
320
+ await this.ensureInitialized();
320
321
  const resolvedName = this.resolveName(typeOrAlias);
321
322
  return this.metadataMap.get(resolvedName);
322
323
  }
323
324
  /**
324
325
  * Get default configuration for a reranker
325
326
  */
326
- getDefaultConfig(typeOrAlias) {
327
- const metadata = this.getRerankerMetadata(typeOrAlias);
327
+ async getDefaultConfig(typeOrAlias) {
328
+ const metadata = await this.getRerankerMetadata(typeOrAlias);
328
329
  return metadata?.defaultConfig;
329
330
  }
330
331
  /**
@@ -337,20 +338,23 @@ export class RerankerFactory extends BaseFactory {
337
338
  /**
338
339
  * Get all aliases mapped to their types
339
340
  */
340
- getTypeAliases() {
341
+ async getTypeAliases() {
342
+ await this.ensureInitialized();
341
343
  return this.getAliases();
342
344
  }
343
345
  /**
344
346
  * Check if a type exists
345
347
  */
346
- hasType(typeOrAlias) {
348
+ async hasType(typeOrAlias) {
349
+ await this.ensureInitialized();
347
350
  const resolved = this.resolveName(typeOrAlias);
348
351
  return this.has(resolved);
349
352
  }
350
353
  /**
351
354
  * Get rerankers suitable for a use case
352
355
  */
353
- getRerankersForUseCase(useCase) {
356
+ async getRerankersForUseCase(useCase) {
357
+ await this.ensureInitialized();
354
358
  const matches = [];
355
359
  const useCaseLower = useCase.toLowerCase();
356
360
  for (const [type, metadata] of this.metadataMap) {
@@ -364,7 +368,8 @@ export class RerankerFactory extends BaseFactory {
364
368
  /**
365
369
  * Get rerankers that don't require external APIs
366
370
  */
367
- getLocalRerankers() {
371
+ async getLocalRerankers() {
372
+ await this.ensureInitialized();
368
373
  const matches = [];
369
374
  for (const [type, metadata] of this.metadataMap) {
370
375
  if (!metadata.requiresExternalAPI) {
@@ -376,7 +381,8 @@ export class RerankerFactory extends BaseFactory {
376
381
  /**
377
382
  * Get rerankers that don't require AI models
378
383
  */
379
- getModelFreeRerankers() {
384
+ async getModelFreeRerankers() {
385
+ await this.ensureInitialized();
380
386
  const matches = [];
381
387
  for (const [type, metadata] of this.metadataMap) {
382
388
  if (!metadata.requiresModel) {
@@ -388,7 +394,8 @@ export class RerankerFactory extends BaseFactory {
388
394
  /**
389
395
  * Get all reranker metadata
390
396
  */
391
- getAllMetadata() {
397
+ async getAllMetadata() {
398
+ await this.ensureInitialized();
392
399
  return new Map(this.metadataMap);
393
400
  }
394
401
  /**
@@ -199,6 +199,10 @@ export type OpenAICompatSSEResult = {
199
199
  }>;
200
200
  finishReason: "stop" | "length" | "tool_calls" | "function_call" | "content_filter" | null;
201
201
  usage?: OpenAICompatUsage;
202
+ /** Response id from the first stream chunk that carried one. */
203
+ id?: string;
204
+ /** Served model from the first stream chunk that carried one. */
205
+ model?: string;
202
206
  };
203
207
  export type OpenAICompatStreamChunk = {
204
208
  content: string;
@@ -116,6 +116,13 @@ export declare function withStreamingTimeout<T>(generator: AsyncGenerator<T>, ti
116
116
  export declare function createTimeoutController(timeout: number | string | undefined, provider: string, operation: "generate" | "stream"): {
117
117
  controller: AbortController;
118
118
  cleanup: () => void;
119
+ /**
120
+ * Re-arm the timeout window from now. Streaming consumers call this on
121
+ * every chunk so the timeout bounds *idle* time rather than total
122
+ * duration — a slow-but-alive upstream is not killed mid-generation.
123
+ * No-op once the controller has already aborted.
124
+ */
125
+ reset: () => void;
119
126
  timeoutMs: number;
120
127
  } | null;
121
128
  /**
@@ -313,7 +313,7 @@ export function createTimeoutController(timeout, provider, operation) {
313
313
  return null;
314
314
  }
315
315
  const controller = new AbortController();
316
- const timer = setTimeout(() => {
316
+ const fire = () => {
317
317
  // NOTE: we cannot stamp the AI SDK's ai.streamText/ai.generateText span
318
318
  // from here — the setTimeout callback runs in the async context captured
319
319
  // at schedule time, which is BEFORE the AI SDK span exists. Instead we
@@ -321,11 +321,19 @@ export function createTimeoutController(timeout, provider, operation) {
321
321
  // wrapper, which sets span.status = ERROR + message. ContextEnricher's
322
322
  // SpanStatusCode.ERROR branch then surfaces level=ERROR + status_message.
323
323
  controller.abort(new TimeoutError(`${provider} ${operation} operation timed out after ${timeout}`, timeoutMs, provider, operation));
324
- }, timeoutMs);
324
+ };
325
+ let timer = setTimeout(fire, timeoutMs);
325
326
  const cleanup = () => {
326
327
  clearTimeout(timer);
327
328
  };
328
- return { controller, cleanup, timeoutMs };
329
+ const reset = () => {
330
+ if (controller.signal.aborted) {
331
+ return;
332
+ }
333
+ clearTimeout(timer);
334
+ timer = setTimeout(fire, timeoutMs);
335
+ };
336
+ return { controller, cleanup, reset, timeoutMs };
329
337
  }
330
338
  /**
331
339
  * Compose an external abort signal with a timeout controller's signal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.22.5",
3
+ "version": "11.23.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {