@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "19.90.1",
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.78",
44
+ "@anthropic-ai/sdk": "^0.115",
45
45
  "@aws-sdk/client-bedrock-runtime": "^3",
46
46
  "@bufbuild/protobuf": "^2.11",
47
- "@google/genai": "^1.43",
48
- "@f5-sales-demo/pi-utils": "19.90.1",
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
- if (block && block.type === "toolCall") {
858
- block.partialJson += event.delta.partial_json;
859
- block.arguments = parseStreamingJson(block.partialJson);
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: outputConfigEffort };
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: outputConfigEffort };
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;