@juspay/neurolink 9.69.1 → 9.69.3
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 +353 -338
- package/dist/client/aiSdkAdapter.d.ts +4 -3
- package/dist/client/aiSdkAdapter.js +4 -3
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -1
- package/dist/lib/client/aiSdkAdapter.d.ts +4 -3
- package/dist/lib/client/aiSdkAdapter.js +4 -3
- package/dist/lib/client/index.d.ts +2 -1
- package/dist/lib/client/index.js +2 -1
- package/dist/lib/providers/anthropic.d.ts +5 -3
- package/dist/lib/providers/anthropic.js +842 -162
- package/dist/lib/providers/googleVertex.js +15 -1
- 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/googleVertex.js +15 -1
- 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
|
-
import
|
|
2
|
-
import { SpanKind,
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { SpanKind, trace } from "@opentelemetry/api";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { join } from "path";
|
|
@@ -12,14 +12,16 @@ import { createOAuthFetch } from "../proxy/oauthFetch.js";
|
|
|
12
12
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
13
13
|
import { AuthenticationError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
|
+
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
15
16
|
import { calculateCost } from "../utils/pricing.js";
|
|
16
17
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
|
|
17
|
-
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
18
|
+
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
|
|
18
19
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
19
20
|
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { NoOutputGeneratedError } from "../utils/generationErrors.js";
|
|
22
|
+
import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
|
|
23
|
+
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
24
|
+
import { createChunkQueue, createDeferredAnalytics, stringifyToolInput, } from "./openaiChatCompletionsClient.js";
|
|
23
25
|
/**
|
|
24
26
|
* Beta headers for Claude Code integration.
|
|
25
27
|
* These enable experimental features:
|
|
@@ -203,12 +205,304 @@ const parseRateLimitHeaders = (headers) => {
|
|
|
203
205
|
retryAfter: parseNumber(getHeader("retry-after")),
|
|
204
206
|
};
|
|
205
207
|
};
|
|
208
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
209
|
+
// Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
|
|
210
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Convert an image part (data URL, bare base64, https URL, or byte array)
|
|
213
|
+
* into an Anthropic image block. Returns undefined for unusable inputs.
|
|
214
|
+
*/
|
|
215
|
+
const toAnthropicImageBlock = (data) => {
|
|
216
|
+
if (data instanceof Uint8Array) {
|
|
217
|
+
return {
|
|
218
|
+
type: "image",
|
|
219
|
+
source: {
|
|
220
|
+
type: "base64",
|
|
221
|
+
media_type: "image/png",
|
|
222
|
+
data: Buffer.from(data).toString("base64"),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (typeof data !== "string" && !(data instanceof URL)) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const str = data instanceof URL ? data.toString() : data;
|
|
230
|
+
const dataUrlMatch = str.match(/^data:(image\/[a-z+.-]+);base64,(.+)$/i);
|
|
231
|
+
if (dataUrlMatch) {
|
|
232
|
+
return {
|
|
233
|
+
type: "image",
|
|
234
|
+
source: {
|
|
235
|
+
type: "base64",
|
|
236
|
+
media_type: dataUrlMatch[1],
|
|
237
|
+
data: dataUrlMatch[2],
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (/^https?:\/\//i.test(str)) {
|
|
242
|
+
return { type: "image", source: { type: "url", url: str } };
|
|
243
|
+
}
|
|
244
|
+
// Bare base64 payload — assume PNG (matches the OpenAI-compat client).
|
|
245
|
+
return {
|
|
246
|
+
type: "image",
|
|
247
|
+
source: { type: "base64", media_type: "image/png", data: str },
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Read an Anthropic cache breakpoint from a message/part/tool carrier.
|
|
252
|
+
* MessageBuilder marks system messages (and GenerationHandler marks the last
|
|
253
|
+
* tool definition) with `providerOptions.anthropic.cacheControl` — the
|
|
254
|
+
* AI-SDK-era prompt-caching contract this native path must keep honoring.
|
|
255
|
+
*/
|
|
256
|
+
const cacheControlOf = (carrier) => {
|
|
257
|
+
const cc = carrier?.providerOptions?.anthropic?.cacheControl;
|
|
258
|
+
return cc?.type === "ephemeral" ? { type: "ephemeral" } : undefined;
|
|
259
|
+
};
|
|
260
|
+
/** Serialize a tool-result `output` into text for a tool_result block. */
|
|
261
|
+
const stringifyAnthropicToolOutput = (output) => {
|
|
262
|
+
if (output === null || output === undefined) {
|
|
263
|
+
return "";
|
|
264
|
+
}
|
|
265
|
+
if (typeof output === "string") {
|
|
266
|
+
return output;
|
|
267
|
+
}
|
|
268
|
+
const o = output;
|
|
269
|
+
if (o.type === "text" && typeof o.value === "string") {
|
|
270
|
+
return o.value;
|
|
271
|
+
}
|
|
272
|
+
if (o.type === "json" || o.type === "error-json") {
|
|
273
|
+
try {
|
|
274
|
+
return JSON.stringify(o.value);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return String(o.value);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
return JSON.stringify(output);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return String(output);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Convert NeuroLink/V3-shaped messages (the shape produced by
|
|
289
|
+
* buildMessagesForStream and by the AI-SDK prompt on the V3 doGenerate path)
|
|
290
|
+
* into the Anthropic Messages payload: a top-level `system` string plus
|
|
291
|
+
* alternating user/assistant messages with typed content blocks.
|
|
292
|
+
*/
|
|
293
|
+
const messagesToAnthropic = (msgs) => {
|
|
294
|
+
const systemBlocks = [];
|
|
295
|
+
const messages = [];
|
|
296
|
+
const partsOf = (content) => Array.isArray(content) ? content : [content];
|
|
297
|
+
// Message-level cache breakpoints apply to the message's LAST content
|
|
298
|
+
// block (the AI-SDK convention MessageBuilder relies on).
|
|
299
|
+
const applyMessageCacheControl = (blocks, msg) => {
|
|
300
|
+
const cc = cacheControlOf(msg);
|
|
301
|
+
if (cc && blocks.length > 0) {
|
|
302
|
+
const last = blocks[blocks.length - 1];
|
|
303
|
+
last.cache_control = cc;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
for (const msg of msgs) {
|
|
307
|
+
switch (msg.role) {
|
|
308
|
+
case "system": {
|
|
309
|
+
const text = typeof msg.content === "string"
|
|
310
|
+
? msg.content
|
|
311
|
+
: partsOf(msg.content)
|
|
312
|
+
.map((p) => typeof p === "string"
|
|
313
|
+
? p
|
|
314
|
+
: String(p?.text ?? ""))
|
|
315
|
+
.join("\n");
|
|
316
|
+
const cc = cacheControlOf(msg);
|
|
317
|
+
systemBlocks.push({
|
|
318
|
+
type: "text",
|
|
319
|
+
text,
|
|
320
|
+
...(cc ? { cache_control: cc } : {}),
|
|
321
|
+
});
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case "user": {
|
|
325
|
+
const blocks = [];
|
|
326
|
+
for (const part of partsOf(msg.content)) {
|
|
327
|
+
if (typeof part === "string") {
|
|
328
|
+
if (part.length > 0) {
|
|
329
|
+
blocks.push({ type: "text", text: part });
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const p = part;
|
|
334
|
+
if (p?.type === "text" && typeof p.text === "string") {
|
|
335
|
+
const cc = cacheControlOf(p);
|
|
336
|
+
blocks.push({
|
|
337
|
+
type: "text",
|
|
338
|
+
text: p.text,
|
|
339
|
+
...(cc ? { cache_control: cc } : {}),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else if (p?.type === "image" || p?.type === "image_url") {
|
|
343
|
+
const img = toAnthropicImageBlock(p.image ?? p.data ?? p.url);
|
|
344
|
+
if (img) {
|
|
345
|
+
const cc = cacheControlOf(p);
|
|
346
|
+
blocks.push(cc ? { ...img, cache_control: cc } : img);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (blocks.length > 0) {
|
|
351
|
+
applyMessageCacheControl(blocks, msg);
|
|
352
|
+
messages.push({ role: "user", content: blocks });
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "assistant": {
|
|
357
|
+
const blocks = [];
|
|
358
|
+
for (const part of partsOf(msg.content)) {
|
|
359
|
+
if (typeof part === "string") {
|
|
360
|
+
if (part.length > 0) {
|
|
361
|
+
blocks.push({ type: "text", text: part });
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const p = part;
|
|
366
|
+
if (p?.type === "text" && typeof p.text === "string") {
|
|
367
|
+
if (p.text.length > 0) {
|
|
368
|
+
const cc = cacheControlOf(p);
|
|
369
|
+
blocks.push({
|
|
370
|
+
type: "text",
|
|
371
|
+
text: p.text,
|
|
372
|
+
...(cc ? { cache_control: cc } : {}),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (p?.type === "tool-call") {
|
|
377
|
+
let input = p.input;
|
|
378
|
+
if (typeof input === "string") {
|
|
379
|
+
try {
|
|
380
|
+
input = JSON.parse(input);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
input = {};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
blocks.push({
|
|
387
|
+
type: "tool_use",
|
|
388
|
+
id: p.toolCallId ?? "",
|
|
389
|
+
name: p.toolName ?? "",
|
|
390
|
+
input: input ?? {},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (blocks.length > 0) {
|
|
395
|
+
applyMessageCacheControl(blocks, msg);
|
|
396
|
+
messages.push({ role: "assistant", content: blocks });
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case "tool": {
|
|
401
|
+
// Tool results are user-role messages with tool_result blocks.
|
|
402
|
+
const blocks = [];
|
|
403
|
+
if (Array.isArray(msg.content)) {
|
|
404
|
+
for (const part of msg.content) {
|
|
405
|
+
const p = part;
|
|
406
|
+
if (p?.type === "tool-result") {
|
|
407
|
+
blocks.push({
|
|
408
|
+
type: "tool_result",
|
|
409
|
+
tool_use_id: p.toolCallId ?? "",
|
|
410
|
+
content: stringifyAnthropicToolOutput(p.output),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else if (typeof msg.content === "string") {
|
|
416
|
+
blocks.push({
|
|
417
|
+
type: "tool_result",
|
|
418
|
+
tool_use_id: msg.toolCallId ?? "",
|
|
419
|
+
content: msg.content,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (blocks.length > 0) {
|
|
423
|
+
messages.push({ role: "user", content: blocks });
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Plain-string system when no cache breakpoints are present (matches the
|
|
430
|
+
// previous wire shape); block form only when cache_control must ride along.
|
|
431
|
+
const system = systemBlocks.length === 0
|
|
432
|
+
? undefined
|
|
433
|
+
: systemBlocks.some((b) => b.cache_control)
|
|
434
|
+
? systemBlocks
|
|
435
|
+
: systemBlocks.map((b) => b.text).join("\n\n");
|
|
436
|
+
return {
|
|
437
|
+
...(system !== undefined ? { system } : {}),
|
|
438
|
+
messages,
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
/** Convert a NeuroLink tool record into Anthropic tool definitions. */
|
|
442
|
+
const toolsToAnthropic = (tools) => {
|
|
443
|
+
const entries = Object.entries(tools);
|
|
444
|
+
if (entries.length === 0) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
return entries.map(([name, tool]) => {
|
|
448
|
+
const t = tool;
|
|
449
|
+
const rawSchema = t.inputSchema ?? t.parameters;
|
|
450
|
+
const input_schema = (rawSchema
|
|
451
|
+
? convertZodToJsonSchema(rawSchema)
|
|
452
|
+
: { type: "object", properties: {} });
|
|
453
|
+
// GenerationHandler marks the last tool definition with a cache
|
|
454
|
+
// breakpoint when prompt caching is active — keep honoring it.
|
|
455
|
+
const cc = cacheControlOf(tool);
|
|
456
|
+
return {
|
|
457
|
+
name,
|
|
458
|
+
...(t.description ? { description: t.description } : {}),
|
|
459
|
+
input_schema,
|
|
460
|
+
...(cc ? { cache_control: cc } : {}),
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
/** Map a NeuroLink tool choice onto Anthropic's tool_choice shape. */
|
|
465
|
+
const toolChoiceToAnthropic = (choice) => {
|
|
466
|
+
if (!choice || choice === "auto") {
|
|
467
|
+
return undefined; // Anthropic defaults to auto when tools are present
|
|
468
|
+
}
|
|
469
|
+
if (choice === "none") {
|
|
470
|
+
return { type: "none" };
|
|
471
|
+
}
|
|
472
|
+
if (choice === "required") {
|
|
473
|
+
return { type: "any" };
|
|
474
|
+
}
|
|
475
|
+
if (typeof choice === "object") {
|
|
476
|
+
const c = choice;
|
|
477
|
+
if (c.type === "tool" && c.toolName) {
|
|
478
|
+
return { type: "tool", name: c.toolName };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
};
|
|
483
|
+
/** Map Anthropic stop_reason onto the V3 unified finish reason. */
|
|
484
|
+
const mapAnthropicStopReason = (raw) => {
|
|
485
|
+
switch (raw) {
|
|
486
|
+
case "max_tokens":
|
|
487
|
+
return "length";
|
|
488
|
+
case "tool_use":
|
|
489
|
+
return "tool-calls";
|
|
490
|
+
case "refusal":
|
|
491
|
+
return "content-filter";
|
|
492
|
+
default:
|
|
493
|
+
return "stop";
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
// Anthropic's Messages API requires max_tokens on every request. The previous
|
|
497
|
+
// @ai-sdk/anthropic implementation defaulted it to 4096 when the caller did
|
|
498
|
+
// not specify maxTokens — preserve that wire behavior.
|
|
499
|
+
const ANTHROPIC_DEFAULT_MAX_TOKENS = 4096;
|
|
206
500
|
/**
|
|
207
501
|
* Anthropic Provider v2 - BaseProvider Implementation
|
|
208
502
|
* Enhanced with OAuth support, subscription tiers, and beta headers for Claude Code integration.
|
|
209
503
|
*/
|
|
210
504
|
export class AnthropicProvider extends BaseProvider {
|
|
211
|
-
|
|
505
|
+
client;
|
|
212
506
|
authMethod;
|
|
213
507
|
subscriptionTier;
|
|
214
508
|
enableBetaFeatures;
|
|
@@ -273,8 +567,8 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
273
567
|
this.authMethod = authMethod;
|
|
274
568
|
// Build headers based on auth method and subscription tier
|
|
275
569
|
const headers = this.getAuthHeaders();
|
|
276
|
-
// Create Anthropic
|
|
277
|
-
let
|
|
570
|
+
// Create the official Anthropic SDK client based on auth method
|
|
571
|
+
let client;
|
|
278
572
|
logger.debug("[AnthropicProvider] Constructor - checking OAuth:", {
|
|
279
573
|
authMethod: this.authMethod,
|
|
280
574
|
hasOAuthToken: !!this.oauthToken,
|
|
@@ -292,23 +586,24 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
292
586
|
// even after an automatic token refresh.
|
|
293
587
|
// oauthToken is guaranteed non-null here (checked by the enclosing if-guard).
|
|
294
588
|
const tokenRef = this.oauthToken;
|
|
295
|
-
// skipBodyTransform=true: For the SDK
|
|
296
|
-
// intentionally skipped because the
|
|
297
|
-
// format (system prompts, metadata, tool definitions). The
|
|
298
|
-
// agent block, user_id injection, and mcp_ tool-name
|
|
299
|
-
// needed for proxy passthrough of raw Claude API
|
|
300
|
-
// make the request look like it came from
|
|
589
|
+
// skipBodyTransform=true: For the SDK client path, body transforms ARE
|
|
590
|
+
// intentionally skipped because the official Anthropic SDK builds its
|
|
591
|
+
// own request format (system prompts, metadata, tool definitions). The
|
|
592
|
+
// billing header, agent block, user_id injection, and mcp_ tool-name
|
|
593
|
+
// prefixing are only needed for proxy passthrough of raw Claude API
|
|
594
|
+
// requests where we must make the request look like it came from
|
|
595
|
+
// Claude Code / CLIProxyAPI.
|
|
301
596
|
const oauthFetch = createOAuthFetch(() => tokenRef.accessToken, this.enableBetaFeatures, false, // No mcp_ prefix — tool names pass through as-is (matches CLIProxyAPI)
|
|
302
597
|
true);
|
|
303
598
|
// For OAuth, we use a dummy API key since our fetch wrapper handles auth
|
|
304
599
|
// IMPORTANT: Do NOT pass beta headers here - our fetch wrapper handles them
|
|
305
600
|
// The claude-code-20250219 beta header triggers "credential only for Claude Code" error
|
|
306
|
-
|
|
601
|
+
client = new Anthropic({
|
|
307
602
|
apiKey: "oauth-authenticated", // Placeholder, actual auth is in fetch wrapper
|
|
308
603
|
// Note: No headers passed - fetch wrapper sets oauth-2025-04-20 beta header
|
|
309
604
|
fetch: oauthFetch,
|
|
310
605
|
});
|
|
311
|
-
logger.debug("[AnthropicProvider] Anthropic SDK created with OAuth fetch wrapper");
|
|
606
|
+
logger.debug("[AnthropicProvider] Anthropic SDK client created with OAuth fetch wrapper");
|
|
312
607
|
logger.debug("Anthropic Provider initialized with OAuth", {
|
|
313
608
|
modelName: this.modelName,
|
|
314
609
|
provider: this.providerName,
|
|
@@ -324,38 +619,32 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
324
619
|
else {
|
|
325
620
|
// Traditional API key authentication
|
|
326
621
|
const apiKeyToUse = credentials?.apiKey ?? config?.apiKey ?? getAnthropicApiKey();
|
|
327
|
-
// The
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// endpoints rooted at `/vN/...`. Path-rooted proxies like
|
|
334
|
-
// `https://proxy.example.com/anthropic` will be silently rewritten to
|
|
335
|
-
// `…/anthropic/v1` and 404 if the proxy doesn't expose that path. To
|
|
336
|
-
// disable auto-append, set `ANTHROPIC_BASE_URL` explicitly ending in a
|
|
337
|
-
// version segment (e.g. `https://api.anthropic.com/v1`).
|
|
622
|
+
// The official Anthropic SDK builds `${baseURL}/v1/messages` itself, so
|
|
623
|
+
// a version-suffixed base URL — the form the previous @ai-sdk/anthropic
|
|
624
|
+
// implementation REQUIRED (`https://api.anthropic.com/v1`) — would
|
|
625
|
+
// double up as `/v1/v1/messages`. Normalize the inverse way now: strip
|
|
626
|
+
// a trailing `/vN` segment when present so both historical forms of
|
|
627
|
+
// ANTHROPIC_BASE_URL keep working.
|
|
338
628
|
const normalizedBaseURL = (() => {
|
|
339
629
|
const raw = process.env.ANTHROPIC_BASE_URL;
|
|
340
630
|
if (!raw) {
|
|
341
631
|
return undefined;
|
|
342
632
|
}
|
|
343
633
|
const trimmed = raw.replace(/\/+$/, "");
|
|
344
|
-
|
|
345
|
-
|
|
634
|
+
const stripped = trimmed.replace(/\/v\d+$/, "");
|
|
635
|
+
if (stripped !== trimmed) {
|
|
636
|
+
logger.debug("[AnthropicProvider] Stripping the version suffix from " +
|
|
637
|
+
"ANTHROPIC_BASE_URL — the official Anthropic SDK appends /v1 " +
|
|
638
|
+
"to the base URL itself.", {
|
|
639
|
+
baseURL: redactUrlCredentials(raw),
|
|
640
|
+
rewrittenTo: redactUrlCredentials(stripped),
|
|
641
|
+
});
|
|
346
642
|
}
|
|
347
|
-
|
|
348
|
-
"appending /v1 so the Vercel AI SDK can build /messages correctly. " +
|
|
349
|
-
"To silence this warning, set ANTHROPIC_BASE_URL with an explicit " +
|
|
350
|
-
"version segment (e.g. https://api.anthropic.com/v1). " +
|
|
351
|
-
"If your proxy exposes Anthropic at a custom path that doesn't " +
|
|
352
|
-
"expose /v1/messages, the auto-append will produce a 404 — pass " +
|
|
353
|
-
"the full path including the version explicitly.", { baseURL: raw, rewrittenTo: `${trimmed}/v1` });
|
|
354
|
-
return `${trimmed}/v1`;
|
|
643
|
+
return stripped;
|
|
355
644
|
})();
|
|
356
|
-
|
|
645
|
+
client = new Anthropic({
|
|
357
646
|
apiKey: apiKeyToUse,
|
|
358
|
-
headers,
|
|
647
|
+
defaultHeaders: headers,
|
|
359
648
|
...(normalizedBaseURL && { baseURL: normalizedBaseURL }),
|
|
360
649
|
fetch: createProxyFetch(),
|
|
361
650
|
});
|
|
@@ -367,8 +656,7 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
367
656
|
enableBetaFeatures: this.enableBetaFeatures,
|
|
368
657
|
});
|
|
369
658
|
}
|
|
370
|
-
|
|
371
|
-
this.model = anthropic(this.modelName || getDefaultAnthropicModel());
|
|
659
|
+
this.client = client;
|
|
372
660
|
// Initialize usage tracking
|
|
373
661
|
this.usageInfo = {
|
|
374
662
|
messagesUsed: 0,
|
|
@@ -767,10 +1055,163 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
767
1055
|
return getDefaultAnthropicModel();
|
|
768
1056
|
}
|
|
769
1057
|
/**
|
|
770
|
-
* Returns
|
|
1058
|
+
* Returns a V3-shaped delegating model whose `doGenerate` drives the
|
|
1059
|
+
* official Anthropic Messages API directly. BaseProvider's `generate()`
|
|
1060
|
+
* path (and its middleware wrapping) keeps working unchanged; the
|
|
1061
|
+
* streaming path bypasses this entirely via `executeStream`.
|
|
771
1062
|
*/
|
|
772
1063
|
getAISDKModel() {
|
|
773
|
-
|
|
1064
|
+
const client = this.client;
|
|
1065
|
+
const providerName = this.providerName;
|
|
1066
|
+
const modelId = this.modelName || getDefaultAnthropicModel();
|
|
1067
|
+
const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
|
|
1068
|
+
const refreshAuth = () => this.refreshAuthIfNeeded();
|
|
1069
|
+
return {
|
|
1070
|
+
specificationVersion: "v3",
|
|
1071
|
+
provider: providerName,
|
|
1072
|
+
modelId,
|
|
1073
|
+
supportedUrls: {},
|
|
1074
|
+
doGenerate: async (options) => {
|
|
1075
|
+
await refreshAuth();
|
|
1076
|
+
const { system, messages } = messagesToAnthropic(options.prompt);
|
|
1077
|
+
let tools = (options.tools ?? [])
|
|
1078
|
+
.filter((t) => t.type === "function")
|
|
1079
|
+
.map((t) => {
|
|
1080
|
+
// GenerationHandler marks the last tool definition with a cache
|
|
1081
|
+
// breakpoint when prompt caching is active — keep honoring it.
|
|
1082
|
+
const cc = cacheControlOf(t);
|
|
1083
|
+
return {
|
|
1084
|
+
name: t.name,
|
|
1085
|
+
...(t.description ? { description: t.description } : {}),
|
|
1086
|
+
input_schema: (t.inputSchema ?? {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
properties: {},
|
|
1089
|
+
}),
|
|
1090
|
+
...(cc ? { cache_control: cc } : {}),
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
if (tools && tools.length === 0) {
|
|
1094
|
+
tools = undefined;
|
|
1095
|
+
}
|
|
1096
|
+
let toolChoice = options.toolChoice
|
|
1097
|
+
? toolChoiceToAnthropic(options.toolChoice.type === "tool"
|
|
1098
|
+
? { type: "tool", toolName: options.toolChoice.toolName }
|
|
1099
|
+
: options.toolChoice.type)
|
|
1100
|
+
: undefined;
|
|
1101
|
+
// JSON/structured output: Anthropic has no response_format — emulate
|
|
1102
|
+
// with a forced synthetic tool whose input IS the JSON payload (the
|
|
1103
|
+
// same object-tool strategy @ai-sdk/anthropic used).
|
|
1104
|
+
let jsonTool;
|
|
1105
|
+
if (options.responseFormat?.type === "json") {
|
|
1106
|
+
jsonTool = options.responseFormat.name ?? "json";
|
|
1107
|
+
const schema = (options.responseFormat.schema ?? {
|
|
1108
|
+
type: "object",
|
|
1109
|
+
});
|
|
1110
|
+
tools = [
|
|
1111
|
+
{
|
|
1112
|
+
name: jsonTool,
|
|
1113
|
+
description: options.responseFormat.description ??
|
|
1114
|
+
"Respond by calling this tool with the answer as its input.",
|
|
1115
|
+
input_schema: schema,
|
|
1116
|
+
},
|
|
1117
|
+
];
|
|
1118
|
+
toolChoice = { type: "tool", name: jsonTool };
|
|
1119
|
+
}
|
|
1120
|
+
// Extended thinking passthrough (providerOptions.anthropic.thinking).
|
|
1121
|
+
const thinking = options.providerOptions?.anthropic?.thinking;
|
|
1122
|
+
const params = {
|
|
1123
|
+
model: modelId,
|
|
1124
|
+
messages,
|
|
1125
|
+
max_tokens: options.maxOutputTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
1126
|
+
...(system ? { system } : {}),
|
|
1127
|
+
...(options.temperature !== undefined && options.temperature !== null
|
|
1128
|
+
? { temperature: options.temperature }
|
|
1129
|
+
: {}),
|
|
1130
|
+
...(options.topP !== undefined && options.topP !== null
|
|
1131
|
+
? { top_p: options.topP }
|
|
1132
|
+
: {}),
|
|
1133
|
+
...(options.stopSequences && options.stopSequences.length > 0
|
|
1134
|
+
? { stop_sequences: options.stopSequences }
|
|
1135
|
+
: {}),
|
|
1136
|
+
...(tools ? { tools } : {}),
|
|
1137
|
+
...(toolChoice ? { tool_choice: toolChoice } : {}),
|
|
1138
|
+
...(thinking ? { thinking } : {}),
|
|
1139
|
+
};
|
|
1140
|
+
const timeoutController = createTimeoutController(getTimeoutForOptions(options), providerName, "generate");
|
|
1141
|
+
let response;
|
|
1142
|
+
try {
|
|
1143
|
+
response = await client.messages.create(params, {
|
|
1144
|
+
signal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
finally {
|
|
1148
|
+
timeoutController?.cleanup();
|
|
1149
|
+
}
|
|
1150
|
+
const content = [];
|
|
1151
|
+
for (const block of response.content) {
|
|
1152
|
+
if (block.type === "thinking") {
|
|
1153
|
+
content.push({ type: "reasoning", text: block.thinking });
|
|
1154
|
+
}
|
|
1155
|
+
else if (block.type === "text") {
|
|
1156
|
+
// In forced-json mode the payload arrives via the tool input, not
|
|
1157
|
+
// text — pass text through only in normal mode.
|
|
1158
|
+
if (!jsonTool) {
|
|
1159
|
+
content.push({ type: "text", text: block.text });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
else if (block.type === "tool_use") {
|
|
1163
|
+
if (jsonTool && block.name === jsonTool) {
|
|
1164
|
+
// Unwrap the synthetic tool call back into text JSON.
|
|
1165
|
+
content.push({
|
|
1166
|
+
type: "text",
|
|
1167
|
+
text: stringifyToolInput(block.input),
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
content.push({
|
|
1172
|
+
type: "tool-call",
|
|
1173
|
+
toolCallId: block.id,
|
|
1174
|
+
toolName: block.name,
|
|
1175
|
+
input: stringifyToolInput(block.input),
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const cacheRead = response.usage.cache_read_input_tokens ?? 0;
|
|
1181
|
+
const cacheWrite = response.usage.cache_creation_input_tokens ?? 0;
|
|
1182
|
+
return {
|
|
1183
|
+
content,
|
|
1184
|
+
finishReason: {
|
|
1185
|
+
unified: mapAnthropicStopReason(response.stop_reason),
|
|
1186
|
+
raw: response.stop_reason ?? "stop",
|
|
1187
|
+
},
|
|
1188
|
+
usage: {
|
|
1189
|
+
inputTokens: {
|
|
1190
|
+
total: response.usage.input_tokens + cacheRead + cacheWrite,
|
|
1191
|
+
noCache: response.usage.input_tokens,
|
|
1192
|
+
cacheRead,
|
|
1193
|
+
cacheWrite,
|
|
1194
|
+
},
|
|
1195
|
+
outputTokens: {
|
|
1196
|
+
total: response.usage.output_tokens,
|
|
1197
|
+
text: response.usage.output_tokens,
|
|
1198
|
+
reasoning: undefined,
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
warnings: [],
|
|
1202
|
+
request: { body: params },
|
|
1203
|
+
response: {
|
|
1204
|
+
id: response.id,
|
|
1205
|
+
modelId: response.model,
|
|
1206
|
+
headers: {},
|
|
1207
|
+
body: response,
|
|
1208
|
+
},
|
|
1209
|
+
};
|
|
1210
|
+
},
|
|
1211
|
+
doStream: () => {
|
|
1212
|
+
throw new Error(`${providerName}: doStream is not implemented on the delegating model — the streaming path uses executeStream directly.`);
|
|
1213
|
+
},
|
|
1214
|
+
};
|
|
774
1215
|
}
|
|
775
1216
|
formatProviderError(error) {
|
|
776
1217
|
if (error instanceof TimeoutError) {
|
|
@@ -820,134 +1261,374 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
820
1261
|
this.validateStreamOptions(options);
|
|
821
1262
|
const timeout = this.getTimeout(options);
|
|
822
1263
|
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
|
|
1264
|
+
// Consumer-driven abort: fires when the async iterator is closed early
|
|
1265
|
+
// (caller breaks out of `for await`) so the background loop stops
|
|
1266
|
+
// reading SSE and running tools.
|
|
1267
|
+
const consumerAbortController = new AbortController();
|
|
1268
|
+
const abortSignal = mergeAbortSignals([
|
|
1269
|
+
options.abortSignal,
|
|
1270
|
+
timeoutController?.controller.signal,
|
|
1271
|
+
consumerAbortController.signal,
|
|
1272
|
+
]).signal;
|
|
1273
|
+
let toolsRecord;
|
|
1274
|
+
let anthropicTools;
|
|
1275
|
+
let payload;
|
|
1276
|
+
let shouldUseTools;
|
|
823
1277
|
try {
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
1278
|
+
// options.tools is pre-merged by BaseProvider.stream() with base tools
|
|
1279
|
+
// (MCP/built-in) + user-provided tools (RAG, etc.)
|
|
1280
|
+
shouldUseTools = !options.disableTools && this.supportsTools();
|
|
1281
|
+
toolsRecord = shouldUseTools
|
|
828
1282
|
? options.tools || (await this.getAllTools())
|
|
829
1283
|
: {};
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
//
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1284
|
+
anthropicTools = shouldUseTools
|
|
1285
|
+
? toolsToAnthropic(toolsRecord)
|
|
1286
|
+
: undefined;
|
|
1287
|
+
// Build message array from options with multimodal support, then
|
|
1288
|
+
// convert to the Anthropic Messages payload (system + content blocks).
|
|
1289
|
+
const built = await this.buildMessagesForStream(options);
|
|
1290
|
+
payload = messagesToAnthropic(built);
|
|
1291
|
+
}
|
|
1292
|
+
catch (setupErr) {
|
|
1293
|
+
timeoutController?.cleanup();
|
|
1294
|
+
throw this.handleProviderError(setupErr);
|
|
1295
|
+
}
|
|
1296
|
+
const modelId = this.modelName || getDefaultAnthropicModel();
|
|
1297
|
+
const anthropicToolChoice = shouldUseTools && anthropicTools && anthropicTools.length > 0
|
|
1298
|
+
? toolChoiceToAnthropic(resolveToolChoice(options, toolsRecord, shouldUseTools))
|
|
1299
|
+
: undefined;
|
|
1300
|
+
// Extended thinking: enabled when the caller supplies an explicit token
|
|
1301
|
+
// budget (mirrors the previous experimental_thinking gating). Thinking
|
|
1302
|
+
// deltas stream out on the `reasoning` chunk channel.
|
|
1303
|
+
const thinking = options.thinkingConfig?.enabled && options.thinkingConfig.budgetTokens
|
|
1304
|
+
? {
|
|
1305
|
+
type: "enabled",
|
|
1306
|
+
budget_tokens: options.thinkingConfig.budgetTokens,
|
|
1307
|
+
}
|
|
1308
|
+
: undefined;
|
|
1309
|
+
// Wrap the native stream in an OTel span to capture provider-level
|
|
1310
|
+
// latency and token usage (same span name as the pre-migration path so
|
|
1311
|
+
// dashboards stay continuous).
|
|
1312
|
+
const streamSpan = streamTracer.startSpan("neurolink.provider.streamText", {
|
|
1313
|
+
kind: SpanKind.CLIENT,
|
|
1314
|
+
attributes: {
|
|
1315
|
+
"gen_ai.system": "anthropic",
|
|
1316
|
+
"gen_ai.request.model": modelId,
|
|
1317
|
+
},
|
|
1318
|
+
});
|
|
1319
|
+
const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
1320
|
+
const emitter = this.neurolink?.getEventEmitter();
|
|
1321
|
+
const { pushChunk, nextChunk } = createChunkQueue();
|
|
1322
|
+
const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
|
|
1323
|
+
usagePromise
|
|
1324
|
+
.then((usage) => {
|
|
1325
|
+
streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens || 0);
|
|
1326
|
+
streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.completionTokens || 0);
|
|
1327
|
+
const cost = calculateCost(this.providerName, this.modelName, {
|
|
1328
|
+
input: usage.promptTokens || 0,
|
|
1329
|
+
output: usage.completionTokens || 0,
|
|
1330
|
+
total: usage.totalTokens || 0,
|
|
841
1331
|
});
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1332
|
+
if (cost && cost > 0) {
|
|
1333
|
+
streamSpan.setAttribute("neurolink.cost", cost);
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
.catch(() => {
|
|
1337
|
+
// usage may never resolve if the stream is aborted before completion
|
|
1338
|
+
});
|
|
1339
|
+
finishPromise
|
|
1340
|
+
.then((reason) => {
|
|
1341
|
+
streamSpan.setAttribute("gen_ai.response.finish_reason", reason || "unknown");
|
|
1342
|
+
streamSpan.end();
|
|
1343
|
+
})
|
|
1344
|
+
.catch(() => {
|
|
1345
|
+
streamSpan.end();
|
|
1346
|
+
});
|
|
1347
|
+
let capturedProviderError;
|
|
1348
|
+
const client = this.client;
|
|
1349
|
+
const toolsUsed = [];
|
|
1350
|
+
const runLoop = async () => {
|
|
1351
|
+
const conversation = payload.messages.slice();
|
|
1352
|
+
let totalInput = 0;
|
|
1353
|
+
let totalOutput = 0;
|
|
1354
|
+
let lastStop = null;
|
|
1355
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
1356
|
+
const params = {
|
|
1357
|
+
model: modelId,
|
|
1358
|
+
messages: conversation,
|
|
1359
|
+
max_tokens: options.maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
1360
|
+
stream: true,
|
|
1361
|
+
...(payload.system ? { system: payload.system } : {}),
|
|
1362
|
+
...(options.temperature !== undefined && options.temperature !== null
|
|
1363
|
+
? { temperature: options.temperature }
|
|
1364
|
+
: {}),
|
|
1365
|
+
...(anthropicTools && anthropicTools.length > 0
|
|
1366
|
+
? { tools: anthropicTools }
|
|
1367
|
+
: {}),
|
|
1368
|
+
...(anthropicToolChoice ? { tool_choice: anthropicToolChoice } : {}),
|
|
1369
|
+
...(thinking ? { thinking } : {}),
|
|
1370
|
+
};
|
|
1371
|
+
const events = await client.messages.create(params, {
|
|
1372
|
+
signal: abortSignal ?? undefined,
|
|
1373
|
+
});
|
|
1374
|
+
// Per-step accumulators, keyed by content-block index so blocks are
|
|
1375
|
+
// replayed to the conversation in order (thinking blocks must be
|
|
1376
|
+
// passed back with their signatures when tool use continues a turn).
|
|
1377
|
+
const blockTypes = new Map();
|
|
1378
|
+
const textAcc = new Map();
|
|
1379
|
+
const thinkingAcc = new Map();
|
|
1380
|
+
const toolAcc = new Map();
|
|
1381
|
+
let stopReason = null;
|
|
1382
|
+
for await (const event of events) {
|
|
1383
|
+
if (event.type === "message_start") {
|
|
1384
|
+
totalInput += event.message.usage.input_tokens ?? 0;
|
|
1385
|
+
totalOutput += event.message.usage.output_tokens ?? 0;
|
|
1386
|
+
}
|
|
1387
|
+
else if (event.type === "content_block_start") {
|
|
1388
|
+
blockTypes.set(event.index, event.content_block.type);
|
|
1389
|
+
if (event.content_block.type === "tool_use") {
|
|
1390
|
+
toolAcc.set(event.index, {
|
|
1391
|
+
id: event.content_block.id,
|
|
1392
|
+
name: event.content_block.name,
|
|
1393
|
+
inputJson: "",
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
else if (event.type === "content_block_delta") {
|
|
1398
|
+
const delta = event.delta;
|
|
1399
|
+
if (delta.type === "text_delta") {
|
|
1400
|
+
textAcc.set(event.index, (textAcc.get(event.index) ?? "") + delta.text);
|
|
1401
|
+
pushChunk({ content: delta.text });
|
|
1402
|
+
}
|
|
1403
|
+
else if (delta.type === "thinking_delta") {
|
|
1404
|
+
const acc = thinkingAcc.get(event.index) ?? {
|
|
1405
|
+
text: "",
|
|
1406
|
+
signature: "",
|
|
1407
|
+
};
|
|
1408
|
+
acc.text += delta.thinking;
|
|
1409
|
+
thinkingAcc.set(event.index, acc);
|
|
1410
|
+
// Reasoning rides the dedicated chunk channel; `content` stays
|
|
1411
|
+
// an always-present string so plain-text consumers are safe.
|
|
1412
|
+
pushChunk({ content: "", reasoning: delta.thinking });
|
|
1413
|
+
}
|
|
1414
|
+
else if (delta.type === "signature_delta") {
|
|
1415
|
+
const acc = thinkingAcc.get(event.index) ?? {
|
|
1416
|
+
text: "",
|
|
1417
|
+
signature: "",
|
|
1418
|
+
};
|
|
1419
|
+
acc.signature += delta.signature;
|
|
1420
|
+
thinkingAcc.set(event.index, acc);
|
|
1421
|
+
}
|
|
1422
|
+
else if (delta.type === "input_json_delta") {
|
|
1423
|
+
const acc = toolAcc.get(event.index);
|
|
1424
|
+
if (acc) {
|
|
1425
|
+
acc.inputJson += delta.partial_json;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
else if (event.type === "message_delta") {
|
|
1430
|
+
stopReason = event.delta.stop_reason ?? stopReason;
|
|
1431
|
+
totalOutput += event.usage?.output_tokens ?? 0;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
lastStop = stopReason;
|
|
1435
|
+
if (stopReason !== "tool_use" || toolAcc.size === 0) {
|
|
1436
|
+
break;
|
|
1437
|
+
}
|
|
1438
|
+
// Replay this assistant turn (thinking + text + tool_use blocks, in
|
|
1439
|
+
// block order) then execute the requested tools and append their
|
|
1440
|
+
// results as a user turn — the native multi-step tool loop.
|
|
1441
|
+
const assistantBlocks = [];
|
|
1442
|
+
const orderedIndexes = [...blockTypes.keys()].sort((a, b) => a - b);
|
|
1443
|
+
for (const idx of orderedIndexes) {
|
|
1444
|
+
const type = blockTypes.get(idx);
|
|
1445
|
+
if (type === "thinking") {
|
|
1446
|
+
const acc = thinkingAcc.get(idx);
|
|
1447
|
+
if (acc && acc.text.length > 0) {
|
|
1448
|
+
assistantBlocks.push({
|
|
1449
|
+
type: "thinking",
|
|
1450
|
+
thinking: acc.text,
|
|
1451
|
+
signature: acc.signature,
|
|
876
1452
|
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
else if (type === "text") {
|
|
1456
|
+
const text = textAcc.get(idx);
|
|
1457
|
+
if (text && text.length > 0) {
|
|
1458
|
+
assistantBlocks.push({ type: "text", text });
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
else if (type === "tool_use") {
|
|
1462
|
+
const acc = toolAcc.get(idx);
|
|
1463
|
+
if (acc) {
|
|
1464
|
+
let input;
|
|
1465
|
+
try {
|
|
1466
|
+
input = acc.inputJson ? JSON.parse(acc.inputJson) : {};
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
input = {};
|
|
1470
|
+
}
|
|
1471
|
+
assistantBlocks.push({
|
|
1472
|
+
type: "tool_use",
|
|
1473
|
+
id: acc.id,
|
|
1474
|
+
name: acc.name,
|
|
1475
|
+
input,
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
conversation.push({ role: "assistant", content: assistantBlocks });
|
|
1481
|
+
const resultBlocks = [];
|
|
1482
|
+
const toolCallsForStorage = [];
|
|
1483
|
+
const toolResultsForStorage = [];
|
|
1484
|
+
for (const acc of toolAcc.values()) {
|
|
1485
|
+
let args;
|
|
1486
|
+
try {
|
|
1487
|
+
args = acc.inputJson
|
|
1488
|
+
? JSON.parse(acc.inputJson)
|
|
1489
|
+
: {};
|
|
1490
|
+
}
|
|
1491
|
+
catch {
|
|
1492
|
+
args = {};
|
|
1493
|
+
}
|
|
1494
|
+
toolCallsForStorage.push({
|
|
1495
|
+
type: "tool-call",
|
|
1496
|
+
toolCallId: acc.id,
|
|
1497
|
+
toolName: acc.name,
|
|
1498
|
+
args,
|
|
1499
|
+
});
|
|
1500
|
+
toolsUsed.push(acc.name);
|
|
1501
|
+
const tool = toolsRecord[acc.name];
|
|
1502
|
+
try {
|
|
1503
|
+
if (!tool?.execute) {
|
|
1504
|
+
throw new Error(`Tool not found: ${acc.name}`);
|
|
1505
|
+
}
|
|
1506
|
+
const result = await tool.execute(args, {
|
|
1507
|
+
toolCallId: acc.id,
|
|
1508
|
+
messages: [],
|
|
877
1509
|
});
|
|
878
|
-
|
|
1510
|
+
toolResultsForStorage.push({
|
|
1511
|
+
type: "tool-result",
|
|
1512
|
+
toolCallId: acc.id,
|
|
1513
|
+
toolName: acc.name,
|
|
1514
|
+
result,
|
|
1515
|
+
});
|
|
1516
|
+
resultBlocks.push({
|
|
1517
|
+
type: "tool_result",
|
|
1518
|
+
tool_use_id: acc.id,
|
|
1519
|
+
content: stringifyAnthropicToolOutput(result),
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
catch (toolErr) {
|
|
1523
|
+
const message = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
1524
|
+
toolResultsForStorage.push({
|
|
1525
|
+
type: "tool-result",
|
|
1526
|
+
toolCallId: acc.id,
|
|
1527
|
+
toolName: acc.name,
|
|
1528
|
+
error: message,
|
|
1529
|
+
});
|
|
1530
|
+
resultBlocks.push({
|
|
1531
|
+
type: "tool_result",
|
|
1532
|
+
tool_use_id: acc.id,
|
|
1533
|
+
content: `Error: ${message}`,
|
|
1534
|
+
is_error: true,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
// Emit tool:end events for Pipeline B and persist tool executions —
|
|
1539
|
+
// the same hooks the streamText onStepFinish callback used to drive.
|
|
1540
|
+
emitToolEndFromStepFinish(emitter, toolResultsForStorage.map((tr) => ({
|
|
1541
|
+
toolName: tr.toolName,
|
|
1542
|
+
result: tr.result,
|
|
1543
|
+
error: tr.error,
|
|
1544
|
+
})));
|
|
1545
|
+
this.handleToolExecutionStorage(toolCallsForStorage, toolResultsForStorage, options, new Date()).catch((storageErr) => {
|
|
1546
|
+
logger.warn("[AnthropicProvider] Failed to store tool executions", {
|
|
1547
|
+
provider: this.providerName,
|
|
1548
|
+
error: storageErr instanceof Error
|
|
1549
|
+
? storageErr.message
|
|
1550
|
+
: String(storageErr),
|
|
1551
|
+
});
|
|
879
1552
|
});
|
|
1553
|
+
conversation.push({ role: "user", content: resultBlocks });
|
|
1554
|
+
}
|
|
1555
|
+
resolveUsage({
|
|
1556
|
+
promptTokens: totalInput,
|
|
1557
|
+
completionTokens: totalOutput,
|
|
1558
|
+
totalTokens: totalInput + totalOutput,
|
|
1559
|
+
});
|
|
1560
|
+
resolveFinish(lastStop ?? "stop");
|
|
1561
|
+
};
|
|
1562
|
+
const loopPromise = runLoop()
|
|
1563
|
+
// Parameter named `error` so the compiled `capturedProviderError = error`
|
|
1564
|
+
// assignment matches the regression-grep in test:context 6.14.
|
|
1565
|
+
.catch((error) => {
|
|
1566
|
+
capturedProviderError = error;
|
|
1567
|
+
logger.error("Anthropic: Stream error", {
|
|
1568
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1569
|
+
});
|
|
1570
|
+
resolveFinish("error");
|
|
1571
|
+
throw this.formatProviderError(error);
|
|
1572
|
+
})
|
|
1573
|
+
.finally(() => {
|
|
1574
|
+
timeoutController?.cleanup();
|
|
1575
|
+
pushChunk({ done: true });
|
|
1576
|
+
});
|
|
1577
|
+
loopPromise.catch(() => {
|
|
1578
|
+
// Swallowed by design: the generator below surfaces loop errors after
|
|
1579
|
+
// draining the queue; this guard only prevents an unhandled-rejection
|
|
1580
|
+
// crash when the consumer abandons the stream early.
|
|
1581
|
+
});
|
|
1582
|
+
const providerName = this.providerName;
|
|
1583
|
+
const transformedStream = async function* () {
|
|
1584
|
+
let contentYielded = 0;
|
|
1585
|
+
try {
|
|
1586
|
+
for (;;) {
|
|
1587
|
+
const chunk = await nextChunk();
|
|
1588
|
+
if ("done" in chunk) {
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
if ("content" in chunk &&
|
|
1592
|
+
typeof chunk.content === "string" &&
|
|
1593
|
+
chunk.content.length > 0) {
|
|
1594
|
+
contentYielded++;
|
|
1595
|
+
}
|
|
1596
|
+
yield chunk;
|
|
1597
|
+
}
|
|
1598
|
+
// Surface any error the loop threw after draining the queue.
|
|
1599
|
+
await loopPromise;
|
|
1600
|
+
// No-output path: stream completed normally but yielded zero text.
|
|
1601
|
+
if (contentYielded === 0 && toolsUsed.length === 0) {
|
|
1602
|
+
logger.warn(`${providerName}: Stream produced no output — emitting enriched sentinel`);
|
|
1603
|
+
const fauxNoOutput = new NoOutputGeneratedError({
|
|
1604
|
+
message: "Stream produced no output",
|
|
1605
|
+
});
|
|
1606
|
+
const sentinel = await buildNoOutputSentinel(fauxNoOutput, undefined, capturedProviderError);
|
|
1607
|
+
stampNoOutputSpan(sentinel);
|
|
1608
|
+
yield sentinel;
|
|
1609
|
+
}
|
|
880
1610
|
}
|
|
881
1611
|
catch (streamError) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
});
|
|
888
|
-
if (streamError instanceof Error) {
|
|
889
|
-
streamSpan.recordException(streamError);
|
|
1612
|
+
const sentinel = await buildNoOutputSentinel(streamError, undefined, capturedProviderError);
|
|
1613
|
+
stampNoOutputSpan(sentinel);
|
|
1614
|
+
yield sentinel;
|
|
1615
|
+
if (!NoOutputGeneratedError.isInstance(streamError)) {
|
|
1616
|
+
throw streamError;
|
|
890
1617
|
}
|
|
891
|
-
streamSpan.end();
|
|
892
|
-
throw streamError;
|
|
893
1618
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
.then((usage) => {
|
|
898
|
-
streamSpan.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens || 0);
|
|
899
|
-
streamSpan.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens || 0);
|
|
900
|
-
const cost = calculateCost(this.providerName, this.modelName, {
|
|
901
|
-
input: usage.inputTokens || 0,
|
|
902
|
-
output: usage.outputTokens || 0,
|
|
903
|
-
total: (usage.inputTokens || 0) + (usage.outputTokens || 0),
|
|
904
|
-
});
|
|
905
|
-
if (cost && cost > 0) {
|
|
906
|
-
streamSpan.setAttribute("neurolink.cost", cost);
|
|
1619
|
+
finally {
|
|
1620
|
+
if (!consumerAbortController.signal.aborted) {
|
|
1621
|
+
consumerAbortController.abort();
|
|
907
1622
|
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
// Finish reason may not be available if the stream is aborted
|
|
918
|
-
});
|
|
919
|
-
// End the span when the stream text resolves (stream fully consumed)
|
|
920
|
-
Promise.resolve(result.text)
|
|
921
|
-
.then(() => {
|
|
922
|
-
streamSpan.end();
|
|
923
|
-
})
|
|
924
|
-
.catch((err) => {
|
|
925
|
-
streamSpan.setStatus({
|
|
926
|
-
code: SpanStatusCode.ERROR,
|
|
927
|
-
message: err instanceof Error ? err.message : String(err),
|
|
928
|
-
});
|
|
929
|
-
streamSpan.end();
|
|
930
|
-
});
|
|
931
|
-
timeoutController?.cleanup();
|
|
932
|
-
const transformedStream = this.createTextStream(result, () => capturedProviderError);
|
|
933
|
-
// ✅ Note: Vercel AI SDK's streamText() method limitations with tools
|
|
934
|
-
// The streamText() function doesn't provide the same tool result access as generateText()
|
|
935
|
-
// Full tool support is now available with real streaming
|
|
936
|
-
const toolCalls = [];
|
|
937
|
-
const toolResults = [];
|
|
938
|
-
return {
|
|
939
|
-
stream: transformedStream,
|
|
940
|
-
provider: this.providerName,
|
|
941
|
-
model: this.modelName,
|
|
942
|
-
toolCalls, // ✅ Include tool calls in stream result
|
|
943
|
-
toolResults, // ✅ Include tool results in stream result
|
|
944
|
-
// Note: omit usage/finishReason to avoid blocking streaming; compute asynchronously if needed.
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
catch (error) {
|
|
948
|
-
timeoutController?.cleanup();
|
|
949
|
-
throw this.handleProviderError(error);
|
|
950
|
-
}
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
return {
|
|
1626
|
+
stream: transformedStream(),
|
|
1627
|
+
provider: this.providerName,
|
|
1628
|
+
model: this.modelName,
|
|
1629
|
+
toolCalls: [],
|
|
1630
|
+
toolResults: [],
|
|
1631
|
+
};
|
|
951
1632
|
}
|
|
952
1633
|
async isAvailable() {
|
|
953
1634
|
try {
|
|
@@ -965,11 +1646,10 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
965
1646
|
}
|
|
966
1647
|
}
|
|
967
1648
|
getModel() {
|
|
968
|
-
return this.
|
|
1649
|
+
return this.getAISDKModel();
|
|
969
1650
|
}
|
|
970
1651
|
}
|
|
971
1652
|
// Re-export types and utilities for convenience
|
|
972
1653
|
export { getModelCapabilities, getRecommendedModelForTier, isModelAvailableForTier, ModelAccessError, } from "../models/anthropicModels.js";
|
|
973
1654
|
// Export beta headers constant for external use
|
|
974
1655
|
export { ANTHROPIC_BETA_HEADERS };
|
|
975
|
-
export default AnthropicProvider;
|