@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.
@@ -16,12 +16,12 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
16
16
  * `@ai-sdk/openai-compatible` path this migration replaced. The base
17
17
  * client injects the literal "json" word the API requires for that mode.
18
18
  *
19
- * 2. Reasoning support — the opt-in `thinking` request param (non-reasoner
20
- * chat models) and `reasoning_content` surfacing (deepseek-reasoner / R1)
21
- * both require plumbing the thinking signal and the reasoning delta
22
- * through the native base client. That plumbing isn't in place yet, so
23
- * neither is wired here; it is tracked as a base-client follow-up. All
24
- * other behavior is preserved.
19
+ * 2. Reasoning support — `reasoning_content` (deepseek-reasoner / R1) is
20
+ * surfaced automatically by the native base client: streamed deltas
21
+ * arrive as `{ content: "", reasoning }` chunks and the non-streaming
22
+ * result carries `result.reasoning`. The opt-in `thinking` request
23
+ * param (non-reasoner chat models) still needs thinking-signal plumbing
24
+ * and is tracked as a follow-up. All other behavior is preserved.
25
25
  *
26
26
  * @see https://api-docs.deepseek.com
27
27
  */
@@ -27,12 +27,12 @@ const getDefaultDeepSeekModel = () => {
27
27
  * `@ai-sdk/openai-compatible` path this migration replaced. The base
28
28
  * client injects the literal "json" word the API requires for that mode.
29
29
  *
30
- * 2. Reasoning support — the opt-in `thinking` request param (non-reasoner
31
- * chat models) and `reasoning_content` surfacing (deepseek-reasoner / R1)
32
- * both require plumbing the thinking signal and the reasoning delta
33
- * through the native base client. That plumbing isn't in place yet, so
34
- * neither is wired here; it is tracked as a base-client follow-up. All
35
- * other behavior is preserved.
30
+ * 2. Reasoning support — `reasoning_content` (deepseek-reasoner / R1) is
31
+ * surfaced automatically by the native base client: streamed deltas
32
+ * arrive as `{ content: "", reasoning }` chunks and the non-streaming
33
+ * result carries `result.reasoning`. The opt-in `thinking` request
34
+ * param (non-reasoner chat models) still needs thinking-signal plumbing
35
+ * and is tracked as a follow-up. All other behavior is preserved.
36
36
  *
37
37
  * @see https://api-docs.deepseek.com
38
38
  */
@@ -1,5 +1,5 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
- import type { NeurolinkCredentials, OpenAICompatBuildBodyArgs } from "../types/index.js";
2
+ import type { NeurolinkCredentials, OpenAICompatBuildBodyArgs, OpenAICompatChatRequest } from "../types/index.js";
3
3
  import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
4
4
  declare const isNimFieldRejection: (body: string, field: string) => boolean;
5
5
  /**
@@ -12,9 +12,14 @@ declare const stripFieldFromJsonBody: (body: string, field: "reasoning_budget" |
12
12
  * NVIDIA NIM Provider — native HTTP+SSE, no AI SDK.
13
13
  *
14
14
  * Wraps NVIDIA's hosted (or self-hosted) inference endpoints.
15
- * Passes NIM-specific extras (top_k, min_p, repetition_penalty,
16
- * chat_template_kwargs.reasoning_budget) via adjustBuildBodyOptions.
17
- * reasoning_content surfacing is a pending base-client follow-up (not emitted natively yet); all other behavior is preserved.
15
+ * Passes NIM-specific extras (top_k, min_p, repetition_penalty, min_tokens,
16
+ * chat_template, chat_template_kwargs.reasoning_budget) to the wire via the
17
+ * `extraBody` channel of adjustBuildBodyOptions, and retries once without
18
+ * `chat_template`/`reasoning_budget` when a model server rejects them with a
19
+ * 400 (adjustBodyAfter400 — applies on both generate and stream paths).
20
+ * reasoning_content is surfaced automatically by the native base client
21
+ * (streamed as `{ content: "", reasoning }` chunks; `result.reasoning` on
22
+ * the non-streaming path); all other behavior is preserved.
18
23
  *
19
24
  * @see https://docs.api.nvidia.com/nim/reference/
20
25
  */
@@ -32,6 +37,17 @@ export declare class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
32
37
  * value is the full StreamOptions object.
33
38
  */
34
39
  protected adjustBuildBodyOptions(_modelId: string, opts: OpenAICompatBuildBodyArgs["options"]): OpenAICompatBuildBodyArgs["options"] & Record<string, unknown>;
40
+ /**
41
+ * One-shot 400 retry (base hook): some NIM model servers reject
42
+ * `chat_template` or `chat_template_kwargs.reasoning_budget` with a 400
43
+ * naming the field. Strip the rejected field(s) and let the base retry
44
+ * once, restoring the pre-migration fetch-level behavior. Returns
45
+ * undefined for unrelated 400s so they propagate unchanged.
46
+ */
47
+ protected adjustBodyAfter400(body: OpenAICompatChatRequest, error: Error & {
48
+ statusCode?: number;
49
+ responseBody?: string;
50
+ }): OpenAICompatChatRequest | undefined;
35
51
  protected formatProviderError(error: unknown): Error;
36
52
  validateConfiguration(): Promise<boolean>;
37
53
  getConfiguration(): {
@@ -142,9 +142,14 @@ const getDefaultNimModel = () => {
142
142
  * NVIDIA NIM Provider — native HTTP+SSE, no AI SDK.
143
143
  *
144
144
  * Wraps NVIDIA's hosted (or self-hosted) inference endpoints.
145
- * Passes NIM-specific extras (top_k, min_p, repetition_penalty,
146
- * chat_template_kwargs.reasoning_budget) via adjustBuildBodyOptions.
147
- * reasoning_content surfacing is a pending base-client follow-up (not emitted natively yet); all other behavior is preserved.
145
+ * Passes NIM-specific extras (top_k, min_p, repetition_penalty, min_tokens,
146
+ * chat_template, chat_template_kwargs.reasoning_budget) to the wire via the
147
+ * `extraBody` channel of adjustBuildBodyOptions, and retries once without
148
+ * `chat_template`/`reasoning_budget` when a model server rejects them with a
149
+ * 400 (adjustBodyAfter400 — applies on both generate and stream paths).
150
+ * reasoning_content is surfaced automatically by the native base client
151
+ * (streamed as `{ content: "", reasoning }` chunks; `result.reasoning` on
152
+ * the non-streaming path); all other behavior is preserved.
148
153
  *
149
154
  * @see https://docs.api.nvidia.com/nim/reference/
150
155
  */
@@ -193,7 +198,43 @@ export class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
193
198
  const thinkingEnabled = tl !== undefined && tl !== "minimal";
194
199
  const maxTokens = typeof fullOpts.maxTokens === "number" ? fullOpts.maxTokens : undefined;
195
200
  const extra = buildNvidiaNimExtraBody(thinkingEnabled, maxTokens);
196
- return { ...opts, ...extra };
201
+ // Attach via the explicit extraBody channel — buildBody spreads it onto
202
+ // the wire body. (Loose unknown keys on the returned options object are
203
+ // dropped by buildBody, which is why the extras previously never reached
204
+ // the wire.)
205
+ return Object.keys(extra).length > 0
206
+ ? { ...opts, extraBody: extra }
207
+ : opts;
208
+ }
209
+ /**
210
+ * One-shot 400 retry (base hook): some NIM model servers reject
211
+ * `chat_template` or `chat_template_kwargs.reasoning_budget` with a 400
212
+ * naming the field. Strip the rejected field(s) and let the base retry
213
+ * once, restoring the pre-migration fetch-level behavior. Returns
214
+ * undefined for unrelated 400s so they propagate unchanged.
215
+ */
216
+ adjustBodyAfter400(body, error) {
217
+ const responseBody = error.responseBody ?? "";
218
+ let serialized = JSON.stringify(body);
219
+ const strippedFields = [];
220
+ for (const field of ["chat_template", "reasoning_budget"]) {
221
+ if (isNimFieldRejection(responseBody, field)) {
222
+ const next = stripFieldFromJsonBody(serialized, field);
223
+ if (next !== null) {
224
+ serialized = next;
225
+ strippedFields.push(field);
226
+ }
227
+ }
228
+ }
229
+ if (strippedFields.length === 0) {
230
+ return undefined;
231
+ }
232
+ logger.debug("NVIDIA NIM: 400 rejected request field(s) — retrying once without them", {
233
+ strippedFields,
234
+ model: body.model,
235
+ rejection: responseBody.slice(0, 200),
236
+ });
237
+ return JSON.parse(serialized);
197
238
  }
198
239
  formatProviderError(error) {
199
240
  if (error instanceof TimeoutError) {
@@ -70,6 +70,18 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
70
70
  * `max_tokens` and require `max_completion_tokens`.
71
71
  */
72
72
  protected adjustRequestBody(body: OpenAICompatChatRequest, _modelId: string): OpenAICompatChatRequest;
73
+ /**
74
+ * Hook called when a request fails with HTTP 400. Return a modified body to
75
+ * retry ONCE with it; return undefined (default) to propagate the error
76
+ * unchanged. Applies on both the non-streaming doGenerate path and the
77
+ * streaming path. NVIDIA NIM uses this to strip `chat_template` /
78
+ * `chat_template_kwargs.reasoning_budget` when a model server rejects them,
79
+ * restoring the pre-migration fetch-level retry behavior.
80
+ */
81
+ protected adjustBodyAfter400(_body: OpenAICompatChatRequest, _error: Error & {
82
+ statusCode?: number;
83
+ responseBody?: string;
84
+ }): OpenAICompatChatRequest | undefined;
73
85
  /**
74
86
  * Hook called once at the start of every `executeStream` invocation.
75
87
  * Return lifecycle listeners (onUsage / onFinish) to receive deferred
@@ -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;
@@ -16,12 +16,12 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
16
16
  * `@ai-sdk/openai-compatible` path this migration replaced. The base
17
17
  * client injects the literal "json" word the API requires for that mode.
18
18
  *
19
- * 2. Reasoning support — the opt-in `thinking` request param (non-reasoner
20
- * chat models) and `reasoning_content` surfacing (deepseek-reasoner / R1)
21
- * both require plumbing the thinking signal and the reasoning delta
22
- * through the native base client. That plumbing isn't in place yet, so
23
- * neither is wired here; it is tracked as a base-client follow-up. All
24
- * other behavior is preserved.
19
+ * 2. Reasoning support — `reasoning_content` (deepseek-reasoner / R1) is
20
+ * surfaced automatically by the native base client: streamed deltas
21
+ * arrive as `{ content: "", reasoning }` chunks and the non-streaming
22
+ * result carries `result.reasoning`. The opt-in `thinking` request
23
+ * param (non-reasoner chat models) still needs thinking-signal plumbing
24
+ * and is tracked as a follow-up. All other behavior is preserved.
25
25
  *
26
26
  * @see https://api-docs.deepseek.com
27
27
  */
@@ -27,12 +27,12 @@ const getDefaultDeepSeekModel = () => {
27
27
  * `@ai-sdk/openai-compatible` path this migration replaced. The base
28
28
  * client injects the literal "json" word the API requires for that mode.
29
29
  *
30
- * 2. Reasoning support — the opt-in `thinking` request param (non-reasoner
31
- * chat models) and `reasoning_content` surfacing (deepseek-reasoner / R1)
32
- * both require plumbing the thinking signal and the reasoning delta
33
- * through the native base client. That plumbing isn't in place yet, so
34
- * neither is wired here; it is tracked as a base-client follow-up. All
35
- * other behavior is preserved.
30
+ * 2. Reasoning support — `reasoning_content` (deepseek-reasoner / R1) is
31
+ * surfaced automatically by the native base client: streamed deltas
32
+ * arrive as `{ content: "", reasoning }` chunks and the non-streaming
33
+ * result carries `result.reasoning`. The opt-in `thinking` request
34
+ * param (non-reasoner chat models) still needs thinking-signal plumbing
35
+ * and is tracked as a follow-up. All other behavior is preserved.
36
36
  *
37
37
  * @see https://api-docs.deepseek.com
38
38
  */
@@ -1,5 +1,5 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
- import type { NeurolinkCredentials, OpenAICompatBuildBodyArgs } from "../types/index.js";
2
+ import type { NeurolinkCredentials, OpenAICompatBuildBodyArgs, OpenAICompatChatRequest } from "../types/index.js";
3
3
  import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
4
4
  declare const isNimFieldRejection: (body: string, field: string) => boolean;
5
5
  /**
@@ -12,9 +12,14 @@ declare const stripFieldFromJsonBody: (body: string, field: "reasoning_budget" |
12
12
  * NVIDIA NIM Provider — native HTTP+SSE, no AI SDK.
13
13
  *
14
14
  * Wraps NVIDIA's hosted (or self-hosted) inference endpoints.
15
- * Passes NIM-specific extras (top_k, min_p, repetition_penalty,
16
- * chat_template_kwargs.reasoning_budget) via adjustBuildBodyOptions.
17
- * reasoning_content surfacing is a pending base-client follow-up (not emitted natively yet); all other behavior is preserved.
15
+ * Passes NIM-specific extras (top_k, min_p, repetition_penalty, min_tokens,
16
+ * chat_template, chat_template_kwargs.reasoning_budget) to the wire via the
17
+ * `extraBody` channel of adjustBuildBodyOptions, and retries once without
18
+ * `chat_template`/`reasoning_budget` when a model server rejects them with a
19
+ * 400 (adjustBodyAfter400 — applies on both generate and stream paths).
20
+ * reasoning_content is surfaced automatically by the native base client
21
+ * (streamed as `{ content: "", reasoning }` chunks; `result.reasoning` on
22
+ * the non-streaming path); all other behavior is preserved.
18
23
  *
19
24
  * @see https://docs.api.nvidia.com/nim/reference/
20
25
  */
@@ -32,6 +37,17 @@ export declare class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
32
37
  * value is the full StreamOptions object.
33
38
  */
34
39
  protected adjustBuildBodyOptions(_modelId: string, opts: OpenAICompatBuildBodyArgs["options"]): OpenAICompatBuildBodyArgs["options"] & Record<string, unknown>;
40
+ /**
41
+ * One-shot 400 retry (base hook): some NIM model servers reject
42
+ * `chat_template` or `chat_template_kwargs.reasoning_budget` with a 400
43
+ * naming the field. Strip the rejected field(s) and let the base retry
44
+ * once, restoring the pre-migration fetch-level behavior. Returns
45
+ * undefined for unrelated 400s so they propagate unchanged.
46
+ */
47
+ protected adjustBodyAfter400(body: OpenAICompatChatRequest, error: Error & {
48
+ statusCode?: number;
49
+ responseBody?: string;
50
+ }): OpenAICompatChatRequest | undefined;
35
51
  protected formatProviderError(error: unknown): Error;
36
52
  validateConfiguration(): Promise<boolean>;
37
53
  getConfiguration(): {
@@ -142,9 +142,14 @@ const getDefaultNimModel = () => {
142
142
  * NVIDIA NIM Provider — native HTTP+SSE, no AI SDK.
143
143
  *
144
144
  * Wraps NVIDIA's hosted (or self-hosted) inference endpoints.
145
- * Passes NIM-specific extras (top_k, min_p, repetition_penalty,
146
- * chat_template_kwargs.reasoning_budget) via adjustBuildBodyOptions.
147
- * reasoning_content surfacing is a pending base-client follow-up (not emitted natively yet); all other behavior is preserved.
145
+ * Passes NIM-specific extras (top_k, min_p, repetition_penalty, min_tokens,
146
+ * chat_template, chat_template_kwargs.reasoning_budget) to the wire via the
147
+ * `extraBody` channel of adjustBuildBodyOptions, and retries once without
148
+ * `chat_template`/`reasoning_budget` when a model server rejects them with a
149
+ * 400 (adjustBodyAfter400 — applies on both generate and stream paths).
150
+ * reasoning_content is surfaced automatically by the native base client
151
+ * (streamed as `{ content: "", reasoning }` chunks; `result.reasoning` on
152
+ * the non-streaming path); all other behavior is preserved.
148
153
  *
149
154
  * @see https://docs.api.nvidia.com/nim/reference/
150
155
  */
@@ -193,7 +198,43 @@ export class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
193
198
  const thinkingEnabled = tl !== undefined && tl !== "minimal";
194
199
  const maxTokens = typeof fullOpts.maxTokens === "number" ? fullOpts.maxTokens : undefined;
195
200
  const extra = buildNvidiaNimExtraBody(thinkingEnabled, maxTokens);
196
- return { ...opts, ...extra };
201
+ // Attach via the explicit extraBody channel — buildBody spreads it onto
202
+ // the wire body. (Loose unknown keys on the returned options object are
203
+ // dropped by buildBody, which is why the extras previously never reached
204
+ // the wire.)
205
+ return Object.keys(extra).length > 0
206
+ ? { ...opts, extraBody: extra }
207
+ : opts;
208
+ }
209
+ /**
210
+ * One-shot 400 retry (base hook): some NIM model servers reject
211
+ * `chat_template` or `chat_template_kwargs.reasoning_budget` with a 400
212
+ * naming the field. Strip the rejected field(s) and let the base retry
213
+ * once, restoring the pre-migration fetch-level behavior. Returns
214
+ * undefined for unrelated 400s so they propagate unchanged.
215
+ */
216
+ adjustBodyAfter400(body, error) {
217
+ const responseBody = error.responseBody ?? "";
218
+ let serialized = JSON.stringify(body);
219
+ const strippedFields = [];
220
+ for (const field of ["chat_template", "reasoning_budget"]) {
221
+ if (isNimFieldRejection(responseBody, field)) {
222
+ const next = stripFieldFromJsonBody(serialized, field);
223
+ if (next !== null) {
224
+ serialized = next;
225
+ strippedFields.push(field);
226
+ }
227
+ }
228
+ }
229
+ if (strippedFields.length === 0) {
230
+ return undefined;
231
+ }
232
+ logger.debug("NVIDIA NIM: 400 rejected request field(s) — retrying once without them", {
233
+ strippedFields,
234
+ model: body.model,
235
+ rejection: responseBody.slice(0, 200),
236
+ });
237
+ return JSON.parse(serialized);
197
238
  }
198
239
  formatProviderError(error) {
199
240
  if (error instanceof TimeoutError) {
@@ -70,6 +70,18 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
70
70
  * `max_tokens` and require `max_completion_tokens`.
71
71
  */
72
72
  protected adjustRequestBody(body: OpenAICompatChatRequest, _modelId: string): OpenAICompatChatRequest;
73
+ /**
74
+ * Hook called when a request fails with HTTP 400. Return a modified body to
75
+ * retry ONCE with it; return undefined (default) to propagate the error
76
+ * unchanged. Applies on both the non-streaming doGenerate path and the
77
+ * streaming path. NVIDIA NIM uses this to strip `chat_template` /
78
+ * `chat_template_kwargs.reasoning_budget` when a model server rejects them,
79
+ * restoring the pre-migration fetch-level retry behavior.
80
+ */
81
+ protected adjustBodyAfter400(_body: OpenAICompatChatRequest, _error: Error & {
82
+ statusCode?: number;
83
+ responseBody?: string;
84
+ }): OpenAICompatChatRequest | undefined;
73
85
  /**
74
86
  * Hook called once at the start of every `executeStream` invocation.
75
87
  * Return lifecycle listeners (onUsage / onFinish) to receive deferred