@fractaal/pi-ai 0.86.8 → 1.4.1

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 (250) hide show
  1. package/README.md +105 -45
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +180 -141
  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 -10
  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 +130 -96
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/classifier-shared.d.ts +21 -0
  12. package/dist/api/classifier-shared.d.ts.map +1 -0
  13. package/dist/api/classifier-shared.js +99 -0
  14. package/dist/api/classifier-shared.js.map +1 -0
  15. package/dist/api/cloudflare-workers-ai-system-one.d.ts.map +1 -1
  16. package/dist/api/cloudflare-workers-ai-system-one.js +10 -1
  17. package/dist/api/cloudflare-workers-ai-system-one.js.map +1 -1
  18. package/dist/api/constrained-sampling.d.ts +11 -1
  19. package/dist/api/constrained-sampling.d.ts.map +1 -1
  20. package/dist/api/constrained-sampling.js +33 -18
  21. package/dist/api/constrained-sampling.js.map +1 -1
  22. package/dist/api/google-generative-ai.d.ts.map +1 -1
  23. package/dist/api/google-generative-ai.js +4 -10
  24. package/dist/api/google-generative-ai.js.map +1 -1
  25. package/dist/api/google-shared.d.ts +2 -1
  26. package/dist/api/google-shared.d.ts.map +1 -1
  27. package/dist/api/google-shared.js +60 -3
  28. package/dist/api/google-shared.js.map +1 -1
  29. package/dist/api/google-vertex.d.ts.map +1 -1
  30. package/dist/api/google-vertex.js +9 -15
  31. package/dist/api/google-vertex.js.map +1 -1
  32. package/dist/api/lazy.d.ts +7 -2
  33. package/dist/api/lazy.d.ts.map +1 -1
  34. package/dist/api/lazy.js +28 -8
  35. package/dist/api/lazy.js.map +1 -1
  36. package/dist/api/llama-cpp-classify.d.ts.map +1 -1
  37. package/dist/api/llama-cpp-classify.js +4 -1
  38. package/dist/api/llama-cpp-classify.js.map +1 -1
  39. package/dist/api/mistral-conversations.d.ts.map +1 -1
  40. package/dist/api/mistral-conversations.js +50 -60
  41. package/dist/api/mistral-conversations.js.map +1 -1
  42. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  43. package/dist/api/openai-codex-responses.js +23 -14
  44. package/dist/api/openai-codex-responses.js.map +1 -1
  45. package/dist/api/openai-completions.d.ts.map +1 -1
  46. package/dist/api/openai-completions.js +83 -87
  47. package/dist/api/openai-completions.js.map +1 -1
  48. package/dist/api/openai-decisions.d.ts +4 -0
  49. package/dist/api/openai-decisions.d.ts.map +1 -0
  50. package/dist/api/openai-decisions.js +162 -0
  51. package/dist/api/openai-decisions.js.map +1 -0
  52. package/dist/api/openai-decisions.lazy.d.ts +3 -0
  53. package/dist/api/openai-decisions.lazy.d.ts.map +1 -0
  54. package/dist/api/openai-decisions.lazy.js +4 -0
  55. package/dist/api/openai-decisions.lazy.js.map +1 -0
  56. package/dist/api/openai-responses-shared.d.ts +26 -0
  57. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  58. package/dist/api/openai-responses-shared.js +132 -66
  59. package/dist/api/openai-responses-shared.js.map +1 -1
  60. package/dist/api/openai-responses.d.ts.map +1 -1
  61. package/dist/api/openai-responses.js +38 -19
  62. package/dist/api/openai-responses.js.map +1 -1
  63. package/dist/api/openrouter-images.d.ts.map +1 -1
  64. package/dist/api/openrouter-images.js +5 -3
  65. package/dist/api/openrouter-images.js.map +1 -1
  66. package/dist/api/pi-messages.d.ts +2 -0
  67. package/dist/api/pi-messages.d.ts.map +1 -1
  68. package/dist/api/pi-messages.js +18 -12
  69. package/dist/api/pi-messages.js.map +1 -1
  70. package/dist/api/simple-options.d.ts.map +1 -1
  71. package/dist/api/simple-options.js +2 -1
  72. package/dist/api/simple-options.js.map +1 -1
  73. package/dist/api/system-one-shared.d.ts +29 -3
  74. package/dist/api/system-one-shared.d.ts.map +1 -1
  75. package/dist/api/system-one-shared.js +54 -86
  76. package/dist/api/system-one-shared.js.map +1 -1
  77. package/dist/api/transform-messages.d.ts +16 -1
  78. package/dist/api/transform-messages.d.ts.map +1 -1
  79. package/dist/api/transform-messages.js +28 -15
  80. package/dist/api/transform-messages.js.map +1 -1
  81. package/dist/api/typesafe-system-one.d.ts.map +1 -1
  82. package/dist/api/typesafe-system-one.js +12 -1
  83. package/dist/api/typesafe-system-one.js.map +1 -1
  84. package/dist/auth/oauth/anthropic.d.ts.map +1 -1
  85. package/dist/auth/oauth/anthropic.js +14 -6
  86. package/dist/auth/oauth/anthropic.js.map +1 -1
  87. package/dist/auth/oauth/openai-chatgpt.js +1 -1
  88. package/dist/auth/oauth/openai-chatgpt.js.map +1 -1
  89. package/dist/auth/oauth/openai-codex.d.ts.map +1 -1
  90. package/dist/auth/oauth/openai-codex.js +4 -4
  91. package/dist/auth/oauth/openai-codex.js.map +1 -1
  92. package/dist/auth/types.d.ts +9 -0
  93. package/dist/auth/types.d.ts.map +1 -1
  94. package/dist/auth/types.js.map +1 -1
  95. package/dist/compat/extension-oauth-types.d.ts +4 -0
  96. package/dist/compat/extension-oauth-types.d.ts.map +1 -1
  97. package/dist/compat/extension-oauth-types.js.map +1 -1
  98. package/dist/index.d.ts +3 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +3 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/models.d.ts +9 -0
  103. package/dist/models.d.ts.map +1 -1
  104. package/dist/models.js +23 -7
  105. package/dist/models.js.map +1 -1
  106. package/dist/providers/compat-schema.d.ts +509 -0
  107. package/dist/providers/compat-schema.d.ts.map +1 -0
  108. package/dist/providers/compat-schema.js +396 -0
  109. package/dist/providers/compat-schema.js.map +1 -0
  110. package/dist/providers/data/.manifest.json +1 -1
  111. package/dist/providers/data/amazon-bedrock.json +1 -1
  112. package/dist/providers/data/anthropic.json +1 -1
  113. package/dist/providers/data/azure.json +1 -1
  114. package/dist/providers/data/baseten.json +1 -1
  115. package/dist/providers/data/cerebras.json +1 -1
  116. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  117. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  118. package/dist/providers/data/fireworks.json +1 -1
  119. package/dist/providers/data/github-copilot.json +1 -1
  120. package/dist/providers/data/google-vertex.json +1 -1
  121. package/dist/providers/data/google.json +1 -1
  122. package/dist/providers/data/groq.json +1 -1
  123. package/dist/providers/data/huggingface.json +1 -1
  124. package/dist/providers/data/kimi-coding.json +1 -1
  125. package/dist/providers/data/meta.json +1 -1
  126. package/dist/providers/data/minimax-cn.json +1 -1
  127. package/dist/providers/data/minimax.json +1 -1
  128. package/dist/providers/data/mistral.json +1 -1
  129. package/dist/providers/data/moonshotai-cn.json +1 -1
  130. package/dist/providers/data/moonshotai.json +1 -1
  131. package/dist/providers/data/nvidia.json +1 -1
  132. package/dist/providers/data/openai-codex.json +1 -1
  133. package/dist/providers/data/openai.json +1 -1
  134. package/dist/providers/data/opencode-go.json +1 -1
  135. package/dist/providers/data/opencode.json +1 -1
  136. package/dist/providers/data/openrouter.json +1 -1
  137. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  138. package/dist/providers/data/qwen-token-plan.json +1 -1
  139. package/dist/providers/data/radius.json +1 -1
  140. package/dist/providers/data/together.json +1 -1
  141. package/dist/providers/data/typesafe.json +1 -1
  142. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  143. package/dist/providers/data/xai.json +1 -1
  144. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  145. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  146. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  147. package/dist/providers/data/xiaomi.json +1 -1
  148. package/dist/providers/data/zai-coding-cn.json +1 -1
  149. package/dist/providers/faux.d.ts.map +1 -1
  150. package/dist/providers/faux.js +31 -9
  151. package/dist/providers/faux.js.map +1 -1
  152. package/dist/providers/kimi-coding.d.ts +1 -1
  153. package/dist/providers/kimi-coding.js +3 -3
  154. package/dist/providers/kimi-coding.js.map +1 -1
  155. package/dist/providers/model-schema.d.ts +91 -0
  156. package/dist/providers/model-schema.d.ts.map +1 -0
  157. package/dist/providers/model-schema.js +57 -0
  158. package/dist/providers/model-schema.js.map +1 -0
  159. package/dist/providers/openai.d.ts.map +1 -1
  160. package/dist/providers/openai.js +12 -3
  161. package/dist/providers/openai.js.map +1 -1
  162. package/dist/providers/opencode-go.d.ts +3 -1
  163. package/dist/providers/opencode-go.d.ts.map +1 -1
  164. package/dist/providers/opencode-go.js +2 -0
  165. package/dist/providers/opencode-go.js.map +1 -1
  166. package/dist/providers/openrouter-api.d.ts +40 -0
  167. package/dist/providers/openrouter-api.d.ts.map +1 -0
  168. package/dist/providers/openrouter-api.js +41 -0
  169. package/dist/providers/openrouter-api.js.map +1 -0
  170. package/dist/providers/openrouter.d.ts +11 -1
  171. package/dist/providers/openrouter.d.ts.map +1 -1
  172. package/dist/providers/openrouter.js +52 -1
  173. package/dist/providers/openrouter.js.map +1 -1
  174. package/dist/providers/radius-config.d.ts.map +1 -1
  175. package/dist/providers/radius-config.js +2 -1
  176. package/dist/providers/radius-config.js.map +1 -1
  177. package/dist/providers/radius.d.ts.map +1 -1
  178. package/dist/providers/radius.js +4 -12
  179. package/dist/providers/radius.js.map +1 -1
  180. package/dist/telemetry.d.ts +288 -0
  181. package/dist/telemetry.d.ts.map +1 -0
  182. package/dist/telemetry.js +192 -0
  183. package/dist/telemetry.js.map +1 -0
  184. package/dist/types.d.ts +44 -307
  185. package/dist/types.d.ts.map +1 -1
  186. package/dist/types.js.map +1 -1
  187. package/dist/utils/error-body.d.ts.map +1 -1
  188. package/dist/utils/error-body.js +2 -1
  189. package/dist/utils/error-body.js.map +1 -1
  190. package/dist/utils/estimate.js +1 -1
  191. package/dist/utils/estimate.js.map +1 -1
  192. package/dist/utils/event-stream.d.ts +10 -1
  193. package/dist/utils/event-stream.d.ts.map +1 -1
  194. package/dist/utils/event-stream.js +25 -0
  195. package/dist/utils/event-stream.js.map +1 -1
  196. package/dist/utils/identity.d.ts +65 -0
  197. package/dist/utils/identity.d.ts.map +1 -0
  198. package/dist/utils/identity.js +141 -0
  199. package/dist/utils/identity.js.map +1 -0
  200. package/dist/utils/json-parse.d.ts +10 -0
  201. package/dist/utils/json-parse.d.ts.map +1 -1
  202. package/dist/utils/json-parse.js +26 -1
  203. package/dist/utils/json-parse.js.map +1 -1
  204. package/dist/utils/model-operations.d.ts +3 -1
  205. package/dist/utils/model-operations.d.ts.map +1 -1
  206. package/dist/utils/model-operations.js +6 -0
  207. package/dist/utils/model-operations.js.map +1 -1
  208. package/dist/utils/openrouter-cost.d.ts +24 -0
  209. package/dist/utils/openrouter-cost.d.ts.map +1 -0
  210. package/dist/utils/openrouter-cost.js +34 -0
  211. package/dist/utils/openrouter-cost.js.map +1 -0
  212. package/dist/utils/overflow.d.ts +1 -0
  213. package/dist/utils/overflow.d.ts.map +1 -1
  214. package/dist/utils/overflow.js +6 -1
  215. package/dist/utils/overflow.js.map +1 -1
  216. package/dist/utils/provider-retry.d.ts +2 -0
  217. package/dist/utils/provider-retry.d.ts.map +1 -1
  218. package/dist/utils/provider-retry.js +2 -0
  219. package/dist/utils/provider-retry.js.map +1 -1
  220. package/dist/utils/retry.d.ts.map +1 -1
  221. package/dist/utils/retry.js +26 -10
  222. package/dist/utils/retry.js.map +1 -1
  223. package/dist/utils/streaming-json.d.ts +84 -0
  224. package/dist/utils/streaming-json.d.ts.map +1 -0
  225. package/dist/utils/streaming-json.js +513 -0
  226. package/dist/utils/streaming-json.js.map +1 -0
  227. package/dist/utils/text.d.ts +7 -0
  228. package/dist/utils/text.d.ts.map +1 -1
  229. package/dist/utils/text.js +9 -0
  230. package/dist/utils/text.js.map +1 -1
  231. package/dist/utils/transcript.d.ts +22 -8
  232. package/dist/utils/transcript.d.ts.map +1 -1
  233. package/dist/utils/transcript.js +29 -18
  234. package/dist/utils/transcript.js.map +1 -1
  235. package/dist/utils/validation.d.ts.map +1 -1
  236. package/dist/utils/validation.js +3 -0
  237. package/dist/utils/validation.js.map +1 -1
  238. package/dist/utils/version.d.ts +2 -0
  239. package/dist/utils/version.d.ts.map +1 -0
  240. package/dist/utils/version.js +3 -0
  241. package/dist/utils/version.js.map +1 -0
  242. package/package.json +3 -3
  243. package/dist/utils/assistant-message-frame.d.ts +0 -77
  244. package/dist/utils/assistant-message-frame.d.ts.map +0 -1
  245. package/dist/utils/assistant-message-frame.js +0 -425
  246. package/dist/utils/assistant-message-frame.js.map +0 -1
  247. package/dist/utils/pi-user-agent.d.ts +0 -2
  248. package/dist/utils/pi-user-agent.d.ts.map +0 -1
  249. package/dist/utils/pi-user-agent.js +0 -12
  250. package/dist/utils/pi-user-agent.js.map +0 -1
@@ -4,17 +4,19 @@ 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 } from "../utils/json-parse.js";
9
+ import { applyOpenRouterReportedCost, isOpenRouterModel, } from "../utils/openrouter-cost.js";
9
10
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
11
  import { retryProviderRequest } from "../utils/provider-retry.js";
11
12
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
13
+ import { ToolCallArgumentsStream } from "../utils/streaming-json.js";
12
14
  import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
13
15
  import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "../utils/transcript.js";
14
16
  import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
15
17
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
16
18
  import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
17
- import { transformMessages } from "./transform-messages.js";
19
+ import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
18
20
  /**
19
21
  * Resolve cache retention preference.
20
22
  * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
@@ -75,6 +77,16 @@ const fromClaudeCodeName = (name, tools) => {
75
77
  }
76
78
  return name;
77
79
  };
80
+ function convertImageBlock(image) {
81
+ return {
82
+ type: "image",
83
+ source: {
84
+ type: "base64",
85
+ media_type: image.mimeType,
86
+ data: image.data,
87
+ },
88
+ };
89
+ }
78
90
  /**
79
91
  * Convert content blocks to Anthropic API format
80
92
  */
@@ -92,14 +104,7 @@ function convertContentBlocks(content) {
92
104
  text: sanitizeSurrogates(block.text),
93
105
  };
94
106
  }
95
- return {
96
- type: "image",
97
- source: {
98
- type: "base64",
99
- media_type: block.mimeType,
100
- data: block.data,
101
- },
102
- };
107
+ return convertImageBlock(block);
103
108
  });
104
109
  // If only images (no text), add placeholder text block
105
110
  const hasText = blocks.some((b) => b.type === "text");
@@ -157,9 +162,6 @@ function mergeHeaders(...headerSources) {
157
162
  }
158
163
  return merged;
159
164
  }
160
- function mergeClientHeaders(...headerSources) {
161
- return mergeHeaders({ "User-Agent": getPiUserAgent() }, ...headerSources);
162
- }
163
165
  function hasHeader(headers, name) {
164
166
  if (!headers)
165
167
  return false;
@@ -378,14 +380,12 @@ export const stream = (model, context, options) => {
378
380
  const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
379
381
  const currentTools = getCurrentTools(normalizedContext.messages);
380
382
  (async () => {
381
- const providerThinkingLevel = model.compat?.supportsMidConvoEffort ? (options?.effort ?? "high") : undefined;
382
383
  const output = {
383
384
  role: "assistant",
384
385
  content: [],
385
386
  api: model.api,
386
387
  provider: model.provider,
387
388
  model: model.id,
388
- ...(providerThinkingLevel === undefined ? {} : { providerThinkingLevel }),
389
389
  usage: {
390
390
  input: 0,
391
391
  output: 0,
@@ -421,28 +421,67 @@ export const stream = (model, context, options) => {
421
421
  }
422
422
  const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
423
423
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
424
- const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation);
424
+ const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation, options?.identity);
425
425
  client = created.client;
426
426
  isOAuth = created.isOAuthToken;
427
427
  }
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
428
  const requestOptions = {
434
429
  ...(options?.signal ? { signal: options.signal } : {}),
435
430
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
436
431
  maxRetries: 0,
437
432
  };
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
- });
433
+ let disablePreferredStrictTools = false;
434
+ let response;
435
+ for (;;) {
436
+ options?.signal?.throwIfAborted();
437
+ const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
438
+ if (built.providerThinkingLevel)
439
+ output.providerThinkingLevel = built.providerThinkingLevel;
440
+ let params = built.params;
441
+ const nextParams = await options?.onPayload?.(params, model);
442
+ if (nextParams !== undefined) {
443
+ params = { ...nextParams, stream: true };
444
+ }
445
+ try {
446
+ response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
447
+ maxRetries: options?.maxRetries,
448
+ maxRetryDelayMs: options?.maxRetryDelayMs,
449
+ signal: options?.signal,
450
+ });
451
+ break;
452
+ }
453
+ catch (error) {
454
+ // Only downgrade a rejected request, never an already-started stream.
455
+ if (options?.signal?.aborted ||
456
+ disablePreferredStrictTools ||
457
+ !built.hasPreferredStrictTools ||
458
+ !(error instanceof Anthropic.APIError) ||
459
+ error.status !== 400 ||
460
+ error.type !== "invalid_request_error" ||
461
+ ![
462
+ "The compiled grammar is too large",
463
+ "Too many strict tools (",
464
+ "Schemas contains too many parameters with union types (",
465
+ ].some((message) => error.message.includes(message))) {
466
+ throw error;
467
+ }
468
+ disablePreferredStrictTools = true;
469
+ appendAssistantMessageDiagnostic(output, {
470
+ type: "anthropic_strict_tools_fallback",
471
+ timestamp: Date.now(),
472
+ error: { message: error.message },
473
+ details: { reason: "strict_tool_limits_exceeded" },
474
+ });
475
+ }
476
+ }
443
477
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
444
478
  stream.push({ type: "start", partial: output });
445
- const blocks = output.content;
479
+ // Stream state per Anthropic block index, kept off the content blocks so it never reaches partials.
480
+ const openBlocks = new Map();
481
+ const openBlock = (anthropicIndex) => {
482
+ const open = openBlocks.get(anthropicIndex);
483
+ return open === undefined ? undefined : { ...open, block: output.content[open.contentIndex] };
484
+ };
446
485
  for await (const event of iterateAnthropicEvents(response, options?.signal)) {
447
486
  await options?.onProviderStreamEvent?.(event, model);
448
487
  if (event.type === "message_start") {
@@ -477,105 +516,92 @@ export const stream = (model, context, options) => {
477
516
  continue;
478
517
  }
479
518
  if (event.content_block.type === "text") {
480
- const block = {
481
- type: "text",
482
- text: event.content_block.text ?? "",
483
- index: event.index,
484
- };
485
- output.content.push(block);
519
+ output.content.push({ type: "text", text: event.content_block.text ?? "" });
520
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
486
521
  stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
487
522
  }
488
523
  else if (event.content_block.type === "thinking") {
489
- const block = {
524
+ output.content.push({
490
525
  type: "thinking",
491
526
  thinking: event.content_block.thinking ?? "",
492
527
  thinkingSignature: event.content_block.signature ?? "",
493
- index: event.index,
494
- };
495
- output.content.push(block);
528
+ });
529
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
496
530
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
497
531
  }
498
532
  else if (event.content_block.type === "redacted_thinking") {
499
- const block = {
533
+ output.content.push({
500
534
  type: "thinking",
501
535
  thinking: "[Reasoning redacted]",
502
536
  thinkingSignature: event.content_block.data,
503
537
  redacted: true,
504
- index: event.index,
505
- };
506
- output.content.push(block);
538
+ });
539
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
507
540
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
508
541
  }
509
542
  else if (event.content_block.type === "tool_use") {
510
- const block = {
543
+ output.content.push({
511
544
  type: "toolCall",
512
545
  id: event.content_block.id,
513
546
  name: isOAuth
514
547
  ? fromClaudeCodeName(event.content_block.name, currentTools)
515
548
  : event.content_block.name,
516
549
  arguments: event.content_block.input ?? {},
517
- partialJson: "",
518
- index: event.index,
519
- };
520
- output.content.push(block);
550
+ });
551
+ openBlocks.set(event.index, {
552
+ contentIndex: output.content.length - 1,
553
+ arguments: new ToolCallArgumentsStream(),
554
+ });
521
555
  stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
522
556
  }
523
557
  }
524
558
  else if (event.type === "content_block_delta") {
559
+ const open = openBlock(event.index);
525
560
  if (event.delta.type === "text_delta") {
526
- const index = blocks.findIndex((b) => b.index === event.index);
527
- const block = blocks[index];
528
- if (block && block.type === "text") {
529
- block.text += event.delta.text;
561
+ if (open?.block.type === "text") {
562
+ open.block.text += event.delta.text;
530
563
  stream.push({
531
564
  type: "text_delta",
532
- contentIndex: index,
565
+ contentIndex: open.contentIndex,
533
566
  delta: event.delta.text,
534
567
  partial: output,
535
568
  });
536
569
  }
537
570
  }
538
571
  else if (event.delta.type === "thinking_delta") {
539
- const index = blocks.findIndex((b) => b.index === event.index);
540
- const block = blocks[index];
541
- if (block && block.type === "thinking") {
542
- block.thinking += event.delta.thinking;
572
+ if (open?.block.type === "thinking") {
573
+ open.block.thinking += event.delta.thinking;
543
574
  stream.push({
544
575
  type: "thinking_delta",
545
- contentIndex: index,
576
+ contentIndex: open.contentIndex,
546
577
  delta: event.delta.thinking,
547
578
  partial: output,
548
579
  });
549
580
  }
550
581
  }
551
582
  else if (event.delta.type === "input_json_delta") {
552
- const index = blocks.findIndex((b) => b.index === event.index);
553
- const block = blocks[index];
554
- if (block && block.type === "toolCall") {
555
- block.partialJson += event.delta.partial_json;
556
- block.arguments = parseStreamingJson(block.partialJson);
583
+ if (open?.block.type === "toolCall" && open.arguments !== undefined) {
584
+ open.block.arguments = open.arguments.append(event.delta.partial_json);
557
585
  stream.push({
558
586
  type: "toolcall_delta",
559
- contentIndex: index,
587
+ contentIndex: open.contentIndex,
560
588
  delta: event.delta.partial_json,
561
589
  partial: output,
562
590
  });
563
591
  }
564
592
  }
565
593
  else if (event.delta.type === "signature_delta") {
566
- const index = blocks.findIndex((b) => b.index === event.index);
567
- const block = blocks[index];
568
- if (block && block.type === "thinking") {
569
- block.thinkingSignature = block.thinkingSignature || "";
570
- block.thinkingSignature += event.delta.signature;
594
+ if (open?.block.type === "thinking") {
595
+ open.block.thinkingSignature = open.block.thinkingSignature || "";
596
+ open.block.thinkingSignature += event.delta.signature;
571
597
  }
572
598
  }
573
599
  }
574
600
  else if (event.type === "content_block_stop") {
575
- const index = blocks.findIndex((b) => b.index === event.index);
576
- const block = blocks[index];
577
- if (block) {
578
- delete block.index;
601
+ const open = openBlock(event.index);
602
+ openBlocks.delete(event.index);
603
+ if (open) {
604
+ const { block, contentIndex: index } = open;
579
605
  if (block.type === "text") {
580
606
  stream.push({
581
607
  type: "text_end",
@@ -592,11 +618,8 @@ export const stream = (model, context, options) => {
592
618
  partial: output,
593
619
  });
594
620
  }
595
- else if (block.type === "toolCall") {
596
- block.arguments = parseStreamingJson(block.partialJson);
597
- // Finalize in-place and strip the scratch buffer so replay only
598
- // carries parsed arguments.
599
- delete block.partialJson;
621
+ else if (block.type === "toolCall" && open.arguments !== undefined) {
622
+ Object.assign(block, open.arguments.finish());
600
623
  stream.push({
601
624
  type: "toolcall_end",
602
625
  contentIndex: index,
@@ -648,6 +671,11 @@ export const stream = (model, context, options) => {
648
671
  output.usage.totalTokens =
649
672
  output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
650
673
  calculateCost(usageModel, output.usage);
674
+ if (isOpenRouterModel(model) && event.usage) {
675
+ // OpenRouter adds billing fields to streamed message_delta usage at runtime,
676
+ // although its OpenAPI schema and the Anthropic SDK do not type those fields.
677
+ applyOpenRouterReportedCost(output.usage, event.usage);
678
+ }
651
679
  }
652
680
  }
653
681
  if (options?.signal?.aborted) {
@@ -676,11 +704,6 @@ export const stream = (model, context, options) => {
676
704
  stream.end();
677
705
  }
678
706
  catch (error) {
679
- for (const block of output.content) {
680
- delete block.index;
681
- // partialJson is only a streaming scratch buffer; never persist it.
682
- delete block.partialJson;
683
- }
684
707
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
685
708
  output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
686
709
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -748,7 +771,7 @@ export const streamSimple = (model, context, options) => {
748
771
  function isOAuthToken(apiKey) {
749
772
  return apiKey.includes("sk-ant-oat");
750
773
  }
751
- function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sessionId, federation) {
774
+ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sessionId, federation, identity) {
752
775
  // Copilot: Bearer auth.
753
776
  if (model.provider === "github-copilot") {
754
777
  const client = new PiAnthropic({
@@ -757,14 +780,14 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
757
780
  baseURL: model.baseUrl,
758
781
  dangerouslyAllowBrowser: true,
759
782
  fetch,
760
- defaultHeaders: mergeClientHeaders({
783
+ defaultHeaders: mergeHeaders(getIdentityHeaders(model, identity), {
761
784
  accept: "application/json",
762
785
  "anthropic-dangerous-direct-browser-access": "true",
763
786
  }, model.headers, dynamicHeaders, optionsHeaders),
764
787
  });
765
788
  return { client, isOAuthToken: false };
766
789
  }
767
- // OAuth: Bearer auth, Claude Code identity headers
790
+ // OAuth: Bearer auth, Claude Code identity headers instead of the client identity.
768
791
  if (apiKey && isOAuthToken(apiKey)) {
769
792
  const client = new PiAnthropic({
770
793
  apiKey: null,
@@ -772,7 +795,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
772
795
  baseURL: model.baseUrl,
773
796
  dangerouslyAllowBrowser: true,
774
797
  fetch,
775
- defaultHeaders: mergeClientHeaders({
798
+ defaultHeaders: mergeHeaders({
776
799
  accept: "application/json",
777
800
  "anthropic-dangerous-direct-browser-access": "true",
778
801
  "user-agent": `claude-cli/${claudeCodeVersion}`,
@@ -788,7 +811,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
788
811
  const header = compat.sessionAffinityFormat === "openrouter" ? "x-session-id" : "x-session-affinity";
789
812
  sessionAffinityHeaders[header] = sessionId;
790
813
  }
791
- const defaultHeaders = mergeClientHeaders({
814
+ const defaultHeaders = mergeHeaders(getIdentityHeaders(model, identity), {
792
815
  accept: "application/json",
793
816
  "anthropic-dangerous-direct-browser-access": "true",
794
817
  }, sessionAffinityHeaders, model.headers, optionsHeaders);
@@ -855,7 +878,7 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
855
878
  features.push(INLINE_TOOLS_BETA);
856
879
  return [...new Set(features)];
857
880
  }
858
- function buildParams(model, context, isOAuthToken, options) {
881
+ function buildParams(model, context, isOAuthToken, options, disablePreferredStrictTools = false) {
859
882
  const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
860
883
  const compat = getAnthropicCompat(model);
861
884
  const initialSystemMessage = getInitialSystemMessage(context.messages);
@@ -866,18 +889,28 @@ function buildParams(model, context, isOAuthToken, options) {
866
889
  // by value in a `tool_addition` block, which also expresses same-name redefinitions.
867
890
  // Anthropic rejects a tool list where every tool is deferred, so there must be an initial
868
891
  // active tool to anchor the placeholder. Otherwise the current tool list is sent.
892
+ let hasPreferredStrictTools = false;
893
+ const convertToolDefinitions = (tools, toolCacheControl) => {
894
+ const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
895
+ if (definitions.some((definition, index) => {
896
+ const config = tools[index].constrainedSampling;
897
+ return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
898
+ })) {
899
+ hasPreferredStrictTools = true;
900
+ }
901
+ return definitions;
902
+ };
869
903
  const initialTools = initialSystemMessage?.toolsAdded ?? [];
870
904
  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);
905
+ const thinkingDisabled = model.reasoning && options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null;
906
+ // Effort cannot change between messages while thinking is disabled, so "off" sends no effort markers.
907
+ const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
908
+ const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
874
909
  const activeEffort = options?.effort ?? "high";
875
910
  const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
876
911
  const params = {
877
912
  model: model.id,
878
- messages: model.compat?.supportsMidConvoEffort === true
879
- ? insertThinkingLevelMessages(converted, activeEffort)
880
- : converted.messages,
913
+ messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
881
914
  max_tokens: options?.maxTokens ?? model.maxTokens,
882
915
  stream: true,
883
916
  ...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),
@@ -922,50 +955,50 @@ function buildParams(model, context, isOAuthToken, options) {
922
955
  // placeholder. The list never changes afterwards: later tools are defined by value in
923
956
  // `tool_addition` blocks and withdrawn by `tool_removal`, so the cached prefix survives
924
957
  // every tool change.
925
- params.tools = [
926
- ...convertTools(initialTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
927
- DEFERRED_TOOL_PLACEHOLDER,
928
- ];
958
+ params.tools = [...convertToolDefinitions(initialTools, toolCacheControl), DEFERRED_TOOL_PLACEHOLDER];
929
959
  }
930
960
  else {
931
961
  const tools = getCurrentTools(context.messages);
932
962
  if (tools.length > 0) {
933
- params.tools = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl);
963
+ params.tools = convertToolDefinitions(tools, toolCacheControl);
934
964
  }
935
965
  }
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) {
966
+ if (thinkingDisabled) {
967
+ // No effort is sent, so the model default applies. Anthropic rejects `block_binding`
968
+ // with disabled thinking.
969
+ params.thinking = { type: "disabled" };
970
+ }
971
+ else if (effortMessages) {
972
+ // Managed effort models use adaptive thinking so prefix mismatches can
973
+ // be dropped instead of surfacing as persistent 400 responses.
939
974
  params.thinking = {
940
975
  type: "adaptive",
941
976
  display: options?.thinkingDisplay ?? "summarized",
942
977
  block_binding: { prefix_mismatch_behavior: "drop_block" },
943
978
  };
944
- params.output_config = { effort: "high" };
979
+ // Effort markers set the level for every reply. The top-level effort is part of the cached
980
+ // prompt, so it stays fixed. It is "low" because organizations can cap effort and reject
981
+ // requests containing any level above the cap.
982
+ params.output_config = { effort: "low" };
945
983
  }
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
- };
984
+ else if (model.reasoning && options?.thinkingEnabled) {
985
+ // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
986
+ // older Claude 4 models (whose API default is also "summarized").
987
+ const display = options.thinkingDisplay ?? "summarized";
988
+ if (model.compat?.forceAdaptiveThinking === true) {
989
+ // Adaptive thinking: Claude decides when and how much to think.
990
+ params.thinking = { type: "adaptive", display };
991
+ if (options.effort) {
992
+ params.output_config = { effort: options.effort };
965
993
  }
966
994
  }
967
- else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) {
968
- params.thinking = { type: "disabled" };
995
+ else {
996
+ // Budget-based thinking for older models
997
+ params.thinking = {
998
+ type: "enabled",
999
+ budget_tokens: options.thinkingBudgetTokens || 1024,
1000
+ display,
1001
+ };
969
1002
  }
970
1003
  }
971
1004
  if (options?.metadata) {
@@ -986,17 +1019,17 @@ function buildParams(model, context, isOAuthToken, options) {
986
1019
  if (allowedFallbackModels && allowedFallbackModels.length > 0) {
987
1020
  params.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));
988
1021
  }
989
- return params;
1022
+ return { params, hasPreferredStrictTools, providerThinkingLevel: effortMessages ? activeEffort : undefined };
990
1023
  }
991
1024
  // Normalize tool call IDs to match Anthropic's required pattern and length
992
1025
  function normalizeToolCallId(id) {
993
1026
  return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
994
1027
  }
995
- function convertToolResult(msg) {
1028
+ function convertToolResult(msg, content = msg.content) {
996
1029
  return {
997
1030
  type: "tool_result",
998
1031
  tool_use_id: msg.toolCallId,
999
- content: convertContentBlocks(msg.content),
1032
+ content: convertContentBlocks(content),
1000
1033
  is_error: msg.isError,
1001
1034
  };
1002
1035
  }
@@ -1006,7 +1039,9 @@ convertToolDefinitions) {
1006
1039
  const params = [];
1007
1040
  const assistantLevels = new Map();
1008
1041
  // Later system messages are held back and emitted directly before the next assistant
1009
- // message (or at the end of the transcript). Anthropic requires `tool_result` blocks to
1042
+ // message that is sent (or at the end of the transcript). Assistant messages that convert to
1043
+ // no blocks are dropped, so holding past them keeps an update from landing before a user
1044
+ // message (#11091). Anthropic requires `tool_result` blocks to
1010
1045
  // immediately follow their `tool_use`, so a system message between them is rejected; this
1011
1046
  // also mirrors where the managed-effort system messages are inserted. As a result an
1012
1047
  // update placed before a user message in the transcript lands after it on the wire.
@@ -1061,14 +1096,7 @@ convertToolDefinitions) {
1061
1096
  };
1062
1097
  }
1063
1098
  else {
1064
- return {
1065
- type: "image",
1066
- source: {
1067
- type: "base64",
1068
- media_type: item.mimeType,
1069
- data: item.data,
1070
- },
1071
- };
1099
+ return convertImageBlock(item);
1072
1100
  }
1073
1101
  });
1074
1102
  const filteredBlocks = blocks.filter((b) => {
@@ -1086,7 +1114,6 @@ convertToolDefinitions) {
1086
1114
  }
1087
1115
  }
1088
1116
  else if (msg.role === "assistant") {
1089
- flushPendingSystemMessages();
1090
1117
  const blocks = [];
1091
1118
  for (const block of msg.content) {
1092
1119
  if (block.type === "text") {
@@ -1144,6 +1171,7 @@ convertToolDefinitions) {
1144
1171
  }
1145
1172
  if (blocks.length === 0)
1146
1173
  continue;
1174
+ flushPendingSystemMessages();
1147
1175
  const messageIndex = params.length;
1148
1176
  params.push({
1149
1177
  role: "assistant",
@@ -1159,16 +1187,24 @@ convertToolDefinitions) {
1159
1187
  else if (msg.role === "toolResult") {
1160
1188
  // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.
1161
1189
  const toolResults = [];
1190
+ // Anthropic rejects error tool results that contain anything but text, so their images follow
1191
+ // all tool results of the turn, which must come first in the user message (#10794).
1192
+ const errorImages = [];
1162
1193
  let j = i;
1163
1194
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
1164
- toolResults.push(convertToolResult(transformedMessages[j]));
1195
+ const toolMsg = transformedMessages[j];
1196
+ const split = splitErrorToolResultImages(toolMsg);
1197
+ toolResults.push(convertToolResult(toolMsg, split?.content));
1198
+ if (split) {
1199
+ errorImages.push({ type: "text", text: split.caption }, ...split.images.map(convertImageBlock));
1200
+ }
1165
1201
  j++;
1166
1202
  }
1167
1203
  // Skip the messages we've already processed.
1168
1204
  i = j - 1;
1169
1205
  params.push({
1170
1206
  role: "user",
1171
- content: toolResults,
1207
+ content: [...toolResults, ...errorImages],
1172
1208
  });
1173
1209
  }
1174
1210
  }
@@ -1255,11 +1291,14 @@ const isAnthropicStrictUnsupportedKeyword = (key, value) => {
1255
1291
  return typeof value !== "string" || !ANTHROPIC_STRICT_STRING_FORMATS.has(value);
1256
1292
  return false;
1257
1293
  };
1258
- function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl) {
1294
+ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, disablePreferredStrictTools = false) {
1259
1295
  if (!tools)
1260
1296
  return [];
1261
1297
  return tools.map((tool, index) => {
1262
- const strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1298
+ const config = tool.constrainedSampling;
1299
+ const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
1300
+ ? undefined
1301
+ : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1263
1302
  const parameters = getJsonSchemaToolParameters(tool, strict);
1264
1303
  const schema = parameters;
1265
1304
  const legacyInputSchema = {