@vib-rato/ai 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +3354 -0
  2. package/README.md +1194 -0
  3. package/dist/types/adapter-internals/provider-safety-stop.d.ts +45 -0
  4. package/dist/types/api-registry.d.ts +30 -0
  5. package/dist/types/auth-broker/client.d.ts +80 -0
  6. package/dist/types/auth-broker/index.d.ts +5 -0
  7. package/dist/types/auth-broker/redact.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +153 -0
  10. package/dist/types/auth-broker/server.d.ts +32 -0
  11. package/dist/types/auth-broker/types.d.ts +132 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +555 -0
  13. package/dist/types/auth-gateway/http.d.ts +40 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +70 -0
  16. package/dist/types/auth-gateway/types.d.ts +129 -0
  17. package/dist/types/auth-storage.d.ts +1074 -0
  18. package/dist/types/cli.d.ts +2 -0
  19. package/dist/types/codex-tools.d.ts +4 -0
  20. package/dist/types/context-cap-policy.d.ts +68 -0
  21. package/dist/types/core.d.ts +35 -0
  22. package/dist/types/index.d.ts +55 -0
  23. package/dist/types/model-cache.d.ts +24 -0
  24. package/dist/types/model-manager.d.ts +77 -0
  25. package/dist/types/model-pricing.d.ts +3 -0
  26. package/dist/types/model-retirements.d.ts +6 -0
  27. package/dist/types/model-thinking.d.ts +100 -0
  28. package/dist/types/models.d.ts +21 -0
  29. package/dist/types/openai-completions-compat.d.ts +34 -0
  30. package/dist/types/provider-details.d.ts +24 -0
  31. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  32. package/dist/types/provider-models/descriptors.d.ts +48 -0
  33. package/dist/types/provider-models/google.d.ts +20 -0
  34. package/dist/types/provider-models/index.d.ts +5 -0
  35. package/dist/types/provider-models/ollama.d.ts +7 -0
  36. package/dist/types/provider-models/openai-compat.d.ts +293 -0
  37. package/dist/types/provider-models/special.d.ts +29 -0
  38. package/dist/types/providers/amazon-bedrock.d.ts +60 -0
  39. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  40. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  41. package/dist/types/providers/anthropic.d.ts +280 -0
  42. package/dist/types/providers/aws-credential-config.d.ts +19 -0
  43. package/dist/types/providers/aws-credentials.d.ts +43 -0
  44. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  45. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  46. package/dist/types/providers/azure-openai-responses.d.ts +22 -0
  47. package/dist/types/providers/composer-discipline.d.ts +32 -0
  48. package/dist/types/providers/cursor/client-version.d.ts +10 -0
  49. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  50. package/dist/types/providers/cursor/gen/agent_pb.d.ts +16769 -0
  51. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  52. package/dist/types/providers/cursor.d.ts +72 -0
  53. package/dist/types/providers/dashscope-token-plan-headers.d.ts +57 -0
  54. package/dist/types/providers/error-message.d.ts +27 -0
  55. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  56. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  57. package/dist/types/providers/google-auth.d.ts +26 -0
  58. package/dist/types/providers/google-gemini-cli.d.ts +75 -0
  59. package/dist/types/providers/google-gemini-headers.d.ts +43 -0
  60. package/dist/types/providers/google-shared.d.ts +183 -0
  61. package/dist/types/providers/google-types.d.ts +138 -0
  62. package/dist/types/providers/google-vertex.d.ts +11 -0
  63. package/dist/types/providers/google.d.ts +4 -0
  64. package/dist/types/providers/grammar.d.ts +1 -0
  65. package/dist/types/providers/kimi.d.ts +27 -0
  66. package/dist/types/providers/kiro-api-key.d.ts +50 -0
  67. package/dist/types/providers/kiro-codewhisperer.d.ts +11 -0
  68. package/dist/types/providers/mock.d.ts +189 -0
  69. package/dist/types/providers/ollama.d.ts +41 -0
  70. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  71. package/dist/types/providers/openai-bounded-rate-limits.d.ts +3 -0
  72. package/dist/types/providers/openai-chat-server-schema.d.ts +1733 -0
  73. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  74. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  75. package/dist/types/providers/openai-codex/request-transformer.d.ts +50 -0
  76. package/dist/types/providers/openai-codex/response-handler.d.ts +18 -0
  77. package/dist/types/providers/openai-codex-responses.d.ts +71 -0
  78. package/dist/types/providers/openai-completions-compat.d.ts +6 -0
  79. package/dist/types/providers/openai-completions.d.ts +35 -0
  80. package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
  81. package/dist/types/providers/openai-request-transform.d.ts +4 -0
  82. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  83. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  84. package/dist/types/providers/openai-responses-shared.d.ts +106 -0
  85. package/dist/types/providers/openai-responses.d.ts +37 -0
  86. package/dist/types/providers/pi-native-client.d.ts +29 -0
  87. package/dist/types/providers/pi-native-server.d.ts +60 -0
  88. package/dist/types/providers/register-builtins.d.ts +59 -0
  89. package/dist/types/providers/synthetic.d.ts +26 -0
  90. package/dist/types/providers/transform-messages.d.ts +33 -0
  91. package/dist/types/providers/vision-guard.d.ts +8 -0
  92. package/dist/types/rate-limit-utils.d.ts +19 -0
  93. package/dist/types/stream.d.ts +45 -0
  94. package/dist/types/types.d.ts +1073 -0
  95. package/dist/types/usage/claude.d.ts +3 -0
  96. package/dist/types/usage/gemini.d.ts +2 -0
  97. package/dist/types/usage/github-copilot.d.ts +7 -0
  98. package/dist/types/usage/google-antigravity.d.ts +2 -0
  99. package/dist/types/usage/grok-cli.d.ts +17 -0
  100. package/dist/types/usage/kimi.d.ts +4 -0
  101. package/dist/types/usage/minimax-code.d.ts +2 -0
  102. package/dist/types/usage/openai-codex.d.ts +3 -0
  103. package/dist/types/usage/shared.d.ts +1 -0
  104. package/dist/types/usage/zai.d.ts +2 -0
  105. package/dist/types/usage.d.ts +264 -0
  106. package/dist/types/utils/abort.d.ts +19 -0
  107. package/dist/types/utils/anthropic-auth.d.ts +39 -0
  108. package/dist/types/utils/block-symbols.d.ts +6 -0
  109. package/dist/types/utils/discovery/antigravity.d.ts +67 -0
  110. package/dist/types/utils/discovery/codex.d.ts +38 -0
  111. package/dist/types/utils/discovery/cursor.d.ts +49 -0
  112. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  113. package/dist/types/utils/discovery/index.d.ts +4 -0
  114. package/dist/types/utils/discovery/openai-compatible.d.ts +81 -0
  115. package/dist/types/utils/event-stream.d.ts +41 -0
  116. package/dist/types/utils/fallback-transport.d.ts +110 -0
  117. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  118. package/dist/types/utils/foundry.d.ts +11 -0
  119. package/dist/types/utils/h2-fetch.d.ts +22 -0
  120. package/dist/types/utils/http-inspector.d.ts +59 -0
  121. package/dist/types/utils/idle-iterator.d.ts +122 -0
  122. package/dist/types/utils/json-parse.d.ts +98 -0
  123. package/dist/types/utils/oauth/alibaba-token-plan.d.ts +19 -0
  124. package/dist/types/utils/oauth/anthropic.d.ts +41 -0
  125. package/dist/types/utils/oauth/api-key-login.d.ts +38 -0
  126. package/dist/types/utils/oauth/api-key-validation.d.ts +39 -0
  127. package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
  128. package/dist/types/utils/oauth/callback-server.d.ts +80 -0
  129. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  130. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  131. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  132. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  133. package/dist/types/utils/oauth/deepinfra.d.ts +1 -0
  134. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  135. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  136. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  137. package/dist/types/utils/oauth/fugu.d.ts +1 -0
  138. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  139. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  140. package/dist/types/utils/oauth/glm-zcode.d.ts +71 -0
  141. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  142. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  143. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  144. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  145. package/dist/types/utils/oauth/index.d.ts +39 -0
  146. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  147. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  148. package/dist/types/utils/oauth/kimi.d.ts +17 -0
  149. package/dist/types/utils/oauth/kiro.d.ts +71 -0
  150. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  151. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  152. package/dist/types/utils/oauth/mara.d.ts +1 -0
  153. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  154. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  155. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  156. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  157. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  158. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  159. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  160. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  161. package/dist/types/utils/oauth/opengateway.d.ts +1 -0
  162. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  163. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  164. package/dist/types/utils/oauth/perplexity.d.ts +4 -0
  165. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  166. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  167. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  168. package/dist/types/utils/oauth/sglang.d.ts +16 -0
  169. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  170. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  171. package/dist/types/utils/oauth/together.d.ts +1 -0
  172. package/dist/types/utils/oauth/types.d.ts +56 -0
  173. package/dist/types/utils/oauth/venice.d.ts +18 -0
  174. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  175. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  176. package/dist/types/utils/oauth/xai.d.ts +30 -0
  177. package/dist/types/utils/oauth/xiaomi.d.ts +25 -0
  178. package/dist/types/utils/oauth/zai.d.ts +18 -0
  179. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  180. package/dist/types/utils/overflow.d.ts +14 -0
  181. package/dist/types/utils/parse-bind.d.ts +26 -0
  182. package/dist/types/utils/provider-response.d.ts +6 -0
  183. package/dist/types/utils/provider-safety-stop.d.ts +8 -0
  184. package/dist/types/utils/proxy.d.ts +7 -0
  185. package/dist/types/utils/retry-after.d.ts +3 -0
  186. package/dist/types/utils/retry-budget.d.ts +1 -0
  187. package/dist/types/utils/retry.d.ts +29 -0
  188. package/dist/types/utils/schema/adapt.d.ts +24 -0
  189. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  190. package/dist/types/utils/schema/dereference.d.ts +11 -0
  191. package/dist/types/utils/schema/draft.d.ts +10 -0
  192. package/dist/types/utils/schema/equality.d.ts +4 -0
  193. package/dist/types/utils/schema/fields.d.ts +49 -0
  194. package/dist/types/utils/schema/index.d.ts +14 -0
  195. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  196. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  197. package/dist/types/utils/schema/normalize.d.ts +93 -0
  198. package/dist/types/utils/schema/root-combinator.d.ts +12 -0
  199. package/dist/types/utils/schema/spill.d.ts +8 -0
  200. package/dist/types/utils/schema/stamps.d.ts +25 -0
  201. package/dist/types/utils/schema/types.d.ts +4 -0
  202. package/dist/types/utils/schema/wire.d.ts +54 -0
  203. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  204. package/dist/types/utils/sse-debug.d.ts +10 -0
  205. package/dist/types/utils/tool-call-healing.d.ts +80 -0
  206. package/dist/types/utils/tool-choice-capability.d.ts +57 -0
  207. package/dist/types/utils/tool-choice.d.ts +50 -0
  208. package/dist/types/utils/validation.d.ts +17 -0
  209. package/dist/types/utils.d.ts +117 -0
  210. package/package.json +152 -0
  211. package/src/adapter-internals/provider-safety-stop.d.ts +45 -0
  212. package/src/adapter-internals/provider-safety-stop.ts +156 -0
  213. package/src/api-registry.d.ts +30 -0
  214. package/src/api-registry.ts +96 -0
  215. package/src/auth-broker/client.ts +444 -0
  216. package/src/auth-broker/index.ts +5 -0
  217. package/src/auth-broker/redact.ts +39 -0
  218. package/src/auth-broker/refresher.ts +130 -0
  219. package/src/auth-broker/remote-store.ts +1576 -0
  220. package/src/auth-broker/server.ts +764 -0
  221. package/src/auth-broker/types.ts +164 -0
  222. package/src/auth-broker/wire-schemas.ts +261 -0
  223. package/src/auth-gateway/http.ts +198 -0
  224. package/src/auth-gateway/index.ts +3 -0
  225. package/src/auth-gateway/server.ts +1315 -0
  226. package/src/auth-gateway/types.ts +160 -0
  227. package/src/auth-storage.ts +7312 -0
  228. package/src/cli.ts +269 -0
  229. package/src/codex-tools.d.ts +4 -0
  230. package/src/codex-tools.ts +24 -0
  231. package/src/context-cap-policy.d.ts +68 -0
  232. package/src/context-cap-policy.ts +123 -0
  233. package/src/core.ts +44 -0
  234. package/src/index.ts +61 -0
  235. package/src/model-cache.ts +236 -0
  236. package/src/model-manager.ts +744 -0
  237. package/src/model-pricing.d.ts +3 -0
  238. package/src/model-pricing.ts +68 -0
  239. package/src/model-retirements.d.ts +6 -0
  240. package/src/model-retirements.ts +19 -0
  241. package/src/model-thinking.d.ts +100 -0
  242. package/src/model-thinking.ts +1054 -0
  243. package/src/models.d.ts +21 -0
  244. package/src/models.json +94672 -0
  245. package/src/models.json.d.ts +9 -0
  246. package/src/models.ts +126 -0
  247. package/src/openai-completions-compat.d.ts +34 -0
  248. package/src/openai-completions-compat.ts +383 -0
  249. package/src/prompts/composer-bash-policy-recovery.md +1 -0
  250. package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
  251. package/src/prompts/cursor-composer-edit-discipline.md +7 -0
  252. package/src/prompts/turn-aborted-guidance.md +4 -0
  253. package/src/provider-details.ts +90 -0
  254. package/src/provider-models/bundled-references.ts +38 -0
  255. package/src/provider-models/descriptors.ts +392 -0
  256. package/src/provider-models/google.ts +92 -0
  257. package/src/provider-models/index.ts +5 -0
  258. package/src/provider-models/ollama.ts +159 -0
  259. package/src/provider-models/openai-compat.ts +2920 -0
  260. package/src/provider-models/special.ts +185 -0
  261. package/src/providers/amazon-bedrock.d.ts +60 -0
  262. package/src/providers/amazon-bedrock.ts +939 -0
  263. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  264. package/src/providers/anthropic-messages-server.ts +839 -0
  265. package/src/providers/anthropic.d.ts +280 -0
  266. package/src/providers/anthropic.ts +4421 -0
  267. package/src/providers/aws-credential-config.d.ts +19 -0
  268. package/src/providers/aws-credential-config.ts +179 -0
  269. package/src/providers/aws-credentials.d.ts +43 -0
  270. package/src/providers/aws-credentials.ts +457 -0
  271. package/src/providers/aws-eventstream.d.ts +38 -0
  272. package/src/providers/aws-eventstream.ts +185 -0
  273. package/src/providers/aws-sigv4.d.ts +55 -0
  274. package/src/providers/aws-sigv4.ts +218 -0
  275. package/src/providers/azure-openai-responses.d.ts +22 -0
  276. package/src/providers/azure-openai-responses.ts +447 -0
  277. package/src/providers/composer-discipline.d.ts +32 -0
  278. package/src/providers/composer-discipline.ts +95 -0
  279. package/src/providers/cursor/client-version.d.ts +10 -0
  280. package/src/providers/cursor/client-version.ts +10 -0
  281. package/src/providers/cursor/exec-modern.d.ts +98 -0
  282. package/src/providers/cursor/exec-modern.ts +497 -0
  283. package/src/providers/cursor/gen/agent_pb.d.ts +16769 -0
  284. package/src/providers/cursor/gen/agent_pb.ts +19780 -0
  285. package/src/providers/cursor/proto/agent.proto +4533 -0
  286. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  287. package/src/providers/cursor/proto/buf.yaml +17 -0
  288. package/src/providers/cursor-pi-args.d.ts +119 -0
  289. package/src/providers/cursor-pi-args.ts +187 -0
  290. package/src/providers/cursor.d.ts +72 -0
  291. package/src/providers/cursor.ts +3396 -0
  292. package/src/providers/dashscope-token-plan-headers.d.ts +57 -0
  293. package/src/providers/dashscope-token-plan-headers.ts +84 -0
  294. package/src/providers/error-message.d.ts +27 -0
  295. package/src/providers/error-message.ts +21 -0
  296. package/src/providers/github-copilot-headers.d.ts +40 -0
  297. package/src/providers/github-copilot-headers.ts +140 -0
  298. package/src/providers/gitlab-duo.d.ts +27 -0
  299. package/src/providers/gitlab-duo.ts +393 -0
  300. package/src/providers/google-auth.d.ts +26 -0
  301. package/src/providers/google-auth.ts +262 -0
  302. package/src/providers/google-gemini-cli.d.ts +75 -0
  303. package/src/providers/google-gemini-cli.ts +969 -0
  304. package/src/providers/google-gemini-headers.d.ts +43 -0
  305. package/src/providers/google-gemini-headers.ts +100 -0
  306. package/src/providers/google-shared.d.ts +183 -0
  307. package/src/providers/google-shared.ts +1105 -0
  308. package/src/providers/google-types.d.ts +138 -0
  309. package/src/providers/google-types.ts +167 -0
  310. package/src/providers/google-vertex.d.ts +11 -0
  311. package/src/providers/google-vertex.ts +124 -0
  312. package/src/providers/google.d.ts +4 -0
  313. package/src/providers/google.ts +41 -0
  314. package/src/providers/grammar.d.ts +1 -0
  315. package/src/providers/grammar.ts +70 -0
  316. package/src/providers/kimi.d.ts +27 -0
  317. package/src/providers/kimi.ts +52 -0
  318. package/src/providers/kiro-api-key.d.ts +50 -0
  319. package/src/providers/kiro-api-key.ts +786 -0
  320. package/src/providers/kiro-codewhisperer.d.ts +11 -0
  321. package/src/providers/kiro-codewhisperer.ts +600 -0
  322. package/src/providers/mock.ts +526 -0
  323. package/src/providers/ollama.d.ts +41 -0
  324. package/src/providers/ollama.ts +645 -0
  325. package/src/providers/openai-anthropic-shim.d.ts +31 -0
  326. package/src/providers/openai-anthropic-shim.ts +156 -0
  327. package/src/providers/openai-bounded-rate-limits.d.ts +3 -0
  328. package/src/providers/openai-bounded-rate-limits.ts +57 -0
  329. package/src/providers/openai-chat-server-schema.ts +254 -0
  330. package/src/providers/openai-chat-server.ts +724 -0
  331. package/src/providers/openai-codex/constants.d.ts +26 -0
  332. package/src/providers/openai-codex/constants.ts +43 -0
  333. package/src/providers/openai-codex/request-transformer.d.ts +50 -0
  334. package/src/providers/openai-codex/request-transformer.ts +219 -0
  335. package/src/providers/openai-codex/response-handler.d.ts +18 -0
  336. package/src/providers/openai-codex/response-handler.ts +111 -0
  337. package/src/providers/openai-codex-responses.d.ts +71 -0
  338. package/src/providers/openai-codex-responses.ts +3288 -0
  339. package/src/providers/openai-completions-compat.d.ts +6 -0
  340. package/src/providers/openai-completions-compat.ts +6 -0
  341. package/src/providers/openai-completions.d.ts +35 -0
  342. package/src/providers/openai-completions.ts +2294 -0
  343. package/src/providers/openai-opencodex-responses.ts +174 -0
  344. package/src/providers/openai-request-transform.d.ts +4 -0
  345. package/src/providers/openai-request-transform.ts +136 -0
  346. package/src/providers/openai-responses-server-schema.ts +290 -0
  347. package/src/providers/openai-responses-server.ts +1268 -0
  348. package/src/providers/openai-responses-shared.d.ts +106 -0
  349. package/src/providers/openai-responses-shared.ts +1253 -0
  350. package/src/providers/openai-responses.d.ts +37 -0
  351. package/src/providers/openai-responses.ts +989 -0
  352. package/src/providers/pi-native-client.d.ts +29 -0
  353. package/src/providers/pi-native-client.ts +243 -0
  354. package/src/providers/pi-native-server.ts +488 -0
  355. package/src/providers/register-builtins.d.ts +59 -0
  356. package/src/providers/register-builtins.ts +544 -0
  357. package/src/providers/synthetic.d.ts +26 -0
  358. package/src/providers/synthetic.ts +50 -0
  359. package/src/providers/transform-messages.d.ts +33 -0
  360. package/src/providers/transform-messages.ts +408 -0
  361. package/src/providers/vision-guard.d.ts +8 -0
  362. package/src/providers/vision-guard.ts +31 -0
  363. package/src/rate-limit-utils.d.ts +19 -0
  364. package/src/rate-limit-utils.ts +102 -0
  365. package/src/stream.d.ts +45 -0
  366. package/src/stream.ts +1306 -0
  367. package/src/types.d.ts +1073 -0
  368. package/src/types.ts +1305 -0
  369. package/src/usage/claude.ts +449 -0
  370. package/src/usage/gemini.ts +250 -0
  371. package/src/usage/github-copilot.ts +421 -0
  372. package/src/usage/google-antigravity.ts +201 -0
  373. package/src/usage/grok-cli.ts +259 -0
  374. package/src/usage/kimi.ts +285 -0
  375. package/src/usage/minimax-code.ts +31 -0
  376. package/src/usage/openai-codex.ts +503 -0
  377. package/src/usage/shared.ts +10 -0
  378. package/src/usage/zai.ts +247 -0
  379. package/src/usage.ts +190 -0
  380. package/src/utils/abort.d.ts +19 -0
  381. package/src/utils/abort.ts +51 -0
  382. package/src/utils/anthropic-auth.ts +95 -0
  383. package/src/utils/block-symbols.d.ts +6 -0
  384. package/src/utils/block-symbols.ts +11 -0
  385. package/src/utils/discovery/antigravity.ts +275 -0
  386. package/src/utils/discovery/codex.ts +362 -0
  387. package/src/utils/discovery/cursor.ts +388 -0
  388. package/src/utils/discovery/gemini.ts +248 -0
  389. package/src/utils/discovery/index.ts +4 -0
  390. package/src/utils/discovery/openai-compatible.ts +379 -0
  391. package/src/utils/event-stream.d.ts +41 -0
  392. package/src/utils/event-stream.ts +269 -0
  393. package/src/utils/fallback-transport.d.ts +110 -0
  394. package/src/utils/fallback-transport.ts +411 -0
  395. package/src/utils/fireworks-model-id.d.ts +10 -0
  396. package/src/utils/fireworks-model-id.ts +30 -0
  397. package/src/utils/foundry.d.ts +11 -0
  398. package/src/utils/foundry.ts +18 -0
  399. package/src/utils/h2-fetch.ts +60 -0
  400. package/src/utils/http-inspector.d.ts +59 -0
  401. package/src/utils/http-inspector.ts +380 -0
  402. package/src/utils/idle-iterator.d.ts +122 -0
  403. package/src/utils/idle-iterator.ts +410 -0
  404. package/src/utils/json-parse.d.ts +98 -0
  405. package/src/utils/json-parse.ts +607 -0
  406. package/src/utils/oauth/alibaba-token-plan.ts +60 -0
  407. package/src/utils/oauth/anthropic.ts +233 -0
  408. package/src/utils/oauth/api-key-login.ts +98 -0
  409. package/src/utils/oauth/api-key-validation.ts +344 -0
  410. package/src/utils/oauth/bizrouter.ts +15 -0
  411. package/src/utils/oauth/callback-server.d.ts +80 -0
  412. package/src/utils/oauth/callback-server.ts +359 -0
  413. package/src/utils/oauth/cerebras.ts +16 -0
  414. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  415. package/src/utils/oauth/commandcode.ts +17 -0
  416. package/src/utils/oauth/cursor.ts +157 -0
  417. package/src/utils/oauth/deepinfra.ts +15 -0
  418. package/src/utils/oauth/deepseek.ts +53 -0
  419. package/src/utils/oauth/firepass.ts +24 -0
  420. package/src/utils/oauth/fireworks.ts +15 -0
  421. package/src/utils/oauth/fugu.ts +15 -0
  422. package/src/utils/oauth/github-copilot.d.ts +38 -0
  423. package/src/utils/oauth/github-copilot.ts +362 -0
  424. package/src/utils/oauth/gitlab-duo.ts +123 -0
  425. package/src/utils/oauth/glm-zcode.d.ts +71 -0
  426. package/src/utils/oauth/glm-zcode.ts +433 -0
  427. package/src/utils/oauth/google-antigravity.ts +200 -0
  428. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  429. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  430. package/src/utils/oauth/huggingface.ts +62 -0
  431. package/src/utils/oauth/index.ts +558 -0
  432. package/src/utils/oauth/kagi.ts +47 -0
  433. package/src/utils/oauth/kilo.ts +87 -0
  434. package/src/utils/oauth/kimi.d.ts +17 -0
  435. package/src/utils/oauth/kimi.ts +275 -0
  436. package/src/utils/oauth/kiro.ts +448 -0
  437. package/src/utils/oauth/litellm.ts +47 -0
  438. package/src/utils/oauth/lm-studio.ts +38 -0
  439. package/src/utils/oauth/mara.ts +16 -0
  440. package/src/utils/oauth/minimax-code.ts +78 -0
  441. package/src/utils/oauth/moonshot.ts +16 -0
  442. package/src/utils/oauth/nanogpt.ts +15 -0
  443. package/src/utils/oauth/nvidia.ts +70 -0
  444. package/src/utils/oauth/oauth.html +199 -0
  445. package/src/utils/oauth/ollama-cloud.ts +28 -0
  446. package/src/utils/oauth/ollama.ts +47 -0
  447. package/src/utils/oauth/openai-codex.ts +299 -0
  448. package/src/utils/oauth/opencode.ts +49 -0
  449. package/src/utils/oauth/opengateway.ts +15 -0
  450. package/src/utils/oauth/openrouter.ts +16 -0
  451. package/src/utils/oauth/parallel.ts +46 -0
  452. package/src/utils/oauth/perplexity.ts +225 -0
  453. package/src/utils/oauth/pkce.ts +18 -0
  454. package/src/utils/oauth/qianfan.ts +58 -0
  455. package/src/utils/oauth/qwen-portal.ts +60 -0
  456. package/src/utils/oauth/sglang.ts +42 -0
  457. package/src/utils/oauth/synthetic.ts +15 -0
  458. package/src/utils/oauth/tavily.ts +46 -0
  459. package/src/utils/oauth/together.ts +16 -0
  460. package/src/utils/oauth/types.d.ts +56 -0
  461. package/src/utils/oauth/types.ts +122 -0
  462. package/src/utils/oauth/venice.ts +59 -0
  463. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  464. package/src/utils/oauth/vllm.ts +42 -0
  465. package/src/utils/oauth/xai.ts +246 -0
  466. package/src/utils/oauth/xiaomi.ts +199 -0
  467. package/src/utils/oauth/zai.ts +60 -0
  468. package/src/utils/oauth/zenmux.ts +15 -0
  469. package/src/utils/overflow.ts +275 -0
  470. package/src/utils/parse-bind.ts +81 -0
  471. package/src/utils/provider-response.d.ts +6 -0
  472. package/src/utils/provider-response.ts +30 -0
  473. package/src/utils/provider-safety-stop.ts +8 -0
  474. package/src/utils/proxy.d.ts +7 -0
  475. package/src/utils/proxy.ts +652 -0
  476. package/src/utils/retry-after.d.ts +3 -0
  477. package/src/utils/retry-after.ts +110 -0
  478. package/src/utils/retry-budget.d.ts +1 -0
  479. package/src/utils/retry-budget.ts +4 -0
  480. package/src/utils/retry.d.ts +29 -0
  481. package/src/utils/retry.ts +67 -0
  482. package/src/utils/schema/CONSTRAINTS.md +164 -0
  483. package/src/utils/schema/adapt.d.ts +24 -0
  484. package/src/utils/schema/adapt.ts +36 -0
  485. package/src/utils/schema/compatibility.d.ts +30 -0
  486. package/src/utils/schema/compatibility.ts +435 -0
  487. package/src/utils/schema/dereference.d.ts +11 -0
  488. package/src/utils/schema/dereference.ts +98 -0
  489. package/src/utils/schema/draft.d.ts +10 -0
  490. package/src/utils/schema/draft.ts +341 -0
  491. package/src/utils/schema/equality.d.ts +4 -0
  492. package/src/utils/schema/equality.ts +97 -0
  493. package/src/utils/schema/fields.d.ts +49 -0
  494. package/src/utils/schema/fields.ts +190 -0
  495. package/src/utils/schema/index.d.ts +14 -0
  496. package/src/utils/schema/index.ts +14 -0
  497. package/src/utils/schema/json-schema-validator.d.ts +12 -0
  498. package/src/utils/schema/json-schema-validator.ts +577 -0
  499. package/src/utils/schema/meta-validator.d.ts +2 -0
  500. package/src/utils/schema/meta-validator.ts +167 -0
  501. package/src/utils/schema/normalize.d.ts +93 -0
  502. package/src/utils/schema/normalize.ts +1588 -0
  503. package/src/utils/schema/root-combinator.d.ts +12 -0
  504. package/src/utils/schema/root-combinator.ts +143 -0
  505. package/src/utils/schema/spill.d.ts +8 -0
  506. package/src/utils/schema/spill.ts +43 -0
  507. package/src/utils/schema/stamps.d.ts +25 -0
  508. package/src/utils/schema/stamps.ts +97 -0
  509. package/src/utils/schema/types.d.ts +4 -0
  510. package/src/utils/schema/types.ts +11 -0
  511. package/src/utils/schema/wire.d.ts +54 -0
  512. package/src/utils/schema/wire.ts +213 -0
  513. package/src/utils/schema/zod-decontaminate.d.ts +31 -0
  514. package/src/utils/schema/zod-decontaminate.ts +331 -0
  515. package/src/utils/sse-debug.d.ts +10 -0
  516. package/src/utils/sse-debug.ts +289 -0
  517. package/src/utils/tool-call-healing.d.ts +80 -0
  518. package/src/utils/tool-call-healing.ts +298 -0
  519. package/src/utils/tool-choice-capability.d.ts +57 -0
  520. package/src/utils/tool-choice-capability.ts +633 -0
  521. package/src/utils/tool-choice.d.ts +50 -0
  522. package/src/utils/tool-choice.ts +99 -0
  523. package/src/utils/validation.ts +1080 -0
  524. package/src/utils.d.ts +117 -0
  525. package/src/utils.ts +523 -0
@@ -0,0 +1,4421 @@
1
+ import * as nodeCrypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import { scheduler } from "node:timers/promises";
5
+ import * as tls from "node:tls";
6
+ import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
7
+ import type {
8
+ ContentBlockParam,
9
+ MessageCreateParamsStreaming,
10
+ MessageParam,
11
+ RawMessageStreamEvent,
12
+ } from "@anthropic-ai/sdk/resources/messages";
13
+ import {
14
+ $credentialEnv,
15
+ $env,
16
+ extractHttpStatusFromError,
17
+ isEnoent,
18
+ isRetryableError,
19
+ isUnexpectedSocketCloseMessage,
20
+ logger,
21
+ readSseEvents,
22
+ } from "@vib-rato/utils";
23
+ import {
24
+ isProviderSafetyStopAdapterInvocation,
25
+ mintProviderSafetyStop,
26
+ PROVIDER_SAFETY_STOP_ADAPTER_CAPABILITY,
27
+ } from "../adapter-internals/provider-safety-stop";
28
+ import {
29
+ hasOpus47ApiRestrictions,
30
+ mapEffortToAnthropicAdaptiveEffort,
31
+ supportsAnthropicAdaptiveThinkingDisplay as supportsAdaptiveThinkingDisplay,
32
+ } from "../model-thinking";
33
+ import { calculateCost } from "../models";
34
+ import { isUsageLimitError } from "../rate-limit-utils";
35
+ import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
36
+ import type {
37
+ Api,
38
+ AssistantMessage,
39
+ CacheRetention,
40
+ Context,
41
+ FetchImpl,
42
+ ImageContent,
43
+ Message,
44
+ Model,
45
+ ProviderSessionState,
46
+ RedactedThinkingContent,
47
+ ServiceTier,
48
+ SimpleStreamOptions,
49
+ StopReason,
50
+ StreamFunction,
51
+ StreamOptions,
52
+ TextContent,
53
+ ThinkingContent,
54
+ Tool,
55
+ ToolCall,
56
+ ToolResultMessage,
57
+ Usage,
58
+ } from "../types";
59
+ import { resolveServiceTier } from "../types";
60
+ import {
61
+ isAnthropicOAuthToken,
62
+ isRecord,
63
+ normalizeSystemPrompts,
64
+ normalizeToolCallId,
65
+ resolveCacheRetention,
66
+ sanitizeJsonStrings,
67
+ } from "../utils";
68
+ import { createAbortSourceTracker } from "../utils/abort";
69
+ import { AssistantMessageEventStream } from "../utils/event-stream";
70
+ import { transportFailureFacts } from "../utils/fallback-transport";
71
+ import { isFoundryEnabled } from "../utils/foundry";
72
+ import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
73
+ import {
74
+ FirstEventTimeoutError,
75
+ getProviderFirstEventTimeoutFallbackMs,
76
+ getProviderStreamIdleTimeoutFallbackMs,
77
+ getStreamFirstEventTimeoutMs,
78
+ getStreamIdleTimeoutMs,
79
+ iterateWithIdleTimeout,
80
+ resolveAnthropicSdkRequestTimeoutMs,
81
+ } from "../utils/idle-iterator";
82
+ import {
83
+ captureUnicodeEscapeEvidence,
84
+ isCompleteJson,
85
+ parseJsonWithRepair,
86
+ parseStreamingJson,
87
+ } from "../utils/json-parse";
88
+ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
89
+ import { GLM_ZCODE_ANTHROPIC_BASE_URL } from "../utils/oauth/glm-zcode";
90
+ import { notifyProviderResponse } from "../utils/provider-response";
91
+ import { isCopilotTransientModelError } from "../utils/retry";
92
+ import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
93
+ import { resolveRetryBudget } from "../utils/retry-budget";
94
+ import {
95
+ COMBINATOR_KEYS,
96
+ flattenToolRootCombinators,
97
+ isJsonSchemaObjectNode,
98
+ NO_STRICT,
99
+ toolWireSchema,
100
+ } from "../utils/schema";
101
+ import { spillToDescription } from "../utils/schema/spill";
102
+ import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
103
+ import {
104
+ isForcedToolChoiceUnsupportedError,
105
+ markToolChoiceIncapability,
106
+ type ResolveToolChoiceResult,
107
+ resolveToolChoice,
108
+ } from "../utils/tool-choice-capability";
109
+ import {
110
+ buildCopilotDynamicHeaders,
111
+ hasCopilotVisionInput,
112
+ resolveGitHubCopilotBaseUrl,
113
+ } from "./github-copilot-headers";
114
+ import { hasAdjacentPrivateThinkingBlocks, transformMessages } from "./transform-messages";
115
+ import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
116
+
117
+ export type AnthropicHeaderOptions = {
118
+ apiKey: string;
119
+ baseUrl?: string;
120
+ isOAuth?: boolean;
121
+ extraBetas?: string[];
122
+ stream?: boolean;
123
+ modelHeaders?: Record<string, string>;
124
+ isCloudflareAiGateway?: boolean;
125
+ /**
126
+ * Attach ZCode client "source" headers (User-Agent: ZCode/<ver>, X-Title,
127
+ * X-ZCode-Agent: glm, X-Platform, etc.) so api.z.ai recognizes the caller as
128
+ * the ZCode client, exactly like ZCode's `buildZCodeSourceHeaders` does for
129
+ * GLM providers. glm-zcode only.
130
+ */
131
+ zcodeSourceHeaders?: boolean;
132
+ };
133
+
134
+ export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
135
+ const trimmed = baseUrl?.trim();
136
+ if (!trimmed) {
137
+ return undefined;
138
+ }
139
+ const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
140
+ return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
141
+ }
142
+
143
+ // Build deduplicated beta header string
144
+ export function buildBetaHeader(baseBetas: string[], extraBetas: string[]): string {
145
+ const seen = new Set<string>();
146
+ const result: string[] = [];
147
+ for (const beta of [...baseBetas, ...extraBetas]) {
148
+ const trimmed = beta.trim();
149
+ if (trimmed && !seen.has(trimmed)) {
150
+ seen.add(trimmed);
151
+ result.push(trimmed);
152
+ }
153
+ }
154
+ return result.join(",");
155
+ }
156
+
157
+ const claudeCodeBetaDefaults = [
158
+ "claude-code-20250219",
159
+ "oauth-2025-04-20",
160
+ "context-management-2025-06-27",
161
+ "prompt-caching-scope-2026-01-05",
162
+ ];
163
+ const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
164
+ const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
165
+ const fastModeBeta = "fast-mode-2026-02-01";
166
+
167
+ function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined {
168
+ if (!headers) return undefined;
169
+ const normalizedName = headerName.toLowerCase();
170
+ for (const [key, value] of Object.entries(headers)) {
171
+ if (key.toLowerCase() === normalizedName) return value;
172
+ }
173
+ return undefined;
174
+ }
175
+
176
+ function isClaudeCodeClientUserAgent(userAgent: string | undefined): userAgent is string {
177
+ if (!userAgent) return false;
178
+ return userAgent.toLowerCase().startsWith("claude-cli");
179
+ }
180
+
181
+ function isAnthropicApiBaseUrl(baseUrl?: string): boolean {
182
+ if (!baseUrl) return true;
183
+ try {
184
+ const url = new URL(baseUrl);
185
+ return url.protocol.toLowerCase() === "https:" && url.hostname.toLowerCase() === "api.anthropic.com";
186
+ } catch {
187
+ return false;
188
+ }
189
+ }
190
+
191
+ const sharedHeaders = {
192
+ "Accept-Encoding": "gzip, deflate, br, zstd",
193
+ Connection: "keep-alive",
194
+ "Content-Type": "application/json",
195
+ "Anthropic-Version": "2023-06-01",
196
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
197
+ "X-App": "cli",
198
+ };
199
+
200
+ // ZCode bakes its app version and runtime env at build time. Mirror the values
201
+ // from the analyzed ZCode 3.1.2 desktop bundle (`resolveRuntimeZCodeEnv` returns
202
+ // "production" for non-test builds). Both are overridable for forward-compat.
203
+ const ZCODE_APP_VERSION = process.env.ZCODE_APP_VERSION?.trim() || "3.1.2";
204
+ const ZCODE_RELEASE_CHANNEL = process.env.ZCODE_RELEASE_CHANNEL?.trim() || "production";
205
+
206
+ // Mirrors ZCode's `normalizePrintableHeaderValue`: only printable ASCII passes.
207
+ function normalizePrintableHeaderValue(value: string | undefined): string | undefined {
208
+ const trimmed = value?.trim();
209
+ if (trimmed && /^[\x20-\x7e]+$/.test(trimmed)) return trimmed;
210
+ return undefined;
211
+ }
212
+
213
+ // Mirrors ZCode's `normalizeOsCategory`.
214
+ function normalizeOsCategory(platform: NodeJS.Platform): string {
215
+ switch (platform) {
216
+ case "darwin":
217
+ return "macos";
218
+ case "win32":
219
+ return "windows";
220
+ default:
221
+ return "linux";
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Replicates ZCode's `buildZCodeSourceHeaders()` + GLM `X-ZCode-Agent` tag
227
+ * (host bundle `Bl` / `buildConnectivitySourceHeaders` for GLM providers), so
228
+ * api.z.ai sees vib's glm-zcode requests as the ZCode client. Dynamic values
229
+ * (platform/arch, locale, timezone, OS version) are resolved at runtime exactly
230
+ * as ZCode does; printable-ASCII-only and conditionally omitted when empty.
231
+ */
232
+ export function buildZCodeSourceHeaders(): Record<string, string> {
233
+ const platform = process.platform;
234
+ const arch = process.arch;
235
+ const appVersion = normalizePrintableHeaderValue(ZCODE_APP_VERSION);
236
+ const releaseChannel = normalizePrintableHeaderValue(ZCODE_RELEASE_CHANNEL);
237
+ let locale: string | undefined;
238
+ let timezone: string | undefined;
239
+ try {
240
+ const resolved = Intl.DateTimeFormat().resolvedOptions();
241
+ locale = normalizePrintableHeaderValue(resolved.locale);
242
+ timezone = normalizePrintableHeaderValue(resolved.timeZone);
243
+ } catch {}
244
+ const osVersion = normalizePrintableHeaderValue(os.version());
245
+ const headers: Record<string, string> = {
246
+ "User-Agent": `ZCode/${appVersion ?? "unknown"}`,
247
+ "HTTP-Referer": "https://zcode.z.ai",
248
+ "X-Title": "Z Code@electron",
249
+ "X-Platform": `${platform}-${arch}`,
250
+ "X-Client-Language": locale ?? "unknown",
251
+ "X-Client-Timezone": timezone ?? "unknown",
252
+ "X-Os-Category": normalizeOsCategory(platform),
253
+ "X-ZCode-Agent": "glm",
254
+ };
255
+ if (appVersion) headers["X-ZCode-App-Version"] = appVersion;
256
+ if (releaseChannel) headers["X-Release-Channel"] = releaseChannel;
257
+ if (osVersion) headers["X-Os-Version"] = osVersion;
258
+ return headers;
259
+ }
260
+
261
+ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
262
+ const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
263
+ const extraBetas = options.extraBetas ?? [];
264
+ const stream = options.stream ?? false;
265
+ const betaHeader = buildBetaHeader(claudeCodeBetaDefaults, extraBetas);
266
+ const acceptHeader = stream ? "text/event-stream" : "application/json";
267
+ const modelHeaders = Object.fromEntries(
268
+ Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
269
+ );
270
+
271
+ if (options.isCloudflareAiGateway) {
272
+ return {
273
+ ...modelHeaders,
274
+ Accept: acceptHeader,
275
+ ...sharedHeaders,
276
+ "Anthropic-Beta": betaHeader,
277
+ "cf-aig-authorization": `Bearer ${options.apiKey}`,
278
+ };
279
+ }
280
+
281
+ if (oauthToken) {
282
+ const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
283
+ const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
284
+ ? incomingUserAgent
285
+ : `claude-cli/${claudeCodeVersion} (external, cli)`;
286
+ return {
287
+ ...modelHeaders,
288
+ ...claudeCodeHeaders,
289
+ Accept: acceptHeader,
290
+ Authorization: `Bearer ${options.apiKey}`,
291
+ ...sharedHeaders,
292
+ "Anthropic-Beta": betaHeader,
293
+ "User-Agent": userAgent,
294
+ };
295
+ } else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
296
+ const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
297
+ // ZCode merges its source headers LAST for GLM providers (`withZCodeSourceHeaders`
298
+ // → `{ ...base, ...extra, ...source }`), so they win over any incoming User-Agent.
299
+ const zcodeSourceHeaders = options.zcodeSourceHeaders ? buildZCodeSourceHeaders() : undefined;
300
+ return {
301
+ ...modelHeaders,
302
+ Accept: acceptHeader,
303
+ Authorization: `Bearer ${options.apiKey}`,
304
+ ...sharedHeaders,
305
+ "Anthropic-Beta": betaHeader,
306
+ ...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
307
+ ...(zcodeSourceHeaders ?? {}),
308
+ };
309
+ } else {
310
+ return {
311
+ ...modelHeaders,
312
+ Accept: acceptHeader,
313
+ ...sharedHeaders,
314
+ "Anthropic-Beta": betaHeader,
315
+ "X-Api-Key": options.apiKey,
316
+ };
317
+ }
318
+ }
319
+
320
+ type AnthropicCacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
321
+
322
+ type AnthropicSamplingParams = MessageCreateParamsStreaming & {
323
+ top_p?: number;
324
+ top_k?: number;
325
+ };
326
+
327
+ const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
328
+ let warnedStopSequencesTrim = false;
329
+
330
+ const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
331
+
332
+ /**
333
+ * Scope of a classified replayed-thinking repair currently applied to this
334
+ * session: `latest` drops native thinking from the newest assistant turn, `all`
335
+ * stops replaying native thinking entirely. Persisted across stream
336
+ * re-invocations so a repair that keeps being rejected is not re-attempted from
337
+ * scratch on every turn (issue #4011), and released again by the first stream
338
+ * that completes. Unclassifiable masked `api_error` repairs remain local to the
339
+ * current stream invocation because a transient masked failure must not degrade
340
+ * later turns.
341
+ */
342
+ type AnthropicThinkingReplayRepairScope = "none" | "latest" | "all";
343
+
344
+ /**
345
+ * Repairs are bounded independently of `PROVIDER_MAX_RETRIES` because they do
346
+ * not consume the provider retry budget: without their own ceiling an
347
+ * unacceptable request shape retries forever (issue #4011). The ceiling spans
348
+ * the session rather than a single stream, and only a completed stream re-arms
349
+ * it — an unacceptable shape never completes, so it can never buy more repairs.
350
+ */
351
+ const ANTHROPIC_MAX_THINKING_REPAIRS = 1;
352
+
353
+ type AnthropicPayloadFingerprint = {
354
+ sha256: string;
355
+ bytes: number;
356
+ };
357
+
358
+ type AnthropicThinkingRepairCandidate = {
359
+ scope: Exclude<AnthropicThinkingReplayRepairScope, "none">;
360
+ params: MessageCreateParamsStreaming;
361
+ fingerprint: AnthropicPayloadFingerprint;
362
+ };
363
+
364
+ type AnthropicProviderSessionState = ProviderSessionState & {
365
+ strictToolsDisabled: boolean;
366
+ fastModeDisabled: boolean;
367
+ generatedCacheBudget: GeneratedCacheBudget;
368
+ thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope;
369
+ thinkingReplayRepairAttempts: number;
370
+ thinkingReplayRejectedPayload?: AnthropicPayloadFingerprint;
371
+ /**
372
+ * Managed-mode escalation for the CPA alias-restore failure (issue #4338):
373
+ * corrective steering recorded against one exact turn, applied by the next
374
+ * managed attempt that rebuilds the same turn and released on success.
375
+ */
376
+ cpaToolAliasSteering?: AnthropicCpaToolAliasSteering;
377
+ };
378
+
379
+ type AnthropicCpaToolAliasSteering = {
380
+ /** Corrective steering text appended to the next build of the same turn. */
381
+ message: string;
382
+ /**
383
+ * Fingerprint of the last user message when the failure was recorded. The
384
+ * steering only applies to a rebuild of the same logical turn; a later turn
385
+ * with a different prompt expires it instead of replaying a stale
386
+ * correction.
387
+ */
388
+ turnFingerprint: string;
389
+ };
390
+
391
+ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
392
+ const state: AnthropicProviderSessionState = {
393
+ strictToolsDisabled: false,
394
+ fastModeDisabled: false,
395
+ generatedCacheBudget: 2,
396
+ thinkingReplayRepairScope: "none",
397
+ thinkingReplayRepairAttempts: 0,
398
+ close: () => {
399
+ state.strictToolsDisabled = false;
400
+ state.fastModeDisabled = false;
401
+ state.generatedCacheBudget = 2;
402
+ state.thinkingReplayRepairScope = "none";
403
+ state.thinkingReplayRepairAttempts = 0;
404
+ state.thinkingReplayRejectedPayload = undefined;
405
+ state.cpaToolAliasSteering = undefined;
406
+ },
407
+ };
408
+ return state;
409
+ }
410
+
411
+ function getAnthropicProviderSessionState(
412
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
413
+ ): AnthropicProviderSessionState | undefined {
414
+ if (!providerSessionState) return undefined;
415
+ const existing = providerSessionState.get(ANTHROPIC_PROVIDER_SESSION_STATE_KEY) as
416
+ | AnthropicProviderSessionState
417
+ | undefined;
418
+ if (existing) return existing;
419
+ const created = createAnthropicProviderSessionState();
420
+ providerSessionState.set(ANTHROPIC_PROVIDER_SESSION_STATE_KEY, created);
421
+ return created;
422
+ }
423
+
424
+ /**
425
+ * Clears the in-session "server rejected fast mode" sticky flag. Call when the
426
+ * caller is explicitly re-arming `serviceTier: "priority"` (e.g. user toggled
427
+ * `/fast on` after a previous turn auto-disabled it) so the next request
428
+ * actually carries `speed: "fast"` again. No-op when the map or state entry
429
+ * hasn't been materialized yet.
430
+ */
431
+ export function clearAnthropicFastModeFallback(
432
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
433
+ ): void {
434
+ if (!providerSessionState) return;
435
+ const state = providerSessionState.get(ANTHROPIC_PROVIDER_SESSION_STATE_KEY) as
436
+ | AnthropicProviderSessionState
437
+ | undefined;
438
+ if (state) state.fastModeDisabled = false;
439
+ }
440
+
441
+ function isAnthropicStrictGrammarTooLargeError(error: unknown): boolean {
442
+ if (extractHttpStatusFromError(error) !== 400) return false;
443
+ const message = error instanceof Error ? error.message : String(error);
444
+ const isStrictGrammarTooLarge = /compiled grammar/i.test(message) && /too large/i.test(message);
445
+ const isSchemaCompilationTooComplex =
446
+ /schema/i.test(message) && /too complex/i.test(message) && /compil/i.test(message);
447
+ return /invalid_request_error/i.test(message) && (isStrictGrammarTooLarge || isSchemaCompilationTooComplex);
448
+ }
449
+
450
+ export function isAnthropicFastModeUnsupportedError(error: unknown): boolean {
451
+ const status = extractHttpStatusFromError(error);
452
+ if (status !== 400 && status !== 429) return false;
453
+ const message = error instanceof Error ? error.message : String(error);
454
+ // 400 invalid_request_error — model doesn't accept `speed` at all.
455
+ // Observed: "'Anthropic model-opus-4-5-20251101' does not support the `speed` parameter."
456
+ // Stay tolerant of phrasing drift ("is not supported", quoted vs backticked field).
457
+ if (
458
+ status === 400 &&
459
+ /invalid_request_error/i.test(message) &&
460
+ /\bspeed\b/i.test(message) &&
461
+ /not support/i.test(message)
462
+ ) {
463
+ return true;
464
+ }
465
+ // 429 rate_limit_error — account lacks the extra-usage entitlement fast mode requires.
466
+ // Observed: "Extra usage is required for fast mode."
467
+ if (status === 429 && /rate_limit_error/i.test(message) && /fast mode/i.test(message)) {
468
+ return true;
469
+ }
470
+ return false;
471
+ }
472
+
473
+ /**
474
+ * Proxies (e.g. CLIProxyAPI) can deliver Anthropic's 400 body as an in-stream
475
+ * SSE `error` event on an HTTP 200 response; the thrown error then carries no
476
+ * HTTP status at all (issue #3900). Accept both the direct 400 and the
477
+ * statusless SSE shape — the strict `invalid_request_error` message checks in
478
+ * each matcher keep the statusless branch from claiming unrelated failures.
479
+ */
480
+ function isAnthropicInvalidRequestStatus(error: unknown): boolean {
481
+ const status = extractHttpStatusFromError(error);
482
+ return status === 400 || status === undefined;
483
+ }
484
+
485
+ export function isAnthropicThinkingBlockMutationError(error: unknown): boolean {
486
+ if (!isAnthropicInvalidRequestStatus(error)) return false;
487
+ const message = error instanceof Error ? error.message : String(error);
488
+ return (
489
+ /invalid_request_error/i.test(message) &&
490
+ /thinking|redacted_thinking/i.test(message) &&
491
+ /latest assistant message/i.test(message) &&
492
+ /cannot be modified/i.test(message)
493
+ );
494
+ }
495
+
496
+ /**
497
+ * 400 shape where a replayed `thinking`/`redacted_thinking` block fails signature
498
+ * validation, e.g. `messages.5.content.24: Invalid \`signature\` in \`thinking\` block`.
499
+ * Unlike the latest-assistant mutation error above, the cited block can sit anywhere
500
+ * in the replayed history, so recovery must repair every assistant message rather
501
+ * than only the latest one.
502
+ */
503
+ export function isAnthropicThinkingSignatureInvalidError(error: unknown): boolean {
504
+ if (!isAnthropicInvalidRequestStatus(error)) return false;
505
+ const message = error instanceof Error ? error.message : String(error);
506
+ return (
507
+ /invalid_request_error/i.test(message) &&
508
+ /thinking|redacted_thinking/i.test(message) &&
509
+ /invalid\s+`?signature`?/i.test(message)
510
+ );
511
+ }
512
+
513
+ /**
514
+ * CLIProxyAPI replaces Anthropic's rejection body wholesale instead of forwarding
515
+ * it: the client only ever sees
516
+ * `{"type":"error","error":{"type":"api_error","message":"An error occurred while
517
+ * processing the request."}}`, delivered as an in-stream SSE `error` event on an
518
+ * HTTP 200 response, so neither the status nor the message survives. Captured CPA
519
+ * traces for that masked shape carry the thinking-integrity 400 upstream (issue
520
+ * #3900), and the generic body matches no transient phrase either, so the turn
521
+ * dies unrecoverably. Nothing in the payload names the cause; callers must pair
522
+ * this with a request that actually replays signed thinking blocks before
523
+ * treating it as a thinking-replay rejection.
524
+ */
525
+ export function isAnthropicMaskedProxyRejection(error: unknown): boolean {
526
+ const status = extractHttpStatusFromError(error);
527
+ if (status !== undefined && status !== 400) return false;
528
+ const message = error instanceof Error ? error.message : String(error);
529
+ // A body that still names its error type is classified by the strict matchers.
530
+ if (/invalid_request_error/i.test(message)) return false;
531
+ return /"type"\s*:\s*"api_error"/.test(message) && /an error occurred while processing/i.test(message);
532
+ }
533
+
534
+ function fingerprintAnthropicPayload(params: MessageCreateParamsStreaming): AnthropicPayloadFingerprint {
535
+ const body = JSON.stringify({ ...params, stream: true });
536
+ return {
537
+ sha256: nodeCrypto.createHash("sha256").update(body).digest("hex"),
538
+ bytes: Buffer.byteLength(body),
539
+ };
540
+ }
541
+
542
+ function anthropicPayloadChanged(left: AnthropicPayloadFingerprint, right: AnthropicPayloadFingerprint): boolean {
543
+ return left.bytes !== right.bytes || left.sha256 !== right.sha256;
544
+ }
545
+
546
+ function extractAnthropicCitedContentPath(error: unknown): { messageIndex: number; contentIndex: number } | undefined {
547
+ const message = error instanceof Error ? error.message : String(error);
548
+ const match = /messages\.(\d+)\.content\.(\d+)/i.exec(message);
549
+ if (!match) return undefined;
550
+ return { messageIndex: Number(match[1]), contentIndex: Number(match[2]) };
551
+ }
552
+
553
+ function countNativeThinkingBlocks(content: unknown): number {
554
+ if (!Array.isArray(content)) return 0;
555
+ return content.filter(block => {
556
+ if (!isRecord(block)) return false;
557
+ return block.type === "thinking" || block.type === "redacted_thinking";
558
+ }).length;
559
+ }
560
+
561
+ function describeAnthropicOutgoingPath(error: unknown, params: MessageCreateParamsStreaming): string {
562
+ const cited = extractAnthropicCitedContentPath(error);
563
+ const messages = params.messages;
564
+ let latestAssistantIndex = -1;
565
+ for (let index = messages.length - 1; index >= 0; index--) {
566
+ if (messages[index]?.role === "assistant") {
567
+ latestAssistantIndex = index;
568
+ break;
569
+ }
570
+ }
571
+ const latest = latestAssistantIndex >= 0 ? messages[latestAssistantIndex] : undefined;
572
+ const latestThinking = countNativeThinkingBlocks(latest?.content);
573
+ const latestDescription =
574
+ latest === undefined
575
+ ? "Vibrato's outgoing request has no assistant message"
576
+ : `Vibrato's latest outgoing assistant message is messages[${latestAssistantIndex}] with ${latestThinking} native thinking block(s)`;
577
+ if (!cited) return `${latestDescription}; the rejection did not contain a messages.N.content.M path`;
578
+ const outgoing = messages[cited.messageIndex];
579
+ if (!outgoing) {
580
+ return `Anthropic cited messages.${cited.messageIndex}.content.${cited.contentIndex}, but Vibrato's outgoing request has only ${messages.length} messages; ${latestDescription}`;
581
+ }
582
+ const contentBlocks = Array.isArray(outgoing.content) ? outgoing.content.length : 1;
583
+ return `Anthropic cited messages.${cited.messageIndex}.content.${cited.contentIndex}, but Vibrato's outgoing messages[${cited.messageIndex}] has role=${outgoing.role} and ${contentBlocks} content block(s); ${latestDescription}`;
584
+ }
585
+
586
+ function createAnthropicThinkingRepairNoopError(
587
+ error: unknown,
588
+ params: MessageCreateParamsStreaming,
589
+ fingerprint: AnthropicPayloadFingerprint,
590
+ capturedDiagnostic?: string,
591
+ ): Error {
592
+ const diagnostic = describeAnthropicOutgoingPath(error, params);
593
+ const terminal = new Error(
594
+ `Anthropic thinking-replay repair was not sent because both latest-assistant and all-assistant transforms produced the same ${fingerprint.bytes}-byte payload (sha256=${fingerprint.sha256}). ${diagnostic}. Vibrato did not resend the rejected body and did not change thinking mode.${capturedDiagnostic ? `\n${capturedDiagnostic}` : ""}`,
595
+ );
596
+ const status = extractHttpStatusFromError(error);
597
+ if (status !== undefined) (terminal as Error & { status?: number }).status = status;
598
+ (terminal as Error & { anthropicHttp400AlreadyCaptured?: boolean }).anthropicHttp400AlreadyCaptured = true;
599
+ return terminal;
600
+ }
601
+
602
+ /**
603
+ * Anthropic rejects a request carrying more than four `cache_control`
604
+ * breakpoints. An Anthropic-compatible gateway may attach its own block-level
605
+ * markers before forwarding, and those never appear in the params we serialize,
606
+ * so no amount of local counting can predict the total. The rejection is the
607
+ * only evidence that our generated marker is one too many, and it is worth
608
+ * exactly one retry with generated caching suppressed.
609
+ *
610
+ * Our own pre-flight `validateCacheControls` failure is deliberately not
611
+ * matched: it carries no `invalid_request_error` wording, so a local bug stays
612
+ * loud instead of being silently retried.
613
+ */
614
+ export function isAnthropicCacheBreakpointOverflowError(error: unknown): boolean {
615
+ if (!isAnthropicInvalidRequestStatus(error)) return false;
616
+ const message = error instanceof Error ? error.message : String(error);
617
+ if (!/invalid_request_error/i.test(message)) return false;
618
+ if (!/cache_control/i.test(message)) return false;
619
+ // Observed: "A maximum of 4 blocks with cache_control may be provided. Found 5."
620
+ // Stay tolerant of phrasing drift around the limit and the reported total.
621
+ return /maximum of \d+ blocks/i.test(message) || /at most \d+ blocks/i.test(message);
622
+ }
623
+
624
+ export type AnthropicContextManagementInjectionDiagnostic = {
625
+ strategy: string;
626
+ message: string;
627
+ captureNote: string;
628
+ };
629
+
630
+ const CLEAR_THINKING_STRATEGY_PATTERN = /\b(clear_thinking_[a-z0-9_-]{1,64})\b/i;
631
+
632
+ function formatAnthropicDiagnosticBaseUrl(requestUrl: unknown): string {
633
+ if (typeof requestUrl !== "string") return "the configured Anthropic base URL";
634
+ try {
635
+ const url = new URL(requestUrl);
636
+ const basePath = url.pathname.replace(/\/v1\/messages\/?$/, "");
637
+ return `${url.origin}${basePath}`;
638
+ } catch {
639
+ return "the configured Anthropic base URL";
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Diagnose a context-management strategy named by an Anthropic 400 but absent
645
+ * from the body Vibrato sent. This mismatch is evidence of intermediary mutation,
646
+ * not permission to silently enable thinking or retry the request.
647
+ */
648
+ export function diagnoseAnthropicContextManagementInjection(
649
+ error: unknown,
650
+ dump: RawHttpRequestDump | undefined,
651
+ ): AnthropicContextManagementInjectionDiagnostic | undefined {
652
+ if (extractHttpStatusFromError(error) !== 400) return undefined;
653
+ if (dump?.api !== "anthropic-messages" || !isRecord(dump.body)) return undefined;
654
+ if (isAnthropicApiBaseUrl(dump.url)) return undefined;
655
+ if (Object.hasOwn(dump.body, "thinking") || Object.hasOwn(dump.body, "context_management")) return undefined;
656
+
657
+ const errorMessage = error instanceof Error ? error.message : String(error);
658
+ if (!/invalid_request_error/i.test(errorMessage)) return undefined;
659
+ const strategy = CLEAR_THINKING_STRATEGY_PATTERN.exec(errorMessage)?.[1];
660
+ if (!strategy) return undefined;
661
+ if (!/\bstrategy\b/i.test(errorMessage) || !/\bthinking\b/i.test(errorMessage)) return undefined;
662
+
663
+ const baseUrl = formatAnthropicDiagnosticBaseUrl(dump.url);
664
+ return {
665
+ strategy,
666
+ message: [
667
+ `Vibrato did not send \`thinking\` or \`context_management\`, but the Anthropic 400 names the \`${strategy}\` context-management strategy.`,
668
+ `An intermediary at ${baseUrl} likely injected that strategy into the outgoing request.`,
669
+ "Enable thinking explicitly for this model, or fix/replace the intermediary so it does not add clear-thinking edits to requests without thinking. Vibrato did not auto-enable thinking or retry because that would change request cost and semantics.",
670
+ ].join("\n"),
671
+ captureNote: `The HTTP 400 references context-management strategy ${strategy}, but this captured outgoing body contains neither thinking nor context_management; an intermediary may have added it after Vibrato sent the request.`,
672
+ };
673
+ }
674
+
675
+ async function finalizeAnthropicErrorMessage(error: unknown, dump: RawHttpRequestDump | undefined): Promise<string> {
676
+ if (
677
+ error instanceof Error &&
678
+ (error as Error & { anthropicHttp400AlreadyCaptured?: boolean }).anthropicHttp400AlreadyCaptured
679
+ ) {
680
+ return error.message;
681
+ }
682
+ const diagnostic = diagnoseAnthropicContextManagementInjection(error, dump);
683
+ if (diagnostic && dump) {
684
+ dump.diagnostics = {
685
+ ...(dump.diagnostics ?? {}),
686
+ anthropicContextManagement: {
687
+ strategy: diagnostic.strategy,
688
+ note: diagnostic.captureNote,
689
+ },
690
+ };
691
+ }
692
+ const message = await finalizeErrorMessage(error, dump);
693
+ return diagnostic ? `${message}\n\n${diagnostic.message}` : message;
694
+ }
695
+
696
+ /**
697
+ * CPA's Claude-OAuth layer cloaks downstream tool names into
698
+ * `mcp__<server>__<token>_<base>` aliases upstream. When the model emits a
699
+ * tool call whose alias embeds a token that appears nowhere in the request,
700
+ * CPA cannot restore the name and kills the whole stream with an HTTP 500 SSE
701
+ * `error` event instead of forwarding the call (issue #4338). The signature is
702
+ * precise and machine-parseable: it quotes the rejected alias and the failure
703
+ * mode verbatim. Native Anthropic never emits this phrasing, so the text itself
704
+ * is the route gate.
705
+ */
706
+ const CPA_TOOL_ALIAS_RESTORE_PATTERN =
707
+ /cannot restore Claude OAuth MCP tool alias \\?"([^"\\\\]+)\\?": no unique request-local match/i;
708
+
709
+ /**
710
+ * Aliases observed as `mcp__<server>__<token>_<base>` with a 12-character
711
+ * lowercase-alphanumeric token segment (`find` = `yw7zaf6emg3l` in both
712
+ * captured traces). Tolerate 8-16 chars so extraction survives token-length
713
+ * drift while staying out of the base name; a base that itself contains
714
+ * underscores (`todo_write`) is preserved by the trailing `.+`.
715
+ */
716
+ const CPA_TOOL_ALIAS_BASE_PATTERN = /^mcp__[^_]+__[a-z0-9]{8,16}_(.+)$/;
717
+
718
+ export interface CpaToolAliasRestoreFailure {
719
+ /** The rejected tool-call name exactly as CPA quoted it. */
720
+ alias: string;
721
+ /**
722
+ * Base tool name parsed out of the alias (`mcp__<server>__<token>_<base>`
723
+ * → `<base>`), when the alias shape is well-formed. `undefined` for a
724
+ * malformed alias — callers must then fall back to direct discovery and
725
+ * never invent a name.
726
+ */
727
+ baseName?: string;
728
+ }
729
+
730
+ /**
731
+ * Classifies the CPA alias-restore signature and extracts the rejected alias
732
+ * plus its base tool name. Claims only statusless in-stream SSE error events
733
+ * and HTTP 5xx failures: a non-5xx status carrying this text is not the
734
+ * observed CPA delivery shape and is left to the other classifiers.
735
+ */
736
+ export function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined {
737
+ const status = extractHttpStatusFromError(error);
738
+ if (status !== undefined && status < 500) return undefined;
739
+ const message = error instanceof Error ? error.message : String(error);
740
+ const match = CPA_TOOL_ALIAS_RESTORE_PATTERN.exec(message);
741
+ if (!match) return undefined;
742
+ const alias = match[1]!;
743
+ return { alias, baseName: CPA_TOOL_ALIAS_BASE_PATTERN.exec(alias)?.[1] };
744
+ }
745
+
746
+ export function isCpaToolAliasRestoreFailure(error: unknown): boolean {
747
+ return parseCpaToolAliasRestoreFailure(error) !== undefined;
748
+ }
749
+
750
+ function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
751
+ const tools = params.tools as Array<{ strict?: unknown }> | undefined;
752
+ return tools?.some(tool => tool.strict === true) ?? false;
753
+ }
754
+
755
+ /**
756
+ * `speed` lives on `BetaMessageCreateParams` (client.beta.messages) but this
757
+ * provider posts via `client.messages.create`, whose param type doesn't
758
+ * include it. This alias narrows the cast to one place.
759
+ */
760
+ type ParamsWithSpeed = MessageCreateParamsStreaming & { speed?: "fast" };
761
+
762
+ function dropAnthropicFastMode(params: MessageCreateParamsStreaming): void {
763
+ delete (params as ParamsWithSpeed).speed;
764
+ }
765
+
766
+ function dropAnthropicStrictTools(params: MessageCreateParamsStreaming): void {
767
+ const tools = params.tools as Array<{ strict?: unknown }> | undefined;
768
+ if (!tools) return;
769
+ for (const tool of tools) {
770
+ delete tool.strict;
771
+ }
772
+ }
773
+
774
+ function isClaudeFamilyModel(model: Model<"anthropic-messages">): boolean {
775
+ // Classify the same identifier the request body serializes (`params.model =
776
+ // model.id` in buildParams); a differing `wireModelId` is not dispatched by
777
+ // this transport, so it must not drive the cache decision either.
778
+ const id = model.id;
779
+ const shortId = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
780
+ return shortId.toLowerCase().startsWith("claude-");
781
+ }
782
+
783
+ /**
784
+ * How many breakpoints we are still willing to generate after a gateway has
785
+ * rejected a previous attempt. `explicit` mode normally emits two (a reusable
786
+ * prefix anchor on the last assistant turn plus a refresh point on the current
787
+ * user turn), so stepping down to one still caches the prefix, and only the
788
+ * final step gives caching up entirely.
789
+ */
790
+ type GeneratedCacheBudget = 2 | 1 | 0;
791
+
792
+ function getCacheControl(
793
+ model: Model<"anthropic-messages">,
794
+ baseUrl: string,
795
+ cacheRetention?: CacheRetention,
796
+ generatedCacheBudget: GeneratedCacheBudget = 2,
797
+ ): { mode: AnthropicCacheMode; cacheControl?: AnthropicCacheControl } {
798
+ if (generatedCacheBudget === 0) return { mode: "none" };
799
+ const retention = resolveCacheRetention(cacheRetention ?? model.cacheRetention, "long");
800
+ if (retention === "none") return { mode: "none" };
801
+
802
+ const isCanonicalApi = isAnthropicApiBaseUrl(baseUrl);
803
+ const promptCacheMode = model.compat?.promptCacheMode;
804
+ const mode: AnthropicCacheMode =
805
+ promptCacheMode === "none"
806
+ ? "none"
807
+ : promptCacheMode === "explicit"
808
+ ? "explicit"
809
+ : promptCacheMode === "automatic"
810
+ ? "automatic"
811
+ : isCanonicalApi
812
+ ? "automatic"
813
+ : isClaudeFamilyModel(model)
814
+ ? "explicit"
815
+ : "none";
816
+ if (mode === "none") return { mode };
817
+
818
+ const supportsLongCacheRetention = isCanonicalApi
819
+ ? getAnthropicCompat(model).supportsLongCacheRetention
820
+ : model.compat?.supportsLongCacheRetention === true;
821
+ return {
822
+ mode,
823
+ cacheControl: {
824
+ type: "ephemeral",
825
+ ...(retention === "long" && supportsLongCacheRetention ? { ttl: "1h" } : {}),
826
+ },
827
+ };
828
+ }
829
+
830
+ // Stealth mode: Mimic Anthropic Code headers and tool prefixing.
831
+ export const claudeCodeVersion = "2.1.219";
832
+ export const claudeCodeEntrypoint = "sdk-cli";
833
+ export const claudeToolPrefix: string = "proxy_";
834
+ export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
835
+
836
+ export function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
837
+ switch (platform.toLowerCase()) {
838
+ case "darwin":
839
+ return "MacOS";
840
+ case "windows":
841
+ case "win32":
842
+ return "Windows";
843
+ case "linux":
844
+ return "Linux";
845
+ case "freebsd":
846
+ return "FreeBSD";
847
+ default:
848
+ return `Other::${platform.toLowerCase()}`;
849
+ }
850
+ }
851
+
852
+ export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
853
+ switch (arch.toLowerCase()) {
854
+ case "amd64":
855
+ case "x64":
856
+ return "x64";
857
+ case "arm64":
858
+ case "aarch64":
859
+ return "arm64";
860
+ case "386":
861
+ case "x86":
862
+ case "ia32":
863
+ return "x86";
864
+ default:
865
+ return `other::${arch.toLowerCase()}`;
866
+ }
867
+ }
868
+
869
+ export const claudeCodeHeaders = {
870
+ "X-Stainless-Retry-Count": "0",
871
+ "X-Stainless-Runtime-Version": "v24.3.0",
872
+ "X-Stainless-Package-Version": "0.74.0",
873
+ "X-Stainless-Runtime": "node",
874
+ "X-Stainless-Lang": "js",
875
+ "X-Stainless-Arch": mapStainlessArch(process.arch),
876
+ "X-Stainless-Os": mapStainlessOs(process.platform),
877
+ "X-Stainless-Timeout": "600",
878
+ } as const;
879
+
880
+ const enforcedHeaderKeys = new Set(
881
+ [
882
+ ...Object.keys(claudeCodeHeaders),
883
+ "Accept",
884
+ "Accept-Encoding",
885
+ "Connection",
886
+ "Content-Type",
887
+ "Anthropic-Version",
888
+ "Anthropic-Dangerous-Direct-Browser-Access",
889
+ "Anthropic-Beta",
890
+ "User-Agent",
891
+ "X-App",
892
+ "Authorization",
893
+ "X-Api-Key",
894
+ "cf-aig-authorization",
895
+ ].map(key => key.toLowerCase()),
896
+ );
897
+
898
+ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
899
+
900
+ function createClaudeBillingHeader(payload: unknown): string {
901
+ const payloadJson = JSON.stringify(payload) ?? "";
902
+ const cch = nodeCrypto.createHash("sha256").update(payloadJson).digest("hex").slice(0, 5);
903
+ const randomBytes = new Uint8Array(2);
904
+ crypto.getRandomValues(randomBytes);
905
+ const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
906
+ .join("")
907
+ .slice(0, 3);
908
+ return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint=${claudeCodeEntrypoint}; cch=${cch};`;
909
+ }
910
+
911
+ const CLAUDE_CLOAKING_USER_ID_REGEX =
912
+ /^user_[0-9a-fA-F]{64}_account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
913
+
914
+ export function isClaudeCloakingUserId(userId: string): boolean {
915
+ return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
916
+ }
917
+
918
+ /**
919
+ * Real Anthropic Code sends `metadata.user_id` as a JSON-stringified object of the
920
+ * shape `{ device_id, account_uuid, session_id, ...extra }` (see
921
+ * services/api/Anthropic model.ts → getAPIMetadata). Accept that shape so callers that
922
+ * supply a stable `session_id` aren't silently overwritten with fresh entropy
923
+ * on every request, which would inflate the backend session count.
924
+ */
925
+ function isClaudeJsonUserId(userId: string): boolean {
926
+ if (userId.length === 0 || userId[0] !== "{") return false;
927
+ let parsed: unknown;
928
+ try {
929
+ parsed = JSON.parse(userId);
930
+ } catch {
931
+ return false;
932
+ }
933
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
934
+ const obj = parsed as Record<string, unknown>;
935
+ return typeof obj.session_id === "string" && obj.session_id.length > 0;
936
+ }
937
+
938
+ export function generateClaudeCloakingUserId(): string {
939
+ const userHash = nodeCrypto.randomBytes(32).toString("hex");
940
+ const accountId = nodeCrypto.randomUUID().toLowerCase();
941
+ const sessionId = nodeCrypto.randomUUID().toLowerCase();
942
+ return `user_${userHash}_account_${accountId}_session_${sessionId}`;
943
+ }
944
+
945
+ function resolveAnthropicMetadataUserId(userId: unknown, isOAuthToken: boolean): string | undefined {
946
+ if (typeof userId === "string") {
947
+ if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
948
+ return userId;
949
+ }
950
+ }
951
+
952
+ if (!isOAuthToken) return undefined;
953
+ return generateClaudeCloakingUserId();
954
+ }
955
+ const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
956
+ export const applyClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
957
+ if (!prefixOverride) return name;
958
+ if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
959
+ return `${prefixOverride}${name}`;
960
+ };
961
+
962
+ export const stripClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
963
+ if (!prefixOverride) return name;
964
+ if (!name.startsWith(prefixOverride)) return name;
965
+ return name.slice(prefixOverride.length);
966
+ };
967
+
968
+ // Anthropic requires image `data` to be standard (RFC 4648) base64: the standard
969
+ // alphabet only, correct quartet grouping, and padding (when present) confined to
970
+ // a trailing `=`/`==`. A resident image whose blob went missing bakes a
971
+ // human-readable placeholder into `data` (e.g. "[Session resident imageData blob
972
+ // missing: …]"), and other callers can pass whitespace, data URLs, or URL-safe
973
+ // variants — all of which the API rejects with a 400 `invalid base64 data` that
974
+ // fails the *entire* request and bricks the session. Validate the wire format
975
+ // strictly and degrade anything that is not standard base64 to text.
976
+ //
977
+ // Accepts canonical padded forms and their unpadded equivalents; rejects
978
+ // length % 4 === 1, misplaced/overlong padding, whitespace, data URLs, URL-safe
979
+ // (`-`/`_`) alphabets, prose, and empty input. The pattern has no nested
980
+ // quantifier, so even oversized inputs are rejected in linear time.
981
+ const ANTHROPIC_BASE64_IMAGE_DATA = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
982
+ function isAnthropicBase64ImageData(data: string): boolean {
983
+ return data.length > 0 && data.length % 4 !== 1 && ANTHROPIC_BASE64_IMAGE_DATA.test(data);
984
+ }
985
+
986
+ /**
987
+ * Convert content blocks to Anthropic API format
988
+ */
989
+ function convertContentBlocks(
990
+ content: (TextContent | ImageContent)[],
991
+ supportsImages = true,
992
+ ):
993
+ | string
994
+ | Array<
995
+ | { type: "text"; text: string }
996
+ | {
997
+ type: "image";
998
+ source: {
999
+ type: "base64";
1000
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
1001
+ data: string;
1002
+ };
1003
+ }
1004
+ > {
1005
+ const textBlocks = content
1006
+ .filter((block): block is TextContent => block.type === "text")
1007
+ .map(block => block.text.toWellFormed())
1008
+ .filter(text => text.trim().length > 0);
1009
+ const imageBlocks: ImageContent[] = [];
1010
+ for (const block of content) {
1011
+ if (block.type !== "image") continue;
1012
+ if (isAnthropicBase64ImageData(block.data)) {
1013
+ imageBlocks.push(block);
1014
+ continue;
1015
+ }
1016
+ // Non-base64 image payload (e.g. a missing-blob placeholder): degrade to
1017
+ // text so one lost image cannot invalidate the entire request.
1018
+ const text = block.data.toWellFormed().trim();
1019
+ if (text.length > 0) textBlocks.push(text);
1020
+ }
1021
+ const omittedImages = !supportsImages && imageBlocks.length > 0;
1022
+ if (imageBlocks.length === 0 || !supportsImages) {
1023
+ if (omittedImages) {
1024
+ textBlocks.push(NON_VISION_IMAGE_PLACEHOLDER);
1025
+ }
1026
+ return textBlocks.join("\n").toWellFormed();
1027
+ }
1028
+
1029
+ const blocks = [
1030
+ ...textBlocks.map(text => ({
1031
+ type: "text" as const,
1032
+ text,
1033
+ })),
1034
+ ...imageBlocks.map(block => ({
1035
+ type: "image" as const,
1036
+ source: {
1037
+ type: "base64" as const,
1038
+ media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
1039
+ data: block.data,
1040
+ },
1041
+ })),
1042
+ ];
1043
+
1044
+ if (!textBlocks.length) {
1045
+ blocks.unshift({
1046
+ type: "text" as const,
1047
+ text: "(see attached image)",
1048
+ });
1049
+ }
1050
+
1051
+ return blocks;
1052
+ }
1053
+
1054
+ export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
1055
+ export type AnthropicThinkingDisplay = "summarized" | "omitted";
1056
+
1057
+ export interface AnthropicOptions extends StreamOptions {
1058
+ /**
1059
+ * Enable extended thinking.
1060
+ * For Opus 4.6+: uses adaptive thinking (Anthropic model decides when/how much to think).
1061
+ * For older models: uses budget-based thinking with thinkingBudgetTokens.
1062
+ */
1063
+ thinkingEnabled?: boolean;
1064
+ /**
1065
+ * Token budget for extended thinking (older models only).
1066
+ * Ignored for Opus 4.6+ which uses adaptive thinking.
1067
+ */
1068
+ thinkingBudgetTokens?: number;
1069
+ /**
1070
+ * Effort level for adaptive thinking (Opus 4.6+ only).
1071
+ * Controls how much thinking Anthropic model allocates:
1072
+ * - "max": Always thinks with no constraints
1073
+ * - "high": Always thinks, deep reasoning (default)
1074
+ * - "medium": Moderate thinking, may skip for simple queries
1075
+ * - "low": Minimal thinking, skips for simple tasks
1076
+ * Ignored for older models.
1077
+ */
1078
+ effort?: AnthropicEffort;
1079
+ /**
1080
+ * Optional reasoning level fallback for direct Anthropic provider usage.
1081
+ * Converted to adaptive effort when effort is not explicitly provided.
1082
+ */
1083
+ reasoning?: SimpleStreamOptions["reasoning"];
1084
+ /**
1085
+ * Controls how Anthropic returns thinking content when the selected thinking
1086
+ * transport supports a display option. Defaults to "summarized" where the
1087
+ * API accepts it.
1088
+ */
1089
+ thinkingDisplay?: AnthropicThinkingDisplay;
1090
+ interleavedThinking?: boolean;
1091
+ toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
1092
+ betas?: string[] | string;
1093
+ /**
1094
+ * Realization of `serviceTier: "priority"` on Anthropic models. When
1095
+ * `"priority"`, sets `speed: "fast"` on the request and appends the
1096
+ * `fast-mode-2026-02-01` beta header. Anthropic rejects unsupported models
1097
+ * with `invalid_request_error`, which triggers an in-provider one-shot
1098
+ * fallback (see `fastModeDisabled` provider state).
1099
+ *
1100
+ * Other `ServiceTier` values are currently ignored on this provider.
1101
+ */
1102
+ serviceTier?: ServiceTier;
1103
+ /** Force OAuth bearer auth mode for proxy tokens that don't match Anthropic token prefixes. */
1104
+ isOAuth?: boolean;
1105
+ /**
1106
+ * Pre-built Anthropic client instance. When provided, skips internal client
1107
+ * construction entirely. Use this to inject alternative SDK clients such as
1108
+ * `AnthropicVertex` that shares the same messaging API.
1109
+ */
1110
+ client?: Anthropic;
1111
+ }
1112
+
1113
+ export type AnthropicClientOptionsArgs = {
1114
+ model: Model<"anthropic-messages">;
1115
+ apiKey: string;
1116
+ extraBetas?: string[];
1117
+ stream?: boolean;
1118
+ interleavedThinking?: boolean;
1119
+ headers?: Record<string, string>;
1120
+ dynamicHeaders?: Record<string, string>;
1121
+ isOAuth?: boolean;
1122
+ hasTools?: boolean;
1123
+ onSseEvent?: AnthropicOptions["onSseEvent"];
1124
+ fetch?: FetchImpl;
1125
+ requestMaxRetries?: number;
1126
+ maxRetryDelayMs?: number;
1127
+ streamFirstEventTimeoutMs?: number;
1128
+ streamIdleTimeoutMs?: number;
1129
+ };
1130
+
1131
+ export type AnthropicClientOptionsResult = {
1132
+ isOAuthToken: boolean;
1133
+ apiKey: string | null;
1134
+ authToken?: string | null;
1135
+ baseURL?: string;
1136
+ maxRetries: number;
1137
+ timeout?: number;
1138
+ dangerouslyAllowBrowser: boolean;
1139
+ defaultHeaders: Record<string, string>;
1140
+ logLevel: AnthropicSdkClientOptions["logLevel"];
1141
+ fetch?: AnthropicSdkClientOptions["fetch"];
1142
+ fetchOptions?: AnthropicSdkClientOptions["fetchOptions"];
1143
+ };
1144
+
1145
+ const CLAUDE_CODE_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
1146
+
1147
+ type FoundryTlsOptions = {
1148
+ ca?: string | string[];
1149
+ cert?: string;
1150
+ key?: string;
1151
+ };
1152
+
1153
+ export function resolveGlmZcodeAnthropicBaseUrl(): string {
1154
+ const configured = $credentialEnv("ZCODE_PLAN_ANTHROPIC_BASE_URL")?.trim();
1155
+ if (!configured || /[\u0000-\u001f\u007f-\u009f]/u.test(configured)) {
1156
+ return GLM_ZCODE_ANTHROPIC_BASE_URL;
1157
+ }
1158
+ try {
1159
+ const parsed = new URL(configured);
1160
+ if (
1161
+ parsed.protocol !== "https:" ||
1162
+ parsed.hostname.length === 0 ||
1163
+ parsed.username.length > 0 ||
1164
+ parsed.password.length > 0 ||
1165
+ parsed.search.length > 0 ||
1166
+ parsed.hash.length > 0
1167
+ ) {
1168
+ return GLM_ZCODE_ANTHROPIC_BASE_URL;
1169
+ }
1170
+ return normalizeAnthropicBaseUrl(parsed.toString()) ?? GLM_ZCODE_ANTHROPIC_BASE_URL;
1171
+ } catch {
1172
+ return GLM_ZCODE_ANTHROPIC_BASE_URL;
1173
+ }
1174
+ }
1175
+
1176
+ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
1177
+ if (model.provider === "github-copilot") {
1178
+ return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
1179
+ }
1180
+ // glm-zcode logs in via ZCode's OAuth but auto-provisions a real Z.AI API key and
1181
+ // calls api.z.ai directly (no zcode.z.ai gateway, no captcha). Pin the base so dynamic
1182
+ // discovery / stale bundled catalogs / model cache can't redirect it elsewhere.
1183
+ if (model.provider === "glm-zcode") {
1184
+ return resolveGlmZcodeAnthropicBaseUrl();
1185
+ }
1186
+ if (model.provider === "anthropic" && isFoundryEnabled()) {
1187
+ const foundryBaseUrl = normalizeAnthropicBaseUrl($credentialEnv("FOUNDRY_BASE_URL"));
1188
+ if (foundryBaseUrl) {
1189
+ return foundryBaseUrl;
1190
+ }
1191
+ }
1192
+ if (model.provider === "anthropic") {
1193
+ return normalizeAnthropicBaseUrl(model.baseUrl) ?? "https://api.anthropic.com";
1194
+ }
1195
+ return normalizeAnthropicBaseUrl(model.baseUrl);
1196
+ }
1197
+
1198
+ function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
1199
+ const source = rawHeaders?.trim();
1200
+ if (!source) return undefined;
1201
+
1202
+ const parsed: Record<string, string> = {};
1203
+ for (const token of source.split(/\r?\n|,/)) {
1204
+ const entry = token.trim();
1205
+ if (!entry) continue;
1206
+ const separatorIndex = entry.indexOf(":");
1207
+ if (separatorIndex <= 0) continue;
1208
+ const key = entry.slice(0, separatorIndex).trim();
1209
+ const value = entry.slice(separatorIndex + 1).trim();
1210
+ if (!key || !value) continue;
1211
+ parsed[key] = value;
1212
+ }
1213
+
1214
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
1215
+ }
1216
+
1217
+ function resolveAnthropicCustomHeaders(model: Model<"anthropic-messages">): Record<string, string> | undefined {
1218
+ if (model.provider !== "anthropic") return undefined;
1219
+ if (!isFoundryEnabled()) return undefined;
1220
+ return parseAnthropicCustomHeaders($env.ANTHROPIC_CUSTOM_HEADERS);
1221
+ }
1222
+
1223
+ function looksLikeFilePath(value: string): boolean {
1224
+ return value.includes("/") || value.includes("\\") || /\.(pem|crt|cer|key)$/i.test(value);
1225
+ }
1226
+
1227
+ function resolvePemValue(value: string | undefined, name: string): string | undefined {
1228
+ const trimmed = value?.trim();
1229
+ if (!trimmed) return undefined;
1230
+
1231
+ const inline = trimmed.replace(/\\n/g, "\n");
1232
+ if (inline.includes("-----BEGIN")) {
1233
+ return inline;
1234
+ }
1235
+
1236
+ if (looksLikeFilePath(trimmed)) {
1237
+ try {
1238
+ return fs.readFileSync(trimmed, "utf8");
1239
+ } catch (error) {
1240
+ if (isEnoent(error)) {
1241
+ throw new Error(`${name} path does not exist: ${trimmed}`);
1242
+ }
1243
+ throw error;
1244
+ }
1245
+ }
1246
+
1247
+ return inline;
1248
+ }
1249
+
1250
+ function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
1251
+ if (model.provider !== "anthropic") return undefined;
1252
+ if (!isFoundryEnabled()) return undefined;
1253
+
1254
+ const ca = resolvePemValue($env.NODE_EXTRA_CA_CERTS, "NODE_EXTRA_CA_CERTS");
1255
+ const cert = resolvePemValue($env.CLAUDE_CODE_CLIENT_CERT, "CLAUDE_CODE_CLIENT_CERT");
1256
+ const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
1257
+
1258
+ if ((cert && !key) || (!cert && key)) {
1259
+ throw new Error("Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.");
1260
+ }
1261
+
1262
+ const options: FoundryTlsOptions = {};
1263
+ if (ca) options.ca = [...tls.rootCertificates, ca];
1264
+ if (cert) options.cert = cert;
1265
+ if (key) options.key = key;
1266
+ return Object.keys(options).length > 0 ? options : undefined;
1267
+ }
1268
+
1269
+ function buildClaudeCodeTlsFetchOptions(
1270
+ model: Model<"anthropic-messages">,
1271
+ baseUrl: string | undefined,
1272
+ ): AnthropicSdkClientOptions["fetchOptions"] | undefined {
1273
+ if (model.provider !== "anthropic") return undefined;
1274
+ if (!baseUrl) return undefined;
1275
+
1276
+ let serverName: string;
1277
+ try {
1278
+ serverName = new URL(baseUrl).hostname;
1279
+ } catch {
1280
+ return undefined;
1281
+ }
1282
+
1283
+ if (!serverName) return undefined;
1284
+
1285
+ const foundryTlsOptions = resolveFoundryTlsOptions(model);
1286
+
1287
+ return {
1288
+ tls: {
1289
+ rejectUnauthorized: true,
1290
+ serverName,
1291
+ ...(CLAUDE_CODE_TLS_CIPHERS ? { ciphers: CLAUDE_CODE_TLS_CIPHERS } : {}),
1292
+ ...(foundryTlsOptions ?? {}),
1293
+ },
1294
+ };
1295
+ }
1296
+ function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
1297
+ const merged: Record<string, string> = {};
1298
+ for (const headers of headerSources) {
1299
+ if (headers) {
1300
+ Object.assign(merged, headers);
1301
+ }
1302
+ }
1303
+ return merged;
1304
+ }
1305
+
1306
+ const ANTHROPIC_RETRY_DELAY_CAP_MS = 60_000;
1307
+ const ANTHROPIC_RATE_LIMIT_HEADER_PREFIX = "anthropic-ratelimit-";
1308
+
1309
+ function getSafeAnthropicHeaderEvidence(headers: Headers): string[] {
1310
+ const evidence: string[] = [];
1311
+ for (const [name, value] of headers) {
1312
+ const lowerName = name.toLowerCase();
1313
+ if (
1314
+ lowerName === "retry-after" ||
1315
+ lowerName === "retry-after-ms" ||
1316
+ lowerName.startsWith(ANTHROPIC_RATE_LIMIT_HEADER_PREFIX)
1317
+ ) {
1318
+ evidence.push(`${lowerName}=${value}`);
1319
+ }
1320
+ }
1321
+ return evidence.sort();
1322
+ }
1323
+
1324
+ function getStringProperty(source: unknown, key: string): string | undefined {
1325
+ if (!isRecord(source)) return undefined;
1326
+ const value = source[key];
1327
+ return typeof value === "string" ? value : undefined;
1328
+ }
1329
+
1330
+ function appendAnthropicRateLimitEvidence(bodyText: string, headers: Headers): string {
1331
+ const evidence = getSafeAnthropicHeaderEvidence(headers);
1332
+ if (evidence.length === 0) return bodyText;
1333
+
1334
+ const suffix = ` Anthropic rate-limit evidence: ${evidence.join(", ")}`;
1335
+ try {
1336
+ const parsed = JSON.parse(bodyText) as unknown;
1337
+ if (isRecord(parsed)) {
1338
+ const error = parsed.error;
1339
+ if (isRecord(error) && typeof error.message === "string" && !error.message.includes(suffix)) {
1340
+ return JSON.stringify({ ...parsed, error: { ...error, message: `${error.message}${suffix}` } });
1341
+ }
1342
+ if (typeof parsed.message === "string" && !parsed.message.includes(suffix)) {
1343
+ return JSON.stringify({ ...parsed, message: `${parsed.message}${suffix}` });
1344
+ }
1345
+ }
1346
+ } catch {}
1347
+
1348
+ return bodyText.includes(suffix) ? bodyText : `${bodyText}${suffix}`;
1349
+ }
1350
+
1351
+ function isAnthropicUsageExhaustionResponse(
1352
+ bodyText: string,
1353
+ headers: Headers,
1354
+ retryAfterMs: number | undefined,
1355
+ retryDelayCapMs: number,
1356
+ ): boolean {
1357
+ const overageReason = headers.get("anthropic-ratelimit-unified-overage-disabled-reason")?.toLowerCase();
1358
+ if (overageReason === "out_of_credits") return true;
1359
+ if (retryAfterMs !== undefined && retryAfterMs > retryDelayCapMs) return true;
1360
+
1361
+ try {
1362
+ const parsed = JSON.parse(bodyText) as unknown;
1363
+ const error = isRecord(parsed) ? parsed.error : undefined;
1364
+ const type = getStringProperty(error, "type") ?? getStringProperty(parsed, "type");
1365
+ const message = getStringProperty(error, "message") ?? getStringProperty(parsed, "message") ?? bodyText;
1366
+ return (
1367
+ /rate_limit_error/i.test(type ?? "") &&
1368
+ (/request would exceed your account.?s rate limit/i.test(message) || /out_of_credits/i.test(message))
1369
+ );
1370
+ } catch {
1371
+ return /request would exceed your account.?s rate limit|out_of_credits/i.test(bodyText);
1372
+ }
1373
+ }
1374
+
1375
+ function wrapAnthropicFetchForBoundedRateLimits(baseFetch: FetchImpl, maxRetryDelayMs: number | undefined): FetchImpl {
1376
+ const retryDelayCapMs = maxRetryDelayMs ?? ANTHROPIC_RETRY_DELAY_CAP_MS;
1377
+ return Object.assign(
1378
+ async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
1379
+ const response = await baseFetch(input, init);
1380
+ if (response.status !== 429 || retryDelayCapMs === 0) return response;
1381
+
1382
+ const headers = new Headers(response.headers);
1383
+ const retryAfterMs = getRetryAfterMsFromHeaders(headers);
1384
+ const bodyText = await response
1385
+ .clone()
1386
+ .text()
1387
+ .catch(() => "");
1388
+ if (!isAnthropicUsageExhaustionResponse(bodyText, headers, retryAfterMs, retryDelayCapMs)) return response;
1389
+
1390
+ headers.set("x-should-retry", "false");
1391
+ return new Response(appendAnthropicRateLimitEvidence(bodyText, headers), {
1392
+ status: response.status,
1393
+ statusText: response.statusText,
1394
+ headers,
1395
+ });
1396
+ },
1397
+ baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
1398
+ );
1399
+ }
1400
+
1401
+ // The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
1402
+ // We surface the resulting provider error ourselves, so keep the SDK quiet.
1403
+ const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
1404
+
1405
+ const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
1406
+ "message_start",
1407
+ "message_delta",
1408
+ "message_stop",
1409
+ "content_block_start",
1410
+ "content_block_delta",
1411
+ "content_block_stop",
1412
+ ]);
1413
+
1414
+ async function* iterateAnthropicEvents(
1415
+ response: Response,
1416
+ signal?: AbortSignal,
1417
+ onSseEvent?: AnthropicOptions["onSseEvent"],
1418
+ ): AsyncGenerator<RawMessageStreamEvent> {
1419
+ if (!response.body) {
1420
+ throw new Error("Attempted to iterate over an Anthropic response with no body");
1421
+ }
1422
+
1423
+ let sawMessageStart = false;
1424
+ let sawMessageEnd = false;
1425
+
1426
+ for await (const sse of readSseEvents(response.body, signal)) {
1427
+ notifyRawSseEvent(onSseEvent, sse);
1428
+ if (sse.event === "error") {
1429
+ throw new Error(sse.data);
1430
+ }
1431
+
1432
+ if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) {
1433
+ continue;
1434
+ }
1435
+
1436
+ try {
1437
+ const event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);
1438
+ if (event.type === "message_start") {
1439
+ sawMessageStart = true;
1440
+ } else if (event.type === "message_stop") {
1441
+ sawMessageEnd = true;
1442
+ }
1443
+ yield event;
1444
+ } catch (error) {
1445
+ const message = error instanceof Error ? error.message : String(error);
1446
+ throw new Error(
1447
+ `Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join("\\n")}`,
1448
+ );
1449
+ }
1450
+ }
1451
+
1452
+ if (sawMessageStart && !sawMessageEnd) {
1453
+ throw createAnthropicStreamEnvelopeError("stream ended before message_stop");
1454
+ }
1455
+ }
1456
+
1457
+ type AnthropicRawResponseRequest = {
1458
+ asResponse(): Promise<Response>;
1459
+ };
1460
+
1461
+ function hasAnthropicRawResponseRequest(request: unknown): request is AnthropicRawResponseRequest {
1462
+ return isRecord(request) && typeof request.asResponse === "function";
1463
+ }
1464
+
1465
+ type AnthropicStreamWithResponseRequest = {
1466
+ withResponse(): Promise<{
1467
+ data: AsyncIterable<RawMessageStreamEvent>;
1468
+ response: Response;
1469
+ request_id: string | null;
1470
+ }>;
1471
+ };
1472
+
1473
+ function hasAnthropicStreamWithResponseRequest(request: unknown): request is AnthropicStreamWithResponseRequest {
1474
+ return isRecord(request) && typeof request.withResponse === "function";
1475
+ }
1476
+
1477
+ async function getAnthropicStreamResponse(
1478
+ request: unknown,
1479
+ signal?: AbortSignal,
1480
+ onSseEvent?: AnthropicOptions["onSseEvent"],
1481
+ ): Promise<{ events: AsyncIterable<RawMessageStreamEvent>; response: Response; requestId: string | null }> {
1482
+ if (hasAnthropicRawResponseRequest(request)) {
1483
+ const response = await request.asResponse();
1484
+ return {
1485
+ events: iterateAnthropicEvents(response, signal, onSseEvent),
1486
+ response,
1487
+ requestId: response.headers.get("request-id"),
1488
+ };
1489
+ }
1490
+ if (hasAnthropicStreamWithResponseRequest(request)) {
1491
+ const { data, response, request_id } = await request.withResponse();
1492
+ return { events: data, response, requestId: request_id };
1493
+ }
1494
+ throw new Error("Anthropic SDK request did not expose a stream response");
1495
+ }
1496
+
1497
+ function getAnthropicCompat(
1498
+ model: Model<"anthropic-messages">,
1499
+ ): Required<Omit<NonNullable<Model<"anthropic-messages">["compat"]>, "toolChoiceSupport">> &
1500
+ Pick<NonNullable<Model<"anthropic-messages">["compat"]>, "toolChoiceSupport"> {
1501
+ return {
1502
+ disableStrictTools: model.compat?.disableStrictTools ?? false,
1503
+ disableAdaptiveThinking: model.compat?.disableAdaptiveThinking ?? false,
1504
+ supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
1505
+ supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
1506
+ supportsToolChoice: model.compat?.supportsToolChoice ?? true,
1507
+ supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
1508
+ promptCacheMode: model.compat?.promptCacheMode ?? "none",
1509
+ toolChoiceSupport: model.compat?.toolChoiceSupport,
1510
+ };
1511
+ }
1512
+
1513
+ const PROVIDER_MAX_RETRIES = 3;
1514
+ const PROVIDER_BASE_DELAY_MS = 2000;
1515
+ const ANTHROPIC_CUSTOM_ENDPOINT_FIRST_EVENT_GRACE_MS = 120_000;
1516
+ const ANTHROPIC_LARGE_REQUEST_BYTES = 1_000_000;
1517
+ const ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS = 1;
1518
+ const ANTHROPIC_SMALL_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS = 2;
1519
+
1520
+ function classifyAnthropicEndpoint(baseUrl: string): "canonical" | "custom" {
1521
+ try {
1522
+ const url = new URL(baseUrl);
1523
+ return url.protocol.toLowerCase() === "https:" &&
1524
+ url.hostname.toLowerCase() === "api.anthropic.com" &&
1525
+ (url.port === "" || url.port === "443") &&
1526
+ url.username === "" &&
1527
+ url.password === "" &&
1528
+ url.search === "" &&
1529
+ url.hash === "" &&
1530
+ (url.pathname === "" || url.pathname === "/")
1531
+ ? "canonical"
1532
+ : "custom";
1533
+ } catch {
1534
+ return "custom";
1535
+ }
1536
+ }
1537
+
1538
+ function resolveAnthropicFirstEventWatchdogMs(
1539
+ firstEventTimeoutMs: number | undefined,
1540
+ endpointClass: "canonical" | "custom",
1541
+ requestBytes: number,
1542
+ ): number | undefined {
1543
+ if (
1544
+ firstEventTimeoutMs === undefined ||
1545
+ firstEventTimeoutMs <= 0 ||
1546
+ endpointClass === "canonical" ||
1547
+ requestBytes < ANTHROPIC_LARGE_REQUEST_BYTES
1548
+ ) {
1549
+ return firstEventTimeoutMs;
1550
+ }
1551
+ return firstEventTimeoutMs + ANTHROPIC_CUSTOM_ENDPOINT_FIRST_EVENT_GRACE_MS;
1552
+ }
1553
+
1554
+ function resolveAnthropicFirstEventTimeoutMaxAttempts(requestBytes: number): number {
1555
+ return requestBytes >= ANTHROPIC_LARGE_REQUEST_BYTES
1556
+ ? ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS
1557
+ : ANTHROPIC_SMALL_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS;
1558
+ }
1559
+
1560
+ function normalizeStreamFailure(error: unknown): unknown {
1561
+ if (error instanceof Error) return error;
1562
+ if (error !== null && typeof error === "object") {
1563
+ // Structured rejections (e.g. `{ status, error, headers }` from an SDK or
1564
+ // injected client) carry transport metadata downstream classification
1565
+ // reads; wrap them in a mutable Error but copy every enumerable own
1566
+ // property so status/provider-code/header extraction still works.
1567
+ let message: string;
1568
+ try {
1569
+ message = JSON.stringify(error) || String(error);
1570
+ } catch {
1571
+ message = String(error);
1572
+ }
1573
+ const wrapper = new Error(message);
1574
+ Object.assign(wrapper, error as object);
1575
+ return wrapper;
1576
+ }
1577
+ // Primitive rejections (string/number/boolean/null/undefined): wrap with the
1578
+ // same string form downstream matchers already use (String(error)).
1579
+ return new Error(String(error));
1580
+ }
1581
+
1582
+ function attachAnthropicGraceFailureFacts(
1583
+ error: unknown,
1584
+ args: {
1585
+ elapsedMs: number;
1586
+ requestBytes: number;
1587
+ firstEventTimeoutMs: number | undefined;
1588
+ endpointClass: "canonical" | "custom";
1589
+ awaitingFirstEvent: boolean;
1590
+ },
1591
+ ): void {
1592
+ if (
1593
+ !(error instanceof Error) ||
1594
+ !args.awaitingFirstEvent ||
1595
+ args.firstEventTimeoutMs === undefined ||
1596
+ args.firstEventTimeoutMs <= 0 ||
1597
+ args.elapsedMs < args.firstEventTimeoutMs ||
1598
+ args.endpointClass !== "custom" ||
1599
+ args.requestBytes < ANTHROPIC_LARGE_REQUEST_BYTES
1600
+ ) {
1601
+ return;
1602
+ }
1603
+ Object.assign(error, {
1604
+ requestBytes: args.requestBytes,
1605
+ firstEventElapsedMs: args.elapsedMs,
1606
+ firstEventTimeoutMs: args.firstEventTimeoutMs,
1607
+ endpointClass: args.endpointClass,
1608
+ retryMaxAttempts: ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS,
1609
+ });
1610
+ }
1611
+
1612
+ function createAnthropicFirstEventTimeoutError(args: {
1613
+ elapsedMs: number;
1614
+ requestBytes: number;
1615
+ firstEventTimeoutMs: number | undefined;
1616
+ endpointClass: "canonical" | "custom";
1617
+ /** Uploads this provider invocation already consumed before the timeout. */
1618
+ providerAttemptsConsumed?: number;
1619
+ }): FirstEventTimeoutError {
1620
+ const totalCeiling = resolveAnthropicFirstEventTimeoutMaxAttempts(args.requestBytes);
1621
+ // The session counts a whole provider invocation as one attempt, so the
1622
+ // ceiling handed up must bound TOTAL uploads across the invocation: subtract
1623
+ // the provider replays already spent inside this invocation. A small request
1624
+ // whose first upload 529'd and whose replay then timed out has already
1625
+ // consumed two uploads; reporting the full two-attempt ceiling would let the
1626
+ // session upload a third time.
1627
+ const retryMaxAttempts = Math.max(1, totalCeiling - (args.providerAttemptsConsumed ?? 0));
1628
+ const timeoutLabel = args.firstEventTimeoutMs === undefined ? "disabled" : `${args.firstEventTimeoutMs}ms`;
1629
+ return new FirstEventTimeoutError(
1630
+ `Anthropic stream timed out while waiting for the first event (elapsed=${args.elapsedMs}ms request_bytes=${args.requestBytes} endpoint=${args.endpointClass} configured_timeout=${timeoutLabel}; override with PI_STREAM_FIRST_EVENT_TIMEOUT_MS)`,
1631
+ {
1632
+ requestBytes: args.requestBytes,
1633
+ firstEventElapsedMs: args.elapsedMs,
1634
+ firstEventTimeoutMs: args.firstEventTimeoutMs,
1635
+ endpointClass: args.endpointClass,
1636
+ retryMaxAttempts,
1637
+ },
1638
+ );
1639
+ }
1640
+
1641
+ /**
1642
+ * Check if an error from the Anthropic SDK is a rate-limit/transient error that
1643
+ * should be retried before any content has been emitted.
1644
+ *
1645
+ * Includes malformed JSON stream-envelope parse errors seen from some
1646
+ * Anthropic-compatible proxy endpoints.
1647
+ */
1648
+ /** Transient stream corruption errors where the response was truncated mid-JSON. */
1649
+ function isTransientStreamParseError(error: unknown): boolean {
1650
+ if (!(error instanceof Error)) return false;
1651
+ return /json parse error|unterminated string|unexpected end of json input/i.test(error.message);
1652
+ }
1653
+
1654
+ const ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX = "Anthropic stream envelope error:";
1655
+
1656
+ function createAnthropicStreamEnvelopeError(message: string): Error {
1657
+ return new Error(`${ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX} ${message}`);
1658
+ }
1659
+
1660
+ const ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES = new Set([
1661
+ "content_block_start",
1662
+ "content_block_delta",
1663
+ "content_block_stop",
1664
+ "message_delta",
1665
+ "message_stop",
1666
+ "message_start",
1667
+ ]);
1668
+
1669
+ function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
1670
+ if (typeof eventType !== "string") return false;
1671
+ if (eventType === "ping") return true;
1672
+ return !ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES.has(eventType);
1673
+ }
1674
+
1675
+ function createAnthropicStreamProgressPredicate(): (event: unknown) => boolean {
1676
+ let outputTokens = -1;
1677
+
1678
+ return event => {
1679
+ if (!isRecord(event) || typeof event.type !== "string") return false;
1680
+ if (
1681
+ event.type === "message_start" ||
1682
+ event.type === "content_block_start" ||
1683
+ event.type === "content_block_stop" ||
1684
+ event.type === "message_stop"
1685
+ ) {
1686
+ return true;
1687
+ }
1688
+ if (event.type === "content_block_delta") {
1689
+ if (!isRecord(event.delta)) return false;
1690
+ const delta = event.delta;
1691
+ return (
1692
+ (typeof delta.text === "string" && delta.text.length > 0) ||
1693
+ (typeof delta.thinking === "string" && delta.thinking.length > 0) ||
1694
+ (typeof delta.partial_json === "string" && delta.partial_json.length > 0) ||
1695
+ (typeof delta.signature === "string" && delta.signature.length > 0)
1696
+ );
1697
+ }
1698
+ if (event.type === "message_delta") {
1699
+ if (isRecord(event.delta) && event.delta.stop_reason != null) return true;
1700
+ if (!isRecord(event.usage) || typeof event.usage.output_tokens !== "number") return false;
1701
+ if (event.usage.output_tokens <= outputTokens) return false;
1702
+ outputTokens = event.usage.output_tokens;
1703
+ return true;
1704
+ }
1705
+ return false;
1706
+ };
1707
+ }
1708
+
1709
+ function isTransientStreamEnvelopeError(error: unknown): boolean {
1710
+ if (!(error instanceof Error)) return false;
1711
+ return (
1712
+ error.message.includes(ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX) ||
1713
+ /stream event order|before message_start|before terminal stop signal/i.test(error.message)
1714
+ );
1715
+ }
1716
+
1717
+ function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
1718
+ if (!(error instanceof Error)) return false;
1719
+ return /stream event order|before message_start/i.test(error.message);
1720
+ }
1721
+
1722
+ export function isProviderRetryableError(error: unknown, provider?: string): boolean {
1723
+ if (!(error instanceof Error)) return false;
1724
+ if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
1725
+ const msg = error.message.toLowerCase();
1726
+ if (isUsageLimitError(error.message)) return false;
1727
+ if (
1728
+ isUnexpectedSocketCloseMessage(msg) ||
1729
+ /rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
1730
+ msg,
1731
+ ) ||
1732
+ isTransientStreamParseError(error) ||
1733
+ isProviderRetryableStreamEnvelopeError(error)
1734
+ ) {
1735
+ return true;
1736
+ }
1737
+ return isRetryableError(error);
1738
+ }
1739
+
1740
+ function createEmptyUsage(premiumRequests?: number): Usage {
1741
+ return {
1742
+ input: 0,
1743
+ output: 0,
1744
+ cacheRead: 0,
1745
+ cacheWrite: 0,
1746
+ totalTokens: 0,
1747
+ ...(premiumRequests === undefined ? {} : { premiumRequests }),
1748
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1749
+ };
1750
+ }
1751
+
1752
+ export type AnthropicUsageLike = {
1753
+ cache_creation?: { ephemeral_5m_input_tokens?: number | null; ephemeral_1h_input_tokens?: number | null } | null;
1754
+ server_tool_use?: { web_search_requests?: number | null; web_fetch_requests?: number | null } | null;
1755
+ };
1756
+
1757
+ /**
1758
+ * Capture Anthropic's optional cache-creation TTL breakdown and server-tool-use
1759
+ * counters into the harness Usage shape. Only sets fields that were reported, so
1760
+ * a `message_delta` that omits `cache_creation` does not clobber the breakdown
1761
+ * established at `message_start`.
1762
+ */
1763
+ export function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLike): void {
1764
+ const cacheCreation = source.cache_creation;
1765
+ if (cacheCreation) {
1766
+ const fiveMinute = cacheCreation.ephemeral_5m_input_tokens ?? 0;
1767
+ const oneHour = cacheCreation.ephemeral_1h_input_tokens ?? 0;
1768
+ if (fiveMinute > 0 || oneHour > 0) {
1769
+ usage.cttl = {
1770
+ ...(fiveMinute > 0 ? { ephemeral5m: fiveMinute } : {}),
1771
+ ...(oneHour > 0 ? { ephemeral1h: oneHour } : {}),
1772
+ };
1773
+ }
1774
+ }
1775
+ const serverToolUse = source.server_tool_use;
1776
+ if (serverToolUse) {
1777
+ const webSearch = serverToolUse.web_search_requests ?? 0;
1778
+ const webFetch = serverToolUse.web_fetch_requests ?? 0;
1779
+ if (webSearch > 0 || webFetch > 0) {
1780
+ usage.server = {
1781
+ ...(webSearch > 0 ? { webSearch } : {}),
1782
+ ...(webFetch > 0 ? { webFetch } : {}),
1783
+ };
1784
+ }
1785
+ }
1786
+ }
1787
+
1788
+ /**
1789
+ * Unique request-local tool whose wire name equals the parsed base, if any.
1790
+ * Only an exact, singular match is trusted; zero or multiple matches yield
1791
+ * `undefined` so the repair never guesses among ambiguous aliases.
1792
+ */
1793
+ function resolveCpaCallableToolName(
1794
+ params: MessageCreateParamsStreaming,
1795
+ failure: CpaToolAliasRestoreFailure,
1796
+ ): string | undefined {
1797
+ if (failure.baseName === undefined) return undefined;
1798
+ const tools = params.tools as Array<{ name?: string }> | undefined;
1799
+ if (!tools) return undefined;
1800
+ let match: string | undefined;
1801
+ for (const tool of tools) {
1802
+ if (tool.name !== failure.baseName) continue;
1803
+ if (match !== undefined) return undefined;
1804
+ match = tool.name;
1805
+ }
1806
+ return match;
1807
+ }
1808
+
1809
+ /**
1810
+ * Corrective steering for the one retry after a CPA alias-restore failure. A
1811
+ * provable unique callable name is stated deterministically; otherwise the
1812
+ * model is directed at tool discovery instead of being handed an invented
1813
+ * name, mirroring the agent loop's "not found → discover and activate"
1814
+ * guidance. The rejected alias is echoed verbatim so the model knows which
1815
+ * call was wrong; nothing else from the request is quoted.
1816
+ */
1817
+ function buildCpaToolAliasSteering(failure: CpaToolAliasRestoreFailure, callableToolName?: string): string {
1818
+ const rejected = `Your previous tool call "${failure.alias}" was rejected by the Claude OAuth proxy: the tool name is not callable in this request.`;
1819
+ if (callableToolName !== undefined) {
1820
+ return `${rejected} The callable tool is "${callableToolName}". Call it by exactly that name; do not construct or reconstruct prefixed or aliased tool names.`;
1821
+ }
1822
+ return `${rejected} If you need this capability, call \`search_tool_bm25\` to discover and activate the matching tool, then retry.`;
1823
+ }
1824
+
1825
+ /**
1826
+ * Actionable terminal error for a CPA alias-restore failure that survived the
1827
+ * single corrective attempt. Deliberately statusless: no HTTP status, no
1828
+ * transport facts, and no recognizable status phrase, so neither the provider
1829
+ * generic 5xx retry nor the managed fallback controller re-sends the unchanged
1830
+ * request. Only the rejected alias and the deterministic callable name (when
1831
+ * provable) are quoted — never the request body or headers.
1832
+ */
1833
+ function createCpaToolAliasTerminalError(failure: CpaToolAliasRestoreFailure, callableToolName?: string): Error {
1834
+ const base = `Claude OAuth proxy rejected tool call "${failure.alias}": the proxy cannot restore the Claude OAuth MCP tool alias (no unique request-local match), and the corrective retry was rejected again.`;
1835
+ const guidance =
1836
+ callableToolName !== undefined
1837
+ ? ` The callable tool name is "${callableToolName}"; call it by exactly that name.`
1838
+ : ` No unique callable tool name could be determined; discover the correct tool name before retrying.`;
1839
+ return new Error(`${base}${guidance} The turn was not re-sent.`);
1840
+ }
1841
+
1842
+ /**
1843
+ * Stable identity for the logical turn currently being prompted: the
1844
+ * serialized content of the last user message. Fallback rebuilds of the same
1845
+ * turn keep the same fingerprint; the next user prompt changes it.
1846
+ */
1847
+ function cpaTurnFingerprint(messages: Message[]): string {
1848
+ for (let index = messages.length - 1; index >= 0; index--) {
1849
+ const message = messages[index];
1850
+ if (message.role !== "user") continue;
1851
+ const content = message.content;
1852
+ const serialized = typeof content === "string" ? content : JSON.stringify(content);
1853
+ return `${serialized.length}:${serialized}`;
1854
+ }
1855
+ return "";
1856
+ }
1857
+
1858
+ /**
1859
+ * Append corrective steering as a trailing user turn, preserving role
1860
+ * alternation by merging into the last user message when it is already a user
1861
+ * turn.
1862
+ */
1863
+ function appendCpaSteeringToMessages(params: MessageCreateParamsStreaming, text: string): void {
1864
+ const messages = params.messages as MessageParam[];
1865
+ const last = messages[messages.length - 1];
1866
+ if (last && last.role === "user") {
1867
+ last.content = Array.isArray(last.content)
1868
+ ? [...last.content, { type: "text", text }]
1869
+ : `${last.content}\n\n${text}`;
1870
+ } else {
1871
+ messages.push({ role: "user", content: text });
1872
+ }
1873
+ }
1874
+
1875
+ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1876
+ model: Model<"anthropic-messages">,
1877
+ context: Context,
1878
+ options?: AnthropicOptions,
1879
+ ): AssistantMessageEventStream => {
1880
+ const stream = new AssistantMessageEventStream();
1881
+
1882
+ (async () => {
1883
+ const startTime = Date.now();
1884
+ let firstTokenTime: number | undefined;
1885
+
1886
+ const copilotDynamicHeaders =
1887
+ model.provider === "github-copilot"
1888
+ ? buildCopilotDynamicHeaders({
1889
+ messages: context.messages,
1890
+ hasImages: hasCopilotVisionInput(context.messages),
1891
+ premiumMultiplier: model.premiumMultiplier,
1892
+ headers: { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
1893
+ initiatorOverride: options?.initiatorOverride,
1894
+ })
1895
+ : undefined;
1896
+ const output: AssistantMessage = {
1897
+ role: "assistant",
1898
+ content: [],
1899
+ api: model.api as Api,
1900
+ provider: model.provider,
1901
+ model: model.id,
1902
+ usage: createEmptyUsage(copilotDynamicHeaders?.premiumRequests),
1903
+ stopReason: "stop",
1904
+ timestamp: Date.now(),
1905
+ };
1906
+ let rawRequestDump: RawHttpRequestDump | undefined;
1907
+ let activeAbortTracker = createAbortSourceTracker(options?.signal);
1908
+
1909
+ try {
1910
+ let client: Anthropic;
1911
+ let isOAuthToken: boolean;
1912
+
1913
+ if (options?.client) {
1914
+ client = options.client;
1915
+ isOAuthToken = false;
1916
+ } else {
1917
+ const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
1918
+
1919
+ const extraBetas = normalizeExtraBetas(options?.betas);
1920
+ const wantsAnthropicPriority = resolveServiceTier(options?.serviceTier, model.provider) === "priority";
1921
+ if (wantsAnthropicPriority && !extraBetas.includes(fastModeBeta)) {
1922
+ extraBetas.push(fastModeBeta);
1923
+ }
1924
+
1925
+ const created = createClient(model, {
1926
+ model,
1927
+ apiKey,
1928
+ extraBetas,
1929
+ stream: true,
1930
+ interleavedThinking: options?.interleavedThinking ?? true,
1931
+ headers: options?.headers,
1932
+ dynamicHeaders: copilotDynamicHeaders?.headers,
1933
+ isOAuth: options?.isOAuth,
1934
+ hasTools: !!context.tools?.length,
1935
+ onSseEvent: options?.onSseEvent
1936
+ ? event => options.onSseEvent!(event, model, options?.attemptScope)
1937
+ : undefined,
1938
+ fetch: options?.fetch,
1939
+ requestMaxRetries: options?.requestMaxRetries,
1940
+ maxRetryDelayMs: options?.maxRetryDelayMs,
1941
+ streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
1942
+ streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
1943
+ });
1944
+ client = created.client;
1945
+ isOAuthToken = created.isOAuthToken;
1946
+ }
1947
+ const baseUrl =
1948
+ resolveAnthropicBaseUrl(model, options?.apiKey ?? getEnvApiKey(model.provider) ?? "") ??
1949
+ "https://api.anthropic.com";
1950
+ const providerSessionState = getAnthropicProviderSessionState(options?.providerSessionState);
1951
+ let disableStrictTools =
1952
+ (providerSessionState?.strictToolsDisabled ?? false) || (model.compat?.disableStrictTools ?? false);
1953
+ let strictFallbackErrorMessage: string | undefined;
1954
+ let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
1955
+ let droppedForcedToolChoice = false;
1956
+ // Exactly one corrective retry per request for the CPA alias-restore
1957
+ // signature (issue #4338); recurrence terminalizes instead of resending.
1958
+ let cpaAliasRepairApplied = false;
1959
+ let thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope =
1960
+ providerSessionState?.thinkingReplayRepairScope ?? "none";
1961
+ let thinkingReplayRepairAttempts = providerSessionState?.thinkingReplayRepairAttempts ?? 0;
1962
+ // A scope inherited from an earlier turn can only have come from the
1963
+ // deterministic branch below — the speculative masked-`api_error` probe is
1964
+ // never persisted — so a completed stream must not release it.
1965
+ let thinkingReplayRepairPersistent = thinkingReplayRepairScope !== "none";
1966
+ let generatedCacheBudget: GeneratedCacheBudget = providerSessionState?.generatedCacheBudget ?? 2;
1967
+ const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
1968
+ // Degradation state is cumulative: every fallback rebuild must merge all
1969
+ // repairs activated so far. Rebuilding from only the immediate call lets
1970
+ // a later strict/forced-tool/fast-mode fallback reintroduce the rejected
1971
+ // shape (e.g. invalid thinking signatures or forced tool_choice), and
1972
+ // the one-shot thinking-repair guard then blocks recovery.
1973
+ let nextParams = buildParams(
1974
+ model,
1975
+ baseUrl,
1976
+ context,
1977
+ isOAuthToken,
1978
+ options,
1979
+ disableStrictTools,
1980
+ {
1981
+ repairLatestAssistantThinking: thinkingReplayRepairScope === "latest",
1982
+ repairAllAssistantThinking: thinkingReplayRepairScope === "all",
1983
+ },
1984
+ generatedCacheBudget,
1985
+ );
1986
+ if (droppedForcedToolChoice) {
1987
+ delete nextParams.tool_choice;
1988
+ }
1989
+ if (disableStrictTools) {
1990
+ dropAnthropicStrictTools(nextParams);
1991
+ }
1992
+ if (dropFastMode) {
1993
+ dropAnthropicFastMode(nextParams);
1994
+ }
1995
+ const replacementPayload = await options?.onPayload?.(nextParams, model, options?.attemptScope);
1996
+ if (replacementPayload !== undefined) {
1997
+ nextParams = replacementPayload as typeof nextParams;
1998
+ }
1999
+ // Managed-mode CPA steering (issue #4338): a previous managed attempt of
2000
+ // this exact turn recorded a corrective tool-name message. Apply it only
2001
+ // while the turn is unchanged; a different user prompt expires it so a
2002
+ // stale correction never leaks into a later turn.
2003
+ const cpaSteering = providerSessionState?.cpaToolAliasSteering;
2004
+ if (cpaSteering) {
2005
+ if (cpaSteering.turnFingerprint === cpaTurnFingerprint(context.messages)) {
2006
+ appendCpaSteeringToMessages(nextParams, cpaSteering.message);
2007
+ } else {
2008
+ providerSessionState.cpaToolAliasSteering = undefined;
2009
+ }
2010
+ }
2011
+ validateCacheControls(nextParams as AnthropicCacheParams);
2012
+ return nextParams;
2013
+ };
2014
+ let params = await prepareParams();
2015
+ const setRawRequestDump = (body: MessageCreateParamsStreaming): void => {
2016
+ rawRequestDump = {
2017
+ provider: model.provider,
2018
+ api: output.api,
2019
+ model: model.id,
2020
+ method: "POST",
2021
+ url: `${baseUrl}/v1/messages`,
2022
+ body,
2023
+ };
2024
+ };
2025
+ setRawRequestDump(params);
2026
+ const inheritedRejectedPayload = providerSessionState?.thinkingReplayRejectedPayload;
2027
+ if (
2028
+ inheritedRejectedPayload &&
2029
+ thinkingReplayRepairScope !== "none" &&
2030
+ !anthropicPayloadChanged(inheritedRejectedPayload, fingerprintAnthropicPayload(params))
2031
+ ) {
2032
+ throw createAnthropicThinkingRepairNoopError(
2033
+ new Error(
2034
+ "invalid_request_error: persisted Anthropic thinking repair did not change the outgoing payload",
2035
+ ),
2036
+ params,
2037
+ inheritedRejectedPayload,
2038
+ );
2039
+ }
2040
+ if (providerSessionState?.thinkingReplayRejectedPayload) {
2041
+ providerSessionState.thinkingReplayRejectedPayload = undefined;
2042
+ }
2043
+
2044
+ type Block = (
2045
+ | ThinkingContent
2046
+ | RedactedThinkingContent
2047
+ | TextContent
2048
+ | (ToolCall & { partialJson: string })
2049
+ ) & { index: number };
2050
+ const blocks = output.content as Block[];
2051
+ const blocksByAnthropicIndex = new Map<number, Block>();
2052
+ const truncatedToolCalls = new Set<ToolCall>();
2053
+ // Bounded diagnostic for degraded primitive increments: at most one
2054
+ // warning per delta type per stream invocation, naming only the
2055
+ // envelope shape (delta type and received typeof) — never the payload.
2056
+ const degradedIncrementDiagnostics = new Set<string>();
2057
+ const noteDegradedIncrement = (deltaType: string, received: unknown): void => {
2058
+ if (degradedIncrementDiagnostics.has(deltaType)) return;
2059
+ degradedIncrementDiagnostics.add(deltaType);
2060
+ logger.warn("anthropic: degraded non-string stream increment to empty string", {
2061
+ model: model.id,
2062
+ provider: model.provider,
2063
+ deltaType,
2064
+ receivedType: received === null ? "null" : typeof received,
2065
+ });
2066
+ };
2067
+
2068
+ // Derive from the ACTUAL request shape, not the option default: the request
2069
+ // only sends `display: "summarized"` on specific paths (adaptive display is
2070
+ // omitted for models where supportsAdaptiveThinkingDisplay is false). Defaulting
2071
+ // to summarized would mislabel raw thinking as a provider-displayable summary.
2072
+ const summarizedThinking =
2073
+ (params.thinking as { display?: AnthropicThinkingDisplay } | undefined)?.display === "summarized";
2074
+ const reasoningBuffers = new WeakMap<object, string>();
2075
+ const getBlockByAnthropicIndex = (anthropicIndex: number) => {
2076
+ const block = blocksByAnthropicIndex.get(anthropicIndex);
2077
+ if (!block) return { block: undefined, contentIndex: -1 };
2078
+ return { block, contentIndex: blocks.indexOf(block) };
2079
+ };
2080
+ const trackBlockByAnthropicIndex = (anthropicIndex: number, block: Block) => {
2081
+ const orphaned = blocksByAnthropicIndex.get(anthropicIndex);
2082
+ if (orphaned) {
2083
+ if (orphaned.type === "toolCall") {
2084
+ orphaned.incompleteArguments = true;
2085
+ orphaned.incompleteArgumentsReason = "ambiguous";
2086
+ truncatedToolCalls.add(orphaned);
2087
+ }
2088
+ if (block.type === "toolCall") {
2089
+ block.incompleteArguments = true;
2090
+ block.incompleteArgumentsReason = "ambiguous";
2091
+ }
2092
+ throw new Error("Anthropic stream reused an active content block index");
2093
+ }
2094
+ blocksByAnthropicIndex.set(anthropicIndex, block);
2095
+ };
2096
+ const resetOutputForRetry = () => {
2097
+ output.content.length = 0;
2098
+ output.responseId = undefined;
2099
+ output.errorKind = undefined;
2100
+ output.errorStatus = undefined;
2101
+ output.errorMessage = strictFallbackErrorMessage;
2102
+ output.providerPayload = undefined;
2103
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
2104
+ output.stopReason = "stop";
2105
+ firstTokenTime = undefined;
2106
+ truncatedToolCalls.clear();
2107
+ };
2108
+ const idleTimeoutMs =
2109
+ options?.streamIdleTimeoutMs ??
2110
+ getStreamIdleTimeoutMs(getProviderStreamIdleTimeoutFallbackMs(model.provider));
2111
+ const firstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(model.provider);
2112
+ const firstEventTimeoutMs =
2113
+ options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
2114
+ const endpointClass = classifyAnthropicEndpoint(options?.client?.baseURL ?? baseUrl);
2115
+ stream.push({ type: "start", partial: output });
2116
+ // Retry loop for transient errors from the stream.
2117
+ // Provider-level transport/rate-limit failures: only before any streamed content starts.
2118
+ // Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
2119
+ let providerRetryAttempt = 0;
2120
+ // Total uploads this invocation has spent, including corrective-policy
2121
+ // replays (strict-tool/forced-tool/fast-mode/thinking/CPA) that reset
2122
+ // providerRetryAttempt before `continue`. The timeout ceiling must bound
2123
+ // TOTAL uploads, so it reads this counter, not the resettable one.
2124
+ let providerUploadCount = 0;
2125
+ while (true) {
2126
+ let firstEventWaitStartedAt: number | undefined;
2127
+ const requestBytes = fingerprintAnthropicPayload(params).bytes;
2128
+ const firstEventWatchdogMs = resolveAnthropicFirstEventWatchdogMs(
2129
+ firstEventTimeoutMs,
2130
+ endpointClass,
2131
+ requestBytes,
2132
+ );
2133
+ const requestUploadCeilingBound =
2134
+ endpointClass === "custom" &&
2135
+ requestBytes >= ANTHROPIC_LARGE_REQUEST_BYTES &&
2136
+ firstEventTimeoutMs !== undefined &&
2137
+ firstEventTimeoutMs > 0;
2138
+ // Retries reset output.content; drop stale block correlations from the aborted attempt.
2139
+ blocksByAnthropicIndex.clear();
2140
+ truncatedToolCalls.clear();
2141
+ activeAbortTracker = createAbortSourceTracker(options?.signal);
2142
+ let firstEventTimeoutAbortError: FirstEventTimeoutError | undefined;
2143
+ const idleTimeoutAbortError = new Error("Anthropic stream stalled while waiting for the next event");
2144
+ const { requestSignal } = activeAbortTracker;
2145
+ setRawRequestDump(params);
2146
+ options?.onStreamCreated?.();
2147
+ const anthropicRequest = client.messages.create(
2148
+ { ...params, stream: true },
2149
+ {
2150
+ signal: requestSignal,
2151
+ ...(requestUploadCeilingBound ? { maxRetries: 0 } : {}),
2152
+ },
2153
+ );
2154
+ let streamedReplayUnsafeContent = false;
2155
+ let sawProviderSafetyStop = false;
2156
+ let sawFirstSemanticEvent = false;
2157
+
2158
+ try {
2159
+ const {
2160
+ events: anthropicStream,
2161
+ response,
2162
+ requestId,
2163
+ } = await getAnthropicStreamResponse(
2164
+ anthropicRequest,
2165
+ requestSignal,
2166
+ options?.client ? event => options?.onSseEvent?.(event, model, options?.attemptScope) : undefined,
2167
+ );
2168
+ await notifyProviderResponse(options, response, model, requestId);
2169
+ firstEventWaitStartedAt = Date.now();
2170
+ let sawEvent = false;
2171
+ let sawMessageStart = false;
2172
+ let sawTerminalEnvelope = false;
2173
+ let sawMessageStop = false;
2174
+ const isProgressEvent = createAnthropicStreamProgressPredicate();
2175
+
2176
+ for await (const event of iterateWithIdleTimeout(anthropicStream, {
2177
+ idleTimeoutMs,
2178
+ firstItemTimeoutMs: firstEventWatchdogMs,
2179
+ errorMessage: idleTimeoutAbortError.message,
2180
+ firstItemErrorMessage: "Anthropic stream timed out while waiting for the first event",
2181
+ onIdle: () => activeAbortTracker.abortLocally(idleTimeoutAbortError),
2182
+ onFirstItemTimeout: () => {
2183
+ firstEventTimeoutAbortError = createAnthropicFirstEventTimeoutError({
2184
+ elapsedMs: Date.now() - (firstEventWaitStartedAt ?? Date.now()),
2185
+ requestBytes,
2186
+ firstEventTimeoutMs,
2187
+ endpointClass,
2188
+ providerAttemptsConsumed: providerUploadCount,
2189
+ });
2190
+ activeAbortTracker.abortLocally(firstEventTimeoutAbortError);
2191
+ },
2192
+ abortSignal: options?.signal,
2193
+ isProgressItem: event => {
2194
+ if (!isRecord(event) || (!sawMessageStart && event.type !== "message_start")) return false;
2195
+ const progress = isProgressEvent(event);
2196
+ if (progress) sawFirstSemanticEvent = true;
2197
+ return progress;
2198
+ },
2199
+ })) {
2200
+ sawEvent = true;
2201
+ if (sawMessageStop) {
2202
+ throw createAnthropicStreamEnvelopeError("received event after message_stop");
2203
+ }
2204
+ if (sawProviderSafetyStop) {
2205
+ if (event.type === "message_stop") {
2206
+ sawTerminalEnvelope = true;
2207
+ sawMessageStop = true;
2208
+ }
2209
+ continue;
2210
+ }
2211
+
2212
+ if (event.type === "message_start") {
2213
+ if (sawMessageStart) {
2214
+ continue;
2215
+ }
2216
+ sawMessageStart = true;
2217
+ applyAnthropicUsageExtras(output.usage, event.message.usage);
2218
+ output.responseId = event.message.id;
2219
+ output.usage.input = event.message.usage.input_tokens || 0;
2220
+ output.usage.output = event.message.usage.output_tokens || 0;
2221
+ output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
2222
+ output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
2223
+ output.usage.totalTokens =
2224
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
2225
+ calculateCost(model, output.usage);
2226
+ continue;
2227
+ }
2228
+
2229
+ if (!sawMessageStart) {
2230
+ if (shouldIgnoreAnthropicPreambleEvent(event.type)) {
2231
+ continue;
2232
+ }
2233
+ throw createAnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
2234
+ }
2235
+
2236
+ if (event.type === "content_block_start") {
2237
+ if (!firstTokenTime) firstTokenTime = Date.now();
2238
+ if (event.content_block.type === "text") {
2239
+ streamedReplayUnsafeContent = true;
2240
+ const block: Block = {
2241
+ type: "text",
2242
+ text: "",
2243
+ index: event.index,
2244
+ };
2245
+ output.content.push(block);
2246
+ trackBlockByAnthropicIndex(event.index, block);
2247
+ stream.push({
2248
+ type: "text_start",
2249
+ contentIndex: output.content.length - 1,
2250
+ partial: output,
2251
+ });
2252
+ } else if (event.content_block.type === "thinking") {
2253
+ const block: Block = {
2254
+ type: "thinking",
2255
+ thinking: "",
2256
+ thinkingSignature: "",
2257
+ index: event.index,
2258
+ };
2259
+ output.content.push(block);
2260
+ trackBlockByAnthropicIndex(event.index, block);
2261
+ // Emit thinking_start FIRST so a reasoning item is open before any
2262
+ // summary-start: the Responses SSE encoder only accepts a summary
2263
+ // start when state.open.kind === "reasoning", otherwise the
2264
+ // reasoning_summary_part.added frame is dropped and deltas arrive
2265
+ // out of order.
2266
+ stream.push({
2267
+ type: "thinking_start",
2268
+ contentIndex: output.content.length - 1,
2269
+ partial: output,
2270
+ });
2271
+ if (summarizedThinking) {
2272
+ reasoningBuffers.set(block, "");
2273
+ stream.push({
2274
+ type: "reasoning_summary_start",
2275
+ contentIndex: output.content.length - 1,
2276
+ partial: output,
2277
+ });
2278
+ }
2279
+ } else if (event.content_block.type === "redacted_thinking") {
2280
+ const block: Block = {
2281
+ type: "redactedThinking",
2282
+ data: event.content_block.data,
2283
+ index: event.index,
2284
+ };
2285
+ output.content.push(block);
2286
+ trackBlockByAnthropicIndex(event.index, block);
2287
+ } else if (event.content_block.type === "tool_use") {
2288
+ streamedReplayUnsafeContent = true;
2289
+ const initialArguments: unknown = event.content_block.input;
2290
+ if (
2291
+ initialArguments === null ||
2292
+ typeof initialArguments !== "object" ||
2293
+ Array.isArray(initialArguments)
2294
+ ) {
2295
+ throw new Error("Anthropic tool_use started with non-object arguments");
2296
+ }
2297
+ const block: Block = {
2298
+ type: "toolCall",
2299
+ id: event.content_block.id,
2300
+ name: isOAuthToken
2301
+ ? stripClaudeToolPrefix(event.content_block.name)
2302
+ : event.content_block.name,
2303
+ arguments: initialArguments as Record<string, unknown>,
2304
+ partialJson: "",
2305
+ index: event.index,
2306
+ };
2307
+ output.content.push(block);
2308
+ trackBlockByAnthropicIndex(event.index, block);
2309
+ stream.push({
2310
+ type: "toolcall_start",
2311
+ contentIndex: output.content.length - 1,
2312
+ partial: output,
2313
+ });
2314
+ }
2315
+ } else if (event.type === "content_block_delta") {
2316
+ if (event.delta.type === "text_delta") {
2317
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2318
+ if (block && block.type === "text") {
2319
+ const rawTextDelta: unknown = event.delta.text;
2320
+ if (typeof rawTextDelta !== "string") {
2321
+ noteDegradedIncrement("text_delta", rawTextDelta);
2322
+ }
2323
+ const textDelta = typeof rawTextDelta === "string" ? rawTextDelta : "";
2324
+ block.text += textDelta;
2325
+ stream.push({
2326
+ type: "text_delta",
2327
+ contentIndex: index,
2328
+ delta: textDelta,
2329
+ partial: output,
2330
+ });
2331
+ }
2332
+ } else if (event.delta.type === "thinking_delta") {
2333
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2334
+ if (block && block.type === "thinking") {
2335
+ const rawThinkingDelta: unknown = event.delta.thinking;
2336
+ if (typeof rawThinkingDelta !== "string") {
2337
+ noteDegradedIncrement("thinking_delta", rawThinkingDelta);
2338
+ }
2339
+ const thinkingDelta = typeof rawThinkingDelta === "string" ? rawThinkingDelta : "";
2340
+ block.thinking += thinkingDelta;
2341
+ if (summarizedThinking) {
2342
+ const summary = (reasoningBuffers.get(block) ?? "") + thinkingDelta;
2343
+ reasoningBuffers.set(block, summary);
2344
+ stream.push({
2345
+ type: "reasoning_summary_delta",
2346
+ contentIndex: index,
2347
+ delta: thinkingDelta,
2348
+ partial: output,
2349
+ });
2350
+ } else {
2351
+ stream.push({
2352
+ type: "thinking_delta",
2353
+ contentIndex: index,
2354
+ delta: thinkingDelta,
2355
+ partial: output,
2356
+ });
2357
+ }
2358
+ }
2359
+ } else if (event.delta.type === "input_json_delta") {
2360
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2361
+ if (block && block.type === "toolCall") {
2362
+ const rawJsonDelta: unknown = event.delta.partial_json;
2363
+ if (typeof rawJsonDelta !== "string") {
2364
+ // Tool-argument fragments are positional JSON text: erasing or
2365
+ // coercing any malformed increment (primitive OR object/function)
2366
+ // assembles valid-but-wrong arguments — e.g. `{"n":1` + numeric
2367
+ // primitive erased to "" + `3}` parses as {"n":13} and executes.
2368
+ // Prose/thinking/signature anomalies are safe to degrade; tool
2369
+ // arguments fail the turn closed. The payload never enters the
2370
+ // error.
2371
+ throw new Error(
2372
+ "Anthropic stream sent a non-string input_json_delta tool-argument increment; failing the turn instead of assembling wrong tool arguments",
2373
+ );
2374
+ }
2375
+ const jsonDelta = rawJsonDelta;
2376
+ block.partialJson += jsonDelta;
2377
+ block.arguments = parseStreamingJson(block.partialJson);
2378
+ stream.push({
2379
+ type: "toolcall_delta",
2380
+ contentIndex: index,
2381
+ delta: jsonDelta,
2382
+ partial: output,
2383
+ });
2384
+ }
2385
+ } else if (event.delta.type === "signature_delta") {
2386
+ const { block } = getBlockByAnthropicIndex(event.index);
2387
+ if (block && block.type === "thinking") {
2388
+ block.thinkingSignature = block.thinkingSignature || "";
2389
+ const rawSignatureDelta: unknown = event.delta.signature;
2390
+ if (typeof rawSignatureDelta === "string") {
2391
+ block.thinkingSignature += rawSignatureDelta;
2392
+ } else {
2393
+ noteDegradedIncrement("signature_delta", rawSignatureDelta);
2394
+ }
2395
+ }
2396
+ }
2397
+ } else if (event.type === "content_block_stop") {
2398
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
2399
+ if (block) {
2400
+ blocksByAnthropicIndex.delete(event.index);
2401
+ delete (block as { index?: number }).index;
2402
+ if (block.type === "text") {
2403
+ stream.push({
2404
+ type: "text_end",
2405
+ contentIndex: index,
2406
+ content: block.text,
2407
+ partial: output,
2408
+ });
2409
+ } else if (block.type === "thinking") {
2410
+ if (summarizedThinking) {
2411
+ const summaryText = reasoningBuffers.get(block) ?? "";
2412
+ const mutable = block as {
2413
+ provenance?: "summary" | "raw" | "mixed";
2414
+ summaryText?: string;
2415
+ };
2416
+ if (mutable.summaryText === undefined) mutable.summaryText = summaryText;
2417
+ if (mutable.provenance === undefined) mutable.provenance = "summary";
2418
+ stream.push({
2419
+ type: "reasoning_summary_end",
2420
+ contentIndex: index,
2421
+ content: summaryText,
2422
+ partial: output,
2423
+ });
2424
+ }
2425
+ stream.push({
2426
+ type: "thinking_end",
2427
+ contentIndex: index,
2428
+ content: block.thinking,
2429
+ partial: output,
2430
+ });
2431
+ } else if (block.type === "toolCall") {
2432
+ if (!isCompleteJson(block.partialJson)) {
2433
+ truncatedToolCalls.add(block);
2434
+ block.incompleteArguments = true;
2435
+ block.incompleteArgumentsReason = "truncated";
2436
+ }
2437
+ if (block.partialJson.trim()) {
2438
+ const parsedArguments: unknown = parseStreamingJson(block.partialJson);
2439
+ if (
2440
+ parsedArguments === null ||
2441
+ typeof parsedArguments !== "object" ||
2442
+ Array.isArray(parsedArguments)
2443
+ ) {
2444
+ throw new Error("Anthropic tool_use completed with non-object arguments");
2445
+ }
2446
+ block.arguments = parsedArguments as Record<string, unknown>;
2447
+ captureUnicodeEscapeEvidence(block, block.partialJson);
2448
+ }
2449
+ delete (block as { partialJson?: string }).partialJson;
2450
+ stream.push({
2451
+ type: "toolcall_end",
2452
+ contentIndex: index,
2453
+ toolCall: block,
2454
+ partial: output,
2455
+ });
2456
+ }
2457
+ }
2458
+ } else if (event.type === "message_delta") {
2459
+ const rawStopReason = event.delta.stop_reason as string | null | undefined;
2460
+ const stopDetails = event.delta.stop_details;
2461
+ const isProviderSafetyStop =
2462
+ rawStopReason === "refusal" ||
2463
+ rawStopReason === "sensitive" ||
2464
+ stopDetails?.type === "refusal" ||
2465
+ stopDetails?.type === "sensitive";
2466
+ if (rawStopReason) {
2467
+ output.stopReason = isProviderSafetyStop ? "error" : mapStopReason(rawStopReason);
2468
+ sawTerminalEnvelope = true;
2469
+ }
2470
+ if (isProviderSafetyStop) {
2471
+ sawProviderSafetyStop = true;
2472
+ sawTerminalEnvelope = true;
2473
+ output.stopReason = "error";
2474
+ // Mint the terminal kind with adapter provenance: the
2475
+ // structured refusal signal was parsed from the stream
2476
+ // delta, so the mark (not the wire field) carries the
2477
+ // authority (#4777).
2478
+ const authenticated = mintProviderSafetyStop(
2479
+ output,
2480
+ stopDetails?.type === "refusal" || stopDetails?.type === "sensitive"
2481
+ ? stopDetails.type
2482
+ : rawStopReason === "sensitive"
2483
+ ? "sensitive"
2484
+ : "refusal",
2485
+ PROVIDER_SAFETY_STOP_ADAPTER_CAPABILITY,
2486
+ options?.fetch ?? options?.client,
2487
+ isProviderSafetyStopAdapterInvocation(options),
2488
+ );
2489
+ if (!authenticated) {
2490
+ output.transportFailure = {
2491
+ kind: "transport",
2492
+ status: 500,
2493
+ providerCode: "untrusted_safety_stop",
2494
+ };
2495
+ }
2496
+ if (stopDetails?.type === "refusal") {
2497
+ const explanation = stopDetails.explanation?.trim();
2498
+ const category = stopDetails.category;
2499
+ const label = category ? `Refusal (${category})` : "Refusal";
2500
+ output.errorMessage = explanation ? `${label}: ${explanation}` : label;
2501
+ } else if (!output.errorMessage) {
2502
+ output.errorMessage =
2503
+ rawStopReason === "refusal"
2504
+ ? "Refusal (no details provided)"
2505
+ : "Content flagged by safety filters";
2506
+ }
2507
+ } else if (output.stopReason === "error" && !output.errorMessage) {
2508
+ // Anthropic flagged an error-class stop without populating stop_details.
2509
+ // Surface the raw reason instead of falling through to the generic
2510
+ // "unknown error" string when we throw below.
2511
+ output.errorMessage = `Anthropic stream ended with stop_reason: ${rawStopReason ?? "unknown"}`;
2512
+ }
2513
+ if (event.usage.input_tokens != null) {
2514
+ output.usage.input = event.usage.input_tokens;
2515
+ }
2516
+ if (event.usage.output_tokens != null) {
2517
+ output.usage.output = event.usage.output_tokens;
2518
+ }
2519
+ if (event.usage.cache_read_input_tokens != null) {
2520
+ output.usage.cacheRead = event.usage.cache_read_input_tokens;
2521
+ }
2522
+ if (event.usage.cache_creation_input_tokens != null) {
2523
+ output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
2524
+ }
2525
+ applyAnthropicUsageExtras(output.usage, event.usage);
2526
+ output.usage.totalTokens =
2527
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
2528
+ calculateCost(model, output.usage);
2529
+ } else if (event.type === "message_stop") {
2530
+ sawTerminalEnvelope = true;
2531
+ sawMessageStop = true;
2532
+ }
2533
+ }
2534
+
2535
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
2536
+ if (firstEventTimeoutError) {
2537
+ throw firstEventTimeoutError;
2538
+ }
2539
+ if (activeAbortTracker.wasCallerAbort()) {
2540
+ throw new Error("Request was aborted");
2541
+ }
2542
+ if (!sawEvent || !sawMessageStart) {
2543
+ throw createAnthropicStreamEnvelopeError("stream ended before message_start");
2544
+ }
2545
+ if (!sawTerminalEnvelope) {
2546
+ throw createAnthropicStreamEnvelopeError("stream ended before terminal stop signal");
2547
+ }
2548
+
2549
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
2550
+ throw new Error(output.errorMessage ?? "An unknown error occurred");
2551
+ }
2552
+ // The first stream that completes is the only evidence available that this
2553
+ // session is not the #4011 loop, which never produced one. Release the
2554
+ // repair escalation and the budget it consumed: the masked `api_error`
2555
+ // branch above fires on an error nobody can classify, so keeping its
2556
+ // guess would silently strip native thinking replay from every later
2557
+ // turn of the session over what may have been one transient blip.
2558
+ //
2559
+ // A deterministic rejection ("cannot be modified" / invalid signature) is
2560
+ // the opposite case: it cites blocks that stay in this session's history,
2561
+ // so releasing the repair here makes the next turn replay the same blocks
2562
+ // and spend another rejected round trip on every turn that follows. That
2563
+ // repair has to outlive the stream it fixed.
2564
+ if (
2565
+ providerSessionState &&
2566
+ !thinkingReplayRepairPersistent &&
2567
+ (thinkingReplayRepairScope !== "none" || thinkingReplayRepairAttempts > 0)
2568
+ ) {
2569
+ providerSessionState.thinkingReplayRepairScope = "none";
2570
+ providerSessionState.thinkingReplayRepairAttempts = 0;
2571
+ }
2572
+ // Release a recorded CPA steering once any stream completes: a
2573
+ // successful stream consumed it, and a failed one ends the turn (a
2574
+ // later turn's different user prompt would expire it anyway).
2575
+ if (providerSessionState?.cpaToolAliasSteering) {
2576
+ providerSessionState.cpaToolAliasSteering = undefined;
2577
+ }
2578
+ break;
2579
+ } catch (streamError) {
2580
+ const localAbortReason = activeAbortTracker.getLocalAbortReason();
2581
+ // Normalize unknown rejections (a primitive string from an injected
2582
+ // custom client is a supported surface) to a mutable Error. Boxed
2583
+ // primitives silently discard every fact stamped below, which let
2584
+ // a ceiling-bound upload slip past the one-attempt ceiling and
2585
+ // string-matched corrective branches re-upload the body.
2586
+ const streamFailure = localAbortReason ?? normalizeStreamFailure(streamError);
2587
+ attachAnthropicGraceFailureFacts(streamFailure, {
2588
+ elapsedMs: Date.now() - (firstEventWaitStartedAt ?? Date.now()),
2589
+ requestBytes,
2590
+ firstEventTimeoutMs,
2591
+ endpointClass,
2592
+ awaitingFirstEvent: !sawFirstSemanticEvent,
2593
+ });
2594
+ // A ceiling-bound upload failed before stream iteration began (for
2595
+ // example an immediate 529 from withResponse()): the grace clock
2596
+ // never started, so the facts above cannot apply, but the one-attempt
2597
+ // upload ceiling must still bound the outer provider retry loop.
2598
+ // Otherwise the multi-megabyte body is re-uploaded up to the default
2599
+ // streamMaxRetries budget despite the ceiling. Once iteration has
2600
+ // begun, only the grace-clock path above decides.
2601
+ if (requestUploadCeilingBound && firstEventWaitStartedAt === undefined) {
2602
+ Object.assign(streamFailure as Error, {
2603
+ requestBytes,
2604
+ endpointClass,
2605
+ retryMaxAttempts: ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS,
2606
+ });
2607
+ }
2608
+ const firstEventRetryMaxAttempts =
2609
+ typeof (streamFailure as { retryMaxAttempts?: unknown }).retryMaxAttempts === "number"
2610
+ ? (streamFailure as { retryMaxAttempts: number }).retryMaxAttempts
2611
+ : undefined;
2612
+ if (localAbortReason || sawProviderSafetyStop) {
2613
+ throw streamFailure;
2614
+ }
2615
+ if (firstEventRetryMaxAttempts !== undefined && providerRetryAttempt + 1 >= firstEventRetryMaxAttempts) {
2616
+ throw streamFailure;
2617
+ }
2618
+ if (
2619
+ !options?.fallbackManaged &&
2620
+ !options?.disableProviderRetries &&
2621
+ !disableStrictTools &&
2622
+ firstTokenTime === undefined &&
2623
+ hasStrictAnthropicTools(params) &&
2624
+ isAnthropicStrictGrammarTooLargeError(streamFailure)
2625
+ ) {
2626
+ strictFallbackErrorMessage = await finalizeErrorMessage(streamFailure, rawRequestDump);
2627
+ if (providerSessionState) {
2628
+ providerSessionState.strictToolsDisabled = true;
2629
+ }
2630
+ disableStrictTools = true;
2631
+ params = await prepareParams();
2632
+ providerRetryAttempt = 0;
2633
+ providerUploadCount++;
2634
+ resetOutputForRetry();
2635
+ continue;
2636
+ }
2637
+ if (
2638
+ !droppedForcedToolChoice &&
2639
+ firstTokenTime === undefined &&
2640
+ !options?.fallbackManaged &&
2641
+ !options?.disableProviderRetries &&
2642
+ isSentForcedAnthropicToolChoice(params.tool_choice) &&
2643
+ isForcedToolChoiceUnsupportedError(streamFailure, true)
2644
+ ) {
2645
+ const message = await finalizeErrorMessage(streamFailure, rawRequestDump);
2646
+ logger.debug("anthropic: forced tool_choice unsupported, retrying with auto tool choice", {
2647
+ model: model.id,
2648
+ error: message,
2649
+ });
2650
+ markToolChoiceIncapability(model, "auto", message);
2651
+ stream.push({
2652
+ type: "toolChoiceIncapability",
2653
+ api: output.api,
2654
+ provider: model.provider,
2655
+ model: model.id,
2656
+ requestedLevel: resolveToolChoice(model, options?.toolChoice).requestedLevel,
2657
+ resolvedLevel: "auto",
2658
+ reason: message,
2659
+ registryKey: resolveToolChoice(model, options?.toolChoice).registryKey,
2660
+ });
2661
+ droppedForcedToolChoice = true;
2662
+ params = await prepareParams();
2663
+ providerRetryAttempt = 0;
2664
+ providerUploadCount++;
2665
+ resetOutputForRetry();
2666
+ continue;
2667
+ }
2668
+ const thinkingSignatureInvalid = isAnthropicThinkingSignatureInvalidError(streamFailure);
2669
+ const thinkingBlocksImmutable = isAnthropicThinkingBlockMutationError(streamFailure);
2670
+ const maskedProxyRejection = isAnthropicMaskedProxyRejection(streamFailure);
2671
+ if (
2672
+ !options?.fallbackManaged &&
2673
+ !options?.disableProviderRetries &&
2674
+ thinkingReplayRepairScope === "none" &&
2675
+ thinkingReplayRepairAttempts < ANTHROPIC_MAX_THINKING_REPAIRS &&
2676
+ firstTokenTime === undefined &&
2677
+ (thinkingSignatureInvalid ||
2678
+ thinkingBlocksImmutable ||
2679
+ // Masked proxy rejection: unclassifiable on its own, so the replayed
2680
+ // request shape is the evidence. Without signed thinking blocks in
2681
+ // flight there is nothing to repair and the error must surface.
2682
+ (maskedProxyRejection && hasNativeThinkingBlocks(params.messages)))
2683
+ ) {
2684
+ const rejectedFingerprint = fingerprintAnthropicPayload(params);
2685
+ const scopes: Array<Exclude<AnthropicThinkingReplayRepairScope, "none">> = thinkingSignatureInvalid
2686
+ ? ["all"]
2687
+ : ["latest", "all"];
2688
+ let candidate: AnthropicThinkingRepairCandidate | undefined;
2689
+ const transforms: Record<string, unknown> = {};
2690
+ for (const scope of scopes) {
2691
+ thinkingReplayRepairScope = scope;
2692
+ const candidateParams = await prepareParams();
2693
+ const fingerprint = fingerprintAnthropicPayload(candidateParams);
2694
+ const changed = anthropicPayloadChanged(rejectedFingerprint, fingerprint);
2695
+ transforms[scope] = { changed, sha256: fingerprint.sha256, bytes: fingerprint.bytes };
2696
+ if (changed) {
2697
+ candidate = { scope, params: candidateParams, fingerprint };
2698
+ break;
2699
+ }
2700
+ }
2701
+ if (rawRequestDump) {
2702
+ rawRequestDump.diagnostics = {
2703
+ ...(rawRequestDump.diagnostics ?? {}),
2704
+ anthropicThinkingRepair: {
2705
+ rejected: rejectedFingerprint,
2706
+ disposition: candidate ? `send-${candidate.scope}` : "no-op-terminal",
2707
+ transforms,
2708
+ outgoingMismatch: describeAnthropicOutgoingPath(streamFailure, params),
2709
+ },
2710
+ };
2711
+ }
2712
+ const captured = await finalizeAnthropicErrorMessage(streamFailure, rawRequestDump);
2713
+ logger.warn("anthropic: thinking replay rejected; evaluated bounded repair", {
2714
+ model: model.id,
2715
+ disposition: candidate ? `send-${candidate.scope}` : "no-op-terminal",
2716
+ rejectedSha256: rejectedFingerprint.sha256,
2717
+ rejectedBytes: rejectedFingerprint.bytes,
2718
+ diagnostic: captured,
2719
+ });
2720
+ if (!candidate) {
2721
+ thinkingReplayRepairScope = "none";
2722
+ throw createAnthropicThinkingRepairNoopError(streamFailure, params, rejectedFingerprint, captured);
2723
+ }
2724
+ const nextScope = candidate.scope;
2725
+ thinkingReplayRepairAttempts++;
2726
+ logger.debug("anthropic: repairing assistant thinking replay after provider rejection", {
2727
+ model: model.id,
2728
+ scope: nextScope,
2729
+ attempt: thinkingReplayRepairAttempts,
2730
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
2731
+ });
2732
+ thinkingReplayRepairScope = nextScope;
2733
+ // Anything but the masked probe is caused by blocks that remain in
2734
+ // history, so this repair must survive the stream it is about to fix.
2735
+ if (!maskedProxyRejection) thinkingReplayRepairPersistent = true;
2736
+ if (providerSessionState) {
2737
+ providerSessionState.thinkingReplayRepairAttempts = thinkingReplayRepairAttempts;
2738
+ if (!maskedProxyRejection) {
2739
+ providerSessionState.thinkingReplayRepairScope = nextScope;
2740
+ }
2741
+ }
2742
+ params = candidate.params;
2743
+ // The corrective replay uploads the repaired body: count it so the
2744
+ // first-event timeout ceiling bounds TOTAL uploads (issue #4464).
2745
+ providerUploadCount++;
2746
+ // The provider retry budget is deliberately NOT reset here: a repair that
2747
+ // keeps being rejected must run out instead of renewing the budget it is
2748
+ // supposed to consume (issue #4011).
2749
+ resetOutputForRetry();
2750
+ continue;
2751
+ }
2752
+ // Managed attempts never take the repair branch above: the fallback
2753
+ // controller owns retries, so the provider must not retry inside the
2754
+ // attempt it was handed. That left the repair unreachable for the
2755
+ // coding agent, which prompts exclusively through managed attempts —
2756
+ // every turn rebuilt the same replay from the same history, drew the
2757
+ // same deterministic 400, and the session never converged (issue
2758
+ // #4262: one rejected 1.3 MB request every 12s, indefinitely).
2759
+ // Recording the escalation costs no round trip and keeps the retry
2760
+ // boundary intact: the next managed attempt builds a repaired replay.
2761
+ // The masked `api_error` stays out — it names no cause and may be a
2762
+ // transient blip, so only a rejection that provably indicts the
2763
+ // replayed thinking blocks may cost the session its native replay.
2764
+ if (
2765
+ options?.fallbackManaged &&
2766
+ providerSessionState &&
2767
+ providerSessionState.thinkingReplayRepairScope !== "all" &&
2768
+ firstTokenTime === undefined &&
2769
+ (thinkingSignatureInvalid || thinkingBlocksImmutable) &&
2770
+ hasNativeThinkingBlocks(params.messages)
2771
+ ) {
2772
+ const rejectedFingerprint = fingerprintAnthropicPayload(params);
2773
+ thinkingReplayRepairScope = "all";
2774
+ const candidateParams = await prepareParams();
2775
+ const candidateFingerprint = fingerprintAnthropicPayload(candidateParams);
2776
+ const changed = anthropicPayloadChanged(rejectedFingerprint, candidateFingerprint);
2777
+ if (rawRequestDump) {
2778
+ rawRequestDump.diagnostics = {
2779
+ ...(rawRequestDump.diagnostics ?? {}),
2780
+ anthropicThinkingRepair: {
2781
+ rejected: rejectedFingerprint,
2782
+ disposition: changed ? "record-all-for-managed-retry" : "no-op-terminal",
2783
+ transforms: {
2784
+ all: { changed, sha256: candidateFingerprint.sha256, bytes: candidateFingerprint.bytes },
2785
+ },
2786
+ outgoingMismatch: describeAnthropicOutgoingPath(streamFailure, params),
2787
+ },
2788
+ };
2789
+ }
2790
+ const captured = await finalizeAnthropicErrorMessage(streamFailure, rawRequestDump);
2791
+ logger.warn("anthropic: managed thinking replay rejected; evaluated repair", {
2792
+ model: model.id,
2793
+ disposition: changed ? "record-all-for-managed-retry" : "no-op-terminal",
2794
+ rejectedSha256: rejectedFingerprint.sha256,
2795
+ rejectedBytes: rejectedFingerprint.bytes,
2796
+ diagnostic: captured,
2797
+ });
2798
+ if (!changed) {
2799
+ thinkingReplayRepairScope = "none";
2800
+ throw createAnthropicThinkingRepairNoopError(streamFailure, params, rejectedFingerprint, captured);
2801
+ }
2802
+ logger.debug("anthropic: recording thinking replay repair for the next managed attempt", {
2803
+ model: model.id,
2804
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
2805
+ });
2806
+ providerSessionState.thinkingReplayRepairScope = "all";
2807
+ providerSessionState.thinkingReplayRejectedPayload = rejectedFingerprint;
2808
+ }
2809
+ if (
2810
+ !options?.fallbackManaged &&
2811
+ !options?.disableProviderRetries &&
2812
+ !dropFastMode &&
2813
+ resolveServiceTier(options?.serviceTier, model.provider) === "priority" &&
2814
+ firstTokenTime === undefined &&
2815
+ isAnthropicFastModeUnsupportedError(streamFailure)
2816
+ ) {
2817
+ logger.debug("anthropic: fast mode unsupported, retrying without speed", {
2818
+ model: model.id,
2819
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
2820
+ });
2821
+ if (providerSessionState) {
2822
+ providerSessionState.fastModeDisabled = true;
2823
+ }
2824
+ dropFastMode = true;
2825
+ params = await prepareParams();
2826
+ providerRetryAttempt = 0;
2827
+ providerUploadCount++;
2828
+ resetOutputForRetry();
2829
+ continue;
2830
+ }
2831
+ if (
2832
+ !options?.fallbackManaged &&
2833
+ !options?.disableProviderRetries &&
2834
+ generatedCacheBudget > 0 &&
2835
+ firstTokenTime === undefined &&
2836
+ isAnthropicCacheBreakpointOverflowError(streamFailure)
2837
+ ) {
2838
+ // The gateway's own markers already fill Anthropic's four slots, so
2839
+ // one of ours is the fifth. We cannot see the others, which makes the
2840
+ // rejection the only usable signal — and it says "too many", not
2841
+ // "none allowed". So give up one breakpoint at a time instead of all
2842
+ // caching at once: an endpoint that leaves a single slot free keeps
2843
+ // caching the conversation prefix, which is the marker that matters.
2844
+ const nextBudget: GeneratedCacheBudget = generatedCacheBudget === 2 ? 1 : 0;
2845
+ logger.debug("anthropic: cache breakpoint limit exceeded, reducing generated breakpoints", {
2846
+ model: model.id,
2847
+ from: generatedCacheBudget,
2848
+ to: nextBudget,
2849
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
2850
+ });
2851
+ if (providerSessionState) {
2852
+ providerSessionState.generatedCacheBudget = nextBudget;
2853
+ }
2854
+ generatedCacheBudget = nextBudget;
2855
+ params = await prepareParams();
2856
+ providerRetryAttempt = 0;
2857
+ providerUploadCount++;
2858
+ resetOutputForRetry();
2859
+ continue;
2860
+ }
2861
+ // CPA (Claude-OAuth proxy) alias-restore failure (issue #4338): the
2862
+ // proxy 500s the whole stream because the model emitted a cloaked
2863
+ // `mcp__<server>__<token>_<base>` tool name whose random token segment
2864
+ // matches nothing in the request. The generic 5xx retry below would
2865
+ // blindly re-send the unchanged request and re-sample the same drift;
2866
+ // instead, correct the request exactly once and terminalize on
2867
+ // recurrence. The narrow CPA phrase is the route gate, so native
2868
+ // Anthropic and non-CPA proxies are untouched.
2869
+ const cpaAliasFailure = parseCpaToolAliasRestoreFailure(streamFailure);
2870
+ if (cpaAliasFailure && firstTokenTime === undefined) {
2871
+ if (options?.fallbackManaged || options?.disableProviderRetries) {
2872
+ // The managed fallback controller owns retries: never retry
2873
+ // inside the attempt it handed us. Record the corrective
2874
+ // steering against this exact turn and surface the raw error;
2875
+ // the controller's next attempt rebuilds the request with the
2876
+ // steering. Without shared session state there is nowhere to
2877
+ // record, so fall through to the controller unchanged.
2878
+ if (!providerSessionState) throw streamFailure;
2879
+ const turnFingerprint = cpaTurnFingerprint(context.messages);
2880
+ if (providerSessionState.cpaToolAliasSteering?.turnFingerprint === turnFingerprint) {
2881
+ // The steering was already applied to this attempt and the proxy
2882
+ // rejected again: the single corrective attempt is spent. Surface
2883
+ // an actionable terminal error instead of another unchanged
2884
+ // resend.
2885
+ throw createCpaToolAliasTerminalError(
2886
+ cpaAliasFailure,
2887
+ resolveCpaCallableToolName(params, cpaAliasFailure),
2888
+ );
2889
+ }
2890
+ providerSessionState.cpaToolAliasSteering = {
2891
+ message: buildCpaToolAliasSteering(
2892
+ cpaAliasFailure,
2893
+ resolveCpaCallableToolName(params, cpaAliasFailure),
2894
+ ),
2895
+ turnFingerprint,
2896
+ };
2897
+ logger.debug("anthropic: recording CPA tool alias steering for the next managed attempt", {
2898
+ model: model.id,
2899
+ alias: cpaAliasFailure.alias,
2900
+ baseName: cpaAliasFailure.baseName,
2901
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
2902
+ });
2903
+ throw streamFailure;
2904
+ }
2905
+ if (!cpaAliasRepairApplied) {
2906
+ cpaAliasRepairApplied = true;
2907
+ logger.debug("anthropic: repairing CPA tool alias restore failure with corrective steering", {
2908
+ model: model.id,
2909
+ alias: cpaAliasFailure.alias,
2910
+ baseName: cpaAliasFailure.baseName,
2911
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
2912
+ });
2913
+ appendCpaSteeringToMessages(
2914
+ params,
2915
+ buildCpaToolAliasSteering(cpaAliasFailure, resolveCpaCallableToolName(params, cpaAliasFailure)),
2916
+ );
2917
+ // Exactly one corrective attempt per request: the provider retry
2918
+ // budget is deliberately NOT reset, so a persistent failure runs
2919
+ // out instead of renewing the budget it is supposed to consume
2920
+ // (issue #4011), and the recurrence branch below terminalizes
2921
+ // before the generic 5xx retry can re-send the unchanged request.
2922
+ // This corrective replay uploads the steered body: count it so the
2923
+ // first-event timeout ceiling bounds TOTAL uploads (issue #4464).
2924
+ providerUploadCount++;
2925
+ resetOutputForRetry();
2926
+ continue;
2927
+ }
2928
+ throw createCpaToolAliasTerminalError(
2929
+ cpaAliasFailure,
2930
+ resolveCpaCallableToolName(params, cpaAliasFailure),
2931
+ );
2932
+ }
2933
+ const isTransientEnvelopeFailure =
2934
+ isTransientStreamParseError(streamFailure) || isTransientStreamEnvelopeError(streamFailure);
2935
+ const canRetryTransientEnvelopeFailure = isTransientEnvelopeFailure && !streamedReplayUnsafeContent;
2936
+ const canRetryProviderFailure =
2937
+ firstTokenTime === undefined && isProviderRetryableError(streamFailure, model.provider);
2938
+ if (
2939
+ activeAbortTracker.wasCallerAbort() ||
2940
+ (firstEventRetryMaxAttempts !== undefined &&
2941
+ providerRetryAttempt + 1 >= firstEventRetryMaxAttempts) ||
2942
+ providerRetryAttempt >= resolveRetryBudget(options?.streamMaxRetries, PROVIDER_MAX_RETRIES) ||
2943
+ (!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
2944
+ ) {
2945
+ throw streamFailure;
2946
+ }
2947
+ providerRetryAttempt++;
2948
+ providerUploadCount++;
2949
+ const delayMs = PROVIDER_BASE_DELAY_MS * 2 ** (providerRetryAttempt - 1);
2950
+ if (options?.providerRetryWait) {
2951
+ await options.providerRetryWait(delayMs, options.signal);
2952
+ } else {
2953
+ await scheduler.wait(delayMs, { signal: options?.signal });
2954
+ }
2955
+ resetOutputForRetry();
2956
+ }
2957
+ }
2958
+
2959
+ for (const block of blocksByAnthropicIndex.values()) {
2960
+ delete (block as { index?: number }).index;
2961
+ if (block.type === "toolCall") {
2962
+ truncatedToolCalls.add(block);
2963
+ block.incompleteArguments = true;
2964
+ block.incompleteArgumentsReason = "truncated";
2965
+ if (block.partialJson.trim()) {
2966
+ block.arguments = parseStreamingJson(block.partialJson);
2967
+ captureUnicodeEscapeEvidence(block, block.partialJson);
2968
+ }
2969
+ delete (block as { partialJson?: string }).partialJson;
2970
+ }
2971
+ }
2972
+ blocksByAnthropicIndex.clear();
2973
+ for (const block of output.content) {
2974
+ if (block.type === "toolCall" && truncatedToolCalls.has(block)) {
2975
+ block.incompleteArguments = true;
2976
+ block.incompleteArgumentsReason = "truncated";
2977
+ }
2978
+ }
2979
+ output.duration = Date.now() - startTime;
2980
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
2981
+ if (dropFastMode && resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
2982
+ output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
2983
+ }
2984
+ // Defense-in-depth (#4443): when the provider stream assembles an
2985
+ // assistant message whose content carries directly adjacent private
2986
+ // blocks, emit a bounded diagnostic naming only the envelope shape —
2987
+ // block count and adjacent-pair presence — never raw thinking text,
2988
+ // signatures, or redacted payloads. The send-boundary collapse
2989
+ // remains the wire source of truth; this is a read-only observation.
2990
+ // Scoped to this stream invocation: each completed turn with the
2991
+ // defect is a distinct upstream producer worth surfacing, so the
2992
+ // diagnostic is not latched across invocations.
2993
+ if (hasAdjacentPrivateThinkingBlocks(output.content)) {
2994
+ logger.warn("anthropic: stream assembled assistant content with adjacent thinking blocks", {
2995
+ model: model.id,
2996
+ provider: model.provider,
2997
+ contentBlockCount: output.content.length,
2998
+ hasAdjacentPrivateBlocks: true,
2999
+ });
3000
+ }
3001
+ stream.push({ type: "done", reason: output.stopReason, message: output });
3002
+ stream.end();
3003
+ } catch (error) {
3004
+ for (const block of output.content) {
3005
+ delete (block as { index?: number }).index;
3006
+ delete (block as { partialJson?: string }).partialJson;
3007
+ }
3008
+ const localAbortReason = activeAbortTracker.getLocalAbortReason();
3009
+ output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
3010
+ output.errorStatus = extractHttpStatusFromError(localAbortReason ?? error);
3011
+ output.transportFailure = transportFailureFacts(localAbortReason ?? error) ?? output.transportFailure;
3012
+ if (output.errorKind !== "provider_safety_stop" || !output.errorMessage) {
3013
+ output.errorMessage =
3014
+ localAbortReason?.message ?? (await finalizeAnthropicErrorMessage(error, rawRequestDump));
3015
+ }
3016
+ output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
3017
+ output.duration = Date.now() - startTime;
3018
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
3019
+ stream.push({ type: "error", reason: output.stopReason, error: output });
3020
+ stream.end();
3021
+ }
3022
+ })();
3023
+
3024
+ return stream;
3025
+ };
3026
+
3027
+ export type AnthropicSystemBlock = {
3028
+ type: "text";
3029
+ text: string;
3030
+ cache_control?: AnthropicCacheControl;
3031
+ };
3032
+ type SystemBlockOptions = {
3033
+ includeClaudeCodeInstruction?: boolean;
3034
+ extraInstructions?: string[];
3035
+ billingPayload?: unknown;
3036
+ cacheControl?: AnthropicCacheControl;
3037
+ };
3038
+
3039
+ export function buildAnthropicSystemBlocks(
3040
+ systemPrompt: readonly string[] | undefined,
3041
+ options: SystemBlockOptions = {},
3042
+ ): AnthropicSystemBlock[] | undefined {
3043
+ const { includeClaudeCodeInstruction = false, extraInstructions = [], billingPayload, cacheControl } = options;
3044
+ const blocks: AnthropicSystemBlock[] = [];
3045
+ const sanitizedPrompts = normalizeSystemPrompts(systemPrompt);
3046
+ const trimmedInstructions = extraInstructions.map(instruction => instruction.trim()).filter(Boolean);
3047
+ const hasBillingHeader = sanitizedPrompts.some(prompt => prompt.includes(CLAUDE_BILLING_HEADER_PREFIX));
3048
+
3049
+ if (includeClaudeCodeInstruction && !hasBillingHeader) {
3050
+ const payloadSeed = billingPayload ?? {
3051
+ system: sanitizedPrompts,
3052
+ extraInstructions: trimmedInstructions,
3053
+ };
3054
+ blocks.push(
3055
+ { type: "text", text: createClaudeBillingHeader(payloadSeed) },
3056
+ {
3057
+ type: "text",
3058
+ text: claudeCodeSystemInstruction,
3059
+ },
3060
+ );
3061
+ }
3062
+
3063
+ for (const instruction of trimmedInstructions) {
3064
+ blocks.push({ type: "text", text: instruction });
3065
+ }
3066
+
3067
+ for (const systemPrompt of sanitizedPrompts) {
3068
+ blocks.push({ type: "text", text: systemPrompt });
3069
+ }
3070
+
3071
+ // Attach cache_control to the LAST emitted block only. Anthropic breakpoints are cumulative
3072
+ // prefix cuts, so a single trailing breakpoint covers every preceding block; spreading
3073
+ // cache_control across N blocks wastes slots against the 4-breakpoint cap.
3074
+ const lastIndex = blocks.length - 1;
3075
+ if (cacheControl && lastIndex >= 0) {
3076
+ blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
3077
+ }
3078
+
3079
+ return blocks.length > 0 ? blocks : undefined;
3080
+ }
3081
+
3082
+ export function normalizeExtraBetas(betas?: string[] | string): string[] {
3083
+ if (!betas) return [];
3084
+ const raw = Array.isArray(betas) ? betas : betas.split(",");
3085
+ return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
3086
+ }
3087
+
3088
+ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): AnthropicClientOptionsResult {
3089
+ const {
3090
+ model,
3091
+ apiKey,
3092
+ extraBetas = [],
3093
+ stream = true,
3094
+ interleavedThinking = true,
3095
+ headers,
3096
+ dynamicHeaders,
3097
+ hasTools = false,
3098
+ isOAuth,
3099
+ onSseEvent,
3100
+ } = args;
3101
+ const compat = getAnthropicCompat(model);
3102
+ const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinkingDisplay(model.id);
3103
+ const needsFineGrainedToolStreamingBeta = hasTools && !compat.supportsEagerToolInputStreaming;
3104
+ const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
3105
+ const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
3106
+ const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
3107
+ const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
3108
+ const baseFetch = args.fetch ?? fetch;
3109
+ const boundedFetch = wrapAnthropicFetchForBoundedRateLimits(baseFetch, args.maxRetryDelayMs);
3110
+ const debugFetch = onSseEvent ? wrapFetchForSseDebug(boundedFetch, event => onSseEvent(event, model)) : boundedFetch;
3111
+ // Bound the connect/headers phase. The first-event watchdog arms only after
3112
+ // response headers arrive, so a request whose connection dies before headers
3113
+ // was previously governed only by the Anthropic SDK's 10-minute default per
3114
+ // attempt times its internal retry budget — observable as an endless spinner
3115
+ // right after a completed tool call.
3116
+ const sdkTimeoutMs = resolveAnthropicSdkRequestTimeoutMs(
3117
+ model.provider,
3118
+ args.streamFirstEventTimeoutMs,
3119
+ args.streamIdleTimeoutMs,
3120
+ );
3121
+ if (model.provider === "github-copilot") {
3122
+ const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
3123
+ const betaFeatures = [...extraBetas];
3124
+ if (needsFineGrainedToolStreamingBeta) {
3125
+ betaFeatures.push(fineGrainedToolStreamingBeta);
3126
+ }
3127
+ const defaultHeaders = mergeHeaders(
3128
+ {
3129
+ Accept: stream ? "text/event-stream" : "application/json",
3130
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
3131
+ Authorization: `Bearer ${copilotApiKey}`,
3132
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
3133
+ },
3134
+ model.headers,
3135
+ dynamicHeaders,
3136
+ headers,
3137
+ );
3138
+
3139
+ return {
3140
+ isOAuthToken: false,
3141
+ apiKey: null,
3142
+ authToken: copilotApiKey,
3143
+ baseURL: baseUrl,
3144
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
3145
+ ...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
3146
+ dangerouslyAllowBrowser: true,
3147
+ defaultHeaders,
3148
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
3149
+ fetch: debugFetch,
3150
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
3151
+ };
3152
+ }
3153
+
3154
+ const betaFeatures = [...extraBetas];
3155
+ if (needsFineGrainedToolStreamingBeta) {
3156
+ betaFeatures.push(fineGrainedToolStreamingBeta);
3157
+ }
3158
+ if (needsInterleavedBeta) {
3159
+ betaFeatures.push(interleavedThinkingBeta);
3160
+ }
3161
+
3162
+ const defaultHeaders = buildAnthropicHeaders({
3163
+ apiKey,
3164
+ baseUrl,
3165
+ isOAuth: oauthToken,
3166
+ extraBetas: betaFeatures,
3167
+ stream,
3168
+ modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
3169
+ isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
3170
+ zcodeSourceHeaders: model.provider === "glm-zcode",
3171
+ });
3172
+
3173
+ if (model.provider === "cloudflare-ai-gateway") {
3174
+ return {
3175
+ isOAuthToken: false,
3176
+ apiKey: null,
3177
+ authToken: null,
3178
+ baseURL: baseUrl,
3179
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
3180
+ ...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
3181
+ dangerouslyAllowBrowser: true,
3182
+ defaultHeaders,
3183
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
3184
+ fetch: debugFetch,
3185
+ };
3186
+ }
3187
+
3188
+ // JetBrains AI (Ingrazzio) authenticates with a plain `Authorization: Bearer`
3189
+ // token and rejects requests that also carry `X-Api-Key`. `buildAnthropicHeaders`
3190
+ // already emits the bearer for non-Anthropic hosts, so keep the SDK from adding
3191
+ // its own API-key header on top of it.
3192
+ if (model.provider === "jetbrains-junie") {
3193
+ return {
3194
+ isOAuthToken: false,
3195
+ apiKey: null,
3196
+ authToken: null,
3197
+ baseURL: baseUrl,
3198
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
3199
+ ...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
3200
+ dangerouslyAllowBrowser: true,
3201
+ defaultHeaders,
3202
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
3203
+ fetch: debugFetch,
3204
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
3205
+ };
3206
+ }
3207
+
3208
+ return {
3209
+ isOAuthToken: oauthToken,
3210
+ apiKey: oauthToken ? null : apiKey,
3211
+ authToken: oauthToken ? apiKey : undefined,
3212
+ baseURL: baseUrl,
3213
+ maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
3214
+ ...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
3215
+ dangerouslyAllowBrowser: true,
3216
+ defaultHeaders,
3217
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
3218
+ fetch: debugFetch,
3219
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
3220
+ };
3221
+ }
3222
+
3223
+ function createClient(
3224
+ model: Model<"anthropic-messages">,
3225
+ args: AnthropicClientOptionsArgs,
3226
+ ): { client: Anthropic; isOAuthToken: boolean } {
3227
+ const { isOAuthToken: oauthToken, ...clientOptions } = buildAnthropicClientOptions({ ...args, model });
3228
+ const client = new Anthropic(clientOptions);
3229
+ return { client, isOAuthToken: oauthToken };
3230
+ }
3231
+
3232
+ /**
3233
+ * Anthropic rejects extended thinking combined with a forced tool choice, so such a
3234
+ * request drops `thinking`/`output_config`. Reports whether the forced-choice branch
3235
+ * applied so the caller can keep the replayed history consistent with it.
3236
+ */
3237
+ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): boolean {
3238
+ const toolChoice = params.tool_choice;
3239
+ if (!toolChoice) return false;
3240
+ if (toolChoice.type !== "any" && toolChoice.type !== "tool") return false;
3241
+ delete params.thinking;
3242
+ delete params.output_config;
3243
+ return true;
3244
+ }
3245
+
3246
+ function hasNativeThinkingBlocks(messages: MessageParam[]): boolean {
3247
+ return messages.some(
3248
+ message =>
3249
+ Array.isArray(message.content) &&
3250
+ message.content.some(block => block.type === "thinking" || block.type === "redacted_thinking"),
3251
+ );
3252
+ }
3253
+
3254
+ /**
3255
+ * Would the latest assistant turn lose a thinking block on its way to the wire?
3256
+ *
3257
+ * `convertAnthropicMessages` can only replay a `thinking` block natively when it
3258
+ * still carries the bytes Anthropic signed. A block that arrived as a bare
3259
+ * start/stop pair — no `thinking_delta`, no `signature_delta` — has neither, so
3260
+ * it is silently dropped, and Anthropic rejects the turn it produced for coming
3261
+ * back without it. Same for a `redactedThinking` block whose opaque payload is
3262
+ * gone. Only the latest assistant message is inspected because that is the turn
3263
+ * Anthropic validates against its own output.
3264
+ */
3265
+ function latestAssistantThinkingIsUnreplayable(messages: Message[], model: Model<"anthropic-messages">): boolean {
3266
+ const index = messages.findLastIndex(message => message.role === "assistant");
3267
+ if (index < 0) return false;
3268
+
3269
+ const assistant = messages[index] as AssistantMessage;
3270
+ // Cross-API history degrades to text rather than replaying native blocks, so
3271
+ // nothing is lost and nothing needs repairing.
3272
+ if (assistant.api !== "anthropic-messages") return false;
3273
+ // Endpoints that never sign thinking replay unsigned blocks verbatim.
3274
+ const requiresSignature = !isNonSigningAnthropicEndpoint(model);
3275
+
3276
+ return assistant.content.some(block => {
3277
+ if (block.type === "redactedThinking") return block.data.trim().length === 0;
3278
+ if (block.type !== "thinking") return false;
3279
+ // A block with empty text and no signature cannot go back on the wire:
3280
+ // `convertAnthropicMessages` drops it, and Anthropic rejects the turn for
3281
+ // arriving without it. A block with a valid signature AND non-empty text is
3282
+ // replayable. But a signed block whose text was emptied — e.g. by
3283
+ // clear_thinking_20251015 — carries a stale signature that signing endpoints
3284
+ // reject on replay (issue #4247). Non-signing endpoints replay unsigned
3285
+ // blocks verbatim, so only they treat a missing signature as unreplayable.
3286
+ const hasSignature = !!block.thinkingSignature?.trim();
3287
+ const isEmpty = !block.thinking.trim();
3288
+ if (!hasSignature) return requiresSignature;
3289
+ if (isEmpty && requiresSignature) return true;
3290
+ return false;
3291
+ });
3292
+ }
3293
+
3294
+ function mapAnthropicToolChoice(
3295
+ toolChoice: NonNullable<ResolveToolChoiceResult["resolvedChoice"]>,
3296
+ isOAuthToken: boolean,
3297
+ ): NonNullable<MessageCreateParamsStreaming["tool_choice"]> | undefined {
3298
+ if (typeof toolChoice === "string") {
3299
+ if (toolChoice === "required") return { type: "any" };
3300
+ return { type: toolChoice };
3301
+ }
3302
+ if ("function" in toolChoice) {
3303
+ const name = typeof toolChoice.function === "string" ? toolChoice.function : toolChoice.function.name;
3304
+ return { type: "tool", name: isOAuthToken ? applyClaudeToolPrefix(name) : name };
3305
+ }
3306
+ if ("name" in toolChoice && typeof toolChoice.name === "string") {
3307
+ return {
3308
+ ...toolChoice,
3309
+ type: "tool",
3310
+ name: isOAuthToken ? applyClaudeToolPrefix(toolChoice.name) : toolChoice.name,
3311
+ };
3312
+ }
3313
+ return toolChoice as NonNullable<MessageCreateParamsStreaming["tool_choice"]>;
3314
+ }
3315
+ function isSentForcedAnthropicToolChoice(toolChoice: MessageCreateParamsStreaming["tool_choice"] | undefined): boolean {
3316
+ return toolChoice?.type === "any" || toolChoice?.type === "tool";
3317
+ }
3318
+
3319
+ function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
3320
+ const thinking = params.thinking;
3321
+ if (thinking?.type !== "enabled") return;
3322
+
3323
+ const budgetTokens = thinking.budget_tokens ?? 0;
3324
+ if (budgetTokens <= 0) return;
3325
+
3326
+ const maxTokens = params.max_tokens ?? 0;
3327
+ const requiredMaxTokens = budgetTokens + OUTPUT_FALLBACK_BUFFER;
3328
+ if (maxTokens < requiredMaxTokens) {
3329
+ const modelMaxTokens =
3330
+ Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0
3331
+ ? model.maxTokens
3332
+ : Math.max(maxTokens, requiredMaxTokens);
3333
+ params.max_tokens = Math.min(requiredMaxTokens, modelMaxTokens);
3334
+ }
3335
+ // Anthropic requires budget_tokens strictly below max_tokens; when the cap
3336
+ // cannot fit the requested budget plus the output buffer, shrink the budget
3337
+ // (or disable thinking) instead of sending an invalid pair.
3338
+ const cappedBudget = params.max_tokens - OUTPUT_FALLBACK_BUFFER;
3339
+ if (cappedBudget < budgetTokens) {
3340
+ if (cappedBudget <= 0) {
3341
+ params.thinking = { type: "disabled" };
3342
+ } else {
3343
+ params.thinking = { type: "enabled", budget_tokens: cappedBudget };
3344
+ }
3345
+ }
3346
+ }
3347
+
3348
+ type CacheControlBlock = {
3349
+ cache_control?: AnthropicCacheControl | null;
3350
+ };
3351
+
3352
+ type AnthropicCacheParams = MessageCreateParamsStreaming & {
3353
+ cache_control?: AnthropicCacheControl;
3354
+ };
3355
+
3356
+ type AnthropicCacheMode = "automatic" | "explicit" | "none";
3357
+
3358
+ function isCacheableContentBlock(block: ContentBlockParam): boolean {
3359
+ if (block.type === "thinking" || block.type === "redacted_thinking") return false;
3360
+ return block.type !== "text" || block.text.trim().length > 0;
3361
+ }
3362
+
3363
+ function cacheControlError(path: string, reason: string): Error {
3364
+ return new Error(`Invalid Anthropic cache_control at ${path}: ${reason}`);
3365
+ }
3366
+
3367
+ function validateCacheControl(control: unknown, path: string, seenFiveMinute: { value: boolean }): void {
3368
+ if (!isRecord(control) || control.type !== "ephemeral") {
3369
+ throw cacheControlError(path, 'expected { type: "ephemeral" }');
3370
+ }
3371
+ if (control.ttl !== undefined && control.ttl !== "5m" && control.ttl !== "1h") {
3372
+ throw cacheControlError(path, 'ttl must be "5m" or "1h"');
3373
+ }
3374
+ if (control.ttl === "1h") {
3375
+ if (seenFiveMinute.value) throw cacheControlError(path, "1h TTL must precede 5m TTL");
3376
+ return;
3377
+ }
3378
+ seenFiveMinute.value = true;
3379
+ }
3380
+
3381
+ function validateCacheControls(params: AnthropicCacheParams): void {
3382
+ const seenFiveMinute = { value: false };
3383
+ let count = 0;
3384
+ const validate = (control: unknown, path: string): void => {
3385
+ if (control == null) return;
3386
+ count++;
3387
+ validateCacheControl(control, path, seenFiveMinute);
3388
+ };
3389
+ if (!Array.isArray(params.messages)) throw cacheControlError("messages", "must be an array");
3390
+
3391
+ validate(params.cache_control, "cache_control");
3392
+ for (const [index, tool] of (params.tools ?? []).entries()) {
3393
+ validate((tool as CacheControlBlock).cache_control, `tools[${index}].cache_control`);
3394
+ }
3395
+ if (Array.isArray(params.system)) {
3396
+ for (const [index, block] of params.system.entries()) {
3397
+ validate((block as CacheControlBlock).cache_control, `system[${index}].cache_control`);
3398
+ }
3399
+ }
3400
+ for (const [messageIndex, message] of params.messages.entries()) {
3401
+ if (!Array.isArray(message.content)) continue;
3402
+ for (const [blockIndex, block] of message.content.entries()) {
3403
+ const control = (block as CacheControlBlock).cache_control;
3404
+ if (control != null && !isCacheableContentBlock(block)) {
3405
+ throw cacheControlError(
3406
+ `messages[${messageIndex}].content[${blockIndex}].cache_control`,
3407
+ "block is not cacheable",
3408
+ );
3409
+ }
3410
+ validate(control, `messages[${messageIndex}].content[${blockIndex}].cache_control`);
3411
+ }
3412
+ }
3413
+ if (count > 4) throw cacheControlError("cache_control", "at most four total breakpoints are allowed");
3414
+ }
3415
+
3416
+ function applyCacheControlToLastCacheableBlock(
3417
+ blocks: Array<ContentBlockParam & CacheControlBlock>,
3418
+ cacheControl: AnthropicCacheControl,
3419
+ ): boolean {
3420
+ for (let index = blocks.length - 1; index >= 0; index--) {
3421
+ const block = blocks[index];
3422
+ if (!isCacheableContentBlock(block)) continue;
3423
+ blocks[index] = { ...block, cache_control: { ...cacheControl } };
3424
+ return true;
3425
+ }
3426
+ return false;
3427
+ }
3428
+
3429
+ function isHumanUserMessage(message: MessageCreateParamsStreaming["messages"][number]): boolean {
3430
+ if (message.role !== "user") return false;
3431
+ if (typeof message.content === "string") return true;
3432
+ return message.content.some(block => block.type !== "tool_result");
3433
+ }
3434
+
3435
+ function applyExplicitPromptCaching(
3436
+ params: AnthropicCacheParams,
3437
+ cacheControl: AnthropicCacheControl,
3438
+ budget: GeneratedCacheBudget,
3439
+ ): void {
3440
+ if (budget === 0) return;
3441
+ if (countCacheControlBreakpoints(params) >= 4) return;
3442
+
3443
+ const currentUserIndex = params.messages.findLastIndex(isHumanUserMessage);
3444
+ if (currentUserIndex < 0) return;
3445
+ const currentUser = params.messages[currentUserIndex];
3446
+ if (!currentUser) return;
3447
+
3448
+ // Tool results are encoded as role "user" on the wire but belong to the
3449
+ // assistant tool-use turn immediately before them. Anchor the latest completed
3450
+ // assistant turn so the reusable prefix advances during an agent tool loop,
3451
+ // while keeping the newest tool output outside the cache boundary.
3452
+ //
3453
+ // This anchor is the higher-value marker of the two: it covers the whole
3454
+ // conversation prefix, so a reduced budget is spent here first. It only
3455
+ // consumes budget when a marker is actually placed — on a first turn there is
3456
+ // no assistant message yet, and the reduced budget must still reach the
3457
+ // current-turn marker below rather than emitting nothing at all.
3458
+ let remaining: number = budget;
3459
+ for (let index = params.messages.length - 1; index >= 0; index--) {
3460
+ const message = params.messages[index];
3461
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
3462
+ if (
3463
+ applyCacheControlToLastCacheableBlock(
3464
+ message.content as Array<ContentBlockParam & CacheControlBlock>,
3465
+ cacheControl,
3466
+ )
3467
+ ) {
3468
+ remaining -= 1;
3469
+ break;
3470
+ }
3471
+ }
3472
+
3473
+ if (remaining < 1) return;
3474
+ if (countCacheControlBreakpoints(params) >= 4) return;
3475
+ if (typeof currentUser.content === "string" && currentUser.content.trim()) {
3476
+ currentUser.content = [{ type: "text", text: currentUser.content, cache_control: { ...cacheControl } }];
3477
+ } else if (Array.isArray(currentUser.content)) {
3478
+ applyCacheControlToLastCacheableBlock(
3479
+ currentUser.content as Array<ContentBlockParam & CacheControlBlock>,
3480
+ cacheControl,
3481
+ );
3482
+ }
3483
+ }
3484
+
3485
+ function applyPromptCaching(
3486
+ params: AnthropicCacheParams,
3487
+ cacheMode: AnthropicCacheMode,
3488
+ cacheControl?: AnthropicCacheControl,
3489
+ budget: GeneratedCacheBudget = 2,
3490
+ ): void {
3491
+ if (!cacheControl || cacheMode === "none" || budget === 0) return;
3492
+ validateCacheControls(params);
3493
+ if (cacheMode === "automatic") {
3494
+ // Automatic mode only ever emits one marker, so any non-zero budget
3495
+ // covers it; the zero case already returned above.
3496
+ params.cache_control = { ...cacheControl };
3497
+ return;
3498
+ }
3499
+ applyExplicitPromptCaching(params, cacheControl, budget);
3500
+ validateCacheControls(params);
3501
+ }
3502
+
3503
+ export function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
3504
+ validateCacheControls(params as AnthropicCacheParams);
3505
+ }
3506
+
3507
+ function countCacheControlBreakpoints(params: AnthropicCacheParams): number {
3508
+ let total = params.cache_control ? 1 : 0;
3509
+ for (const tool of params.tools ?? []) if ((tool as CacheControlBlock).cache_control) total++;
3510
+ if (Array.isArray(params.system)) {
3511
+ for (const block of params.system) if ((block as CacheControlBlock).cache_control) total++;
3512
+ }
3513
+ for (const message of params.messages) {
3514
+ if (!Array.isArray(message.content)) continue;
3515
+ for (const block of message.content) if ((block as CacheControlBlock).cache_control) total++;
3516
+ }
3517
+ return total;
3518
+ }
3519
+
3520
+ function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreakpoints: number): void {
3521
+ if (maxBreakpoints !== 4) throw new Error("Anthropic supports exactly four cache breakpoints");
3522
+ validateCacheControls(params as AnthropicCacheParams);
3523
+ }
3524
+
3525
+ function buildParams(
3526
+ model: Model<"anthropic-messages">,
3527
+ baseUrl: string,
3528
+ context: Context,
3529
+ isOAuthToken: boolean,
3530
+ options?: AnthropicOptions,
3531
+ disableStrictTools = false,
3532
+ thinkingRepair?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
3533
+ generatedCacheBudget: GeneratedCacheBudget = 2,
3534
+ ): MessageCreateParamsStreaming {
3535
+ const { mode: cacheMode, cacheControl } = getCacheControl(
3536
+ model,
3537
+ baseUrl,
3538
+ options?.cacheRetention,
3539
+ generatedCacheBudget,
3540
+ );
3541
+
3542
+ const params: AnthropicSamplingParams = {
3543
+ model: model.id,
3544
+ messages: convertAnthropicMessages(context.messages, model, isOAuthToken, thinkingRepair),
3545
+ max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
3546
+ stream: true,
3547
+ };
3548
+ if (options?.temperature !== undefined && !options?.thinkingEnabled) {
3549
+ params.temperature = options.temperature;
3550
+ }
3551
+
3552
+ if (options?.topP !== undefined) {
3553
+ params.top_p = options.topP;
3554
+ }
3555
+ if (options?.topK !== undefined) {
3556
+ params.top_k = options.topK;
3557
+ }
3558
+ if (options?.stopSequences?.length) {
3559
+ const seqs = options.stopSequences;
3560
+ if (seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX && !warnedStopSequencesTrim) {
3561
+ warnedStopSequencesTrim = true;
3562
+ logger.warn("anthropic: stop_sequences exceeds 4; extra entries dropped", {
3563
+ received: seqs.length,
3564
+ kept: ANTHROPIC_STOP_SEQUENCES_MAX,
3565
+ });
3566
+ }
3567
+ params.stop_sequences =
3568
+ seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX ? seqs.slice(0, ANTHROPIC_STOP_SEQUENCES_MAX) : seqs;
3569
+ }
3570
+
3571
+ // Opus 4.7+ rejects non-default sampling parameters with 400 error.
3572
+ if (hasOpus47ApiRestrictions(model.id)) {
3573
+ delete params.top_p;
3574
+ delete params.top_k;
3575
+ delete params.temperature;
3576
+ }
3577
+
3578
+ if (context.tools) {
3579
+ params.tools = convertTools(
3580
+ context.tools,
3581
+ isOAuthToken,
3582
+ // The Claude Code OAuth surface mishandles `strict: true` tools:
3583
+ // streamed tool_use blocks arrive with empty/undefined arguments and
3584
+ // occasionally corrupted names (works with PI_NO_STRICT=1). Never
3585
+ // request strict tool use on OAuth requests.
3586
+ disableStrictTools || isOAuthToken || model.provider === "github-copilot",
3587
+ getAnthropicCompat(model).supportsEagerToolInputStreaming,
3588
+ );
3589
+ }
3590
+
3591
+ if (model.reasoning) {
3592
+ if (options?.thinkingEnabled) {
3593
+ const mode = model.thinking?.mode;
3594
+ const requestedEffort = options.reasoning;
3595
+ const effort =
3596
+ options.effort ??
3597
+ (requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
3598
+
3599
+ const compat = getAnthropicCompat(model);
3600
+ if (mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
3601
+ // Starting with Anthropic model Opus 4.7, adaptive thinking content is omitted from the
3602
+ // response by default. Opt into summarized reasoning so thinking deltas keep
3603
+ // streaming with human-readable content for callers that rely on it.
3604
+ const adaptive: { type: "adaptive"; display?: AnthropicThinkingDisplay } = { type: "adaptive" };
3605
+ if (supportsAdaptiveThinkingDisplay(model.id)) {
3606
+ adaptive.display = options.thinkingDisplay ?? "summarized";
3607
+ }
3608
+ params.thinking = adaptive as typeof params.thinking;
3609
+ if (effort) {
3610
+ // SDK OutputConfig.effort typings may lag Anthropic's adaptive effort literals.
3611
+ // Cast so newly supported levels can pass through before the SDK catches up.
3612
+ params.output_config = { effort } as typeof params.output_config;
3613
+ }
3614
+ } else {
3615
+ params.thinking = {
3616
+ type: "enabled",
3617
+ budget_tokens: options.thinkingBudgetTokens || 1024,
3618
+ display: options.thinkingDisplay ?? "summarized",
3619
+ } as typeof params.thinking;
3620
+ if (mode === "anthropic-budget-effort" && effort) {
3621
+ params.output_config = { effort } as typeof params.output_config;
3622
+ }
3623
+ }
3624
+ }
3625
+ }
3626
+
3627
+ const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
3628
+ if (metadataUserId) {
3629
+ params.metadata = { user_id: metadataUserId };
3630
+ }
3631
+
3632
+ if (resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
3633
+ (params as ParamsWithSpeed).speed = "fast";
3634
+ }
3635
+
3636
+ if (options?.toolChoice) {
3637
+ const resolution = resolveToolChoice(model, options.toolChoice);
3638
+ if (resolution.degraded && resolution.supportSource !== "runtime") {
3639
+ logger.debug("anthropic: degrading tool_choice for model capability", {
3640
+ model: model.id,
3641
+ requestedLevel: resolution.requestedLevel,
3642
+ resolvedLevel: resolution.resolvedLevel,
3643
+ reason: resolution.reason,
3644
+ supportSource: resolution.supportSource,
3645
+ });
3646
+ }
3647
+ if (resolution.resolvedChoice) {
3648
+ const mappedToolChoice = mapAnthropicToolChoice(resolution.resolvedChoice, isOAuthToken);
3649
+ if (mappedToolChoice) {
3650
+ params.tool_choice = mappedToolChoice;
3651
+ }
3652
+ }
3653
+ }
3654
+
3655
+ // A forced tool choice strips `thinking` from the request. Signed thinking blocks
3656
+ // replayed from history belong to a thinking-enabled request, and Anthropic rejects
3657
+ // that pair with `thinking`/`redacted_thinking` blocks "cannot be modified", so the
3658
+ // replay has to degrade in the same rebuild. Runs before the billing/system payload
3659
+ // snapshot so the attribution hash covers the messages actually sent.
3660
+ if (disableThinkingIfToolChoiceForced(params) && hasNativeThinkingBlocks(params.messages)) {
3661
+ params.messages = convertAnthropicMessages(context.messages, model, isOAuthToken, {
3662
+ ...thinkingRepair,
3663
+ repairAllAssistantThinking: true,
3664
+ });
3665
+ }
3666
+
3667
+ // Anthropic compares the latest assistant message against the turn it actually
3668
+ // produced, and rejects it when a `thinking`/`redacted_thinking` block that was
3669
+ // in that response is missing. A block Anthropic streamed as a start/stop pair
3670
+ // with no `thinking_delta` and no `signature_delta` lands in history empty and
3671
+ // unsigned, and `convertAnthropicMessages` then drops it: the turn goes back
3672
+ // carrying only its `tool_use`, and the request is rejected before a token
3673
+ // streams. The rejection is recoverable — the repair drops native thinking from
3674
+ // the whole replay — but only after a full round trip has been spent, and the
3675
+ // condition is visible locally, so detect it here and degrade in the first
3676
+ // build instead of paying for the 400 to discover it.
3677
+ if (
3678
+ !thinkingRepair?.repairAllAssistantThinking &&
3679
+ latestAssistantThinkingIsUnreplayable(context.messages, model) &&
3680
+ hasNativeThinkingBlocks(params.messages)
3681
+ ) {
3682
+ params.messages = convertAnthropicMessages(context.messages, model, isOAuthToken, {
3683
+ ...thinkingRepair,
3684
+ repairAllAssistantThinking: true,
3685
+ });
3686
+ }
3687
+
3688
+ const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
3689
+ const billingSystemPrompts = normalizeSystemPrompts(context.systemPrompt);
3690
+ const billingPayload = shouldInjectClaudeCodeInstruction
3691
+ ? {
3692
+ ...params,
3693
+ ...(billingSystemPrompts.length > 0 ? { system: billingSystemPrompts } : {}),
3694
+ }
3695
+ : undefined;
3696
+ const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
3697
+ includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
3698
+ billingPayload,
3699
+ });
3700
+ if (systemBlocks) {
3701
+ params.system = systemBlocks;
3702
+ }
3703
+ ensureMaxTokensForThinking(params, model);
3704
+ applyPromptCaching(params as AnthropicCacheParams, cacheMode, cacheControl, generatedCacheBudget);
3705
+ enforceCacheControlLimit(params, 4);
3706
+ normalizeCacheControlTtlOrdering(params);
3707
+
3708
+ return params;
3709
+ }
3710
+
3711
+ /**
3712
+ * Z.AI's Anthropic-compatible proxy at `api.z.ai/api/anthropic` deserializes
3713
+ * tool_result blocks into a Python class that accesses `.id`, even though
3714
+ * Anthropic's standard tool_result schema only carries `tool_use_id`. Detect
3715
+ * that endpoint so we can emit the non-standard alias for it without
3716
+ * polluting requests to api.anthropic.com or other compatible proxies.
3717
+ * See: https://github.com/can1357/oh-my-pi/issues/814
3718
+ */
3719
+ function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
3720
+ if (model.provider === "zai" || model.provider === "glm-zcode") return true;
3721
+ const baseUrl = model.baseUrl;
3722
+ if (!baseUrl) return false;
3723
+ try {
3724
+ return new URL(baseUrl).hostname.toLowerCase() === "api.z.ai";
3725
+ } catch {
3726
+ return false;
3727
+ }
3728
+ }
3729
+
3730
+ /**
3731
+ * Returns true for providers whose Anthropic-compatible endpoints do NOT
3732
+ * implement signature-based thinking-chain integrity (DeepSeek, Z.AI, etc.).
3733
+ * For these providers, unsigned thinking blocks must be preserved as
3734
+ * `type: "thinking"` instead of being degraded to text.
3735
+ */
3736
+ function isNonSigningAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
3737
+ // Known non-signing providers
3738
+ if (model.provider === "zai" || model.provider === "glm-zcode" || model.provider === "deepseek") return true;
3739
+ const baseUrl = model.baseUrl;
3740
+ if (!baseUrl) return false;
3741
+ try {
3742
+ const hostname = new URL(baseUrl).hostname.toLowerCase();
3743
+ return hostname === "api.deepseek.com" || hostname.endsWith(".deepseek.com");
3744
+ } catch {
3745
+ return false;
3746
+ }
3747
+ }
3748
+
3749
+ function buildToolResultBlock(model: Model<"anthropic-messages">, msg: ToolResultMessage): ContentBlockParam {
3750
+ const block: ContentBlockParam = {
3751
+ type: "tool_result",
3752
+ tool_use_id: msg.toolCallId,
3753
+ content: convertContentBlocks(msg.content, model.input.includes("image")),
3754
+ is_error: msg.isError,
3755
+ };
3756
+ if (isZaiAnthropicEndpoint(model)) {
3757
+ // Z.AI workaround (issue #814): include `id` aliased to `tool_use_id`.
3758
+ (block as unknown as Record<string, unknown>).id = msg.toolCallId;
3759
+ }
3760
+ return block;
3761
+ }
3762
+
3763
+ /**
3764
+ * Anthropic rejects a replayed assistant message containing adjacent
3765
+ * `thinking`/`redacted_thinking` blocks — even when each block individually
3766
+ * carries a valid signature — with a 400 citing the second block: "cannot be
3767
+ * modified. These blocks must remain as in the original response." (issue #4416)
3768
+ *
3769
+ * The adjacency can originate from the provider stream (two `content_block_start`
3770
+ * events for `thinking` in one message with no intervening `tool_use`), from a
3771
+ * history mutation that removed a separating `tool_use`, or from an earlier
3772
+ * conversion phase in `convertAnthropicMessages` that skipped an empty `text`
3773
+ * block sitting between two thinking blocks. Because that last path exists, the
3774
+ * invariant cannot be enforced in the shared `transformMessages` phase — it must
3775
+ * run on the final wire output.
3776
+ *
3777
+ * This collapses each run of adjacent `thinking`/`redacted_thinking` blocks down
3778
+ * to the first block, preserving its bytes, signature, and type verbatim (never
3779
+ * concatenating, editing, synthesizing, or choosing the last). Blocks separated
3780
+ * by any non-thinking block (`text`, `tool_use`, …) are legitimate
3781
+ * interleaved-thinking shape and pass through unchanged. `thinking` and
3782
+ * `redacted_thinking` are treated as one adjacency class per the API contract.
3783
+ *
3784
+ * The pass is O(n) per message and idempotent: an already-collapsed array is a
3785
+ * no-op, so re-runs through `convertAnthropicMessages` (e.g. forced-tool-choice
3786
+ * or unreplayable-thinking rebuilds) are safe.
3787
+ */
3788
+ function collapseAdjacentThinkingBlocks(messages: MessageParam[]): void {
3789
+ for (const message of messages) {
3790
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
3791
+ const content = message.content;
3792
+ let write = 0;
3793
+ let inThinkingRun = false;
3794
+ for (let read = 0; read < content.length; read++) {
3795
+ const block = content[read];
3796
+ if (block === undefined) continue;
3797
+ const isThinkingBlock = block.type === "thinking" || block.type === "redacted_thinking";
3798
+ if (isThinkingBlock && inThinkingRun) continue; // only the first block of a run survives
3799
+ inThinkingRun = isThinkingBlock;
3800
+ content[write++] = block;
3801
+ }
3802
+ if (write < content.length) content.length = write;
3803
+ }
3804
+ }
3805
+
3806
+ export function convertAnthropicMessages(
3807
+ messages: Message[],
3808
+ model: Model<"anthropic-messages">,
3809
+ isOAuthToken: boolean,
3810
+ options?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
3811
+ ): MessageParam[] {
3812
+ const params: MessageParam[] = [];
3813
+
3814
+ const transformedMessages = transformMessages(messages, model, normalizeToolCallId, options);
3815
+
3816
+ for (let i = 0; i < transformedMessages.length; i++) {
3817
+ const msg = transformedMessages[i];
3818
+
3819
+ if (msg.role === "user" || msg.role === "developer") {
3820
+ if (!msg.content) continue;
3821
+
3822
+ if (typeof msg.content === "string") {
3823
+ if (msg.content.trim().length > 0) {
3824
+ params.push({
3825
+ role: "user",
3826
+ content: msg.content.toWellFormed(),
3827
+ });
3828
+ }
3829
+ } else {
3830
+ const contentBlocks = convertContentBlocks(msg.content, model.input.includes("image"));
3831
+ if (typeof contentBlocks === "string") {
3832
+ if (contentBlocks.trim().length === 0) continue;
3833
+ params.push({
3834
+ role: "user",
3835
+ content: contentBlocks,
3836
+ });
3837
+ continue;
3838
+ }
3839
+ if (contentBlocks.length === 0) continue;
3840
+ params.push({
3841
+ role: "user",
3842
+ content: contentBlocks,
3843
+ });
3844
+ }
3845
+ } else if (msg.role === "assistant") {
3846
+ const blocks: ContentBlockParam[] = [];
3847
+ const hasSignedThinking = msg.content.some(
3848
+ block =>
3849
+ block.type === "thinking" && !!block.thinkingSignature && block.thinkingSignature.trim().length > 0,
3850
+ );
3851
+
3852
+ for (const block of msg.content) {
3853
+ if (block.type === "text") {
3854
+ if (block.text.trim().length === 0) continue;
3855
+ blocks.push({
3856
+ type: "text",
3857
+ text: block.text.toWellFormed(),
3858
+ });
3859
+ } else if (block.type === "thinking") {
3860
+ if (hasSignedThinking) {
3861
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
3862
+ if (block.thinking.trim().length === 0) continue;
3863
+ blocks.push({
3864
+ type: "text",
3865
+ text: block.thinking.toWellFormed(),
3866
+ });
3867
+ continue;
3868
+ }
3869
+ blocks.push({
3870
+ type: "thinking",
3871
+ thinking: block.thinking,
3872
+ signature: block.thinkingSignature,
3873
+ });
3874
+ continue;
3875
+ }
3876
+ if (block.thinking.trim().length === 0) continue;
3877
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
3878
+ if (isNonSigningAnthropicEndpoint(model)) {
3879
+ blocks.push({
3880
+ type: "thinking",
3881
+ thinking: block.thinking.toWellFormed(),
3882
+ signature: "",
3883
+ });
3884
+ } else {
3885
+ blocks.push({
3886
+ type: "text",
3887
+ text: block.thinking.toWellFormed(),
3888
+ });
3889
+ }
3890
+ } else {
3891
+ blocks.push({
3892
+ type: "thinking",
3893
+ thinking: block.thinking.toWellFormed(),
3894
+ signature: block.thinkingSignature,
3895
+ });
3896
+ }
3897
+ } else if (block.type === "redactedThinking") {
3898
+ if (block.data.trim().length === 0) continue;
3899
+ blocks.push({
3900
+ type: "redacted_thinking",
3901
+ data: block.data,
3902
+ });
3903
+ } else if (block.type === "toolCall") {
3904
+ blocks.push({
3905
+ type: "tool_use",
3906
+ id: block.id,
3907
+ name: isOAuthToken ? applyClaudeToolPrefix(block.name) : block.name,
3908
+ input: sanitizeJsonStrings(block.arguments ?? {}),
3909
+ });
3910
+ }
3911
+ }
3912
+ if (blocks.length === 0) continue;
3913
+ params.push({
3914
+ role: "assistant",
3915
+ content: blocks,
3916
+ });
3917
+ } else if (msg.role === "toolResult") {
3918
+ // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
3919
+ const toolResults: ContentBlockParam[] = [];
3920
+
3921
+ // Add the current tool result
3922
+ toolResults.push(buildToolResultBlock(model, msg));
3923
+
3924
+ // Look ahead for consecutive toolResult messages
3925
+ let j = i + 1;
3926
+ while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
3927
+ const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
3928
+ toolResults.push(buildToolResultBlock(model, nextMsg));
3929
+ j++;
3930
+ }
3931
+
3932
+ // Skip the messages we've already processed
3933
+ i = j - 1;
3934
+
3935
+ // Add a single user message with all tool results
3936
+ params.push({
3937
+ role: "user",
3938
+ content: toolResults,
3939
+ });
3940
+ }
3941
+ }
3942
+
3943
+ // Final send-time invariant (issue #4416): collapse any run of adjacent
3944
+ // `thinking`/`redacted_thinking` blocks within one assistant message down to
3945
+ // the first block. This runs on the wire output because earlier phases here
3946
+ // (e.g. skipping empty text blocks) can themselves create the adjacency.
3947
+ collapseAdjacentThinkingBlocks(params);
3948
+ if (params.length > 0 && params[params.length - 1]?.role === "assistant") {
3949
+ params.push({ role: "user", content: "Continue." });
3950
+ }
3951
+
3952
+ return params;
3953
+ }
3954
+
3955
+ /**
3956
+ * JSON Schema whitelist for Anthropic tool `input_schema` nodes.
3957
+ *
3958
+ * Mirrors the Anthropic Python SDK's `lib/_parse/_transform.py::transform_schema`:
3959
+ * we keep only structural/metadata keywords Anthropic's validator honors, and demote
3960
+ * anything else into the node's `description` as `\n\n{key: value, ...}` so the model
3961
+ * still sees the constraint as a natural-language hint.
3962
+ *
3963
+ * `Set` (not `Record<string, true>`) because membership is probed against arbitrary
3964
+ * user/Zod-derived schema keys: a literal Record would falsely match prototype names
3965
+ * like `"toString"` and silently strip valid properties.
3966
+ */
3967
+ const ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP = new Set([
3968
+ "$ref",
3969
+ "$defs",
3970
+ "$schema",
3971
+ "definitions",
3972
+ "type",
3973
+ "anyOf",
3974
+ "oneOf",
3975
+ "allOf",
3976
+ "enum",
3977
+ "const",
3978
+ "description",
3979
+ "title",
3980
+ "default",
3981
+ "nullable",
3982
+ ]);
3983
+ /** Keys preserved on `type: "object"` nodes (in addition to the universal set). */
3984
+ const ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP = new Set(["properties", "required", "additionalProperties"]);
3985
+ /** Keys preserved on `type: "array"` nodes; `minItems` only when its value is 0 or 1. */
3986
+ const ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP = new Set(["items", "prefixItems", "minItems"]);
3987
+ /** Keys preserved on `type: "string"` nodes; `format` only when its value is in the supported list. */
3988
+ const ANTHROPIC_TOOL_SCHEMA_STRING_KEEP = new Set(["format"]);
3989
+ /**
3990
+ * String `format` values Anthropic accepts; everything else (including `pattern`-style
3991
+ * format hints) gets demoted into `description`. Matches `SupportedStringFormats` in the
3992
+ * Anthropic SDK's `_transform.py`.
3993
+ */
3994
+ const ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS = new Set([
3995
+ "date-time",
3996
+ "time",
3997
+ "date",
3998
+ "duration",
3999
+ "email",
4000
+ "hostname",
4001
+ "uri",
4002
+ "ipv4",
4003
+ "ipv6",
4004
+ "uuid",
4005
+ ]);
4006
+ const ANTHROPIC_STRICT_TOOL_ALLOWLIST = new Set(["bash", "python", "edit", "find"]);
4007
+ const MAX_ANTHROPIC_STRICT_TOOLS = 20;
4008
+ const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
4009
+ const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
4010
+
4011
+ /**
4012
+ * Pick the principal non-null scalar type from a `type` keyword. Anthropic accepts
4013
+ * `type` as either a single string or an array (e.g. `["number", "null"]` for a
4014
+ * nullable value); the SDK whitelist is keyed off the scalar type, with `"null"`
4015
+ * ignored so nullable variants are normalized as their underlying type.
4016
+ */
4017
+ function pickAnthropicScalarType(type: unknown): string | undefined {
4018
+ if (typeof type === "string") return type;
4019
+ if (Array.isArray(type)) {
4020
+ for (const entry of type) {
4021
+ if (typeof entry === "string" && entry !== "null") return entry;
4022
+ }
4023
+ }
4024
+ return undefined;
4025
+ }
4026
+
4027
+ function anthropicPerTypeKeep(scalarType: string | undefined): Set<string> | undefined {
4028
+ switch (scalarType) {
4029
+ case "object":
4030
+ return ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP;
4031
+ case "array":
4032
+ return ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP;
4033
+ case "string":
4034
+ return ANTHROPIC_TOOL_SCHEMA_STRING_KEEP;
4035
+ default:
4036
+ return undefined;
4037
+ }
4038
+ }
4039
+
4040
+ /**
4041
+ * Per-schema-object memoization slot for the normalized Anthropic tool form. We stamp
4042
+ * the result onto the host via a `Symbol` property (mirroring `utils/schema/stamps.ts`)
4043
+ * instead of using a `WeakMap`: it's a single hidden-class slot, so warm reads are
4044
+ * direct property access and write-once cycles resolve to the in-progress result.
4045
+ */
4046
+ const kAnthropicToolNormal = Symbol("pi.schema.anthropic.toolNormal");
4047
+
4048
+ /**
4049
+ * Normalize a JSON Schema node for Anthropic tool `input_schema`.
4050
+ *
4051
+ * Applies the full whitelist semantics from the Anthropic Python SDK's
4052
+ * `lib/_parse/_transform.py::transform_schema`:
4053
+ *
4054
+ * 1. Universal keys (`$ref`, `$defs`, `type`, `anyOf`/`oneOf`/`allOf`, `enum`, `const`,
4055
+ * `description`, `title`, `default`, `nullable`) are preserved on every node.
4056
+ * 2. Per-type keys are kept additively (object → `properties`/`required`/`additionalProperties`,
4057
+ * array → `items`/`prefixItems` plus `minItems` only when 0 or 1, string → `format`
4058
+ * only when in the supported value set).
4059
+ * 3. Everything else is demoted into the node's `description` as `\n\n{key: value, ...}`
4060
+ * so the model still sees the constraint as a natural-language hint.
4061
+ *
4062
+ * Object nodes default to `additionalProperties: false`, but explicit open-map
4063
+ * declarations (`additionalProperties: true` or a schema literal — Zod's
4064
+ * `z.record(z.string(), z.unknown())` produces `{}`) are preserved. The strict-mode
4065
+ * pass downstream demotes those shapes to non-strict instead of fabricating a closed
4066
+ * object, so callers like the resolve tool keep working open-map semantics.
4067
+ */
4068
+ export function normalizeAnthropicToolSchema(schema: unknown): unknown {
4069
+ if (Array.isArray(schema)) return schema.map(entry => normalizeAnthropicToolSchema(entry));
4070
+ if (!isRecord(schema)) return schema;
4071
+
4072
+ const slot = schema as Record<symbol, Record<string, unknown> | undefined>;
4073
+ const existing = slot[kAnthropicToolNormal];
4074
+ if (existing !== undefined) return existing;
4075
+
4076
+ const result: Record<string, unknown> = {};
4077
+ // Pre-stamp before recursion so cyclic schemas resolve to the in-progress object
4078
+ // (mirrors the WeakMap-set-before-recurse pattern the original implementation used).
4079
+ Object.defineProperty(schema, kAnthropicToolNormal, { value: result, writable: true, configurable: true });
4080
+
4081
+ const scalarType = pickAnthropicScalarType(schema.type);
4082
+ const perTypeKeep = anthropicPerTypeKeep(scalarType);
4083
+ const spill: Array<[string, unknown]> = [];
4084
+
4085
+ for (const key in schema) {
4086
+ if (!Object.hasOwn(schema, key)) continue;
4087
+ const value = schema[key];
4088
+ if (ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP.has(key) || perTypeKeep?.has(key)) {
4089
+ result[key] = value;
4090
+ } else {
4091
+ spill.push([key, value]);
4092
+ }
4093
+ }
4094
+
4095
+ // Per-type conditional keys: prune within the kept set.
4096
+ if (scalarType === "string") {
4097
+ const format = result.format;
4098
+ if (typeof format === "string" && !ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS.has(format)) {
4099
+ spill.push(["format", format]);
4100
+ delete result.format;
4101
+ }
4102
+ }
4103
+ if (scalarType === "array" && result.minItems !== undefined) {
4104
+ const minItems = result.minItems;
4105
+ if (!(typeof minItems === "number" && (minItems === 0 || minItems === 1))) {
4106
+ spill.push(["minItems", minItems]);
4107
+ delete result.minItems;
4108
+ }
4109
+ }
4110
+ if (scalarType === "object" && result.additionalProperties === undefined) {
4111
+ result.additionalProperties = false;
4112
+ }
4113
+
4114
+ // Recurse on structural keys.
4115
+ if (isRecord(result.properties)) {
4116
+ const normalizedProperties: Record<string, unknown> = {};
4117
+ const sourceProperties = result.properties as Record<string, unknown>;
4118
+ for (const propName in sourceProperties) {
4119
+ if (!Object.hasOwn(sourceProperties, propName)) continue;
4120
+ normalizedProperties[propName] = normalizeAnthropicToolSchema(sourceProperties[propName]);
4121
+ }
4122
+ result.properties = normalizedProperties;
4123
+ }
4124
+ if (isRecord(result.additionalProperties)) {
4125
+ const normalized = normalizeAnthropicToolSchema(result.additionalProperties);
4126
+ if (isRecord(normalized) && Object.keys(normalized).length === 0) {
4127
+ result.additionalProperties = true;
4128
+ } else {
4129
+ result.additionalProperties = normalized;
4130
+ }
4131
+ }
4132
+ if (Array.isArray(result.items)) {
4133
+ result.items = result.items.map(item => normalizeAnthropicToolSchema(item));
4134
+ } else if (isRecord(result.items)) {
4135
+ result.items = normalizeAnthropicToolSchema(result.items);
4136
+ }
4137
+ if (Array.isArray(result.prefixItems)) {
4138
+ result.prefixItems = result.prefixItems.map(item => normalizeAnthropicToolSchema(item));
4139
+ }
4140
+ for (const key of COMBINATOR_KEYS) {
4141
+ const variants = result[key];
4142
+ if (Array.isArray(variants)) {
4143
+ result[key] = variants.map(variant => normalizeAnthropicToolSchema(variant));
4144
+ }
4145
+ }
4146
+ for (const defsKey of ["$defs", "definitions"] as const) {
4147
+ const definitions = result[defsKey];
4148
+ if (!isRecord(definitions)) continue;
4149
+ const normalizedDefs: Record<string, unknown> = {};
4150
+ const sourceDefs = definitions as Record<string, unknown>;
4151
+ for (const name in sourceDefs) {
4152
+ if (!Object.hasOwn(sourceDefs, name)) continue;
4153
+ normalizedDefs[name] = normalizeAnthropicToolSchema(sourceDefs[name]);
4154
+ }
4155
+ result[defsKey] = normalizedDefs;
4156
+ }
4157
+
4158
+ spillToDescription(result, spill);
4159
+ return result;
4160
+ }
4161
+
4162
+ type AnthropicToolInputSchema = Anthropic.Messages.Tool["input_schema"];
4163
+
4164
+ type AnthropicToolSchemaPlan = {
4165
+ inputSchema: AnthropicToolInputSchema;
4166
+ strict: boolean;
4167
+ };
4168
+
4169
+ type AnthropicStrictBudget = {
4170
+ optionalRemaining: number;
4171
+ unionRemaining: number;
4172
+ optionalCount: number;
4173
+ unionCount: number;
4174
+ };
4175
+
4176
+ function hasAnthropicUnionType(schema: Record<string, unknown>): boolean {
4177
+ return Array.isArray(schema.type) || Array.isArray(schema.anyOf);
4178
+ }
4179
+
4180
+ function hasNullVariant(schema: Record<string, unknown>): boolean {
4181
+ if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
4182
+ return Array.isArray(schema.anyOf) && schema.anyOf.some(variant => isRecord(variant) && variant.type === "null");
4183
+ }
4184
+
4185
+ function makeAnthropicNullableSchema(schema: unknown, budget: AnthropicStrictBudget): unknown | undefined {
4186
+ if (isRecord(schema)) {
4187
+ if (hasNullVariant(schema)) return schema;
4188
+ if (Array.isArray(schema.anyOf)) {
4189
+ return { ...schema, anyOf: [...schema.anyOf, { type: "null" }] };
4190
+ }
4191
+ if (Array.isArray(schema.type)) {
4192
+ return { ...schema, type: [...schema.type, "null"] };
4193
+ }
4194
+ }
4195
+
4196
+ if (budget.unionRemaining <= 0) return undefined;
4197
+ budget.unionRemaining--;
4198
+ budget.unionCount++;
4199
+ return { anyOf: [schema, { type: "null" }] };
4200
+ }
4201
+
4202
+ function normalizeAnthropicStrictSchemaNode(
4203
+ schema: unknown,
4204
+ budget: AnthropicStrictBudget,
4205
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
4206
+ ): unknown | undefined {
4207
+ if (Array.isArray(schema)) {
4208
+ const result: unknown[] = [];
4209
+ for (const entry of schema) {
4210
+ const normalized = normalizeAnthropicStrictSchemaNode(entry, budget, cache);
4211
+ if (normalized === undefined) return undefined;
4212
+ result.push(normalized);
4213
+ }
4214
+ return result;
4215
+ }
4216
+
4217
+ if (!isRecord(schema)) return schema;
4218
+
4219
+ const cached = cache.get(schema);
4220
+ if (cached) return cached;
4221
+
4222
+ // Strict tool use only supports closed objects. Open maps stay available on
4223
+ // the non-strict schema plan instead of producing an Anthropic 400.
4224
+ if (isJsonSchemaObjectNode(schema) && schema.additionalProperties !== false) {
4225
+ return undefined;
4226
+ }
4227
+
4228
+ const result: Record<string, unknown> = { ...schema };
4229
+ cache.set(schema, result);
4230
+
4231
+ if (hasAnthropicUnionType(result)) {
4232
+ if (budget.unionRemaining <= 0) return undefined;
4233
+ budget.unionRemaining--;
4234
+ budget.unionCount++;
4235
+ }
4236
+
4237
+ if (isRecord(result.properties)) {
4238
+ const originalRequired = new Set(
4239
+ Array.isArray(result.required)
4240
+ ? result.required.filter((entry): entry is string => typeof entry === "string")
4241
+ : [],
4242
+ );
4243
+ const properties: Record<string, unknown> = {};
4244
+ const required: string[] = [];
4245
+
4246
+ for (const [propertyName, propertySchema] of Object.entries(result.properties)) {
4247
+ const normalizedProperty = normalizeAnthropicStrictSchemaNode(propertySchema, budget, cache);
4248
+ if (normalizedProperty === undefined) return undefined;
4249
+
4250
+ if (originalRequired.has(propertyName)) {
4251
+ properties[propertyName] = normalizedProperty;
4252
+ required.push(propertyName);
4253
+ continue;
4254
+ }
4255
+
4256
+ if (budget.optionalRemaining > 0) {
4257
+ budget.optionalRemaining--;
4258
+ budget.optionalCount++;
4259
+ properties[propertyName] = normalizedProperty;
4260
+ continue;
4261
+ }
4262
+
4263
+ const nullableProperty = makeAnthropicNullableSchema(normalizedProperty, budget);
4264
+ if (nullableProperty === undefined) return undefined;
4265
+ properties[propertyName] = nullableProperty;
4266
+ required.push(propertyName);
4267
+ }
4268
+
4269
+ result.properties = properties;
4270
+ result.required = required;
4271
+ }
4272
+
4273
+ if (Array.isArray(result.items)) {
4274
+ const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
4275
+ if (items === undefined) return undefined;
4276
+ result.items = items;
4277
+ } else if (isRecord(result.items)) {
4278
+ const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
4279
+ if (items === undefined) return undefined;
4280
+ result.items = items;
4281
+ }
4282
+ if (Array.isArray(result.prefixItems)) {
4283
+ const prefixItems = normalizeAnthropicStrictSchemaNode(result.prefixItems, budget, cache);
4284
+ if (prefixItems === undefined) return undefined;
4285
+ result.prefixItems = prefixItems;
4286
+ }
4287
+
4288
+ for (const key of COMBINATOR_KEYS) {
4289
+ const variants = result[key];
4290
+ if (!Array.isArray(variants)) continue;
4291
+ const normalizedVariants = normalizeAnthropicStrictSchemaNode(variants, budget, cache);
4292
+ if (normalizedVariants === undefined) return undefined;
4293
+ result[key] = normalizedVariants;
4294
+ }
4295
+
4296
+ for (const defsKey of ["$defs", "definitions"] as const) {
4297
+ const definitions = result[defsKey];
4298
+ if (!isRecord(definitions)) continue;
4299
+ const normalizedDefinitions: Record<string, unknown> = {};
4300
+ for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
4301
+ const normalizedDefinition = normalizeAnthropicStrictSchemaNode(definitionSchema, budget, cache);
4302
+ if (normalizedDefinition === undefined) return undefined;
4303
+ normalizedDefinitions[definitionName] = normalizedDefinition;
4304
+ }
4305
+ result[defsKey] = normalizedDefinitions;
4306
+ }
4307
+
4308
+ return result;
4309
+ }
4310
+
4311
+ function normalizeAnthropicStrictSchema(
4312
+ schema: Record<string, unknown>,
4313
+ optionalRemaining: number,
4314
+ unionRemaining: number,
4315
+ ): { schema: Record<string, unknown>; optionalCount: number; unionCount: number } | undefined {
4316
+ const budget: AnthropicStrictBudget = {
4317
+ optionalRemaining,
4318
+ unionRemaining,
4319
+ optionalCount: 0,
4320
+ unionCount: 0,
4321
+ };
4322
+ const normalized = normalizeAnthropicStrictSchemaNode(schema, budget, new WeakMap());
4323
+ if (!isRecord(normalized)) return undefined;
4324
+ return { schema: normalized, optionalCount: budget.optionalCount, unionCount: budget.unionCount };
4325
+ }
4326
+
4327
+ function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
4328
+ const jsonSchema = toolWireSchema(tool);
4329
+ return flattenToolRootCombinators(
4330
+ normalizeAnthropicToolSchema({
4331
+ ...jsonSchema,
4332
+ type: "object",
4333
+ properties: isRecord(jsonSchema.properties) ? jsonSchema.properties : {},
4334
+ required: Array.isArray(jsonSchema.required)
4335
+ ? jsonSchema.required.filter((entry): entry is string => typeof entry === "string")
4336
+ : [],
4337
+ }) as Record<string, unknown>,
4338
+ );
4339
+ }
4340
+
4341
+ function buildAnthropicToolSchemaPlans(tools: Tool[], disableStrictTools = false): AnthropicToolSchemaPlan[] {
4342
+ const plans = tools.map(
4343
+ (tool): AnthropicToolSchemaPlan => ({
4344
+ inputSchema: buildAnthropicBaseToolInputSchema(tool) as AnthropicToolInputSchema,
4345
+ strict: false,
4346
+ }),
4347
+ );
4348
+ if (NO_STRICT || disableStrictTools) return plans;
4349
+
4350
+ const candidateIndexes = tools.flatMap((tool, index) => {
4351
+ if (!ANTHROPIC_STRICT_TOOL_ALLOWLIST.has(tool.name)) return [];
4352
+ return tool.strict === false ? [] : [index];
4353
+ });
4354
+
4355
+ let strictToolCount = 0;
4356
+ let strictOptionalParameterCount = 0;
4357
+ let strictUnionParameterCount = 0;
4358
+ for (const index of candidateIndexes) {
4359
+ if (strictToolCount >= MAX_ANTHROPIC_STRICT_TOOLS) break;
4360
+
4361
+ const strictResult = normalizeAnthropicStrictSchema(
4362
+ plans[index].inputSchema as Record<string, unknown>,
4363
+ MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS - strictOptionalParameterCount,
4364
+ MAX_ANTHROPIC_STRICT_UNION_PARAMETERS - strictUnionParameterCount,
4365
+ );
4366
+ if (!strictResult) continue;
4367
+
4368
+ plans[index] = {
4369
+ inputSchema: strictResult.schema as AnthropicToolInputSchema,
4370
+ strict: true,
4371
+ };
4372
+ strictToolCount++;
4373
+ strictOptionalParameterCount += strictResult.optionalCount;
4374
+ strictUnionParameterCount += strictResult.unionCount;
4375
+ }
4376
+
4377
+ return plans;
4378
+ }
4379
+
4380
+ function convertTools(
4381
+ tools: Tool[],
4382
+ isOAuthToken: boolean,
4383
+ disableStrictTools = false,
4384
+ supportsEagerToolInputStreaming = true,
4385
+ ): Anthropic.Messages.Tool[] {
4386
+ if (!tools) return [];
4387
+ const schemaPlans = buildAnthropicToolSchemaPlans(tools, disableStrictTools);
4388
+
4389
+ return tools.map((tool, index) => {
4390
+ const plan = schemaPlans[index];
4391
+ return {
4392
+ name: isOAuthToken ? applyClaudeToolPrefix(tool.name) : tool.name,
4393
+ description: tool.description || "",
4394
+ input_schema: plan.inputSchema,
4395
+ ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
4396
+ ...(plan.strict ? { strict: true } : {}),
4397
+ };
4398
+ });
4399
+ }
4400
+
4401
+ function mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason {
4402
+ switch (reason) {
4403
+ case "end_turn":
4404
+ return "stop";
4405
+ case "max_tokens":
4406
+ return "length";
4407
+ case "tool_use":
4408
+ return "toolUse";
4409
+ case "refusal":
4410
+ return "error";
4411
+ case "pause_turn": // Stop is good enough -> resubmit
4412
+ return "stop";
4413
+ case "stop_sequence":
4414
+ return "stop"; // We don't supply stop sequences, so this should never happen
4415
+ case "sensitive": // Content flagged by safety filters (not yet in SDK types)
4416
+ return "error";
4417
+ default:
4418
+ // Handle unknown stop reasons gracefully (API may add new values)
4419
+ throw new Error(`Unhandled stop reason: ${reason}`);
4420
+ }
4421
+ }