@f5-sales-demo/pi-ai 21.11.4 → 21.11.6

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": "21.11.4",
4
+ "version": "21.11.6",
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",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "^0.115",
47
47
  "@aws-sdk/client-bedrock-runtime": "^3",
48
48
  "@bufbuild/protobuf": "^2.11",
49
- "@f5-sales-demo/pi-utils": "21.11.4",
49
+ "@f5-sales-demo/pi-utils": "21.11.6",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -40,6 +40,7 @@ import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotAuthError
40
40
  import { createFirstEventWatchdog, getStreamFirstEventTimeoutMs, markFirstStreamEvent } from "../utils/idle-iterator";
41
41
  import { parseStreamingJson } from "../utils/json-parse";
42
42
  import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
43
+ import { adaptSchemaForStrict } from "../utils/schema";
43
44
  import {
44
45
  buildCopilotDynamicHeaders,
45
46
  hasCopilotVisionInput,
@@ -741,7 +742,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
741
742
  | ThinkingContent
742
743
  | RedactedThinkingContent
743
744
  | TextContent
744
- | (ToolCall & { partialJson: string })
745
+ | (ToolCall & { partialJson: string; hasJsonDeltas: boolean })
745
746
  ) & { index: number };
746
747
  const blocks = output.content as Block[];
747
748
  const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs();
@@ -861,6 +862,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
861
862
  : event.content_block.name,
862
863
  arguments: (event.content_block.input as Record<string, unknown>) ?? {},
863
864
  partialJson: "",
865
+ hasJsonDeltas: false,
864
866
  index: event.index,
865
867
  };
866
868
  output.content.push(block);
@@ -929,6 +931,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
929
931
  const index = blocks.findIndex(b => b.index === event.index);
930
932
  const block = blocks[index];
931
933
  if (block && block.type === "toolCall") {
934
+ block.hasJsonDeltas = true;
932
935
  block.partialJson += event.delta.partial_json;
933
936
  block.arguments = parseStreamingJson(block.partialJson);
934
937
  stream.push({
@@ -996,8 +999,18 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
996
999
  partial: output,
997
1000
  });
998
1001
  } else if (block.type === "toolCall") {
999
- block.arguments = parseStreamingJson(block.partialJson);
1002
+ if (block.hasJsonDeltas) {
1003
+ try {
1004
+ block.arguments = JSON.parse(block.partialJson) as Record<string, unknown>;
1005
+ } catch (error) {
1006
+ throw new Error(
1007
+ `Anthropic returned malformed completed tool input JSON for "${block.name}"`,
1008
+ { cause: error },
1009
+ );
1010
+ }
1011
+ }
1000
1012
  delete (block as { partialJson?: string }).partialJson;
1013
+ delete (block as { hasJsonDeltas?: boolean }).hasJsonDeltas;
1001
1014
  stream.push({
1002
1015
  type: "toolcall_end",
1003
1016
  contentIndex: index,
@@ -1095,6 +1108,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1095
1108
  for (const block of output.content) {
1096
1109
  delete (block as { index?: number }).index;
1097
1110
  delete (block as { partialJson?: string }).partialJson;
1111
+ delete (block as { hasJsonDeltas?: boolean }).hasJsonDeltas;
1098
1112
  }
1099
1113
  const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
1100
1114
  output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
@@ -1533,7 +1547,15 @@ function buildParams(
1533
1547
  }
1534
1548
 
1535
1549
  if (context.tools) {
1536
- params.tools = convertTools(context.tools, isOAuthToken);
1550
+ const toolChoice = options?.toolChoice;
1551
+ const forcedToolName =
1552
+ model.provider === "anthropic" &&
1553
+ supportsAnthropicStrictToolUse(model.id) &&
1554
+ typeof toolChoice === "object" &&
1555
+ toolChoice.type === "tool"
1556
+ ? toolChoice.name
1557
+ : undefined;
1558
+ params.tools = convertTools(context.tools, isOAuthToken, forcedToolName);
1537
1559
  }
1538
1560
 
1539
1561
  if (options?.thinkingEnabled && model.reasoning) {
@@ -1761,20 +1783,88 @@ export function convertAnthropicMessages(
1761
1783
  return params;
1762
1784
  }
1763
1785
 
1764
- function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
1786
+ const ANTHROPIC_STRICT_TOOL_MODEL_IDS = new Set([
1787
+ "claude-fable-5-1",
1788
+ "claude-mythos-5-1",
1789
+ "claude-fable-5",
1790
+ "claude-mythos-5",
1791
+ "claude-mythos-preview",
1792
+ "claude-opus-5",
1793
+ "claude-opus-4-8",
1794
+ "claude-opus-4-7",
1795
+ "claude-opus-4-6",
1796
+ "claude-sonnet-5",
1797
+ "claude-sonnet-4-6",
1798
+ "claude-sonnet-4-5",
1799
+ "claude-sonnet-4-5-20250929",
1800
+ "claude-opus-4-5",
1801
+ "claude-opus-4-5-20251101",
1802
+ "claude-haiku-4-5",
1803
+ "claude-haiku-4-5-20251001",
1804
+ ]);
1805
+
1806
+ function supportsAnthropicStrictToolUse(modelId: string): boolean {
1807
+ return ANTHROPIC_STRICT_TOOL_MODEL_IDS.has(modelId);
1808
+ }
1809
+
1810
+ function countAnthropicStrictSchemaParameters(schema: Record<string, unknown>): {
1811
+ optional: number;
1812
+ unions: number;
1813
+ } {
1814
+ const counts = { optional: 0, unions: 0 };
1815
+ const seen = new WeakSet<object>();
1816
+ const visit = (node: unknown, isParameter: boolean): void => {
1817
+ if (node === null || typeof node !== "object" || seen.has(node)) return;
1818
+ seen.add(node);
1819
+ if (Array.isArray(node)) {
1820
+ for (const child of node) visit(child, false);
1821
+ seen.delete(node);
1822
+ return;
1823
+ }
1824
+ const record = node as Record<string, unknown>;
1825
+ if (isParameter && (Array.isArray(record.anyOf) || Array.isArray(record.type))) counts.unions++;
1826
+ if (record.type === "object" && record.properties && typeof record.properties === "object") {
1827
+ const required = new Set(Array.isArray(record.required) ? record.required : []);
1828
+ for (const [name, child] of Object.entries(record.properties as Record<string, unknown>)) {
1829
+ if (!required.has(name)) counts.optional++;
1830
+ visit(child, true);
1831
+ }
1832
+ }
1833
+ for (const [key, child] of Object.entries(record)) {
1834
+ if (key !== "properties") visit(child, false);
1835
+ }
1836
+ seen.delete(node);
1837
+ };
1838
+ visit(schema, false);
1839
+ return counts;
1840
+ }
1841
+
1842
+ function isWithinAnthropicStrictSchemaLimits(schema: Record<string, unknown>): boolean {
1843
+ const counts = countAnthropicStrictSchemaParameters(schema);
1844
+ return counts.optional <= 24 && counts.unions <= 16;
1845
+ }
1846
+
1847
+ function convertTools(tools: Tool[], isOAuthToken: boolean, forcedStrictToolName?: string): Anthropic.Messages.Tool[] {
1765
1848
  if (!tools) return [];
1766
1849
 
1767
1850
  return tools.map(tool => {
1768
- const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
1851
+ const jsonSchema = tool.parameters as Record<string, unknown>;
1852
+ const wantsStrict = tool.name === forcedStrictToolName && tool.strict === true;
1853
+ const adapted = adaptSchemaForStrict(jsonSchema, wantsStrict);
1854
+ const useStrict = adapted.strict && isWithinAnthropicStrictSchemaLimits(adapted.schema);
1855
+ const inputSchema = useStrict ? adapted.schema : jsonSchema;
1856
+ const anthropicInputSchema: Anthropic.Messages.Tool.InputSchema = {
1857
+ ...inputSchema,
1858
+ type: "object",
1859
+ properties: (inputSchema.properties as Record<string, unknown> | undefined) ?? {},
1860
+ required: Array.isArray(inputSchema.required) ? (inputSchema.required as string[]) : [],
1861
+ };
1769
1862
 
1770
1863
  return {
1771
1864
  name: isOAuthToken ? applyClaudeToolPrefix(tool.name) : tool.name,
1772
1865
  description: tool.description || "",
1773
- input_schema: {
1774
- type: "object" as const,
1775
- properties: jsonSchema.properties || {},
1776
- required: jsonSchema.required || [],
1777
- },
1866
+ input_schema: anthropicInputSchema,
1867
+ ...(useStrict ? { strict: true } : {}),
1778
1868
  };
1779
1869
  });
1780
1870
  }