@juspay/neurolink 9.69.0 → 9.69.2
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 +8 -0
- package/dist/browser/neurolink.min.js +354 -339
- package/dist/lib/providers/anthropic.d.ts +5 -3
- package/dist/lib/providers/anthropic.js +842 -162
- package/dist/lib/providers/nvidiaNim.d.ts +17 -3
- package/dist/lib/providers/nvidiaNim.js +42 -3
- package/dist/lib/providers/openaiChatCompletionsBase.d.ts +12 -0
- package/dist/lib/providers/openaiChatCompletionsBase.js +68 -8
- package/dist/lib/providers/openaiChatCompletionsClient.js +10 -1
- package/dist/lib/types/openaiCompatible.d.ts +8 -0
- package/dist/lib/utils/noOutputSentinel.d.ts +1 -1
- package/dist/lib/utils/noOutputSentinel.js +1 -1
- package/dist/providers/anthropic.d.ts +5 -3
- package/dist/providers/anthropic.js +842 -162
- package/dist/providers/nvidiaNim.d.ts +17 -3
- package/dist/providers/nvidiaNim.js +42 -3
- package/dist/providers/openaiChatCompletionsBase.d.ts +12 -0
- package/dist/providers/openaiChatCompletionsBase.js +68 -8
- package/dist/providers/openaiChatCompletionsClient.js +10 -1
- package/dist/types/openaiCompatible.d.ts +8 -0
- package/dist/utils/noOutputSentinel.d.ts +1 -1
- package/dist/utils/noOutputSentinel.js +1 -1
- package/package.json +2 -1
- package/dist/lib/providers/anthropicBaseProvider.d.ts +0 -23
- package/dist/lib/providers/anthropicBaseProvider.js +0 -213
- package/dist/providers/anthropicBaseProvider.d.ts +0 -23
- package/dist/providers/anthropicBaseProvider.js +0 -212
|
@@ -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,8 +12,11 @@ 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
|
|
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).
|
|
17
20
|
* reasoning_content is surfaced automatically by the native base client
|
|
18
21
|
* (streamed as `{ content: "", reasoning }` chunks; `result.reasoning` on
|
|
19
22
|
* the non-streaming path); all other behavior is preserved.
|
|
@@ -34,6 +37,17 @@ export declare class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
|
|
|
34
37
|
* value is the full StreamOptions object.
|
|
35
38
|
*/
|
|
36
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;
|
|
37
51
|
protected formatProviderError(error: unknown): Error;
|
|
38
52
|
validateConfiguration(): Promise<boolean>;
|
|
39
53
|
getConfiguration(): {
|
|
@@ -142,8 +142,11 @@ 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
|
|
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).
|
|
147
150
|
* reasoning_content is surfaced automatically by the native base client
|
|
148
151
|
* (streamed as `{ content: "", reasoning }` chunks; `result.reasoning` on
|
|
149
152
|
* the non-streaming path); all other behavior is preserved.
|
|
@@ -195,7 +198,43 @@ export class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
|
|
|
195
198
|
const thinkingEnabled = tl !== undefined && tl !== "minimal";
|
|
196
199
|
const maxTokens = typeof fullOpts.maxTokens === "number" ? fullOpts.maxTokens : undefined;
|
|
197
200
|
const extra = buildNvidiaNimExtraBody(thinkingEnabled, maxTokens);
|
|
198
|
-
|
|
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);
|
|
199
238
|
}
|
|
200
239
|
formatProviderError(error) {
|
|
201
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,13 +268,37 @@ 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"
|
|
@@ -272,7 +308,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
272
308
|
// Reasoner-model output (DeepSeek `reasoning_content`, gateway
|
|
273
309
|
// `reasoning`) becomes a V3 reasoning part ahead of the text part —
|
|
274
310
|
// GenerationHandler joins reasoning parts into `result.reasoning`.
|
|
275
|
-
|
|
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;
|
|
276
314
|
if (typeof reasoningText === "string" && reasoningText.length > 0) {
|
|
277
315
|
content.push({ type: "reasoning", text: reasoningText });
|
|
278
316
|
}
|
|
@@ -307,11 +345,13 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
307
345
|
},
|
|
308
346
|
outputTokens: {
|
|
309
347
|
total: json.usage?.completion_tokens,
|
|
348
|
+
// Clamped at 0 — some gateways report inconsistent usage where
|
|
349
|
+
// reasoning_tokens exceeds completion_tokens.
|
|
310
350
|
text: json.usage?.completion_tokens !== undefined &&
|
|
311
351
|
json.usage?.completion_tokens_details?.reasoning_tokens !==
|
|
312
352
|
undefined
|
|
313
|
-
? json.usage.completion_tokens -
|
|
314
|
-
json.usage.completion_tokens_details.reasoning_tokens
|
|
353
|
+
? Math.max(0, json.usage.completion_tokens -
|
|
354
|
+
json.usage.completion_tokens_details.reasoning_tokens)
|
|
315
355
|
: json.usage?.completion_tokens,
|
|
316
356
|
reasoning: json.usage?.completion_tokens_details?.reasoning_tokens,
|
|
317
357
|
},
|
|
@@ -579,7 +619,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
579
619
|
: {}),
|
|
580
620
|
streaming: true,
|
|
581
621
|
}), args.modelId));
|
|
582
|
-
|
|
622
|
+
let res = await args.fetchImpl(args.url, {
|
|
583
623
|
method: "POST",
|
|
584
624
|
headers: {
|
|
585
625
|
"Content-Type": "application/json",
|
|
@@ -589,7 +629,27 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
589
629
|
...(args.abortSignal ? { signal: args.abortSignal } : {}),
|
|
590
630
|
});
|
|
591
631
|
if (!res.ok) {
|
|
592
|
-
|
|
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
|
+
}
|
|
593
653
|
}
|
|
594
654
|
if (!res.body) {
|
|
595
655
|
throw new Error(`${this.providerName}: stream response had no body`);
|
|
@@ -397,6 +397,13 @@ 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
409
|
export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
|
|
@@ -437,7 +444,9 @@ export const parseSSEStream = async (body, onTextDelta, onReasoningDelta) => {
|
|
|
437
444
|
// Reasoner-model deltas: DeepSeek/NIM emit `reasoning_content`, some
|
|
438
445
|
// gateways emit `reasoning`. The AI SDK's openai-compatible wrapper
|
|
439
446
|
// surfaced these automatically; the native client must do the same.
|
|
440
|
-
|
|
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;
|
|
441
450
|
if (reasoningDelta) {
|
|
442
451
|
result.reasoning += reasoningDelta;
|
|
443
452
|
onReasoningDelta?.(reasoningDelta);
|
|
@@ -248,6 +248,14 @@ export type OpenAICompatBuildBodyArgs = {
|
|
|
248
248
|
frequencyPenalty?: number | null;
|
|
249
249
|
seed?: number | null;
|
|
250
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>;
|
|
251
259
|
};
|
|
252
260
|
tools?: OpenAICompatChatTool[];
|
|
253
261
|
toolChoice?: OpenAICompatToolChoiceWire;
|
|
@@ -44,7 +44,7 @@ underlyingError?: unknown): Promise<{
|
|
|
44
44
|
* observation with the enriched status message. Without this, only
|
|
45
45
|
* `StreamHandler`-based providers produced the rich telemetry; the
|
|
46
46
|
* provider-specific paths (openAI, openaiCompatible, litellm,
|
|
47
|
-
* huggingFace, openRouter,
|
|
47
|
+
* huggingFace, openRouter, anthropic) yielded the sentinel
|
|
48
48
|
* to direct stream consumers but Pipeline B saw nothing.
|
|
49
49
|
*
|
|
50
50
|
* Stamps three attributes:
|
|
@@ -135,7 +135,7 @@ underlyingError) {
|
|
|
135
135
|
* observation with the enriched status message. Without this, only
|
|
136
136
|
* `StreamHandler`-based providers produced the rich telemetry; the
|
|
137
137
|
* provider-specific paths (openAI, openaiCompatible, litellm,
|
|
138
|
-
* huggingFace, openRouter,
|
|
138
|
+
* huggingFace, openRouter, anthropic) yielded the sentinel
|
|
139
139
|
* to direct stream consumers but Pipeline B saw nothing.
|
|
140
140
|
*
|
|
141
141
|
* Stamps three attributes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.69.
|
|
3
|
+
"version": "9.69.2",
|
|
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": {
|
|
@@ -301,6 +301,7 @@
|
|
|
301
301
|
"@ai-sdk/mistral": "^3.0.21",
|
|
302
302
|
"@ai-sdk/openai": "^3.0.37",
|
|
303
303
|
"@ai-sdk/provider": "^3.0.8",
|
|
304
|
+
"@anthropic-ai/sdk": "^0.102.0",
|
|
304
305
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
305
306
|
"@aws-sdk/client-bedrock": "^3.1000.0",
|
|
306
307
|
"@aws-sdk/client-bedrock-runtime": "^3.1000.0",
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { ZodType } from "zod";
|
|
2
|
-
import { type AIProviderName } from "../constants/enums.js";
|
|
3
|
-
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
|
-
import type { StreamOptions, StreamResult } from "../types/index.js";
|
|
5
|
-
import type { LanguageModel, Schema } from "../types/index.js";
|
|
6
|
-
/**
|
|
7
|
-
* Anthropic provider implementation using BaseProvider pattern
|
|
8
|
-
* Migrated from direct API calls to Vercel AI SDK (@ai-sdk/anthropic)
|
|
9
|
-
* Follows exact Google AI interface patterns for compatibility
|
|
10
|
-
*/
|
|
11
|
-
export declare class AnthropicProviderV2 extends BaseProvider {
|
|
12
|
-
constructor(modelName?: string);
|
|
13
|
-
protected getProviderName(): AIProviderName;
|
|
14
|
-
protected getDefaultModel(): string;
|
|
15
|
-
/**
|
|
16
|
-
* Returns the Vercel AI SDK model instance for Anthropic
|
|
17
|
-
*/
|
|
18
|
-
protected getAISDKModel(): LanguageModel;
|
|
19
|
-
protected formatProviderError(error: unknown): Error;
|
|
20
|
-
private getApiKey;
|
|
21
|
-
protected executeStream(options: StreamOptions, _analysisSchema?: ZodType | Schema<unknown>): Promise<StreamResult>;
|
|
22
|
-
}
|
|
23
|
-
export default AnthropicProviderV2;
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2
|
-
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
|
-
import { AnthropicModels } from "../constants/enums.js";
|
|
4
|
-
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
|
-
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
6
|
-
import { logger } from "../utils/logger.js";
|
|
7
|
-
import { buildNoOutputSentinel, detectPostStreamNoOutput, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
|
|
8
|
-
import { calculateCost } from "../utils/pricing.js";
|
|
9
|
-
import { createAnthropicBaseConfig, validateApiKey, } from "../utils/providerConfig.js";
|
|
10
|
-
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
11
|
-
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
12
|
-
import { getModelId } from "./providerTypeUtils.js";
|
|
13
|
-
import { NoOutputGeneratedError } from "../utils/generationErrors.js";
|
|
14
|
-
import { streamText } from "../utils/generation.js";
|
|
15
|
-
const streamTracer = trace.getTracer("neurolink.provider.anthropic");
|
|
16
|
-
/**
|
|
17
|
-
* Anthropic provider implementation using BaseProvider pattern
|
|
18
|
-
* Migrated from direct API calls to Vercel AI SDK (@ai-sdk/anthropic)
|
|
19
|
-
* Follows exact Google AI interface patterns for compatibility
|
|
20
|
-
*/
|
|
21
|
-
export class AnthropicProviderV2 extends BaseProvider {
|
|
22
|
-
constructor(modelName) {
|
|
23
|
-
super(modelName, "anthropic");
|
|
24
|
-
logger.debug("AnthropicProviderV2 initialized", {
|
|
25
|
-
model: this.modelName,
|
|
26
|
-
provider: this.providerName,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
// ===================
|
|
30
|
-
// ABSTRACT METHOD IMPLEMENTATIONS
|
|
31
|
-
// ===================
|
|
32
|
-
getProviderName() {
|
|
33
|
-
return "anthropic";
|
|
34
|
-
}
|
|
35
|
-
getDefaultModel() {
|
|
36
|
-
return process.env.ANTHROPIC_MODEL || AnthropicModels.CLAUDE_3_5_SONNET;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Returns the Vercel AI SDK model instance for Anthropic
|
|
40
|
-
*/
|
|
41
|
-
getAISDKModel() {
|
|
42
|
-
const apiKey = this.getApiKey();
|
|
43
|
-
const anthropic = createAnthropic({ apiKey });
|
|
44
|
-
return anthropic(this.modelName);
|
|
45
|
-
}
|
|
46
|
-
formatProviderError(error) {
|
|
47
|
-
if (error instanceof TimeoutError) {
|
|
48
|
-
return new NetworkError(`Request timed out: ${error.message}`, this.providerName);
|
|
49
|
-
}
|
|
50
|
-
const errorWithStatus = error;
|
|
51
|
-
if (errorWithStatus?.status === 401) {
|
|
52
|
-
return new AuthenticationError("Invalid Anthropic API key. Please check your ANTHROPIC_API_KEY environment variable.", this.providerName);
|
|
53
|
-
}
|
|
54
|
-
if (errorWithStatus?.status === 429) {
|
|
55
|
-
return new RateLimitError("Anthropic rate limit exceeded. Please try again later.", this.providerName);
|
|
56
|
-
}
|
|
57
|
-
if (errorWithStatus?.status === 400) {
|
|
58
|
-
return new ProviderError(`Bad request: ${errorWithStatus?.message || "Invalid request parameters"}`, this.providerName);
|
|
59
|
-
}
|
|
60
|
-
return new ProviderError(`Anthropic error: ${errorWithStatus?.message || String(error) || "Unknown error"}`, this.providerName);
|
|
61
|
-
}
|
|
62
|
-
// Configuration helper - now using consolidated utility
|
|
63
|
-
getApiKey() {
|
|
64
|
-
return validateApiKey(createAnthropicBaseConfig());
|
|
65
|
-
}
|
|
66
|
-
// executeGenerate removed - BaseProvider handles all generation with tools
|
|
67
|
-
async executeStream(options, _analysisSchema) {
|
|
68
|
-
// Note: StreamOptions validation handled differently than TextGenerationOptions
|
|
69
|
-
const model = await this.getAISDKModelWithMiddleware(options);
|
|
70
|
-
const timeout = this.getTimeout(options);
|
|
71
|
-
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
72
|
-
try {
|
|
73
|
-
// Get tools - options.tools is pre-merged by BaseProvider.stream()
|
|
74
|
-
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
75
|
-
const tools = shouldUseTools
|
|
76
|
-
? options.tools || (await this.getAllTools())
|
|
77
|
-
: {};
|
|
78
|
-
// Wrap streamText in an OTel span to capture provider-level latency and token usage
|
|
79
|
-
const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
|
|
80
|
-
kind: SpanKind.CLIENT,
|
|
81
|
-
attributes: {
|
|
82
|
-
"gen_ai.system": "anthropic",
|
|
83
|
-
"gen_ai.request.model": getModelId(model, this.modelName || "unknown"),
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
// Reviewer follow-up: capture upstream provider errors via onError
|
|
87
|
-
// so the post-stream NoOutput detect can propagate the real cause
|
|
88
|
-
// into the sentinel's providerError / modelResponseRaw.
|
|
89
|
-
let capturedProviderError;
|
|
90
|
-
let result;
|
|
91
|
-
try {
|
|
92
|
-
result = streamText({
|
|
93
|
-
model,
|
|
94
|
-
prompt: options.input.text ?? "",
|
|
95
|
-
system: options.systemPrompt,
|
|
96
|
-
temperature: options.temperature,
|
|
97
|
-
maxOutputTokens: options.maxTokens, // No default limit - unlimited unless specified
|
|
98
|
-
maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
|
|
99
|
-
tools,
|
|
100
|
-
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
101
|
-
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
102
|
-
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
103
|
-
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
104
|
-
onError: (event) => {
|
|
105
|
-
capturedProviderError = event.error;
|
|
106
|
-
logger.error("AnthropicBaseProvider: Stream error", {
|
|
107
|
-
error: event.error instanceof Error
|
|
108
|
-
? event.error.message
|
|
109
|
-
: String(event.error),
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
113
|
-
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
114
|
-
logger.warn("[AnthropicBaseProvider] Failed to store tool executions", {
|
|
115
|
-
provider: this.providerName,
|
|
116
|
-
error: error instanceof Error ? error.message : String(error),
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
streamSpan.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
124
|
-
streamSpan.setStatus({
|
|
125
|
-
code: SpanStatusCode.ERROR,
|
|
126
|
-
message: err instanceof Error ? err.message : String(err),
|
|
127
|
-
});
|
|
128
|
-
streamSpan.end();
|
|
129
|
-
throw err;
|
|
130
|
-
}
|
|
131
|
-
// Collect token usage and finish reason asynchronously when the stream completes,
|
|
132
|
-
// then end the span. This avoids blocking the stream consumer.
|
|
133
|
-
Promise.resolve(result.usage)
|
|
134
|
-
.then((usage) => {
|
|
135
|
-
streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens || 0);
|
|
136
|
-
streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens || 0);
|
|
137
|
-
const cost = calculateCost(this.providerName, this.modelName, {
|
|
138
|
-
input: usage.inputTokens || 0,
|
|
139
|
-
output: usage.outputTokens || 0,
|
|
140
|
-
total: (usage.inputTokens || 0) + (usage.outputTokens || 0),
|
|
141
|
-
});
|
|
142
|
-
if (cost && cost > 0) {
|
|
143
|
-
streamSpan.setAttribute("neurolink.cost", cost);
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
.catch(() => {
|
|
147
|
-
// Usage may not be available if the stream is aborted
|
|
148
|
-
});
|
|
149
|
-
Promise.resolve(result.finishReason)
|
|
150
|
-
.then((reason) => {
|
|
151
|
-
streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
|
|
152
|
-
})
|
|
153
|
-
.catch(() => {
|
|
154
|
-
// Finish reason may not be available if the stream is aborted
|
|
155
|
-
});
|
|
156
|
-
Promise.resolve(result.text)
|
|
157
|
-
.then(() => {
|
|
158
|
-
streamSpan.end();
|
|
159
|
-
})
|
|
160
|
-
.catch((err) => {
|
|
161
|
-
streamSpan.setStatus({
|
|
162
|
-
code: SpanStatusCode.ERROR,
|
|
163
|
-
message: err instanceof Error ? err.message : String(err),
|
|
164
|
-
});
|
|
165
|
-
streamSpan.end();
|
|
166
|
-
});
|
|
167
|
-
timeoutController?.cleanup();
|
|
168
|
-
// Transform string stream to content object stream (match Google AI pattern)
|
|
169
|
-
const transformedStream = async function* () {
|
|
170
|
-
let chunkCount = 0;
|
|
171
|
-
try {
|
|
172
|
-
for await (const chunk of result.textStream) {
|
|
173
|
-
chunkCount++;
|
|
174
|
-
yield { content: chunk };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch (streamError) {
|
|
178
|
-
if (NoOutputGeneratedError.isInstance(streamError)) {
|
|
179
|
-
logger.warn("AnthropicBaseProvider: Stream produced no output (NoOutputGeneratedError) — caught from textStream");
|
|
180
|
-
const sentinel = await buildNoOutputSentinel(streamError, result, capturedProviderError);
|
|
181
|
-
stampNoOutputSpan(sentinel);
|
|
182
|
-
yield sentinel;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
throw streamError;
|
|
186
|
-
}
|
|
187
|
-
// Curator P3-6 (round-2 fix): production trigger sets the error
|
|
188
|
-
// on result.finishReason rejection, not on textStream iteration.
|
|
189
|
-
// Surface that path here so the sentinel actually fires.
|
|
190
|
-
if (chunkCount === 0) {
|
|
191
|
-
const detected = await detectPostStreamNoOutput(result, capturedProviderError);
|
|
192
|
-
if (detected) {
|
|
193
|
-
logger.warn("AnthropicBaseProvider: Stream produced no output (NoOutputGeneratedError) — caught from finishReason rejection");
|
|
194
|
-
stampNoOutputSpan(detected.sentinel);
|
|
195
|
-
yield detected.sentinel;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
return {
|
|
200
|
-
stream: transformedStream(),
|
|
201
|
-
provider: this.providerName,
|
|
202
|
-
model: this.modelName,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
timeoutController?.cleanup();
|
|
207
|
-
throw this.handleProviderError(error);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// Export for testing
|
|
212
|
-
export default AnthropicProviderV2;
|
|
213
|
-
//# sourceMappingURL=anthropicBaseProvider.js.map
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { ZodType } from "zod";
|
|
2
|
-
import { type AIProviderName } from "../constants/enums.js";
|
|
3
|
-
import { BaseProvider } from "../core/baseProvider.js";
|
|
4
|
-
import type { StreamOptions, StreamResult } from "../types/index.js";
|
|
5
|
-
import type { LanguageModel, Schema } from "../types/index.js";
|
|
6
|
-
/**
|
|
7
|
-
* Anthropic provider implementation using BaseProvider pattern
|
|
8
|
-
* Migrated from direct API calls to Vercel AI SDK (@ai-sdk/anthropic)
|
|
9
|
-
* Follows exact Google AI interface patterns for compatibility
|
|
10
|
-
*/
|
|
11
|
-
export declare class AnthropicProviderV2 extends BaseProvider {
|
|
12
|
-
constructor(modelName?: string);
|
|
13
|
-
protected getProviderName(): AIProviderName;
|
|
14
|
-
protected getDefaultModel(): string;
|
|
15
|
-
/**
|
|
16
|
-
* Returns the Vercel AI SDK model instance for Anthropic
|
|
17
|
-
*/
|
|
18
|
-
protected getAISDKModel(): LanguageModel;
|
|
19
|
-
protected formatProviderError(error: unknown): Error;
|
|
20
|
-
private getApiKey;
|
|
21
|
-
protected executeStream(options: StreamOptions, _analysisSchema?: ZodType | Schema<unknown>): Promise<StreamResult>;
|
|
22
|
-
}
|
|
23
|
-
export default AnthropicProviderV2;
|