@juspay/neurolink 9.68.21 → 9.69.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.
@@ -86,6 +86,17 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
86
86
  adjustRequestBody(body, _modelId) {
87
87
  return body;
88
88
  }
89
+ /**
90
+ * Hook called when a request fails with HTTP 400. Return a modified body to
91
+ * retry ONCE with it; return undefined (default) to propagate the error
92
+ * unchanged. Applies on both the non-streaming doGenerate path and the
93
+ * streaming path. NVIDIA NIM uses this to strip `chat_template` /
94
+ * `chat_template_kwargs.reasoning_budget` when a model server rejects them,
95
+ * restoring the pre-migration fetch-level retry behavior.
96
+ */
97
+ adjustBodyAfter400(_body, _error) {
98
+ return undefined;
99
+ }
89
100
  /**
90
101
  * Hook called once at the start of every `executeStream` invocation.
91
102
  * Return lifecycle listeners (onUsage / onFinish) to receive deferred
@@ -209,6 +220,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
209
220
  const adjustBuildBodyOptions = this.adjustBuildBodyOptions.bind(this);
210
221
  const adjustResponseFormat = this.adjustResponseFormat.bind(this);
211
222
  const adjustRequestBody = this.adjustRequestBody.bind(this);
223
+ const adjustBodyAfter400 = this.adjustBodyAfter400.bind(this);
212
224
  const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
213
225
  return {
214
226
  specificationVersion: "v3",
@@ -256,19 +268,52 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
256
268
  body: JSON.stringify(body),
257
269
  ...(composedSignal ? { signal: composedSignal } : {}),
258
270
  });
271
+ if (!res.ok) {
272
+ const apiErr = await buildAPIError(url, body, res);
273
+ // One-shot 400 retry: a subclass may strip a rejected field and
274
+ // return a modified body (e.g. NIM's chat_template /
275
+ // reasoning_budget). The retry runs under the SAME timeout
276
+ // controller as the first attempt, so the configured timeout caps
277
+ // the overall call — matching the streaming path, which reuses
278
+ // its composed signal for the retry.
279
+ const retryBody = res.status === 400
280
+ ? adjustBodyAfter400(body, apiErr)
281
+ : undefined;
282
+ if (!retryBody) {
283
+ throw apiErr;
284
+ }
285
+ res = await fetchImpl(url, {
286
+ method: "POST",
287
+ headers: {
288
+ "Content-Type": "application/json",
289
+ ...getAuthHeaders(),
290
+ },
291
+ body: JSON.stringify(retryBody),
292
+ ...(composedSignal ? { signal: composedSignal } : {}),
293
+ });
294
+ if (!res.ok) {
295
+ throw await buildAPIError(url, retryBody, res);
296
+ }
297
+ }
259
298
  }
260
299
  finally {
261
300
  timeoutController?.cleanup();
262
301
  }
263
- if (!res.ok) {
264
- throw await buildAPIError(url, body, res);
265
- }
266
302
  const json = (await res.json());
267
303
  const choice = json.choices?.[0];
268
304
  const text = (typeof choice?.message?.content === "string"
269
305
  ? choice.message.content
270
306
  : "") ?? "";
271
307
  const content = [];
308
+ // Reasoner-model output (DeepSeek `reasoning_content`, gateway
309
+ // `reasoning`) becomes a V3 reasoning part ahead of the text part —
310
+ // GenerationHandler joins reasoning parts into `result.reasoning`.
311
+ // `||` so an empty-string reasoning_content falls through to a
312
+ // non-empty `reasoning` field instead of shadowing it.
313
+ const reasoningText = choice?.message?.reasoning_content || choice?.message?.reasoning;
314
+ if (typeof reasoningText === "string" && reasoningText.length > 0) {
315
+ content.push({ type: "reasoning", text: reasoningText });
316
+ }
272
317
  if (text.length > 0) {
273
318
  content.push({ type: "text", text });
274
319
  }
@@ -300,8 +345,15 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
300
345
  },
301
346
  outputTokens: {
302
347
  total: json.usage?.completion_tokens,
303
- text: json.usage?.completion_tokens,
304
- reasoning: undefined,
348
+ // Clamped at 0 — some gateways report inconsistent usage where
349
+ // reasoning_tokens exceeds completion_tokens.
350
+ text: json.usage?.completion_tokens !== undefined &&
351
+ json.usage?.completion_tokens_details?.reasoning_tokens !==
352
+ undefined
353
+ ? Math.max(0, json.usage.completion_tokens -
354
+ json.usage.completion_tokens_details.reasoning_tokens)
355
+ : json.usage?.completion_tokens,
356
+ reasoning: json.usage?.completion_tokens_details?.reasoning_tokens,
305
357
  },
306
358
  },
307
359
  warnings: [],
@@ -567,7 +619,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
567
619
  : {}),
568
620
  streaming: true,
569
621
  }), args.modelId));
570
- const res = await args.fetchImpl(args.url, {
622
+ let res = await args.fetchImpl(args.url, {
571
623
  method: "POST",
572
624
  headers: {
573
625
  "Content-Type": "application/json",
@@ -577,13 +629,37 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
577
629
  ...(args.abortSignal ? { signal: args.abortSignal } : {}),
578
630
  });
579
631
  if (!res.ok) {
580
- throw await buildAPIError(args.url, body, res);
632
+ const apiErr = await buildAPIError(args.url, body, res);
633
+ // One-shot 400 retry — same hook as the doGenerate path (e.g. NIM
634
+ // strips chat_template/reasoning_budget when a model rejects them).
635
+ const retryBody = res.status === 400
636
+ ? this.adjustBodyAfter400(body, apiErr)
637
+ : undefined;
638
+ if (!retryBody) {
639
+ throw apiErr;
640
+ }
641
+ res = await args.fetchImpl(args.url, {
642
+ method: "POST",
643
+ headers: {
644
+ "Content-Type": "application/json",
645
+ ...this.getAuthHeaders(),
646
+ },
647
+ body: JSON.stringify(retryBody),
648
+ ...(args.abortSignal ? { signal: args.abortSignal } : {}),
649
+ });
650
+ if (!res.ok) {
651
+ throw await buildAPIError(args.url, retryBody, res);
652
+ }
581
653
  }
582
654
  if (!res.body) {
583
655
  throw new Error(`${this.providerName}: stream response had no body`);
584
656
  }
585
657
  return parseSSEStream(res.body, (delta) => {
586
658
  args.pushChunk({ content: delta });
659
+ }, (reasoningDelta) => {
660
+ // Reasoning rides alongside an empty `content` so plain-text
661
+ // consumers (which only read chunk.content) are unaffected.
662
+ args.pushChunk({ content: "", reasoning: reasoningDelta });
587
663
  });
588
664
  }
589
665
  async executeToolBatch(args) {
@@ -35,7 +35,7 @@ export declare const messagesContainJsonWord: (messages: ReadonlyArray<OpenAICom
35
35
  export declare const ensureJsonWordInBody: (body: OpenAICompatChatRequest) => OpenAICompatChatRequest;
36
36
  export declare const requiresMaxCompletionTokens: (modelId: string) => boolean;
37
37
  export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompatChatRequest;
38
- export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
38
+ export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void, onReasoningDelta?: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
39
39
  export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
40
40
  export declare const createDeferredAnalytics: () => {
41
41
  usagePromise: Promise<{
@@ -397,11 +397,19 @@ export const buildBody = (args) => {
397
397
  if (responseFormat) {
398
398
  body.response_format = responseFormat;
399
399
  }
400
+ // Provider-specific extras attached via options.extraBody (the explicit
401
+ // channel from adjustBuildBodyOptions) land verbatim on the wire body.
402
+ // Spread last — a provider that puts a core field here is intentionally
403
+ // overriding it.
404
+ if (options.extraBody) {
405
+ Object.assign(body, options.extraBody);
406
+ }
400
407
  return body;
401
408
  };
402
- export const parseSSEStream = async (body, onTextDelta) => {
409
+ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
403
410
  const result = {
404
411
  text: "",
412
+ reasoning: "",
405
413
  toolCalls: new Map(),
406
414
  finishReason: null,
407
415
  usage: undefined,
@@ -433,6 +441,16 @@ export const parseSSEStream = async (body, onTextDelta) => {
433
441
  result.text += delta.content;
434
442
  onTextDelta(delta.content);
435
443
  }
444
+ // Reasoner-model deltas: DeepSeek/NIM emit `reasoning_content`, some
445
+ // gateways emit `reasoning`. The AI SDK's openai-compatible wrapper
446
+ // surfaced these automatically; the native client must do the same.
447
+ // `||` (not `??`) so an empty-string reasoning_content falls through to
448
+ // a non-empty `reasoning` field instead of shadowing it.
449
+ const reasoningDelta = delta?.reasoning_content || delta?.reasoning;
450
+ if (reasoningDelta) {
451
+ result.reasoning += reasoningDelta;
452
+ onReasoningDelta?.(reasoningDelta);
453
+ }
436
454
  if (delta?.tool_calls) {
437
455
  for (const tc of delta.tool_calls) {
438
456
  let state = result.toolCalls.get(tc.index);
@@ -104,6 +104,8 @@ export type OpenAICompatChatChoiceMessage = {
104
104
  content?: string | null;
105
105
  tool_calls?: OpenAICompatToolCallWire[];
106
106
  refusal?: string | null;
107
+ reasoning_content?: string | null;
108
+ reasoning?: string | null;
107
109
  };
108
110
  export type OpenAICompatChatChoice = {
109
111
  index: number;
@@ -132,6 +134,8 @@ export type OpenAICompatStreamDelta = {
132
134
  };
133
135
  }>;
134
136
  refusal?: string | null;
137
+ reasoning_content?: string | null;
138
+ reasoning?: string | null;
135
139
  };
136
140
  export type OpenAICompatStreamChunkChoice = {
137
141
  index: number;
@@ -186,6 +190,8 @@ export type OpenAICompatMessage = {
186
190
  };
187
191
  export type OpenAICompatSSEResult = {
188
192
  text: string;
193
+ /** Accumulated reasoner-model output (`reasoning_content` / `reasoning` deltas). */
194
+ reasoning: string;
189
195
  toolCalls: Map<number, {
190
196
  id: string;
191
197
  name: string;
@@ -196,6 +202,7 @@ export type OpenAICompatSSEResult = {
196
202
  };
197
203
  export type OpenAICompatStreamChunk = {
198
204
  content: string;
205
+ reasoning?: string;
199
206
  } | {
200
207
  done: true;
201
208
  };
@@ -241,6 +248,14 @@ export type OpenAICompatBuildBodyArgs = {
241
248
  frequencyPenalty?: number | null;
242
249
  seed?: number | null;
243
250
  stopSequences?: string[];
251
+ /**
252
+ * Provider-specific extra wire fields, spread verbatim onto the final
253
+ * request body by `buildBody` (after the standard fields). This is the
254
+ * explicit channel for non-OpenAI knobs (e.g. NVIDIA NIM's `top_k` /
255
+ * `chat_template_kwargs`) — loose unknown keys returned from
256
+ * `adjustBuildBodyOptions` are intentionally NOT forwarded.
257
+ */
258
+ extraBody?: Record<string, unknown>;
244
259
  };
245
260
  tools?: OpenAICompatChatTool[];
246
261
  toolChoice?: OpenAICompatToolChoiceWire;
@@ -498,6 +498,7 @@ export type StreamOptions = {
498
498
  export type StreamResult = {
499
499
  stream: AsyncIterable<{
500
500
  content: string;
501
+ reasoning?: string;
501
502
  } | StreamNoOutputSentinel | {
502
503
  type: "audio";
503
504
  audio: AudioChunk;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.68.21",
3
+ "version": "9.69.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
6
6
  "author": {
@@ -102,6 +102,7 @@
102
102
  "test:image-gen": "npx tsx test/continuous-test-suite-image-gen-extras.ts",
103
103
  "test:ssrf": "npx tsx test/continuous-test-suite-ssrf.ts",
104
104
  "test:log-sanitize": "npx tsx test/continuous-test-suite-log-sanitize.ts",
105
+ "test:sse-client": "npx tsx test/continuous-test-suite-sse-client.ts",
105
106
  "test:stream-span": "npx tsx test/continuous-test-suite-stream-span.ts",
106
107
  "test:credentials": "npx tsx test/continuous-test-suite-credentials.ts",
107
108
  "test:dynamic": "npx tsx test/continuous-test-suite-dynamic.ts",