@earendil-works/pi-ai 1.2.0 → 1.4.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 (168) hide show
  1. package/README.md +83 -37
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +66 -64
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  6. package/dist/api/azure-openai-responses.js +8 -4
  7. package/dist/api/azure-openai-responses.js.map +1 -1
  8. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  9. package/dist/api/bedrock-converse-stream.js +28 -18
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/classifier-shared.d.ts.map +1 -1
  12. package/dist/api/classifier-shared.js +4 -3
  13. package/dist/api/classifier-shared.js.map +1 -1
  14. package/dist/api/cloudflare-workers-ai-system-one.d.ts.map +1 -1
  15. package/dist/api/cloudflare-workers-ai-system-one.js +8 -0
  16. package/dist/api/cloudflare-workers-ai-system-one.js.map +1 -1
  17. package/dist/api/google-generative-ai.js +4 -4
  18. package/dist/api/google-generative-ai.js.map +1 -1
  19. package/dist/api/google-shared.d.ts.map +1 -1
  20. package/dist/api/google-shared.js +3 -1
  21. package/dist/api/google-shared.js.map +1 -1
  22. package/dist/api/google-vertex.d.ts.map +1 -1
  23. package/dist/api/google-vertex.js +9 -9
  24. package/dist/api/google-vertex.js.map +1 -1
  25. package/dist/api/lazy.d.ts +7 -2
  26. package/dist/api/lazy.d.ts.map +1 -1
  27. package/dist/api/lazy.js +24 -5
  28. package/dist/api/lazy.js.map +1 -1
  29. package/dist/api/llama-cpp-classify.d.ts.map +1 -1
  30. package/dist/api/llama-cpp-classify.js +2 -1
  31. package/dist/api/llama-cpp-classify.js.map +1 -1
  32. package/dist/api/mistral-conversations.js +4 -4
  33. package/dist/api/mistral-conversations.js.map +1 -1
  34. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  35. package/dist/api/openai-codex-responses.js +17 -11
  36. package/dist/api/openai-codex-responses.js.map +1 -1
  37. package/dist/api/openai-completions.d.ts.map +1 -1
  38. package/dist/api/openai-completions.js +43 -35
  39. package/dist/api/openai-completions.js.map +1 -1
  40. package/dist/api/openai-responses-shared.d.ts +26 -0
  41. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  42. package/dist/api/openai-responses-shared.js +76 -11
  43. package/dist/api/openai-responses-shared.js.map +1 -1
  44. package/dist/api/openai-responses.d.ts.map +1 -1
  45. package/dist/api/openai-responses.js +38 -13
  46. package/dist/api/openai-responses.js.map +1 -1
  47. package/dist/api/openrouter-images.d.ts.map +1 -1
  48. package/dist/api/openrouter-images.js +4 -3
  49. package/dist/api/openrouter-images.js.map +1 -1
  50. package/dist/api/pi-messages.d.ts.map +1 -1
  51. package/dist/api/pi-messages.js +3 -3
  52. package/dist/api/pi-messages.js.map +1 -1
  53. package/dist/api/simple-options.d.ts.map +1 -1
  54. package/dist/api/simple-options.js +2 -1
  55. package/dist/api/simple-options.js.map +1 -1
  56. package/dist/api/system-one-shared.d.ts +29 -2
  57. package/dist/api/system-one-shared.d.ts.map +1 -1
  58. package/dist/api/system-one-shared.js +55 -3
  59. package/dist/api/system-one-shared.js.map +1 -1
  60. package/dist/api/transform-messages.d.ts +16 -1
  61. package/dist/api/transform-messages.d.ts.map +1 -1
  62. package/dist/api/transform-messages.js +28 -15
  63. package/dist/api/transform-messages.js.map +1 -1
  64. package/dist/api/typesafe-system-one.d.ts.map +1 -1
  65. package/dist/api/typesafe-system-one.js +10 -0
  66. package/dist/api/typesafe-system-one.js.map +1 -1
  67. package/dist/index.d.ts +2 -1
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +2 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/models.d.ts +4 -0
  72. package/dist/models.d.ts.map +1 -1
  73. package/dist/models.js +19 -6
  74. package/dist/models.js.map +1 -1
  75. package/dist/providers/compat-schema.d.ts +171 -0
  76. package/dist/providers/compat-schema.d.ts.map +1 -1
  77. package/dist/providers/compat-schema.js +23 -2
  78. package/dist/providers/compat-schema.js.map +1 -1
  79. package/dist/providers/data/.manifest.json +1 -1
  80. package/dist/providers/data/amazon-bedrock.json +1 -1
  81. package/dist/providers/data/anthropic.json +1 -1
  82. package/dist/providers/data/azure.json +1 -1
  83. package/dist/providers/data/baseten.json +1 -1
  84. package/dist/providers/data/cerebras.json +1 -1
  85. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  86. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  87. package/dist/providers/data/fireworks.json +1 -1
  88. package/dist/providers/data/github-copilot.json +1 -1
  89. package/dist/providers/data/google-vertex.json +1 -1
  90. package/dist/providers/data/google.json +1 -1
  91. package/dist/providers/data/groq.json +1 -1
  92. package/dist/providers/data/huggingface.json +1 -1
  93. package/dist/providers/data/kimi-coding.json +1 -1
  94. package/dist/providers/data/meta.json +1 -1
  95. package/dist/providers/data/minimax-cn.json +1 -1
  96. package/dist/providers/data/minimax.json +1 -1
  97. package/dist/providers/data/mistral.json +1 -1
  98. package/dist/providers/data/moonshotai-cn.json +1 -1
  99. package/dist/providers/data/nvidia.json +1 -1
  100. package/dist/providers/data/openai-codex.json +1 -1
  101. package/dist/providers/data/openai.json +1 -1
  102. package/dist/providers/data/opencode-go.json +1 -1
  103. package/dist/providers/data/opencode.json +1 -1
  104. package/dist/providers/data/openrouter.json +1 -1
  105. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  106. package/dist/providers/data/qwen-token-plan.json +1 -1
  107. package/dist/providers/data/radius.json +1 -1
  108. package/dist/providers/data/together.json +1 -1
  109. package/dist/providers/data/typesafe.json +1 -1
  110. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  111. package/dist/providers/data/xai.json +1 -1
  112. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  113. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  114. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  115. package/dist/providers/data/xiaomi.json +1 -1
  116. package/dist/providers/data/zai-coding-cn.json +1 -1
  117. package/dist/providers/kimi-coding.d.ts +1 -1
  118. package/dist/providers/kimi-coding.js +3 -3
  119. package/dist/providers/kimi-coding.js.map +1 -1
  120. package/dist/providers/model-schema.d.ts.map +1 -1
  121. package/dist/providers/model-schema.js +4 -2
  122. package/dist/providers/model-schema.js.map +1 -1
  123. package/dist/providers/opencode-go.d.ts +3 -1
  124. package/dist/providers/opencode-go.d.ts.map +1 -1
  125. package/dist/providers/opencode-go.js +2 -0
  126. package/dist/providers/opencode-go.js.map +1 -1
  127. package/dist/telemetry.d.ts +288 -0
  128. package/dist/telemetry.d.ts.map +1 -0
  129. package/dist/telemetry.js +191 -0
  130. package/dist/telemetry.js.map +1 -0
  131. package/dist/types.d.ts +18 -5
  132. package/dist/types.d.ts.map +1 -1
  133. package/dist/types.js.map +1 -1
  134. package/dist/utils/identity.d.ts +65 -0
  135. package/dist/utils/identity.d.ts.map +1 -0
  136. package/dist/utils/identity.js +141 -0
  137. package/dist/utils/identity.js.map +1 -0
  138. package/dist/utils/json-parse.d.ts +10 -0
  139. package/dist/utils/json-parse.d.ts.map +1 -1
  140. package/dist/utils/json-parse.js +26 -1
  141. package/dist/utils/json-parse.js.map +1 -1
  142. package/dist/utils/overflow.d.ts +1 -0
  143. package/dist/utils/overflow.d.ts.map +1 -1
  144. package/dist/utils/overflow.js +6 -1
  145. package/dist/utils/overflow.js.map +1 -1
  146. package/dist/utils/retry.d.ts.map +1 -1
  147. package/dist/utils/retry.js +21 -8
  148. package/dist/utils/retry.js.map +1 -1
  149. package/dist/utils/transcript.d.ts +22 -8
  150. package/dist/utils/transcript.d.ts.map +1 -1
  151. package/dist/utils/transcript.js +29 -18
  152. package/dist/utils/transcript.js.map +1 -1
  153. package/dist/utils/validation.d.ts.map +1 -1
  154. package/dist/utils/validation.js +3 -0
  155. package/dist/utils/validation.js.map +1 -1
  156. package/dist/utils/version.d.ts +2 -0
  157. package/dist/utils/version.d.ts.map +1 -0
  158. package/dist/utils/version.js +3 -0
  159. package/dist/utils/version.js.map +1 -0
  160. package/package.json +2 -2
  161. package/dist/utils/assistant-message-frame.d.ts +0 -77
  162. package/dist/utils/assistant-message-frame.d.ts.map +0 -1
  163. package/dist/utils/assistant-message-frame.js +0 -425
  164. package/dist/utils/assistant-message-frame.js.map +0 -1
  165. package/dist/utils/pi-user-agent.d.ts +0 -2
  166. package/dist/utils/pi-user-agent.d.ts.map +0 -1
  167. package/dist/utils/pi-user-agent.js +0 -12
  168. package/dist/utils/pi-user-agent.js.map +0 -1
package/README.md CHANGED
@@ -18,6 +18,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
18
18
  - [Auth](#auth)
19
19
  - [How Auth Resolves](#how-auth-resolves)
20
20
  - [Transforming Request Headers](#transforming-request-headers)
21
+ - [Client Identity](#client-identity)
21
22
  - [Credential Store](#credential-store)
22
23
  - [Environment Variables](#environment-variables)
23
24
  - [Tools](#tools)
@@ -26,7 +27,6 @@ Unified LLM API with provider collections, automatic auth resolution, token and
26
27
  - [Streaming Tool Calls with Partial JSON](#streaming-tool-calls-with-partial-json)
27
28
  - [Validating Tool Arguments](#validating-tool-arguments)
28
29
  - [Complete Event Reference](#complete-event-reference)
29
- - [Compact Assistant Message Frames](#compact-assistant-message-frames)
30
30
  - [Image Input](#image-input)
31
31
  - [Image Generation](#image-generation)
32
32
  - [Classification](#classification)
@@ -35,6 +35,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
35
35
  - [Provider-Specific Options](#provider-specific-options-streamcomplete)
36
36
  - [Streaming Thinking Content](#streaming-thinking-content)
37
37
  - [Stop Reasons](#stop-reasons)
38
+ - [Telemetry](#telemetry)
38
39
  - [Error Handling](#error-handling)
39
40
  - [Aborting Requests](#aborting-requests)
40
41
  - [Continuing After Abort](#continuing-after-abort)
@@ -425,6 +426,33 @@ Header names are merged case-insensitively. Explicit headers override auth/model
425
426
 
426
427
  `transformHeaders` belongs to `Models`, not `Provider`. A `Models` implementation must consume it and remove it before calling `Provider.stream*()`. Provider implementations continue receiving ordinary `ApiStreamOptions` or `SimpleStreamOptions` and never handle the transform themselves. Use this option instead of calling `getAuth(model)` before `stream*()`, which would resolve request auth twice.
427
428
 
429
+ ### Client Identity
430
+
431
+ `identity` controls how requests identify the calling app to providers. Set it on `createModels()` as a default, or per request; a request's fields override the `Models` default field by field:
432
+
433
+ ```typescript
434
+ const models = createModels({
435
+ identity: {
436
+ userAgent: 'detailed',
437
+ version: '2.1.0',
438
+ attribution: { name: 'my-app', url: 'https://my-app.example', categories: ['cloud-agent'] },
439
+ },
440
+ });
441
+
442
+ await models.completeSimple(model, context, { identity: { attribution: null } });
443
+ ```
444
+
445
+ - `userAgent`: `"detailed"` (default) sends `pi/<version> (<platform>; <runtime>; <arch>)`, `"short"` sends `pi`, and `{ custom: "..." }` sends the given value. On Bedrock the value is appended to the AWS SDK user agent.
446
+ - `version`: the version in the detailed `User-Agent`. Default: the pi-ai version.
447
+ - `attribution`: provider attribution headers. Absent (default) or `null` sends none; `null` also turns off attribution from a merged identity. `{ name, url, categories? }` maps to each provider's headers:
448
+ - OpenRouter: `HTTP-Referer: <url>` (the app's identity in OpenRouter's rankings, grouped by origin), `X-OpenRouter-Title: <name>`, and `X-OpenRouter-Categories` (comma-separated).
449
+ - NVIDIA NIM: `X-BILLING-INVOKE-ORIGIN: <name>` with its first letter upper-cased.
450
+ - OpenCode: `x-opencode-client: <name>`.
451
+
452
+ `PI_ATTRIBUTION` is pi's: `{ name: "pi", url: "https://pi.dev", categories: ["cli-agent"] }`.
453
+
454
+ Identity headers are defaults below provider, model, and request headers, so configured headers win and a `null` request header removes one. Providers that send a fixed client identity, such as GitHub Copilot and Anthropic OAuth, keep it. `withClientIdentity(models, identity)` returns a wrapper of a `Models` (or `MutableModels`) whose requests use `identity` over the one the `Models` was created with, for libraries that receive a `Models` from their host. `identity` is plain JSON, so it can be persisted with other request options.
455
+
428
456
  ### Credential Store
429
457
 
430
458
  Stored credentials (API keys entered interactively, OAuth tokens) live in a `CredentialStore` — one type-tagged credential per provider. pi-ai ships an in-memory default; apps inject persistent storage:
@@ -723,34 +751,6 @@ All streaming events emitted during assistant message generation:
723
751
 
724
752
  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
753
 
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
754
  ## Image Input
755
755
 
756
756
  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.
@@ -895,8 +895,8 @@ Classifier models consume structured JSON state, and optionally images, and answ
895
895
  | Provider | Model IDs | Auth |
896
896
  | --- | --- | --- |
897
897
  | `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |
898
- | `openrouter` | `typesafe/jev-1.13`, `~typesafe/jev-latest` | `OPENROUTER_API_KEY` or OpenRouter OAuth |
899
- | `cloudflare-workers-ai` | `typesafe/jev`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
898
+ | `openrouter` | `typesafe/jev-1.13`, `typesafe/jev-1.14`, `~typesafe/jev-latest`, `cloudflare/clef`, `cloudflare/clef-flash`, `cloudflare/clef-omni` | `OPENROUTER_API_KEY` or OpenRouter OAuth |
899
+ | `cloudflare-workers-ai` | `typesafe/jev`, `typesafe/jev-1.14.0`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash`, `@cf/cloudflare/clef-omni` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
900
900
  | `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
901
901
  | `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
902
902
  | `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
@@ -941,6 +941,18 @@ When the service reports token counts, `result.usage` carries them with their co
941
941
 
942
942
  `ClassifierContext.images` holds image blocks (`{ type: "image", data, mimeType }`) judged together with the state. Only models whose `input` includes `"image"` accept them; `classify()` returns an error result for other models.
943
943
 
944
+ ### System One images
945
+
946
+ System One services take images in three forms. The model's `compat.imageInput` chooses the form (`"assets"`, `"images"`, or `"state-image-urls"`), and `compat.sendImageOptInHeader` controls TypeSafe's opt-in header:
947
+
948
+ - **TypeSafe** (`typesafe`, and Jev on Workers AI) takes PNG and JPEG images as `assets`, `{ "<image-1>": { type: "image", format: "png", data_base64 } }`. Every asset must be referenced from the request, so the state is sent as `["<image-1>", ..., state]`. Images are experimental on TypeSafe's own API and need the `TypeSafe-Danger-Zone-Images: true` header, which the API sends with requests that carry images.
949
+ - **Clef on Workers AI** takes up to four PNG, JPEG, or WebP images as an `images` list of data URLs, next to the unchanged state.
950
+ - **OpenRouter** takes images as `{ type: "image_url", image_url: { url } }` parts before the state: `[part, ..., state]`. It answers requests with `assets` but drops the images, without an error.
951
+
952
+ Built-in catalog entries set `compat` explicitly, so they keep their form when `baseUrl` points at a proxy. For models without it, the APIs detect the form: OpenRouter by provider `openrouter` or a `baseUrl` containing `openrouter.ai`, Clef by a model ID starting with `@cf/cloudflare/clef`, and TypeSafe's form with the opt-in header otherwise. A custom provider that reaches OpenRouter through a proxy matches neither check and must set `compat.imageInput: "state-image-urls"` and `compat.sendImageOptInHeader: false`; otherwise OpenRouter drops its images.
953
+
954
+ Vercel AI Gateway and OpenCode list their Jev models with text input only.
955
+
944
956
  ### OpenAI Decisions
945
957
 
946
958
  The `openai-decisions` API calls OpenAI's [Decisions API](https://developers.openai.com/api/docs/guides/decisions) (`POST /v1/decisions`). GPT-6 Luna is its only model. The state is sent as JSON text; with images, the input is one user message with the state followed by the images as data URLs (at most 128). `choice` and `score` questions map to the Decisions types of the same name. `bool` questions become `predicate` questions; predicates have no criteria field, so the meanings of true and false are appended to the instructions. If OpenAI refuses to answer a question, the whole result is an error. Usage reports input tokens only, priced at the catalog input rate with OpenAI's long-context multiplier.
@@ -1099,6 +1111,40 @@ Every `AssistantMessage` includes a `stopReason` field that indicates how the ge
1099
1111
 
1100
1112
  `AssistantMessage` may also include `responseId`, a provider-specific upstream response or message identifier when the underlying API exposes one. Do not assume it is always present across providers.
1101
1113
 
1114
+ ## Telemetry
1115
+
1116
+ Pass an explicit parent span (`TelemetrySpan`, not an adapter) through `telemetry`; omitting it uses the no-op span:
1117
+
1118
+ ```typescript
1119
+ import { InMemoryTelemetryContext } from '@earendil-works/pi-telemetry';
1120
+
1121
+ const telemetry = new InMemoryTelemetryContext();
1122
+ await telemetry.startSpan({ name: 'application.turn' }, async (span) => {
1123
+ await models.completeSimple(model, context, { telemetry: span });
1124
+ });
1125
+ ```
1126
+
1127
+ `Models.stream()` and `streamSimple()` create `pi.ai.request`; `streamDeferred()` creates one `pi.ai.poll` per poll. Their completion helpers use the same spans. These spans include auth/setup, end at the terminal result, and do not require the caller to consume events. Direct provider/API calls are not instrumented.
1128
+
1129
+ `AI_TELEMETRY_SCHEMA`, exported from the root and `@earendil-works/pi-ai/models`, defines the typed Pi-owned vocabulary:
1130
+
1131
+ - Start attributes: `provider`, `api`, `model`, and `reasoning` only when `options.reasoning` is a string. Provider-native reasoning objects are ignored.
1132
+ - End attributes: `stop`, `usage.input`, `usage.output`, `usage.cache_read`, `usage.cache_write`, `cost.total`, and optional `response.id`. `stop: "error"` sets error status with the fixed category `provider_error` and `AssistantMessage.errorMessage` truncated to 500 characters (empty when absent). Cancellation keeps status `ok` with `stop: "aborted"`.
1133
+ - `pi.ai.first_chunk` marks the first normalized event, including setup errors. Each completed text, thinking, or tool-call block emits `pi.ai.block` with `type`, `index`, and `chars`; only tool calls include `tool`. Character counts use JavaScript string length; tool counts sum argument delta lengths without serializing arguments.
1134
+
1135
+ When `options.telemetry.metrics` is available, the same requests record metrics:
1136
+
1137
+ | Metric | Unit | Attributes |
1138
+ | --- | --- | --- |
1139
+ | `pi.ai.request.duration` | `ms` | `provider`, `model`, `stop` |
1140
+ | `pi.ai.first_chunk.duration` | `ms` | `provider`, `model` |
1141
+ | `pi.ai.tokens` | `token` | `provider`, `model`, `kind` (`input`, `output`, `cache_read`, `cache_write`) |
1142
+ | `pi.ai.cost` | `USD` | `provider`, `model` |
1143
+
1144
+ Durations use `performance.now()`. Pending/deferred polls do not add tokens or cost; final completed/error polls do. A parent span without metric capabilities remains valid and simply records no metrics. Adapter-created spans carry the same optional metric and propagation capabilities as their root.
1145
+
1146
+ Telemetry does not record prompts, generated text, reasoning content, or arguments in attributes/events. Error status includes up to 500 characters of provider error text, which may contain sensitive data; apply your export policy accordingly. Telemetry observes the existing forwarding path without another stream consumer or per-delta events. Response identifiers and error messages never become metric attributes.
1147
+
1102
1148
  ## Error Handling
1103
1149
 
1104
1150
  Request failures after a stream is returned never throw: when a request ends with an error (including aborts and tool call validation errors), the streaming API emits an error event and the final message carries the details. Setup failures may emit `error` without `start`; failures after generation begins emit `start`, any observed updates, then `error`. Direct API `streamSimple()` calls throw synchronously when request auth is missing:
@@ -1527,13 +1573,13 @@ Notes:
1527
1573
 
1528
1574
  ## Cross-Provider Handoffs
1529
1575
 
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.
1576
+ 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
1577
 
1532
1578
  When messages from one provider are sent to a different provider, the library automatically transforms them for compatibility:
1533
1579
 
1534
1580
  - **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
1581
+ - **Assistant messages from the same provider, API, and model** are preserved as-is, including thinking blocks
1582
+ - **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
1583
  - **Tool calls and regular text** are preserved unchanged
1538
1584
 
1539
1585
  ```typescript
@@ -1554,7 +1600,7 @@ const claude = models.getModel('anthropic', 'claude-sonnet-4-5')!;
1554
1600
  context.messages.push({ role: 'user', content: 'What is 25 * 18?', timestamp: Date.now() });
1555
1601
  context.messages.push(await models.completeSimple(claude, context, { reasoning: 'medium' }));
1556
1602
 
1557
- // Switch to GPT-5 - it will see Claude's thinking as <thinking> tagged text
1603
+ // Switch to GPT-5 - it sees Claude's answer, but not Claude's thinking
1558
1604
  const gpt5 = models.getModel('openai', 'gpt-5-mini')!;
1559
1605
  context.messages.push({ role: 'user', content: 'Is that calculation correct?', timestamp: Date.now() });
1560
1606
  context.messages.push(await models.complete(gpt5, context));
@@ -1565,7 +1611,7 @@ context.messages.push({ role: 'user', content: 'What was the original question?'
1565
1611
  const geminiResponse = await models.complete(gemini, context);
1566
1612
  ```
1567
1613
 
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.
1614
+ 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
1615
 
1570
1616
  ## System Messages
1571
1617
 
@@ -1598,7 +1644,7 @@ getCurrentTools(messages); // []
1598
1644
 
1599
1645
  A custom `Provider` or `ProviderStreams` implementation reads the prompt and tools the same way from `context.messages`; `context.systemPrompt` and `context.tools` do not exist at that layer.
1600
1646
 
1601
- Models that accept system messages mid-conversation (`supportsMidConvoSystemMessages` in the model's compat settings, set by the generated catalog for verified models) receive each later system message in place, so the cached prefix stays intact; section changes are framed by name for the model. Every other model receives `collapseSystemMessages(transcript)`: the replayed prompt and current tools as the leading system message, with later system messages dropped. Anthropic models that also set `supportsMidConvoToolChanges` send tool changes as native blocks (`inline-tools-2026-09-15` beta): the top-level tool list holds the initial tools plus a stable deferred placeholder and never changes, later tools are defined by value in `tool_addition` blocks (a new definition under an existing name replaces the old one), and removals are `tool_removal` references, so tool changes do not invalidate the prompt cache. That needs at least one initial tool; otherwise the current tool list is sent at the top level with the system text only. OpenAI Responses models with `supportsAdditionalTools` or `supportsToolSearch` anchor additive tool changes at their message; everything else sends the current tool list at the top level.
1647
+ Models that accept system messages mid-conversation (`supportsMidConvoSystemMessages` in the model's compat settings, set by the generated catalog for verified models) receive each later system message in place, so the cached prefix stays intact; section changes are framed by name for the model. Every other model receives `collapseSystemMessages(transcript)`: the replayed prompt and current tools as the leading system message, with later system messages dropped. Anthropic models that also set `supportsMidConvoToolChanges` send tool changes as native blocks (`inline-tools-2026-09-15` beta): the top-level tool list holds the initial tools plus a stable deferred placeholder and never changes, later tools are defined by value in `tool_addition` blocks (a new definition under an existing name replaces the old one), and removals are `tool_removal` references, so tool changes do not invalidate the prompt cache. That needs at least one initial tool; otherwise the current tool list is sent at the top level with the system text only. OpenAI Responses models with `supportsAdditionalTools` declare every tool in `additional_tools` items, like Codex: the initial tools in one at the head of the input and later additions and redefinitions at their message, with no top-level tool list. A later item replaces same-name definitions from earlier items but cannot replace a top-level tool or remove any tool, so removals are announced as text, later updates restate the tools that are still unavailable, and calls to removed tools fail at execution. OpenAI Responses models with only `supportsToolSearch`, and Kimi models with `supportsMidConvoToolAdditions`, anchor new tools at their message; a removal or a redeclared name moves the tool state up to that point into the top-level list once. Everything else sends the current tool list at the top level.
1602
1648
 
1603
1649
  ## Context Serialization
1604
1650
 
@@ -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,CA2WzE,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;AAoRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA2WzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
@@ -4,8 +4,8 @@ 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";
8
- import { getPiUserAgent } from "../utils/pi-user-agent.js";
7
+ import { getIdentityHeaders } from "../utils/identity.js";
8
+ import { parseJsonWithRepair, parseStreamingJson, parseToolCallArguments } from "../utils/json-parse.js";
9
9
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
10
  import { retryProviderRequest } from "../utils/provider-retry.js";
11
11
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.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");
@@ -157,9 +160,6 @@ function mergeHeaders(...headerSources) {
157
160
  }
158
161
  return merged;
159
162
  }
160
- function mergeClientHeaders(...headerSources) {
161
- return mergeHeaders({ "User-Agent": getPiUserAgent() }, ...headerSources);
162
- }
163
163
  function hasHeader(headers, name) {
164
164
  if (!headers)
165
165
  return false;
@@ -378,14 +378,12 @@ export const stream = (model, context, options) => {
378
378
  const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
379
379
  const currentTools = getCurrentTools(normalizedContext.messages);
380
380
  (async () => {
381
- const providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? "high") : undefined;
382
381
  const output = {
383
382
  role: "assistant",
384
383
  content: [],
385
384
  api: model.api,
386
385
  provider: model.provider,
387
386
  model: model.id,
388
- ...(providerThinkingLevel === undefined ? {} : { providerThinkingLevel }),
389
387
  usage: {
390
388
  input: 0,
391
389
  output: 0,
@@ -421,7 +419,7 @@ export const stream = (model, context, options) => {
421
419
  }
422
420
  const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
423
421
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
424
- const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation);
422
+ const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation, options?.identity);
425
423
  client = created.client;
426
424
  isOAuth = created.isOAuthToken;
427
425
  }
@@ -435,6 +433,8 @@ export const stream = (model, context, options) => {
435
433
  for (;;) {
436
434
  options?.signal?.throwIfAborted();
437
435
  const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
436
+ if (built.providerThinkingLevel)
437
+ output.providerThinkingLevel = built.providerThinkingLevel;
438
438
  let params = built.params;
439
439
  const nextParams = await options?.onPayload?.(params, model);
440
440
  if (nextParams !== undefined) {
@@ -625,7 +625,7 @@ export const stream = (model, context, options) => {
625
625
  });
626
626
  }
627
627
  else if (block.type === "toolCall") {
628
- block.arguments = parseStreamingJson(block.partialJson);
628
+ Object.assign(block, parseToolCallArguments(block.partialJson));
629
629
  // Finalize in-place and strip the scratch buffer so replay only
630
630
  // carries parsed arguments.
631
631
  delete block.partialJson;
@@ -780,7 +780,7 @@ export const streamSimple = (model, context, options) => {
780
780
  function isOAuthToken(apiKey) {
781
781
  return apiKey.includes("sk-ant-oat");
782
782
  }
783
- function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sessionId, federation) {
783
+ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sessionId, federation, identity) {
784
784
  // Copilot: Bearer auth.
785
785
  if (model.provider === "github-copilot") {
786
786
  const client = new PiAnthropic({
@@ -789,14 +789,14 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
789
789
  baseURL: model.baseUrl,
790
790
  dangerouslyAllowBrowser: true,
791
791
  fetch,
792
- defaultHeaders: mergeClientHeaders({
792
+ defaultHeaders: mergeHeaders(getIdentityHeaders(model, identity), {
793
793
  accept: "application/json",
794
794
  "anthropic-dangerous-direct-browser-access": "true",
795
795
  }, model.headers, dynamicHeaders, optionsHeaders),
796
796
  });
797
797
  return { client, isOAuthToken: false };
798
798
  }
799
- // OAuth: Bearer auth, Claude Code identity headers
799
+ // OAuth: Bearer auth, Claude Code identity headers instead of the client identity.
800
800
  if (apiKey && isOAuthToken(apiKey)) {
801
801
  const client = new PiAnthropic({
802
802
  apiKey: null,
@@ -804,7 +804,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
804
804
  baseURL: model.baseUrl,
805
805
  dangerouslyAllowBrowser: true,
806
806
  fetch,
807
- defaultHeaders: mergeClientHeaders({
807
+ defaultHeaders: mergeHeaders({
808
808
  accept: "application/json",
809
809
  "anthropic-dangerous-direct-browser-access": "true",
810
810
  "user-agent": `claude-cli/${claudeCodeVersion}`,
@@ -820,7 +820,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
820
820
  const header = compat.sessionAffinityFormat === "openrouter" ? "x-session-id" : "x-session-affinity";
821
821
  sessionAffinityHeaders[header] = sessionId;
822
822
  }
823
- const defaultHeaders = mergeClientHeaders({
823
+ const defaultHeaders = mergeHeaders(getIdentityHeaders(model, identity), {
824
824
  accept: "application/json",
825
825
  "anthropic-dangerous-direct-browser-access": "true",
826
826
  }, sessionAffinityHeaders, model.headers, optionsHeaders);
@@ -911,14 +911,15 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
911
911
  };
912
912
  const initialTools = initialSystemMessage?.toolsAdded ?? [];
913
913
  const nativeToolChanges = compat.supportsMidConvoSystemMessages && compat.supportsMidConvoToolChanges && initialTools.length > 0;
914
- const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, model.compat?.supportsMidConvoEffort === true ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
914
+ const thinkingDisabled = model.reasoning && options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null;
915
+ // Effort cannot change between messages while thinking is disabled, so "off" sends no effort markers.
916
+ const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
917
+ const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
915
918
  const activeEffort = options?.effort ?? "high";
916
919
  const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
917
920
  const params = {
918
921
  model: model.id,
919
- messages: model.compat?.supportsMidConvoEffort === true
920
- ? insertThinkingLevelMessages(converted, activeEffort)
921
- : converted.messages,
922
+ messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
922
923
  max_tokens: options?.maxTokens ?? model.maxTokens,
923
924
  stream: true,
924
925
  ...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),
@@ -971,9 +972,14 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
971
972
  params.tools = convertToolDefinitions(tools, toolCacheControl);
972
973
  }
973
974
  }
974
- // Managed effort models always use adaptive thinking so prefix mismatches can
975
- // be dropped instead of surfacing as persistent 400 responses.
976
- if (model.compat?.supportsMidConvoEffort === true) {
975
+ if (thinkingDisabled) {
976
+ // No effort is sent, so the model default applies. Anthropic rejects `block_binding`
977
+ // with disabled thinking.
978
+ params.thinking = { type: "disabled" };
979
+ }
980
+ else if (effortMessages) {
981
+ // Managed effort models use adaptive thinking so prefix mismatches can
982
+ // be dropped instead of surfacing as persistent 400 responses.
977
983
  params.thinking = {
978
984
  type: "adaptive",
979
985
  display: options?.thinkingDisplay ?? "summarized",
@@ -981,29 +987,24 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
981
987
  };
982
988
  params.output_config = { effort: "high" };
983
989
  }
984
- else if (model.reasoning) {
985
- if (options?.thinkingEnabled) {
986
- // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
987
- // older Claude 4 models (whose API default is also "summarized").
988
- const display = options.thinkingDisplay ?? "summarized";
989
- if (model.compat?.forceAdaptiveThinking === true) {
990
- // Adaptive thinking: Claude decides when and how much to think.
991
- params.thinking = { type: "adaptive", display };
992
- if (options.effort) {
993
- params.output_config = { effort: options.effort };
994
- }
995
- }
996
- else {
997
- // Budget-based thinking for older models
998
- params.thinking = {
999
- type: "enabled",
1000
- budget_tokens: options.thinkingBudgetTokens || 1024,
1001
- display,
1002
- };
990
+ else if (model.reasoning && options?.thinkingEnabled) {
991
+ // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
992
+ // older Claude 4 models (whose API default is also "summarized").
993
+ const display = options.thinkingDisplay ?? "summarized";
994
+ if (model.compat?.forceAdaptiveThinking === true) {
995
+ // Adaptive thinking: Claude decides when and how much to think.
996
+ params.thinking = { type: "adaptive", display };
997
+ if (options.effort) {
998
+ params.output_config = { effort: options.effort };
1003
999
  }
1004
1000
  }
1005
- else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) {
1006
- params.thinking = { type: "disabled" };
1001
+ else {
1002
+ // Budget-based thinking for older models
1003
+ params.thinking = {
1004
+ type: "enabled",
1005
+ budget_tokens: options.thinkingBudgetTokens || 1024,
1006
+ display,
1007
+ };
1007
1008
  }
1008
1009
  }
1009
1010
  if (options?.metadata) {
@@ -1024,17 +1025,17 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
1024
1025
  if (allowedFallbackModels && allowedFallbackModels.length > 0) {
1025
1026
  params.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));
1026
1027
  }
1027
- return { params, hasPreferredStrictTools };
1028
+ return { params, hasPreferredStrictTools, providerThinkingLevel: effortMessages ? activeEffort : undefined };
1028
1029
  }
1029
1030
  // Normalize tool call IDs to match Anthropic's required pattern and length
1030
1031
  function normalizeToolCallId(id) {
1031
1032
  return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
1032
1033
  }
1033
- function convertToolResult(msg) {
1034
+ function convertToolResult(msg, content = msg.content) {
1034
1035
  return {
1035
1036
  type: "tool_result",
1036
1037
  tool_use_id: msg.toolCallId,
1037
- content: convertContentBlocks(msg.content),
1038
+ content: convertContentBlocks(content),
1038
1039
  is_error: msg.isError,
1039
1040
  };
1040
1041
  }
@@ -1099,14 +1100,7 @@ convertToolDefinitions) {
1099
1100
  };
1100
1101
  }
1101
1102
  else {
1102
- return {
1103
- type: "image",
1104
- source: {
1105
- type: "base64",
1106
- media_type: item.mimeType,
1107
- data: item.data,
1108
- },
1109
- };
1103
+ return convertImageBlock(item);
1110
1104
  }
1111
1105
  });
1112
1106
  const filteredBlocks = blocks.filter((b) => {
@@ -1197,16 +1191,24 @@ convertToolDefinitions) {
1197
1191
  else if (msg.role === "toolResult") {
1198
1192
  // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.
1199
1193
  const toolResults = [];
1194
+ // Anthropic rejects error tool results that contain anything but text, so their images follow
1195
+ // all tool results of the turn, which must come first in the user message (#10794).
1196
+ const errorImages = [];
1200
1197
  let j = i;
1201
1198
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
1202
- toolResults.push(convertToolResult(transformedMessages[j]));
1199
+ const toolMsg = transformedMessages[j];
1200
+ const split = splitErrorToolResultImages(toolMsg);
1201
+ toolResults.push(convertToolResult(toolMsg, split?.content));
1202
+ if (split) {
1203
+ errorImages.push({ type: "text", text: split.caption }, ...split.images.map(convertImageBlock));
1204
+ }
1203
1205
  j++;
1204
1206
  }
1205
1207
  // Skip the messages we've already processed.
1206
1208
  i = j - 1;
1207
1209
  params.push({
1208
1210
  role: "user",
1209
- content: toolResults,
1211
+ content: [...toolResults, ...errorImages],
1210
1212
  });
1211
1213
  }
1212
1214
  }