@earendil-works/pi-ai 1.1.0 → 1.3.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.
Files changed (103) hide show
  1. package/README.md +5 -34
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +120 -74
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/bedrock-converse-stream.js +16 -18
  6. package/dist/api/bedrock-converse-stream.js.map +1 -1
  7. package/dist/api/google-shared.d.ts.map +1 -1
  8. package/dist/api/google-shared.js +2 -0
  9. package/dist/api/google-shared.js.map +1 -1
  10. package/dist/api/mistral-conversations.d.ts.map +1 -1
  11. package/dist/api/mistral-conversations.js +31 -15
  12. package/dist/api/mistral-conversations.js.map +1 -1
  13. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  14. package/dist/api/openai-codex-responses.js +2 -1
  15. package/dist/api/openai-codex-responses.js.map +1 -1
  16. package/dist/api/openai-completions.d.ts.map +1 -1
  17. package/dist/api/openai-completions.js +29 -27
  18. package/dist/api/openai-completions.js.map +1 -1
  19. package/dist/api/openai-responses-shared.d.ts +10 -0
  20. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  21. package/dist/api/openai-responses-shared.js +15 -2
  22. package/dist/api/openai-responses-shared.js.map +1 -1
  23. package/dist/api/openai-responses.d.ts.map +1 -1
  24. package/dist/api/openai-responses.js +21 -4
  25. package/dist/api/openai-responses.js.map +1 -1
  26. package/dist/api/transform-messages.d.ts +16 -1
  27. package/dist/api/transform-messages.d.ts.map +1 -1
  28. package/dist/api/transform-messages.js +28 -15
  29. package/dist/api/transform-messages.js.map +1 -1
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +0 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/models.d.ts +5 -0
  35. package/dist/models.d.ts.map +1 -1
  36. package/dist/models.js +1 -0
  37. package/dist/models.js.map +1 -1
  38. package/dist/providers/compat-schema.d.ts +341 -0
  39. package/dist/providers/compat-schema.d.ts.map +1 -0
  40. package/dist/providers/compat-schema.js +383 -0
  41. package/dist/providers/compat-schema.js.map +1 -0
  42. package/dist/providers/data/.manifest.json +1 -1
  43. package/dist/providers/data/amazon-bedrock.json +1 -1
  44. package/dist/providers/data/anthropic.json +1 -1
  45. package/dist/providers/data/baseten.json +1 -1
  46. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  47. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  48. package/dist/providers/data/github-copilot.json +1 -1
  49. package/dist/providers/data/google-vertex.json +1 -1
  50. package/dist/providers/data/google.json +1 -1
  51. package/dist/providers/data/huggingface.json +1 -1
  52. package/dist/providers/data/kimi-coding.json +1 -1
  53. package/dist/providers/data/meta.json +1 -1
  54. package/dist/providers/data/mistral.json +1 -1
  55. package/dist/providers/data/nvidia.json +1 -1
  56. package/dist/providers/data/openai-codex.json +1 -1
  57. package/dist/providers/data/openai.json +1 -1
  58. package/dist/providers/data/opencode-go.json +1 -1
  59. package/dist/providers/data/opencode.json +1 -1
  60. package/dist/providers/data/openrouter.json +1 -1
  61. package/dist/providers/data/radius.json +1 -1
  62. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  63. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  64. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  65. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  66. package/dist/providers/data/xiaomi.json +1 -1
  67. package/dist/providers/kimi-coding.d.ts +1 -1
  68. package/dist/providers/kimi-coding.js +3 -3
  69. package/dist/providers/kimi-coding.js.map +1 -1
  70. package/dist/providers/model-schema.d.ts +91 -0
  71. package/dist/providers/model-schema.d.ts.map +1 -0
  72. package/dist/providers/model-schema.js +55 -0
  73. package/dist/providers/model-schema.js.map +1 -0
  74. package/dist/providers/opencode-go.d.ts +3 -1
  75. package/dist/providers/opencode-go.d.ts.map +1 -1
  76. package/dist/providers/opencode-go.js +2 -0
  77. package/dist/providers/opencode-go.js.map +1 -1
  78. package/dist/providers/openrouter-api.d.ts +40 -0
  79. package/dist/providers/openrouter-api.d.ts.map +1 -0
  80. package/dist/providers/openrouter-api.js +41 -0
  81. package/dist/providers/openrouter-api.js.map +1 -0
  82. package/dist/providers/openrouter.d.ts +11 -1
  83. package/dist/providers/openrouter.d.ts.map +1 -1
  84. package/dist/providers/openrouter.js +52 -1
  85. package/dist/providers/openrouter.js.map +1 -1
  86. package/dist/types.d.ts +10 -303
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/utils/json-parse.d.ts +10 -0
  90. package/dist/utils/json-parse.d.ts.map +1 -1
  91. package/dist/utils/json-parse.js +26 -1
  92. package/dist/utils/json-parse.js.map +1 -1
  93. package/dist/utils/retry.d.ts.map +1 -1
  94. package/dist/utils/retry.js +8 -3
  95. package/dist/utils/retry.js.map +1 -1
  96. package/dist/utils/validation.d.ts.map +1 -1
  97. package/dist/utils/validation.js +3 -0
  98. package/dist/utils/validation.js.map +1 -1
  99. package/package.json +2 -2
  100. package/dist/utils/assistant-message-frame.d.ts +0 -77
  101. package/dist/utils/assistant-message-frame.d.ts.map +0 -1
  102. package/dist/utils/assistant-message-frame.js +0 -425
  103. package/dist/utils/assistant-message-frame.js.map +0 -1
package/README.md CHANGED
@@ -26,7 +26,6 @@ Unified LLM API with provider collections, automatic auth resolution, token and
26
26
  - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)
27
27
  - [Validating Tool Arguments](#validating-tool-arguments)
28
28
  - [Complete Event Reference](#complete-event-reference)
29
- - [Compact Assistant Message Frames](#compact-assistant-message-frames)
30
29
  - [Image Input](#image-input)
31
30
  - [Image Generation](#image-generation)
32
31
  - [Classification](#classification)
@@ -723,34 +722,6 @@ All streaming events emitted during assistant message generation:
723
722
 
724
723
  Streaming events for different content blocks are not guaranteed to be contiguous. Providers may emit deltas for text, thinking, and tool calls in the same upstream chunk, and pi may surface corresponding events interleaved, for example `text_start`, `text_delta`, `toolcall_start`, `text_delta`, `toolcall_delta`. Consumers must use `contentIndex` to associate each delta/end event with its block and must not assume that a block's `*_start`/`*_delta`/`*_end` sequence is uninterrupted by events for other blocks.
725
724
 
726
- ### Compact Assistant Message Frames
727
-
728
- `AssistantMessageFrameEncoder` converts one stream into compact, persistable `AssistantMessageFrame` values. Create one encoder per stream and feed it every event in order. The encoder understands that `partial` is live: a block-start event consumed after the provider has already queued later deltas snapshots the current block once, and covered queued text/thinking deltas produce no duplicate frame. It retains only per-open-block counters plus, temporarily, the raw prefix needed to synchronize an already-advanced tool call. It never clones the growing full partial per token.
729
-
730
- The start frame contains message metadata with empty content. Text and thinking frames store each generated character at most once before the authoritative end frame. Tool calls that were already advanced when their start event was consumed use one compact JSON checkpoint before ordinary deltas resume. Terminal `done` and `error` events produce no frame because final message settlement is separate. A pre-generation `error` therefore produces no frames.
731
-
732
- `reduceAssistantMessageFrames()` is the canonical pure reducer. It reconstructs text, thinking, and tool-call arguments, including interleaved blocks identified by `contentIndex`, and rejects malformed sequences. It performs a single pass over the iterable and returns `undefined` when there is no start frame. End frames replace blocks with the provider's authoritative completed content and metadata. The reducer does not validate tool arguments against a TypeBox schema; call `validateToolCall` before execution.
733
-
734
- ```typescript
735
- import {
736
- AssistantMessageFrameEncoder,
737
- reduceAssistantMessageFrames,
738
- type AssistantMessageFrame,
739
- } from '@earendil-works/pi-ai';
740
-
741
- const encoder = new AssistantMessageFrameEncoder();
742
- const frames: AssistantMessageFrame[] = [];
743
- for await (const event of s) {
744
- const frame = encoder.encode(event);
745
- if (frame) frames.push(frame);
746
- }
747
-
748
- const reconstructedPartial = reduceAssistantMessageFrames(frames);
749
- const finalMessage = await s.result(); // Persist terminal settlement separately.
750
- ```
751
-
752
- An encoder rejects duplicate starts, updates before start, `done` before start, events after a terminal event, duplicate block starts, and block-kind mismatches. An `error` before start is valid and returns no frame.
753
-
754
725
  ## Image Input
755
726
 
756
727
  Models with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.
@@ -1527,13 +1498,13 @@ Notes:
1527
1498
 
1528
1499
  ## Cross-Provider Handoffs
1529
1500
 
1530
- The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including thinking blocks, tool calls, and tool results.
1501
+ The library supports seamless handoffs between different LLM providers within the same conversation. This allows you to switch models mid-conversation while preserving context, including text, tool calls, and tool results.
1531
1502
 
1532
1503
  When messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:
1533
1504
 
1534
1505
  - **User and tool result messages** are passed through unchanged
1535
- - **Assistant messages from the same provider/API** are preserved as-is
1536
- - **Assistant messages from different providers** have their thinking blocks converted to text with `<thinking>` tags
1506
+ - **Assistant messages from the same provider, API, and model** are preserved as-is, including thinking blocks
1507
+ - **Assistant messages from a different model** have their thinking blocks dropped. Signatures and encrypted reasoning only work with the model that produced them, and readable thinking sent as plain text makes the new model treat it as part of the reply
1537
1508
  - **Tool calls and regular text** are preserved unchanged
1538
1509
 
1539
1510
  ```typescript
@@ -1554,7 +1525,7 @@ const claude = models.getModel('anthropic', 'claude-sonnet-4-5')!;
1554
1525
  context.messages.push({ role: 'user', content: 'What is 25 * 18?', timestamp: Date.now() });
1555
1526
  context.messages.push(await models.completeSimple(claude, context, { reasoning: 'medium' }));
1556
1527
 
1557
- // Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text
1528
+ // Switch to GPT-5 - it sees Claude's answer, but not Claude's thinking
1558
1529
  const gpt5 = models.getModel('openai', 'gpt-5-mini')!;
1559
1530
  context.messages.push({ role: 'user', content: 'Is that calculation correct?', timestamp: Date.now() });
1560
1531
  context.messages.push(await models.complete(gpt5, context));
@@ -1565,7 +1536,7 @@ context.messages.push({ role: 'user', content: 'What was the original question?'
1565
1536
  const geminiResponse = await models.complete(gemini, context);
1566
1537
  ```
1567
1538
 
1568
- All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (transformed to tagged text), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages.
1539
+ All providers can handle messages from other providers — text, tool calls and results (including images), thinking blocks (dropped when the model changes), and aborted messages with partial content. This enables flexible workflows: start with a fast model, switch to a more capable one for complex reasoning, or maintain continuity across provider outages.
1569
1540
 
1570
1541
  ## System Messages
1571
1542
 
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAmJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAwRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA0UzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
1
+ {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAuJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAwRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA0WzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
@@ -4,7 +4,7 @@ import { calculateCost } from "../models.js";
4
4
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
5
5
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
6
6
  import { headersToRecord } from "../utils/headers.js";
7
- import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
7
+ import { parseJsonWithRepair, parseStreamingJson, parseToolCallArguments } from "../utils/json-parse.js";
8
8
  import { getPiUserAgent } from "../utils/pi-user-agent.js";
9
9
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
10
  import { retryProviderRequest } from "../utils/provider-retry.js";
@@ -14,7 +14,7 @@ import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "..
14
14
  import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
15
15
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
16
16
  import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
17
- import { transformMessages } from "./transform-messages.js";
17
+ import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
18
18
  /**
19
19
  * Resolve cache retention preference.
20
20
  * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
@@ -75,6 +75,16 @@ const fromClaudeCodeName = (name, tools) => {
75
75
  }
76
76
  return name;
77
77
  };
78
+ function convertImageBlock(image) {
79
+ return {
80
+ type: "image",
81
+ source: {
82
+ type: "base64",
83
+ media_type: image.mimeType,
84
+ data: image.data,
85
+ },
86
+ };
87
+ }
78
88
  /**
79
89
  * Convert content blocks to Anthropic API format
80
90
  */
@@ -92,14 +102,7 @@ function convertContentBlocks(content) {
92
102
  text: sanitizeSurrogates(block.text),
93
103
  };
94
104
  }
95
- return {
96
- type: "image",
97
- source: {
98
- type: "base64",
99
- media_type: block.mimeType,
100
- data: block.data,
101
- },
102
- };
105
+ return convertImageBlock(block);
103
106
  });
104
107
  // If only images (no text), add placeholder text block
105
108
  const hasText = blocks.some((b) => b.type === "text");
@@ -378,14 +381,12 @@ export const stream = (model, context, options) => {
378
381
  const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
379
382
  const currentTools = getCurrentTools(normalizedContext.messages);
380
383
  (async () => {
381
- const providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? "high") : undefined;
382
384
  const output = {
383
385
  role: "assistant",
384
386
  content: [],
385
387
  api: model.api,
386
388
  provider: model.provider,
387
389
  model: model.id,
388
- ...(providerThinkingLevel === undefined ? {} : { providerThinkingLevel }),
389
390
  usage: {
390
391
  input: 0,
391
392
  output: 0,
@@ -425,21 +426,55 @@ export const stream = (model, context, options) => {
425
426
  client = created.client;
426
427
  isOAuth = created.isOAuthToken;
427
428
  }
428
- let params = buildParams(model, normalizedContext, isOAuth, options);
429
- const nextParams = await options?.onPayload?.(params, model);
430
- if (nextParams !== undefined) {
431
- params = { ...nextParams, stream: true };
432
- }
433
429
  const requestOptions = {
434
430
  ...(options?.signal ? { signal: options.signal } : {}),
435
431
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
436
432
  maxRetries: 0,
437
433
  };
438
- const response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
439
- maxRetries: options?.maxRetries,
440
- maxRetryDelayMs: options?.maxRetryDelayMs,
441
- signal: options?.signal,
442
- });
434
+ let disablePreferredStrictTools = false;
435
+ let response;
436
+ for (;;) {
437
+ options?.signal?.throwIfAborted();
438
+ const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
439
+ if (built.providerThinkingLevel)
440
+ output.providerThinkingLevel = built.providerThinkingLevel;
441
+ let params = built.params;
442
+ const nextParams = await options?.onPayload?.(params, model);
443
+ if (nextParams !== undefined) {
444
+ params = { ...nextParams, stream: true };
445
+ }
446
+ try {
447
+ response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
448
+ maxRetries: options?.maxRetries,
449
+ maxRetryDelayMs: options?.maxRetryDelayMs,
450
+ signal: options?.signal,
451
+ });
452
+ break;
453
+ }
454
+ catch (error) {
455
+ // Only downgrade a rejected request, never an already-started stream.
456
+ if (options?.signal?.aborted ||
457
+ disablePreferredStrictTools ||
458
+ !built.hasPreferredStrictTools ||
459
+ !(error instanceof Anthropic.APIError) ||
460
+ error.status !== 400 ||
461
+ error.type !== "invalid_request_error" ||
462
+ ![
463
+ "The compiled grammar is too large",
464
+ "Too many strict tools (",
465
+ "Schemas contains too many parameters with union types (",
466
+ ].some((message) => error.message.includes(message))) {
467
+ throw error;
468
+ }
469
+ disablePreferredStrictTools = true;
470
+ appendAssistantMessageDiagnostic(output, {
471
+ type: "anthropic_strict_tools_fallback",
472
+ timestamp: Date.now(),
473
+ error: { message: error.message },
474
+ details: { reason: "strict_tool_limits_exceeded" },
475
+ });
476
+ }
477
+ }
443
478
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
444
479
  stream.push({ type: "start", partial: output });
445
480
  const blocks = output.content;
@@ -593,7 +628,7 @@ export const stream = (model, context, options) => {
593
628
  });
594
629
  }
595
630
  else if (block.type === "toolCall") {
596
- block.arguments = parseStreamingJson(block.partialJson);
631
+ Object.assign(block, parseToolCallArguments(block.partialJson));
597
632
  // Finalize in-place and strip the scratch buffer so replay only
598
633
  // carries parsed arguments.
599
634
  delete block.partialJson;
@@ -855,7 +890,7 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
855
890
  features.push(INLINE_TOOLS_BETA);
856
891
  return [...new Set(features)];
857
892
  }
858
- function buildParams(model, context, isOAuthToken, options) {
893
+ function buildParams(model, context, isOAuthToken, options, disablePreferredStrictTools = false) {
859
894
  const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
860
895
  const compat = getAnthropicCompat(model);
861
896
  const initialSystemMessage = getInitialSystemMessage(context.messages);
@@ -866,18 +901,28 @@ function buildParams(model, context, isOAuthToken, options) {
866
901
  // by value in a `tool_addition` block, which also expresses same-name redefinitions.
867
902
  // Anthropic rejects a tool list where every tool is deferred, so there must be an initial
868
903
  // active tool to anchor the placeholder. Otherwise the current tool list is sent.
904
+ let hasPreferredStrictTools = false;
905
+ const convertToolDefinitions = (tools, toolCacheControl) => {
906
+ const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
907
+ if (definitions.some((definition, index) => {
908
+ const config = tools[index].constrainedSampling;
909
+ return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
910
+ })) {
911
+ hasPreferredStrictTools = true;
912
+ }
913
+ return definitions;
914
+ };
869
915
  const initialTools = initialSystemMessage?.toolsAdded ?? [];
870
916
  const nativeToolChanges = compat.supportsMidConvoSystemMessages && compat.supportsMidConvoToolChanges && initialTools.length > 0;
871
- const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, model.compat?.supportsMidConvoEffort === true ? model.provider : undefined, nativeToolChanges
872
- ? (tools) => convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools)
873
- : undefined);
917
+ const thinkingDisabled = model.reasoning && options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null;
918
+ // Effort cannot change between messages while thinking is disabled, so "off" sends no effort markers.
919
+ const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
920
+ const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
874
921
  const activeEffort = options?.effort ?? "high";
875
922
  const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
876
923
  const params = {
877
924
  model: model.id,
878
- messages: model.compat?.supportsMidConvoEffort === true
879
- ? insertThinkingLevelMessages(converted, activeEffort)
880
- : converted.messages,
925
+ messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
881
926
  max_tokens: options?.maxTokens ?? model.maxTokens,
882
927
  stream: true,
883
928
  ...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),
@@ -922,20 +967,22 @@ function buildParams(model, context, isOAuthToken, options) {
922
967
  // placeholder. The list never changes afterwards: later tools are defined by value in
923
968
  // `tool_addition` blocks and withdrawn by `tool_removal`, so the cached prefix survives
924
969
  // every tool change.
925
- params.tools = [
926
- ...convertTools(initialTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
927
- DEFERRED_TOOL_PLACEHOLDER,
928
- ];
970
+ params.tools = [...convertToolDefinitions(initialTools, toolCacheControl), DEFERRED_TOOL_PLACEHOLDER];
929
971
  }
930
972
  else {
931
973
  const tools = getCurrentTools(context.messages);
932
974
  if (tools.length > 0) {
933
- params.tools = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl);
975
+ params.tools = convertToolDefinitions(tools, toolCacheControl);
934
976
  }
935
977
  }
936
- // Managed effort models always use adaptive thinking so prefix mismatches can
937
- // be dropped instead of surfacing as persistent 400 responses.
938
- if (model.compat?.supportsMidConvoEffort === true) {
978
+ if (thinkingDisabled) {
979
+ // No effort is sent, so the model default applies. Anthropic rejects `block_binding`
980
+ // with disabled thinking.
981
+ params.thinking = { type: "disabled" };
982
+ }
983
+ else if (effortMessages) {
984
+ // Managed effort models use adaptive thinking so prefix mismatches can
985
+ // be dropped instead of surfacing as persistent 400 responses.
939
986
  params.thinking = {
940
987
  type: "adaptive",
941
988
  display: options?.thinkingDisplay ?? "summarized",
@@ -943,29 +990,24 @@ function buildParams(model, context, isOAuthToken, options) {
943
990
  };
944
991
  params.output_config = { effort: "high" };
945
992
  }
946
- else if (model.reasoning) {
947
- if (options?.thinkingEnabled) {
948
- // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
949
- // older Claude 4 models (whose API default is also "summarized").
950
- const display = options.thinkingDisplay ?? "summarized";
951
- if (model.compat?.forceAdaptiveThinking === true) {
952
- // Adaptive thinking: Claude decides when and how much to think.
953
- params.thinking = { type: "adaptive", display };
954
- if (options.effort) {
955
- params.output_config = { effort: options.effort };
956
- }
957
- }
958
- else {
959
- // Budget-based thinking for older models
960
- params.thinking = {
961
- type: "enabled",
962
- budget_tokens: options.thinkingBudgetTokens || 1024,
963
- display,
964
- };
993
+ else if (model.reasoning && options?.thinkingEnabled) {
994
+ // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
995
+ // older Claude 4 models (whose API default is also "summarized").
996
+ const display = options.thinkingDisplay ?? "summarized";
997
+ if (model.compat?.forceAdaptiveThinking === true) {
998
+ // Adaptive thinking: Claude decides when and how much to think.
999
+ params.thinking = { type: "adaptive", display };
1000
+ if (options.effort) {
1001
+ params.output_config = { effort: options.effort };
965
1002
  }
966
1003
  }
967
- else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) {
968
- params.thinking = { type: "disabled" };
1004
+ else {
1005
+ // Budget-based thinking for older models
1006
+ params.thinking = {
1007
+ type: "enabled",
1008
+ budget_tokens: options.thinkingBudgetTokens || 1024,
1009
+ display,
1010
+ };
969
1011
  }
970
1012
  }
971
1013
  if (options?.metadata) {
@@ -986,17 +1028,17 @@ function buildParams(model, context, isOAuthToken, options) {
986
1028
  if (allowedFallbackModels && allowedFallbackModels.length > 0) {
987
1029
  params.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));
988
1030
  }
989
- return params;
1031
+ return { params, hasPreferredStrictTools, providerThinkingLevel: effortMessages ? activeEffort : undefined };
990
1032
  }
991
1033
  // Normalize tool call IDs to match Anthropic's required pattern and length
992
1034
  function normalizeToolCallId(id) {
993
1035
  return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
994
1036
  }
995
- function convertToolResult(msg) {
1037
+ function convertToolResult(msg, content = msg.content) {
996
1038
  return {
997
1039
  type: "tool_result",
998
1040
  tool_use_id: msg.toolCallId,
999
- content: convertContentBlocks(msg.content),
1041
+ content: convertContentBlocks(content),
1000
1042
  is_error: msg.isError,
1001
1043
  };
1002
1044
  }
@@ -1061,14 +1103,7 @@ convertToolDefinitions) {
1061
1103
  };
1062
1104
  }
1063
1105
  else {
1064
- return {
1065
- type: "image",
1066
- source: {
1067
- type: "base64",
1068
- media_type: item.mimeType,
1069
- data: item.data,
1070
- },
1071
- };
1106
+ return convertImageBlock(item);
1072
1107
  }
1073
1108
  });
1074
1109
  const filteredBlocks = blocks.filter((b) => {
@@ -1159,16 +1194,24 @@ convertToolDefinitions) {
1159
1194
  else if (msg.role === "toolResult") {
1160
1195
  // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.
1161
1196
  const toolResults = [];
1197
+ // Anthropic rejects error tool results that contain anything but text, so their images follow
1198
+ // all tool results of the turn, which must come first in the user message (#10794).
1199
+ const errorImages = [];
1162
1200
  let j = i;
1163
1201
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
1164
- toolResults.push(convertToolResult(transformedMessages[j]));
1202
+ const toolMsg = transformedMessages[j];
1203
+ const split = splitErrorToolResultImages(toolMsg);
1204
+ toolResults.push(convertToolResult(toolMsg, split?.content));
1205
+ if (split) {
1206
+ errorImages.push({ type: "text", text: split.caption }, ...split.images.map(convertImageBlock));
1207
+ }
1165
1208
  j++;
1166
1209
  }
1167
1210
  // Skip the messages we've already processed.
1168
1211
  i = j - 1;
1169
1212
  params.push({
1170
1213
  role: "user",
1171
- content: toolResults,
1214
+ content: [...toolResults, ...errorImages],
1172
1215
  });
1173
1216
  }
1174
1217
  }
@@ -1255,11 +1298,14 @@ const isAnthropicStrictUnsupportedKeyword = (key, value) => {
1255
1298
  return typeof value !== "string" || !ANTHROPIC_STRICT_STRING_FORMATS.has(value);
1256
1299
  return false;
1257
1300
  };
1258
- function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl) {
1301
+ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, disablePreferredStrictTools = false) {
1259
1302
  if (!tools)
1260
1303
  return [];
1261
1304
  return tools.map((tool, index) => {
1262
- const strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1305
+ const config = tool.constrainedSampling;
1306
+ const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
1307
+ ? undefined
1308
+ : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1263
1309
  const parameters = getJsonSchemaToolParameters(tool, strict);
1264
1310
  const schema = parameters;
1265
1311
  const legacyInputSchema = {