@f5-sales-demo/pi-ai 19.90.1 → 19.91.0
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/package.json +4 -4
- package/src/providers/anthropic.ts +104 -17
- package/src/types.ts +49 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-ai",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.91.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"generate-models": "bun scripts/generate-models.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@anthropic-ai/sdk": "^0.
|
|
44
|
+
"@anthropic-ai/sdk": "^0.115",
|
|
45
45
|
"@aws-sdk/client-bedrock-runtime": "^3",
|
|
46
46
|
"@bufbuild/protobuf": "^2.11",
|
|
47
|
-
"@google/genai": "^
|
|
48
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
47
|
+
"@google/genai": "^2.13",
|
|
48
|
+
"@f5-sales-demo/pi-utils": "19.91.0",
|
|
49
49
|
"@sinclair/typebox": "^0.34",
|
|
50
50
|
"@smithy/node-http-handler": "^4.4",
|
|
51
51
|
"ajv": "^8.20",
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as tls from "node:tls";
|
|
4
4
|
import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
|
|
5
5
|
import type {
|
|
6
|
+
CitationsDelta,
|
|
6
7
|
ContentBlockParam,
|
|
7
8
|
MessageCreateParamsStreaming,
|
|
8
9
|
MessageParam,
|
|
@@ -30,6 +31,7 @@ import type {
|
|
|
30
31
|
ToolCall,
|
|
31
32
|
ToolResultMessage,
|
|
32
33
|
Usage,
|
|
34
|
+
WebCitation,
|
|
33
35
|
} from "../types";
|
|
34
36
|
import { isAnthropicOAuthToken, normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
35
37
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
@@ -629,6 +631,24 @@ export function isProviderRetryableError(error: unknown): boolean {
|
|
|
629
631
|
);
|
|
630
632
|
}
|
|
631
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Map an Anthropic `citations_delta` citation to a `WebCitation`.
|
|
636
|
+
*
|
|
637
|
+
* Only web-search citations are carried: the char/page/content-block locations belong to the
|
|
638
|
+
* document-citations API, which cites user-supplied documents rather than search results and has
|
|
639
|
+
* no source URL to render as a chip.
|
|
640
|
+
*/
|
|
641
|
+
function mapAnthropicWebCitation(citation: CitationsDelta["citation"]): WebCitation | undefined {
|
|
642
|
+
if (citation.type !== "web_search_result_location") return undefined;
|
|
643
|
+
return {
|
|
644
|
+
type: "web_search_result_location",
|
|
645
|
+
url: citation.url,
|
|
646
|
+
...(citation.title ? { title: citation.title } : {}),
|
|
647
|
+
...(citation.cited_text ? { citedText: citation.cited_text } : {}),
|
|
648
|
+
...(citation.encrypted_index ? { encryptedIndex: citation.encrypted_index } : {}),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
632
652
|
function createEmptyUsage(premiumRequests?: number): Usage {
|
|
633
653
|
return {
|
|
634
654
|
input: 0,
|
|
@@ -737,6 +757,19 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
737
757
|
const anthropicRequest = client.messages.create({ ...params, stream: true }, { signal: requestSignal });
|
|
738
758
|
let streamedReplayUnsafeContent = false;
|
|
739
759
|
|
|
760
|
+
// SERVER-SIDE tools (Anthropic's built-in web search) are executed by the provider,
|
|
761
|
+
// so their blocks are deliberately NOT pushed into `output.content`: there is nothing
|
|
762
|
+
// for the agent loop to dispatch, and keeping their `encrypted_content` out of history
|
|
763
|
+
// avoids the byte-exact echo requirement that would 400 a follow-up turn. They are
|
|
764
|
+
// tracked here only long enough to report progress — keyed by the stream's block index
|
|
765
|
+
// (to accumulate the query) and by tool id (to name the matching result block).
|
|
766
|
+
// Declared inside the retry loop so a replayed attempt starts clean.
|
|
767
|
+
const serverToolUses = new Map<
|
|
768
|
+
number,
|
|
769
|
+
{ id: string; name: string; partialJson: string; inlineInput: unknown }
|
|
770
|
+
>();
|
|
771
|
+
const serverToolNames = new Map<string, string>();
|
|
772
|
+
|
|
740
773
|
try {
|
|
741
774
|
const { data: anthropicStream } = await anthropicRequest.withResponse();
|
|
742
775
|
const firstEventWatchdog = createFirstEventWatchdog(firstEventTimeoutMs, () =>
|
|
@@ -825,6 +858,30 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
825
858
|
contentIndex: output.content.length - 1,
|
|
826
859
|
partial: output,
|
|
827
860
|
});
|
|
861
|
+
} else if (event.content_block.type === "server_tool_use") {
|
|
862
|
+
// Progress-only: remember the call so its query can be reported once the
|
|
863
|
+
// input JSON has finished streaming (see content_block_stop).
|
|
864
|
+
serverToolUses.set(event.index, {
|
|
865
|
+
id: event.content_block.id,
|
|
866
|
+
name: event.content_block.name,
|
|
867
|
+
partialJson: "",
|
|
868
|
+
// Nothing obliges the provider to stream the input as deltas — it may
|
|
869
|
+
// arrive whole right here. Kept as the fallback for the query.
|
|
870
|
+
inlineInput: event.content_block.input,
|
|
871
|
+
});
|
|
872
|
+
serverToolNames.set(event.content_block.id, event.content_block.name);
|
|
873
|
+
} else if (event.content_block.type === "web_search_tool_result") {
|
|
874
|
+
const toolId = event.content_block.tool_use_id;
|
|
875
|
+
const content = event.content_block.content;
|
|
876
|
+
stream.push({
|
|
877
|
+
type: "server_tool_end",
|
|
878
|
+
toolName: serverToolNames.get(toolId) ?? "web_search",
|
|
879
|
+
toolId,
|
|
880
|
+
...(Array.isArray(content)
|
|
881
|
+
? { resultCount: content.length }
|
|
882
|
+
: { errorCode: content.error_code }),
|
|
883
|
+
partial: output,
|
|
884
|
+
});
|
|
828
885
|
}
|
|
829
886
|
} else if (event.type === "content_block_delta") {
|
|
830
887
|
if (event.delta.type === "text_delta") {
|
|
@@ -852,17 +909,31 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
852
909
|
});
|
|
853
910
|
}
|
|
854
911
|
} else if (event.delta.type === "input_json_delta") {
|
|
912
|
+
const serverToolUse = serverToolUses.get(event.index);
|
|
913
|
+
if (serverToolUse) {
|
|
914
|
+
// A server-side tool's input (the search query). Accumulated but not
|
|
915
|
+
// streamed as a toolcall_delta — there is no toolCall block to attach to.
|
|
916
|
+
serverToolUse.partialJson += event.delta.partial_json;
|
|
917
|
+
} else {
|
|
918
|
+
const index = blocks.findIndex(b => b.index === event.index);
|
|
919
|
+
const block = blocks[index];
|
|
920
|
+
if (block && block.type === "toolCall") {
|
|
921
|
+
block.partialJson += event.delta.partial_json;
|
|
922
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
923
|
+
stream.push({
|
|
924
|
+
type: "toolcall_delta",
|
|
925
|
+
contentIndex: index,
|
|
926
|
+
delta: event.delta.partial_json,
|
|
927
|
+
partial: output,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
} else if (event.delta.type === "citations_delta") {
|
|
855
932
|
const index = blocks.findIndex(b => b.index === event.index);
|
|
856
933
|
const block = blocks[index];
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
block.
|
|
860
|
-
stream.push({
|
|
861
|
-
type: "toolcall_delta",
|
|
862
|
-
contentIndex: index,
|
|
863
|
-
delta: event.delta.partial_json,
|
|
864
|
-
partial: output,
|
|
865
|
-
});
|
|
934
|
+
const citation = mapAnthropicWebCitation(event.delta.citation);
|
|
935
|
+
if (block && block.type === "text" && citation) {
|
|
936
|
+
block.citations = [...(block.citations ?? []), citation];
|
|
866
937
|
}
|
|
867
938
|
} else if (event.delta.type === "signature_delta") {
|
|
868
939
|
const index = blocks.findIndex(b => b.index === event.index);
|
|
@@ -873,6 +944,28 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
873
944
|
}
|
|
874
945
|
}
|
|
875
946
|
} else if (event.type === "content_block_stop") {
|
|
947
|
+
const serverToolUse = serverToolUses.get(event.index);
|
|
948
|
+
if (serverToolUse) {
|
|
949
|
+
// The query has finished streaming, so the activity row can name what is
|
|
950
|
+
// being searched for. Reported here rather than at content_block_start
|
|
951
|
+
// because that is the first moment the query is known — and it still lands
|
|
952
|
+
// well before the provider's search completes.
|
|
953
|
+
serverToolUses.delete(event.index);
|
|
954
|
+
const streamed = parseStreamingJson(serverToolUse.partialJson) as
|
|
955
|
+
| { query?: unknown }
|
|
956
|
+
| undefined;
|
|
957
|
+
const inline = serverToolUse.inlineInput as { query?: unknown } | undefined;
|
|
958
|
+
const rawQuery = typeof streamed?.query === "string" ? streamed.query : inline?.query;
|
|
959
|
+
const query = typeof rawQuery === "string" && rawQuery.length > 0 ? rawQuery : undefined;
|
|
960
|
+
stream.push({
|
|
961
|
+
type: "server_tool_start",
|
|
962
|
+
toolName: serverToolUse.name,
|
|
963
|
+
toolId: serverToolUse.id,
|
|
964
|
+
...(query === undefined ? {} : { query }),
|
|
965
|
+
partial: output,
|
|
966
|
+
});
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
876
969
|
const index = blocks.findIndex(b => b.index === event.index);
|
|
877
970
|
const block = blocks[index];
|
|
878
971
|
if (block) {
|
|
@@ -1426,16 +1519,10 @@ function buildParams(
|
|
|
1426
1519
|
const effort =
|
|
1427
1520
|
options.effort ?? (requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
|
|
1428
1521
|
|
|
1429
|
-
// The pinned @anthropic-ai/sdk (0.78) types `OutputConfig.effort` as
|
|
1430
|
-
// 'low'|'medium'|'high'|'max' — it predates `xhigh`, which the live API
|
|
1431
|
-
// accepts (verified: its 400 enumerates "'low', 'medium', 'high', 'xhigh'
|
|
1432
|
-
// or 'max'"). Cast until the SDK pin is bumped; the wire value is valid.
|
|
1433
|
-
const outputConfigEffort = effort as NonNullable<MessageCreateParamsStreaming["output_config"]>["effort"];
|
|
1434
|
-
|
|
1435
1522
|
if (mode === "anthropic-adaptive") {
|
|
1436
1523
|
params.thinking = { type: "adaptive" };
|
|
1437
1524
|
if (effort) {
|
|
1438
|
-
params.output_config = { effort
|
|
1525
|
+
params.output_config = { effort };
|
|
1439
1526
|
}
|
|
1440
1527
|
} else {
|
|
1441
1528
|
params.thinking = {
|
|
@@ -1443,7 +1530,7 @@ function buildParams(
|
|
|
1443
1530
|
budget_tokens: options.thinkingBudgetTokens || 1024,
|
|
1444
1531
|
};
|
|
1445
1532
|
if (mode === "anthropic-budget-effort" && effort) {
|
|
1446
|
-
params.output_config = { effort
|
|
1533
|
+
params.output_config = { effort };
|
|
1447
1534
|
}
|
|
1448
1535
|
}
|
|
1449
1536
|
// Anthropic requires temperature=1 when thinking is enabled; override any
|
package/src/types.ts
CHANGED
|
@@ -252,10 +252,33 @@ export interface TextSignatureV1 {
|
|
|
252
252
|
phase?: "commentary" | "final_answer";
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* A structured citation annotating a span of assistant text, produced by a provider's
|
|
257
|
+
* SERVER-SIDE search tool (Anthropic `citations_delta` → `web_search_result_location`).
|
|
258
|
+
*
|
|
259
|
+
* Carries the source title/url so hosts can render precise "Sources" chips instead of
|
|
260
|
+
* regex-scraping URLs out of the prose.
|
|
261
|
+
*/
|
|
262
|
+
export interface WebCitation {
|
|
263
|
+
type: "web_search_result_location";
|
|
264
|
+
url: string;
|
|
265
|
+
title?: string;
|
|
266
|
+
/** The span of source material the assistant drew on. */
|
|
267
|
+
citedText?: string;
|
|
268
|
+
/**
|
|
269
|
+
* Opaque provider index for the cited result. Retained for fidelity only — it is never
|
|
270
|
+
* re-sent, because echoing a provider's encrypted citation fields back on a later turn
|
|
271
|
+
* must be byte-exact or the request is rejected.
|
|
272
|
+
*/
|
|
273
|
+
encryptedIndex?: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
255
276
|
export interface TextContent {
|
|
256
277
|
type: "text";
|
|
257
278
|
text: string;
|
|
258
279
|
textSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON)
|
|
280
|
+
/** Structured citations for this span, when the provider ran a server-side search. */
|
|
281
|
+
citations?: WebCitation[];
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
export interface ThinkingContent {
|
|
@@ -429,6 +452,32 @@ export type AssistantMessageEvent =
|
|
|
429
452
|
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
|
|
430
453
|
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
431
454
|
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
|
|
455
|
+
/**
|
|
456
|
+
* A provider-SIDE tool (e.g. Anthropic's built-in web search) started running.
|
|
457
|
+
*
|
|
458
|
+
* Purely a progress signal so hosts can show a live activity row — the provider executes
|
|
459
|
+
* these itself, so unlike `toolcall_*` there is nothing for the agent loop to dispatch and
|
|
460
|
+
* no content block is added to the message.
|
|
461
|
+
*/
|
|
462
|
+
| {
|
|
463
|
+
type: "server_tool_start";
|
|
464
|
+
contentIndex?: undefined;
|
|
465
|
+
toolName: string;
|
|
466
|
+
toolId: string;
|
|
467
|
+
/** The search query, when the provider streamed one. */
|
|
468
|
+
query?: string;
|
|
469
|
+
partial: AssistantMessage;
|
|
470
|
+
}
|
|
471
|
+
/** A provider-side tool finished: either it returned results, or it failed. */
|
|
472
|
+
| {
|
|
473
|
+
type: "server_tool_end";
|
|
474
|
+
contentIndex?: undefined;
|
|
475
|
+
toolName: string;
|
|
476
|
+
toolId: string;
|
|
477
|
+
resultCount?: number;
|
|
478
|
+
errorCode?: string;
|
|
479
|
+
partial: AssistantMessage;
|
|
480
|
+
}
|
|
432
481
|
| {
|
|
433
482
|
type: "done";
|
|
434
483
|
contentIndex?: undefined;
|