@earendil-works/pi-ai 1.4.0 → 1.5.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 (318) hide show
  1. package/README.md +58 -39
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +282 -87
  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 +1 -7
  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 +127 -103
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/bedrock-converse-stream.lazy.d.ts +7 -0
  12. package/dist/api/bedrock-converse-stream.lazy.d.ts.map +1 -1
  13. package/dist/api/bedrock-converse-stream.lazy.js +18 -5
  14. package/dist/api/bedrock-converse-stream.lazy.js.map +1 -1
  15. package/dist/api/constrained-sampling.d.ts +13 -1
  16. package/dist/api/constrained-sampling.d.ts.map +1 -1
  17. package/dist/api/constrained-sampling.js +38 -18
  18. package/dist/api/constrained-sampling.js.map +1 -1
  19. package/dist/api/google-generative-ai.d.ts.map +1 -1
  20. package/dist/api/google-generative-ai.js +0 -6
  21. package/dist/api/google-generative-ai.js.map +1 -1
  22. package/dist/api/google-shared.d.ts +2 -1
  23. package/dist/api/google-shared.d.ts.map +1 -1
  24. package/dist/api/google-shared.js +57 -2
  25. package/dist/api/google-shared.js.map +1 -1
  26. package/dist/api/google-vertex.d.ts.map +1 -1
  27. package/dist/api/google-vertex.js +0 -6
  28. package/dist/api/google-vertex.js.map +1 -1
  29. package/dist/api/lazy.d.ts +1 -1
  30. package/dist/api/lazy.d.ts.map +1 -1
  31. package/dist/api/lazy.js +9 -8
  32. package/dist/api/lazy.js.map +1 -1
  33. package/dist/api/mistral-conversations.d.ts.map +1 -1
  34. package/dist/api/mistral-conversations.js +17 -44
  35. package/dist/api/mistral-conversations.js.map +1 -1
  36. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  37. package/dist/api/openai-codex-responses.js +7 -8
  38. package/dist/api/openai-codex-responses.js.map +1 -1
  39. package/dist/api/openai-completions.d.ts +2 -1
  40. package/dist/api/openai-completions.d.ts.map +1 -1
  41. package/dist/api/openai-completions.js +55 -59
  42. package/dist/api/openai-completions.js.map +1 -1
  43. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  44. package/dist/api/openai-responses-shared.js +58 -57
  45. package/dist/api/openai-responses-shared.js.map +1 -1
  46. package/dist/api/openai-responses.d.ts.map +1 -1
  47. package/dist/api/openai-responses.js +4 -8
  48. package/dist/api/openai-responses.js.map +1 -1
  49. package/dist/api/openrouter-images.js +1 -0
  50. package/dist/api/openrouter-images.js.map +1 -1
  51. package/dist/api/pi-messages.d.ts +2 -0
  52. package/dist/api/pi-messages.d.ts.map +1 -1
  53. package/dist/api/pi-messages.js +24 -9
  54. package/dist/api/pi-messages.js.map +1 -1
  55. package/dist/api/simple-options.d.ts +1 -0
  56. package/dist/api/simple-options.d.ts.map +1 -1
  57. package/dist/api/simple-options.js +16 -2
  58. package/dist/api/simple-options.js.map +1 -1
  59. package/dist/api/strict-tool-limits.d.ts +13 -0
  60. package/dist/api/strict-tool-limits.d.ts.map +1 -0
  61. package/dist/api/strict-tool-limits.js +50 -0
  62. package/dist/api/strict-tool-limits.js.map +1 -0
  63. package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  64. package/dist/auth/oauth/github-copilot.js +4 -11
  65. package/dist/auth/oauth/github-copilot.js.map +1 -1
  66. package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
  67. package/dist/auth/oauth/kimi-coding.js +4 -22
  68. package/dist/auth/oauth/kimi-coding.js.map +1 -1
  69. package/dist/auth/oauth/load.d.ts.map +1 -1
  70. package/dist/auth/oauth/load.js +5 -2
  71. package/dist/auth/oauth/load.js.map +1 -1
  72. package/dist/auth/oauth/meta.d.ts.map +1 -1
  73. package/dist/auth/oauth/meta.js +1 -14
  74. package/dist/auth/oauth/meta.js.map +1 -1
  75. package/dist/auth/oauth/radius.d.ts.map +1 -1
  76. package/dist/auth/oauth/radius.js +9 -3
  77. package/dist/auth/oauth/radius.js.map +1 -1
  78. package/dist/auth/oauth/trusted-url.d.ts +7 -0
  79. package/dist/auth/oauth/trusted-url.d.ts.map +1 -0
  80. package/dist/auth/oauth/trusted-url.js +12 -0
  81. package/dist/auth/oauth/trusted-url.js.map +1 -0
  82. package/dist/auth/types.d.ts +4 -0
  83. package/dist/auth/types.d.ts.map +1 -1
  84. package/dist/auth/types.js.map +1 -1
  85. package/dist/cache-warming.d.ts +53 -0
  86. package/dist/cache-warming.d.ts.map +1 -0
  87. package/dist/cache-warming.js +82 -0
  88. package/dist/cache-warming.js.map +1 -0
  89. package/dist/cli.js +6 -2
  90. package/dist/cli.js.map +1 -1
  91. package/dist/compat/extension-oauth-types.d.ts +4 -0
  92. package/dist/compat/extension-oauth-types.d.ts.map +1 -1
  93. package/dist/compat/extension-oauth-types.js.map +1 -1
  94. package/dist/env-api-keys.d.ts.map +1 -1
  95. package/dist/env-api-keys.js +1 -0
  96. package/dist/env-api-keys.js.map +1 -1
  97. package/dist/index.d.ts +3 -3
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +3 -2
  100. package/dist/index.js.map +1 -1
  101. package/dist/lazy-bedrock-provider.d.ts +10 -0
  102. package/dist/lazy-bedrock-provider.d.ts.map +1 -0
  103. package/dist/lazy-bedrock-provider.js +13 -0
  104. package/dist/lazy-bedrock-provider.js.map +1 -0
  105. package/dist/lazy-oauth.d.ts +10 -0
  106. package/dist/lazy-oauth.d.ts.map +1 -0
  107. package/dist/lazy-oauth.js +23 -0
  108. package/dist/lazy-oauth.js.map +1 -0
  109. package/dist/models.d.ts +7 -0
  110. package/dist/models.d.ts.map +1 -1
  111. package/dist/models.generated.d.ts +4 -0
  112. package/dist/models.generated.d.ts.map +1 -1
  113. package/dist/models.generated.js +5 -1
  114. package/dist/models.generated.js.map +1 -1
  115. package/dist/models.js +20 -11
  116. package/dist/models.js.map +1 -1
  117. package/dist/providers/all.d.ts.map +1 -1
  118. package/dist/providers/all.js +2 -0
  119. package/dist/providers/all.js.map +1 -1
  120. package/dist/providers/amazon-bedrock.models.js +1 -1
  121. package/dist/providers/amazon-bedrock.models.js.map +1 -1
  122. package/dist/providers/ant-ling.models.js +1 -1
  123. package/dist/providers/ant-ling.models.js.map +1 -1
  124. package/dist/providers/anthropic.models.js +1 -1
  125. package/dist/providers/anthropic.models.js.map +1 -1
  126. package/dist/providers/azure.models.js +1 -1
  127. package/dist/providers/azure.models.js.map +1 -1
  128. package/dist/providers/baseten.models.js +1 -1
  129. package/dist/providers/baseten.models.js.map +1 -1
  130. package/dist/providers/cerebras.models.js +1 -1
  131. package/dist/providers/cerebras.models.js.map +1 -1
  132. package/dist/providers/cloudflare-ai-gateway.models.js +1 -1
  133. package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  134. package/dist/providers/cloudflare-workers-ai.models.js +1 -1
  135. package/dist/providers/cloudflare-workers-ai.models.js.map +1 -1
  136. package/dist/providers/compat-schema.d.ts +444 -456
  137. package/dist/providers/compat-schema.d.ts.map +1 -1
  138. package/dist/providers/compat-schema.js +166 -112
  139. package/dist/providers/compat-schema.js.map +1 -1
  140. package/dist/providers/data/.manifest.json +1 -1
  141. package/dist/providers/data/amazon-bedrock.json +1 -1
  142. package/dist/providers/data/anthropic.json +1 -1
  143. package/dist/providers/data/azure.json +1 -1
  144. package/dist/providers/data/baseten.json +1 -1
  145. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  146. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  147. package/dist/providers/data/fireworks.json +1 -1
  148. package/dist/providers/data/github-copilot.json +1 -1
  149. package/dist/providers/data/google-vertex.json +1 -1
  150. package/dist/providers/data/google.json +1 -1
  151. package/dist/providers/data/huggingface.json +1 -1
  152. package/dist/providers/data/kimi-coding.json +1 -1
  153. package/dist/providers/data/mistral.json +1 -1
  154. package/dist/providers/data/moonshotai-cn.json +1 -1
  155. package/dist/providers/data/moonshotai.json +1 -1
  156. package/dist/providers/data/nvidia.json +1 -1
  157. package/dist/providers/data/openai-codex.json +1 -1
  158. package/dist/providers/data/openai.json +1 -1
  159. package/dist/providers/data/opencode-go.json +1 -1
  160. package/dist/providers/data/opencode.json +1 -1
  161. package/dist/providers/data/openrouter.json +1 -1
  162. package/dist/providers/data/qwen-token-plan-cn.json +1 -1
  163. package/dist/providers/data/qwen-token-plan-individual-cn.json +1 -0
  164. package/dist/providers/data/qwen-token-plan-individual.json +1 -1
  165. package/dist/providers/data/qwen-token-plan.json +1 -1
  166. package/dist/providers/data/radius.json +1 -1
  167. package/dist/providers/data/together.json +1 -1
  168. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  169. package/dist/providers/data/xai.json +1 -1
  170. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  171. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  172. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  173. package/dist/providers/data/xiaomi.json +1 -1
  174. package/dist/providers/data/zai-coding-cn.json +1 -1
  175. package/dist/providers/data/zai.json +1 -1
  176. package/dist/providers/deepseek.models.js +1 -1
  177. package/dist/providers/deepseek.models.js.map +1 -1
  178. package/dist/providers/faux.d.ts.map +1 -1
  179. package/dist/providers/faux.js +7 -2
  180. package/dist/providers/faux.js.map +1 -1
  181. package/dist/providers/fireworks.models.js +1 -1
  182. package/dist/providers/fireworks.models.js.map +1 -1
  183. package/dist/providers/github-copilot.d.ts +3 -1
  184. package/dist/providers/github-copilot.d.ts.map +1 -1
  185. package/dist/providers/github-copilot.js +13 -2
  186. package/dist/providers/github-copilot.js.map +1 -1
  187. package/dist/providers/github-copilot.models.js +1 -1
  188. package/dist/providers/github-copilot.models.js.map +1 -1
  189. package/dist/providers/google-vertex.models.js +1 -1
  190. package/dist/providers/google-vertex.models.js.map +1 -1
  191. package/dist/providers/google.models.js +1 -1
  192. package/dist/providers/google.models.js.map +1 -1
  193. package/dist/providers/groq.models.js +1 -1
  194. package/dist/providers/groq.models.js.map +1 -1
  195. package/dist/providers/huggingface.models.js +1 -1
  196. package/dist/providers/huggingface.models.js.map +1 -1
  197. package/dist/providers/kimi-coding.models.js +1 -1
  198. package/dist/providers/kimi-coding.models.js.map +1 -1
  199. package/dist/providers/meta.models.js +1 -1
  200. package/dist/providers/meta.models.js.map +1 -1
  201. package/dist/providers/minimax-cn.models.js +1 -1
  202. package/dist/providers/minimax-cn.models.js.map +1 -1
  203. package/dist/providers/minimax.models.js +1 -1
  204. package/dist/providers/minimax.models.js.map +1 -1
  205. package/dist/providers/mistral.models.js +1 -1
  206. package/dist/providers/mistral.models.js.map +1 -1
  207. package/dist/providers/model-schema.d.ts +65 -65
  208. package/dist/providers/model-schema.d.ts.map +1 -1
  209. package/dist/providers/model-schema.js +41 -30
  210. package/dist/providers/model-schema.js.map +1 -1
  211. package/dist/providers/moonshotai-cn.models.js +1 -1
  212. package/dist/providers/moonshotai-cn.models.js.map +1 -1
  213. package/dist/providers/moonshotai.models.js +1 -1
  214. package/dist/providers/moonshotai.models.js.map +1 -1
  215. package/dist/providers/nvidia.models.js +1 -1
  216. package/dist/providers/nvidia.models.js.map +1 -1
  217. package/dist/providers/openai-codex.js +1 -1
  218. package/dist/providers/openai-codex.js.map +1 -1
  219. package/dist/providers/openai-codex.models.js +1 -1
  220. package/dist/providers/openai-codex.models.js.map +1 -1
  221. package/dist/providers/openai.models.js +1 -1
  222. package/dist/providers/openai.models.js.map +1 -1
  223. package/dist/providers/opencode-go.models.js +1 -1
  224. package/dist/providers/opencode-go.models.js.map +1 -1
  225. package/dist/providers/opencode.js +1 -1
  226. package/dist/providers/opencode.js.map +1 -1
  227. package/dist/providers/opencode.models.js +1 -1
  228. package/dist/providers/opencode.models.js.map +1 -1
  229. package/dist/providers/openrouter.models.js +1 -1
  230. package/dist/providers/openrouter.models.js.map +1 -1
  231. package/dist/providers/qwen-token-plan-cn.models.js +1 -1
  232. package/dist/providers/qwen-token-plan-cn.models.js.map +1 -1
  233. package/dist/providers/qwen-token-plan-individual-cn.d.ts +3 -0
  234. package/dist/providers/qwen-token-plan-individual-cn.d.ts.map +1 -0
  235. package/dist/providers/qwen-token-plan-individual-cn.js +15 -0
  236. package/dist/providers/qwen-token-plan-individual-cn.js.map +1 -0
  237. package/dist/providers/qwen-token-plan-individual-cn.models.d.ts +6 -0
  238. package/dist/providers/qwen-token-plan-individual-cn.models.d.ts.map +1 -0
  239. package/dist/providers/qwen-token-plan-individual-cn.models.js +8 -0
  240. package/dist/providers/qwen-token-plan-individual-cn.models.js.map +1 -0
  241. package/dist/providers/qwen-token-plan-individual.models.js +1 -1
  242. package/dist/providers/qwen-token-plan-individual.models.js.map +1 -1
  243. package/dist/providers/qwen-token-plan.models.js +1 -1
  244. package/dist/providers/qwen-token-plan.models.js.map +1 -1
  245. package/dist/providers/radius-config.d.ts.map +1 -1
  246. package/dist/providers/radius-config.js +2 -1
  247. package/dist/providers/radius-config.js.map +1 -1
  248. package/dist/providers/radius.models.js +1 -1
  249. package/dist/providers/radius.models.js.map +1 -1
  250. package/dist/providers/schema-builders.d.ts +10 -0
  251. package/dist/providers/schema-builders.d.ts.map +1 -0
  252. package/dist/providers/schema-builders.js +18 -0
  253. package/dist/providers/schema-builders.js.map +1 -0
  254. package/dist/providers/together.models.js +1 -1
  255. package/dist/providers/together.models.js.map +1 -1
  256. package/dist/providers/typesafe.models.js +1 -1
  257. package/dist/providers/typesafe.models.js.map +1 -1
  258. package/dist/providers/vercel-ai-gateway.models.js +1 -1
  259. package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  260. package/dist/providers/xai.models.js +1 -1
  261. package/dist/providers/xai.models.js.map +1 -1
  262. package/dist/providers/xiaomi-token-plan-ams.models.js +1 -1
  263. package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  264. package/dist/providers/xiaomi-token-plan-cn.models.js +1 -1
  265. package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  266. package/dist/providers/xiaomi-token-plan-sgp.models.js +1 -1
  267. package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  268. package/dist/providers/xiaomi.models.js +1 -1
  269. package/dist/providers/xiaomi.models.js.map +1 -1
  270. package/dist/providers/zai-coding-cn.models.js +1 -1
  271. package/dist/providers/zai-coding-cn.models.js.map +1 -1
  272. package/dist/providers/zai.models.js +1 -1
  273. package/dist/providers/zai.models.js.map +1 -1
  274. package/dist/telemetry.d.ts.map +1 -1
  275. package/dist/telemetry.js +2 -1
  276. package/dist/telemetry.js.map +1 -1
  277. package/dist/types.d.ts +20 -3
  278. package/dist/types.d.ts.map +1 -1
  279. package/dist/types.js.map +1 -1
  280. package/dist/utils/error-body.d.ts +8 -0
  281. package/dist/utils/error-body.d.ts.map +1 -1
  282. package/dist/utils/error-body.js +30 -2
  283. package/dist/utils/error-body.js.map +1 -1
  284. package/dist/utils/openrouter-cost.d.ts +24 -0
  285. package/dist/utils/openrouter-cost.d.ts.map +1 -0
  286. package/dist/utils/openrouter-cost.js +34 -0
  287. package/dist/utils/openrouter-cost.js.map +1 -0
  288. package/dist/utils/overflow.d.ts.map +1 -1
  289. package/dist/utils/overflow.js +2 -0
  290. package/dist/utils/overflow.js.map +1 -1
  291. package/dist/utils/retry.d.ts.map +1 -1
  292. package/dist/utils/retry.js +6 -0
  293. package/dist/utils/retry.js.map +1 -1
  294. package/dist/utils/streaming-json.d.ts +84 -0
  295. package/dist/utils/streaming-json.d.ts.map +1 -0
  296. package/dist/utils/streaming-json.js +513 -0
  297. package/dist/utils/streaming-json.js.map +1 -0
  298. package/dist/utils/{typebox-helpers.d.ts → string-enum.d.ts} +5 -5
  299. package/dist/utils/string-enum.d.ts.map +1 -0
  300. package/dist/utils/{typebox-helpers.js → string-enum.js} +5 -6
  301. package/dist/utils/string-enum.js.map +1 -0
  302. package/dist/utils/text.d.ts +7 -0
  303. package/dist/utils/text.d.ts.map +1 -1
  304. package/dist/utils/text.js +9 -0
  305. package/dist/utils/text.js.map +1 -1
  306. package/dist/utils/transcript.d.ts +1 -1
  307. package/dist/utils/transcript.js +1 -1
  308. package/dist/utils/transcript.js.map +1 -1
  309. package/dist/utils/validation.d.ts +5 -3
  310. package/dist/utils/validation.d.ts.map +1 -1
  311. package/dist/utils/validation.js +27 -277
  312. package/dist/utils/validation.js.map +1 -1
  313. package/dist/utils/version.d.ts +1 -1
  314. package/dist/utils/version.js +1 -1
  315. package/dist/utils/version.js.map +1 -1
  316. package/package.json +19 -10
  317. package/dist/utils/typebox-helpers.d.ts.map +0 -1
  318. package/dist/utils/typebox-helpers.js.map +0 -1
@@ -1,19 +1,25 @@
1
1
  import Anthropic, {} from "@anthropic-ai/sdk";
2
2
  import { ANTHROPIC_FEDERATION_RULE_ID_ENV, ANTHROPIC_IDENTITY_TOKEN_FILE_ENV, ANTHROPIC_ORGANIZATION_ID_ENV, ANTHROPIC_SERVICE_ACCOUNT_ID_ENV, ANTHROPIC_WORKSPACE_ID_ENV, } from "../env-api-keys.js";
3
3
  import { calculateCost } from "../models.js";
4
+ import { registerSessionResourceCleanup } from "../session-resources.js";
4
5
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
6
+ import { withCauseDetail } from "../utils/error-body.js";
5
7
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
8
+ import { shortHash } from "../utils/hash.js";
6
9
  import { headersToRecord } from "../utils/headers.js";
7
10
  import { getIdentityHeaders } from "../utils/identity.js";
8
- import { parseJsonWithRepair, parseStreamingJson, parseToolCallArguments } from "../utils/json-parse.js";
11
+ import { parseJsonWithRepair } from "../utils/json-parse.js";
12
+ import { applyOpenRouterReportedCost, isOpenRouterModel, } from "../utils/openrouter-cost.js";
9
13
  import { getProviderEnvValue } from "../utils/provider-env.js";
10
14
  import { retryProviderRequest } from "../utils/provider-retry.js";
11
15
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
16
+ import { ToolCallArgumentsStream } from "../utils/streaming-json.js";
12
17
  import { getSystemMessageText, renderSystemMessageUpdate } from "../utils/text.js";
13
18
  import { getCurrentTools, getInitialSystemMessage, resolveTranscript, } from "../utils/transcript.js";
14
- import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
19
+ import { getJsonSchemaToolParameters, prefersJsonSchemaStrictSampling, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
15
20
  import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
16
21
  import { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from "./simple-options.js";
22
+ import { hasExceededStrictToolLimits, isStrictToolLimitErrorMessage, markStrictToolLimitsExceeded, } from "./strict-tool-limits.js";
17
23
  import { splitErrorToolResultImages, transformMessages } from "./transform-messages.js";
18
24
  /**
19
25
  * Resolve cache retention preference.
@@ -120,6 +126,7 @@ const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
120
126
  const MID_CONVERSATION_OUTPUT_CONFIG_BETA = "mid-conversation-output-config-2026-07-01";
121
127
  const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
122
128
  const INLINE_TOOLS_BETA = "inline-tools-2026-09-15";
129
+ const MESSAGE_THREADS_BETA = "message-threads-2026-08-12";
123
130
  /**
124
131
  * Stable deferred tool declared whenever native tool changes are in use. Anthropic adds
125
132
  * hidden prompt scaffolding for mid-conversation tool changes; declaring this placeholder
@@ -149,8 +156,178 @@ function getAnthropicCompat(model) {
149
156
  supportsStrictTools: model.compat?.supportsStrictTools ?? false,
150
157
  supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
151
158
  supportsMidConvoToolChanges: model.compat?.supportsMidConvoToolChanges ?? false,
159
+ // Gateways that drop unknown request fields would silently turn a `continue` into a request
160
+ // without the earlier conversation, so the default is limited to Anthropic's own endpoint.
161
+ // Widen or remove this restriction once proxies generally forward the `thread` field.
162
+ supportsMessageThreads: model.compat?.supportsMessageThreads ??
163
+ (model.provider === "anthropic" && model.baseUrl.includes("api.anthropic.com")),
152
164
  };
153
165
  }
166
+ /** Per session: where the next request can continue, or "off" after the endpoint rejected threads. */
167
+ const threadState = new Map();
168
+ const MAX_THREAD_STATE_ENTRIES = 10_000;
169
+ /** Rejections of a `continue` that a new thread fixes: expired, raced, or a fixed setting changed. */
170
+ const NEW_THREAD_ERRORS = new Set([
171
+ "thread_not_found",
172
+ "thread_already_continued",
173
+ "thread_continue_in_flight",
174
+ "thread_fingerprint_mismatch",
175
+ ]);
176
+ /**
177
+ * Sets a session's thread state, keeping at most `MAX_THREAD_STATE_ENTRIES` sessions.
178
+ *
179
+ * State is normally freed by `cleanupSessionResources(sessionId)` when a session ends, as pi's
180
+ * coding agent does. The cap bounds memory for library users that never call it, for example a
181
+ * server passing a new `sessionId` per conversation. Dropping the least recently updated session
182
+ * is cheap: its thread has most likely expired on the server already (after a few minutes idle),
183
+ * and otherwise its next request just starts a new thread.
184
+ */
185
+ function setThreadState(sessionId, value) {
186
+ // Re-insert so Map order stays oldest-first for the cap.
187
+ threadState.delete(sessionId);
188
+ threadState.set(sessionId, value);
189
+ if (threadState.size > MAX_THREAD_STATE_ENTRIES) {
190
+ const oldest = threadState.keys().next().value;
191
+ if (oldest !== undefined)
192
+ threadState.delete(oldest);
193
+ }
194
+ }
195
+ registerSessionResourceCleanup((sessionId) => {
196
+ if (sessionId)
197
+ threadState.delete(sessionId);
198
+ else
199
+ threadState.clear();
200
+ });
201
+ /**
202
+ * Hash of everything a `continue` builds on: the settings that must stay the same within a thread
203
+ * and the messages the server already holds.
204
+ *
205
+ * - Settings, as sent: the endpoint (a thread exists on one endpoint only), and the fixed request
206
+ * fields. A `continue` omits `system` and `tools` and the server keeps the thread's own, so only
207
+ * this check can notice a change to them, including their cache markers. The server rejects
208
+ * changes to the other fields with `thread_fingerprint_mismatch`; checking them here saves that
209
+ * rejected request. A fixed field missing here costs only that rejection, which also starts a
210
+ * new thread.
211
+ * - Messages, normalized so messages that mean the same to the API hash the same.
212
+ */
213
+ function hashThreadPrefix(model, params, messages) {
214
+ const { system, tools, thinking, output_config, tool_choice, betas, fallbacks } = params;
215
+ const settings = JSON.stringify([
216
+ model.provider,
217
+ model.baseUrl,
218
+ params.model,
219
+ system,
220
+ tools,
221
+ thinking,
222
+ output_config,
223
+ tool_choice,
224
+ betas,
225
+ fallbacks,
226
+ ]);
227
+ const conversation = JSON.stringify(messages, (key, value) => {
228
+ // Cache markers don't change what a message means, but they move: pi puts one on the newest
229
+ // message, so the message that had it in the last request has none in this one. Extensions
230
+ // may add markers anywhere too.
231
+ if (key === "cache_control")
232
+ return undefined;
233
+ // To attach that marker, the newest user message's string content is sent as one text block;
234
+ // the next request sends the same message as a string: "hi" vs [{ type: "text", text: "hi" }].
235
+ if (key === "content" && typeof value === "string")
236
+ return [{ type: "text", text: value }];
237
+ return value;
238
+ });
239
+ return shortHash(settings + conversation);
240
+ }
241
+ function createThreadRequest(sessionId, params) {
242
+ return { sessionId, body: { ...params, thread: { type: "create" } } };
243
+ }
244
+ function prepareThreadRequest(model, sessionId, params) {
245
+ const state = threadState.get(sessionId);
246
+ if (state === "off")
247
+ return undefined;
248
+ if (state !== undefined &&
249
+ params.messages.length > state.prefixCount &&
250
+ hashThreadPrefix(model, params, params.messages.slice(0, state.prefixCount)) === state.prefixHash) {
251
+ // Each reply can be continued once; deleting also keeps a concurrent request from trying.
252
+ threadState.delete(sessionId);
253
+ const { system: _system, tools: _tools, ...rest } = params;
254
+ return {
255
+ body: {
256
+ ...rest,
257
+ messages: params.messages.slice(state.prefixCount),
258
+ thread: { type: "continue", previous_message_id: state.replyId },
259
+ },
260
+ sessionId,
261
+ };
262
+ }
263
+ return createThreadRequest(sessionId, params);
264
+ }
265
+ /** The `error_code` Anthropic puts in `error.details` of thread rejections. */
266
+ function getThreadErrorCode(error) {
267
+ const body = error.error;
268
+ const code = body?.error?.details?.error_code;
269
+ return typeof code === "string" ? code : undefined;
270
+ }
271
+ /**
272
+ * Sends a thread request. A rejection about the thread (never a started stream) is resent once in
273
+ * full: as a new thread when the old one can't be continued, or without `thread` when the endpoint
274
+ * can't use threads, which also turns threads off for the session. Other errors are passed on.
275
+ * Returns the response and the thread request that produced it, if any.
276
+ */
277
+ async function sendThreadRequest(thread, params, send, output, signal) {
278
+ try {
279
+ return { response: await send(thread.body), thread };
280
+ }
281
+ catch (error) {
282
+ if (signal?.aborted || !(error instanceof Anthropic.APIError))
283
+ throw error;
284
+ const errorCode = getThreadErrorCode(error);
285
+ const startNewThread = errorCode !== undefined && NEW_THREAD_ERRORS.has(errorCode);
286
+ // The endpoint can't use threads: it can't thread this request, or it doesn't know the
287
+ // `thread` field and rejects it without a code ("thread: Extra inputs are not permitted"),
288
+ // e.g. with an `anthropic-beta` header that lacks the threads beta.
289
+ const threadsUnsupported = errorCode === "thread_unsupported_request" ||
290
+ (errorCode === undefined && error.status === 400 && error.message.includes("thread"));
291
+ // Any other error (prompt too long, unknown model, strict tool limits, ...) is not about the
292
+ // thread, and resending would only fail again.
293
+ if (!startNewThread && !threadsUnsupported)
294
+ throw error;
295
+ // Recorded before the resend, so a failed resend keeps it.
296
+ appendAssistantMessageDiagnostic(output, {
297
+ type: "anthropic_thread_fallback",
298
+ timestamp: Date.now(),
299
+ error: { message: error.message },
300
+ details: { reason: errorCode ?? String(error.status) },
301
+ });
302
+ if (startNewThread) {
303
+ const created = createThreadRequest(thread.sessionId, params);
304
+ return { response: await send(created.body), thread: created };
305
+ }
306
+ // With the threads beta, a request without `thread` is a normal request.
307
+ const response = await send(params);
308
+ setThreadState(thread.sessionId, "off");
309
+ return { response, thread: undefined };
310
+ }
311
+ }
312
+ /**
313
+ * Records where the session's next request can continue: after `output`, the clean reply to the
314
+ * thread request `thread`, whose full request was `params`.
315
+ */
316
+ function recordThreadReply(model, thread, params, output, isOAuth, allowEmptySignature) {
317
+ // `responseModel` is set only when a fallback model answered; the server does not store those
318
+ // replies, so they cannot be continued.
319
+ if (output.responseId === undefined || output.responseModel !== undefined)
320
+ return;
321
+ // Convert the reply like the next request will, keeping only the assistant message
322
+ // (transformMessages adds placeholder results for trailing tool calls).
323
+ const reply = convertMessages(transformMessages([output], model, normalizeToolCallId), isOAuth, undefined, allowEmptySignature).messages.filter((message) => message.role === "assistant");
324
+ const prefix = [...params.messages, ...reply];
325
+ setThreadState(thread.sessionId, {
326
+ prefixCount: prefix.length,
327
+ prefixHash: hashThreadPrefix(model, params, prefix),
328
+ replyId: output.responseId,
329
+ });
330
+ }
154
331
  function mergeHeaders(...headerSources) {
155
332
  const merged = {};
156
333
  for (const headers of headerSources) {
@@ -375,7 +552,8 @@ async function* iterateAnthropicEvents(response, signal) {
375
552
  }
376
553
  export const stream = (model, context, options) => {
377
554
  const stream = new AssistantMessageEventStream();
378
- const normalizedContext = resolveTranscript(context, getAnthropicCompat(model).supportsMidConvoSystemMessages);
555
+ const compat = getAnthropicCompat(model);
556
+ const normalizedContext = resolveTranscript(context, compat.supportsMidConvoSystemMessages);
379
557
  const currentTools = getCurrentTools(normalizedContext.messages);
380
558
  (async () => {
381
559
  const output = {
@@ -400,6 +578,12 @@ export const stream = (model, context, options) => {
400
578
  let isOAuth;
401
579
  let usageModel = model;
402
580
  let inputTransformations;
581
+ const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
582
+ const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
583
+ // Session whose requests may use threads; undefined when threads don't apply. Sessions where
584
+ // the endpoint rejected threads ("off") still send the threads beta, which the resend that
585
+ // turned them off was accepted with; `prepareThreadRequest()` just sends no `thread`.
586
+ const threadSessionId = compat.supportsMessageThreads ? cacheSessionId : undefined;
403
587
  if (options?.client) {
404
588
  client = options.client;
405
589
  isOAuth = false;
@@ -417,8 +601,6 @@ export const stream = (model, context, options) => {
417
601
  hasImages,
418
602
  });
419
603
  }
420
- const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
421
- const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
422
604
  const created = createClient(model, apiKey, options?.headers, options?.fetch, copilotDynamicHeaders, cacheSessionId, federation, options?.identity);
423
605
  client = created.client;
424
606
  isOAuth = created.isOAuthToken;
@@ -428,24 +610,38 @@ export const stream = (model, context, options) => {
428
610
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
429
611
  maxRetries: 0,
430
612
  };
431
- let disablePreferredStrictTools = false;
613
+ const send = (body) => retryProviderRequest(() => client.beta.messages.create(body, requestOptions).asResponse(), {
614
+ maxRetries: options?.maxRetries,
615
+ maxRetryDelayMs: options?.maxRetryDelayMs,
616
+ signal: options?.signal,
617
+ });
618
+ let disablePreferredStrictTools = hasExceededStrictToolLimits(options?.sessionId);
432
619
  let response;
620
+ let params;
621
+ // The thread request that produced the response; undefined when no `thread` was sent.
622
+ let thread;
433
623
  for (;;) {
434
624
  options?.signal?.throwIfAborted();
435
- const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
625
+ const built = buildParams(model, normalizedContext, isOAuth, options, {
626
+ disablePreferredStrictTools,
627
+ messageThreads: threadSessionId !== undefined,
628
+ });
436
629
  if (built.providerThinkingLevel)
437
630
  output.providerThinkingLevel = built.providerThinkingLevel;
438
- let params = built.params;
631
+ params = built.params;
439
632
  const nextParams = await options?.onPayload?.(params, model);
440
633
  if (nextParams !== undefined) {
441
634
  params = { ...nextParams, stream: true };
442
635
  }
636
+ const prepared = threadSessionId !== undefined && options?.disableContinuation !== true
637
+ ? prepareThreadRequest(model, threadSessionId, params)
638
+ : undefined;
443
639
  try {
444
- response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
445
- maxRetries: options?.maxRetries,
446
- maxRetryDelayMs: options?.maxRetryDelayMs,
447
- signal: options?.signal,
448
- });
640
+ const sent = prepared
641
+ ? await sendThreadRequest(prepared, params, send, output, options?.signal)
642
+ : { response: await send(params), thread: undefined };
643
+ response = sent.response;
644
+ thread = sent.thread;
449
645
  break;
450
646
  }
451
647
  catch (error) {
@@ -456,14 +652,11 @@ export const stream = (model, context, options) => {
456
652
  !(error instanceof Anthropic.APIError) ||
457
653
  error.status !== 400 ||
458
654
  error.type !== "invalid_request_error" ||
459
- ![
460
- "The compiled grammar is too large",
461
- "Too many strict tools (",
462
- "Schemas contains too many parameters with union types (",
463
- ].some((message) => error.message.includes(message))) {
655
+ !isStrictToolLimitErrorMessage(error.message)) {
464
656
  throw error;
465
657
  }
466
658
  disablePreferredStrictTools = true;
659
+ markStrictToolLimitsExceeded(options?.sessionId);
467
660
  appendAssistantMessageDiagnostic(output, {
468
661
  type: "anthropic_strict_tools_fallback",
469
662
  timestamp: Date.now(),
@@ -474,7 +667,12 @@ export const stream = (model, context, options) => {
474
667
  }
475
668
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
476
669
  stream.push({ type: "start", partial: output });
477
- const blocks = output.content;
670
+ // Stream state per Anthropic block index, kept off the content blocks so it never reaches partials.
671
+ const openBlocks = new Map();
672
+ const openBlock = (anthropicIndex) => {
673
+ const open = openBlocks.get(anthropicIndex);
674
+ return open === undefined ? undefined : { ...open, block: output.content[open.contentIndex] };
675
+ };
478
676
  for await (const event of iterateAnthropicEvents(response, options?.signal)) {
479
677
  await options?.onProviderStreamEvent?.(event, model);
480
678
  if (event.type === "message_start") {
@@ -509,105 +707,92 @@ export const stream = (model, context, options) => {
509
707
  continue;
510
708
  }
511
709
  if (event.content_block.type === "text") {
512
- const block = {
513
- type: "text",
514
- text: event.content_block.text ?? "",
515
- index: event.index,
516
- };
517
- output.content.push(block);
710
+ output.content.push({ type: "text", text: event.content_block.text ?? "" });
711
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
518
712
  stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
519
713
  }
520
714
  else if (event.content_block.type === "thinking") {
521
- const block = {
715
+ output.content.push({
522
716
  type: "thinking",
523
717
  thinking: event.content_block.thinking ?? "",
524
718
  thinkingSignature: event.content_block.signature ?? "",
525
- index: event.index,
526
- };
527
- output.content.push(block);
719
+ });
720
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
528
721
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
529
722
  }
530
723
  else if (event.content_block.type === "redacted_thinking") {
531
- const block = {
724
+ output.content.push({
532
725
  type: "thinking",
533
726
  thinking: "[Reasoning redacted]",
534
727
  thinkingSignature: event.content_block.data,
535
728
  redacted: true,
536
- index: event.index,
537
- };
538
- output.content.push(block);
729
+ });
730
+ openBlocks.set(event.index, { contentIndex: output.content.length - 1 });
539
731
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
540
732
  }
541
733
  else if (event.content_block.type === "tool_use") {
542
- const block = {
734
+ output.content.push({
543
735
  type: "toolCall",
544
736
  id: event.content_block.id,
545
737
  name: isOAuth
546
738
  ? fromClaudeCodeName(event.content_block.name, currentTools)
547
739
  : event.content_block.name,
548
740
  arguments: event.content_block.input ?? {},
549
- partialJson: "",
550
- index: event.index,
551
- };
552
- output.content.push(block);
741
+ });
742
+ openBlocks.set(event.index, {
743
+ contentIndex: output.content.length - 1,
744
+ arguments: new ToolCallArgumentsStream(),
745
+ });
553
746
  stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
554
747
  }
555
748
  }
556
749
  else if (event.type === "content_block_delta") {
750
+ const open = openBlock(event.index);
557
751
  if (event.delta.type === "text_delta") {
558
- const index = blocks.findIndex((b) => b.index === event.index);
559
- const block = blocks[index];
560
- if (block && block.type === "text") {
561
- block.text += event.delta.text;
752
+ if (open?.block.type === "text") {
753
+ open.block.text += event.delta.text;
562
754
  stream.push({
563
755
  type: "text_delta",
564
- contentIndex: index,
756
+ contentIndex: open.contentIndex,
565
757
  delta: event.delta.text,
566
758
  partial: output,
567
759
  });
568
760
  }
569
761
  }
570
762
  else if (event.delta.type === "thinking_delta") {
571
- const index = blocks.findIndex((b) => b.index === event.index);
572
- const block = blocks[index];
573
- if (block && block.type === "thinking") {
574
- block.thinking += event.delta.thinking;
763
+ if (open?.block.type === "thinking") {
764
+ open.block.thinking += event.delta.thinking;
575
765
  stream.push({
576
766
  type: "thinking_delta",
577
- contentIndex: index,
767
+ contentIndex: open.contentIndex,
578
768
  delta: event.delta.thinking,
579
769
  partial: output,
580
770
  });
581
771
  }
582
772
  }
583
773
  else if (event.delta.type === "input_json_delta") {
584
- const index = blocks.findIndex((b) => b.index === event.index);
585
- const block = blocks[index];
586
- if (block && block.type === "toolCall") {
587
- block.partialJson += event.delta.partial_json;
588
- block.arguments = parseStreamingJson(block.partialJson);
774
+ if (open?.block.type === "toolCall" && open.arguments !== undefined) {
775
+ open.block.arguments = open.arguments.append(event.delta.partial_json);
589
776
  stream.push({
590
777
  type: "toolcall_delta",
591
- contentIndex: index,
778
+ contentIndex: open.contentIndex,
592
779
  delta: event.delta.partial_json,
593
780
  partial: output,
594
781
  });
595
782
  }
596
783
  }
597
784
  else if (event.delta.type === "signature_delta") {
598
- const index = blocks.findIndex((b) => b.index === event.index);
599
- const block = blocks[index];
600
- if (block && block.type === "thinking") {
601
- block.thinkingSignature = block.thinkingSignature || "";
602
- block.thinkingSignature += event.delta.signature;
785
+ if (open?.block.type === "thinking") {
786
+ open.block.thinkingSignature = open.block.thinkingSignature || "";
787
+ open.block.thinkingSignature += event.delta.signature;
603
788
  }
604
789
  }
605
790
  }
606
791
  else if (event.type === "content_block_stop") {
607
- const index = blocks.findIndex((b) => b.index === event.index);
608
- const block = blocks[index];
609
- if (block) {
610
- delete block.index;
792
+ const open = openBlock(event.index);
793
+ openBlocks.delete(event.index);
794
+ if (open) {
795
+ const { block, contentIndex: index } = open;
611
796
  if (block.type === "text") {
612
797
  stream.push({
613
798
  type: "text_end",
@@ -624,11 +809,8 @@ export const stream = (model, context, options) => {
624
809
  partial: output,
625
810
  });
626
811
  }
627
- else if (block.type === "toolCall") {
628
- Object.assign(block, parseToolCallArguments(block.partialJson));
629
- // Finalize in-place and strip the scratch buffer so replay only
630
- // carries parsed arguments.
631
- delete block.partialJson;
812
+ else if (block.type === "toolCall" && open.arguments !== undefined) {
813
+ Object.assign(block, open.arguments.finish());
632
814
  stream.push({
633
815
  type: "toolcall_end",
634
816
  contentIndex: index,
@@ -680,6 +862,11 @@ export const stream = (model, context, options) => {
680
862
  output.usage.totalTokens =
681
863
  output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
682
864
  calculateCost(usageModel, output.usage);
865
+ if (isOpenRouterModel(model) && event.usage) {
866
+ // OpenRouter adds billing fields to streamed message_delta usage at runtime,
867
+ // although its OpenAPI schema and the Anthropic SDK do not type those fields.
868
+ applyOpenRouterReportedCost(output.usage, event.usage);
869
+ }
683
870
  }
684
871
  }
685
872
  if (options?.signal?.aborted) {
@@ -691,6 +878,8 @@ export const stream = (model, context, options) => {
691
878
  if (output.stopReason === "aborted" || output.stopReason === "error") {
692
879
  throw new Error(output.errorMessage || "An unknown error occurred");
693
880
  }
881
+ if (thread)
882
+ recordThreadReply(model, thread, params, output, isOAuth, compat.allowEmptySignature);
694
883
  if (inputTransformations && inputTransformations.length > 0) {
695
884
  appendAssistantMessageDiagnostic(output, {
696
885
  type: "anthropic_input_transformations",
@@ -708,13 +897,8 @@ export const stream = (model, context, options) => {
708
897
  stream.end();
709
898
  }
710
899
  catch (error) {
711
- for (const block of output.content) {
712
- delete block.index;
713
- // partialJson is only a streaming scratch buffer; never persist it.
714
- delete block.partialJson;
715
- }
716
900
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
717
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
901
+ output.errorMessage = error instanceof Error ? withCauseDetail(error) : JSON.stringify(error);
718
902
  stream.push({ type: "error", reason: output.stopReason, error: output });
719
903
  stream.end();
720
904
  }
@@ -849,7 +1033,7 @@ function createClient(model, apiKey, optionsHeaders, fetch, dynamicHeaders, sess
849
1033
  });
850
1034
  return { client, isOAuthToken: false };
851
1035
  }
852
- function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options) {
1036
+ function getBetaFeatures(model, context, options, { isOAuthToken, nativeToolChanges, messageThreads }) {
853
1037
  let configuredFeatures;
854
1038
  for (const headers of [model.headers, options?.headers]) {
855
1039
  for (const [name, value] of Object.entries(headers ?? {})) {
@@ -885,9 +1069,11 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
885
1069
  }
886
1070
  if (nativeToolChanges)
887
1071
  features.push(INLINE_TOOLS_BETA);
1072
+ if (messageThreads)
1073
+ features.push(MESSAGE_THREADS_BETA);
888
1074
  return [...new Set(features)];
889
1075
  }
890
- function buildParams(model, context, isOAuthToken, options, disablePreferredStrictTools = false) {
1076
+ function buildParams(model, context, isOAuthToken, options, flags) {
891
1077
  const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
892
1078
  const compat = getAnthropicCompat(model);
893
1079
  const initialSystemMessage = getInitialSystemMessage(context.messages);
@@ -900,11 +1086,8 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
900
1086
  // active tool to anchor the placeholder. Otherwise the current tool list is sent.
901
1087
  let hasPreferredStrictTools = false;
902
1088
  const convertToolDefinitions = (tools, toolCacheControl) => {
903
- const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
904
- if (definitions.some((definition, index) => {
905
- const config = tools[index].constrainedSampling;
906
- return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
907
- })) {
1089
+ const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, flags.disablePreferredStrictTools);
1090
+ if (definitions.some((definition, index) => definition.strict === true && prefersJsonSchemaStrictSampling(tools[index]))) {
908
1091
  hasPreferredStrictTools = true;
909
1092
  }
910
1093
  return definitions;
@@ -916,7 +1099,11 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
916
1099
  const effortMessages = model.compat?.supportsMidConvoEffort === true && !thinkingDisabled;
917
1100
  const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, effortMessages ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
918
1101
  const activeEffort = options?.effort ?? "high";
919
- const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
1102
+ const betaFeatures = getBetaFeatures(model, context, options, {
1103
+ isOAuthToken,
1104
+ nativeToolChanges,
1105
+ messageThreads: flags.messageThreads,
1106
+ });
920
1107
  const params = {
921
1108
  model: model.id,
922
1109
  messages: effortMessages ? insertThinkingLevelMessages(converted, activeEffort) : converted.messages,
@@ -985,7 +1172,10 @@ function buildParams(model, context, isOAuthToken, options, disablePreferredStri
985
1172
  display: options?.thinkingDisplay ?? "summarized",
986
1173
  block_binding: { prefix_mismatch_behavior: "drop_block" },
987
1174
  };
988
- params.output_config = { effort: "high" };
1175
+ // Effort markers set the level for every reply. The top-level effort is part of the cached
1176
+ // prompt, so it stays fixed. It is "low" because organizations can cap effort and reject
1177
+ // requests containing any level above the cap.
1178
+ params.output_config = { effort: "low" };
989
1179
  }
990
1180
  else if (model.reasoning && options?.thinkingEnabled) {
991
1181
  // Default to "summarized" so Opus 4.7 and Mythos Preview behave like
@@ -1045,7 +1235,9 @@ convertToolDefinitions) {
1045
1235
  const params = [];
1046
1236
  const assistantLevels = new Map();
1047
1237
  // Later system messages are held back and emitted directly before the next assistant
1048
- // message (or at the end of the transcript). Anthropic requires `tool_result` blocks to
1238
+ // message that is sent (or at the end of the transcript). Assistant messages that convert to
1239
+ // no blocks are dropped, so holding past them keeps an update from landing before a user
1240
+ // message (#11091). Anthropic requires `tool_result` blocks to
1049
1241
  // immediately follow their `tool_use`, so a system message between them is rejected; this
1050
1242
  // also mirrors where the managed-effort system messages are inserted. As a result an
1051
1243
  // update placed before a user message in the transcript lands after it on the wire.
@@ -1118,7 +1310,6 @@ convertToolDefinitions) {
1118
1310
  }
1119
1311
  }
1120
1312
  else if (msg.role === "assistant") {
1121
- flushPendingSystemMessages();
1122
1313
  const blocks = [];
1123
1314
  for (const block of msg.content) {
1124
1315
  if (block.type === "text") {
@@ -1176,6 +1367,7 @@ convertToolDefinitions) {
1176
1367
  }
1177
1368
  if (blocks.length === 0)
1178
1369
  continue;
1370
+ flushPendingSystemMessages();
1179
1371
  const messageIndex = params.length;
1180
1372
  params.push({
1181
1373
  role: "assistant",
@@ -1299,16 +1491,19 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
1299
1491
  if (!tools)
1300
1492
  return [];
1301
1493
  return tools.map((tool, index) => {
1302
- const config = tool.constrainedSampling;
1303
- const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
1494
+ const strict = disablePreferredStrictTools && prefersJsonSchemaStrictSampling(tool)
1304
1495
  ? undefined
1305
1496
  : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1306
1497
  const parameters = getJsonSchemaToolParameters(tool, strict);
1307
1498
  const schema = parameters;
1499
+ // Keep shared definitions so property $refs still resolve. Other root keywords stay out:
1500
+ // Anthropic rejects root anyOf/oneOf/allOf with a 400.
1308
1501
  const legacyInputSchema = {
1309
1502
  type: "object",
1310
1503
  properties: schema.properties ?? {},
1311
1504
  required: schema.required ?? [],
1505
+ ...(schema.$defs === undefined ? {} : { $defs: schema.$defs }),
1506
+ ...(schema.definitions === undefined ? {} : { definitions: schema.definitions }),
1312
1507
  };
1313
1508
  const inputSchema = strict === true
1314
1509
  ? {