@juspay/neurolink 11.22.4 → 11.23.0
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.
- package/CHANGELOG.md +3 -4
- package/dist/browser/neurolink.min.js +277 -277
- package/dist/providers/litellm/client.d.ts +12 -0
- package/dist/providers/litellm/client.js +14 -0
- package/dist/providers/openaiChatCompletionsBase.d.ts +13 -0
- package/dist/providers/openaiChatCompletionsBase.js +92 -4
- package/dist/providers/openaiChatCompletionsClient.js +6 -0
- package/dist/types/openaiCompatible.d.ts +4 -0
- package/dist/utils/timeout.d.ts +7 -0
- package/dist/utils/timeout.js +11 -3
- package/package.json +1 -1
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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;
|
package/dist/utils/timeout.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/utils/timeout.js
CHANGED
|
@@ -313,7 +313,7 @@ export function createTimeoutController(timeout, provider, operation) {
|
|
|
313
313
|
return null;
|
|
314
314
|
}
|
|
315
315
|
const controller = new AbortController();
|
|
316
|
-
const
|
|
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
|
-
}
|
|
324
|
+
};
|
|
325
|
+
let timer = setTimeout(fire, timeoutMs);
|
|
325
326
|
const cleanup = () => {
|
|
326
327
|
clearTimeout(timer);
|
|
327
328
|
};
|
|
328
|
-
|
|
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.
|
|
3
|
+
"version": "11.23.0",
|
|
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": {
|