@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +276 -276
- package/dist/lib/providers/deepseek.d.ts +6 -6
- package/dist/lib/providers/deepseek.js +6 -6
- package/dist/lib/providers/nvidiaNim.d.ts +20 -4
- package/dist/lib/providers/nvidiaNim.js +45 -4
- package/dist/lib/providers/openaiChatCompletionsBase.d.ts +12 -0
- package/dist/lib/providers/openaiChatCompletionsBase.js +83 -7
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +1 -1
- package/dist/lib/providers/openaiChatCompletionsClient.js +19 -1
- package/dist/lib/types/openaiCompatible.d.ts +15 -0
- package/dist/lib/types/stream.d.ts +1 -0
- package/dist/providers/deepseek.d.ts +6 -6
- package/dist/providers/deepseek.js +6 -6
- package/dist/providers/nvidiaNim.d.ts +20 -4
- package/dist/providers/nvidiaNim.js +45 -4
- package/dist/providers/openaiChatCompletionsBase.d.ts +12 -0
- package/dist/providers/openaiChatCompletionsBase.js +83 -7
- package/dist/providers/openaiChatCompletionsClient.d.ts +1 -1
- package/dist/providers/openaiChatCompletionsClient.js +19 -1
- package/dist/types/openaiCompatible.d.ts +15 -0
- package/dist/types/stream.d.ts +1 -0
- package/package.json +2 -1
|
@@ -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 —
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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 —
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
17
|
-
*
|
|
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
|
|
147
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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 —
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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 —
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
17
|
-
*
|
|
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
|
|
147
|
-
*
|
|
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
|
-
|
|
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
|