@gajae-code/ai 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +2644 -0
  2. package/README.md +1181 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +5 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +96 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/types.d.ts +105 -0
  10. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  11. package/dist/types/auth-gateway/http.d.ts +39 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -0
  13. package/dist/types/auth-gateway/server.d.ts +17 -0
  14. package/dist/types/auth-gateway/types.d.ts +115 -0
  15. package/dist/types/auth-storage.d.ts +641 -0
  16. package/dist/types/cli.d.ts +2 -0
  17. package/dist/types/index.d.ts +49 -0
  18. package/dist/types/model-cache.d.ts +17 -0
  19. package/dist/types/model-manager.d.ts +62 -0
  20. package/dist/types/model-thinking.d.ts +71 -0
  21. package/dist/types/models.d.ts +12 -0
  22. package/dist/types/provider-details.d.ts +24 -0
  23. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  24. package/dist/types/provider-models/descriptors.d.ts +48 -0
  25. package/dist/types/provider-models/google.d.ts +20 -0
  26. package/dist/types/provider-models/index.d.ts +5 -0
  27. package/dist/types/provider-models/ollama.d.ts +7 -0
  28. package/dist/types/provider-models/openai-compat.d.ts +237 -0
  29. package/dist/types/provider-models/special.d.ts +16 -0
  30. package/dist/types/providers/amazon-bedrock.d.ts +36 -0
  31. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  32. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  33. package/dist/types/providers/anthropic.d.ts +188 -0
  34. package/dist/types/providers/aws-credentials.d.ts +43 -0
  35. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  36. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  37. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  38. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  39. package/dist/types/providers/cursor.d.ts +42 -0
  40. package/dist/types/providers/error-message.d.ts +27 -0
  41. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  42. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  43. package/dist/types/providers/google-auth.d.ts +24 -0
  44. package/dist/types/providers/google-gemini-cli.d.ts +72 -0
  45. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  46. package/dist/types/providers/google-shared.d.ts +163 -0
  47. package/dist/types/providers/google-types.d.ts +138 -0
  48. package/dist/types/providers/google-vertex.d.ts +7 -0
  49. package/dist/types/providers/google.d.ts +4 -0
  50. package/dist/types/providers/grammar.d.ts +1 -0
  51. package/dist/types/providers/kimi.d.ts +27 -0
  52. package/dist/types/providers/mock.d.ts +175 -0
  53. package/dist/types/providers/ollama.d.ts +6 -0
  54. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  55. package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
  56. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  57. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  58. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  59. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  60. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  61. package/dist/types/providers/openai-completions-compat.d.ts +25 -0
  62. package/dist/types/providers/openai-completions.d.ts +33 -0
  63. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  64. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  65. package/dist/types/providers/openai-responses-shared.d.ts +89 -0
  66. package/dist/types/providers/openai-responses.d.ts +32 -0
  67. package/dist/types/providers/pi-native-client.d.ts +13 -0
  68. package/dist/types/providers/pi-native-server.d.ts +68 -0
  69. package/dist/types/providers/register-builtins.d.ts +31 -0
  70. package/dist/types/providers/synthetic.d.ts +26 -0
  71. package/dist/types/providers/transform-messages.d.ts +12 -0
  72. package/dist/types/providers/vision-guard.d.ts +8 -0
  73. package/dist/types/rate-limit-utils.d.ts +19 -0
  74. package/dist/types/stream.d.ts +24 -0
  75. package/dist/types/types.d.ts +746 -0
  76. package/dist/types/usage/claude.d.ts +3 -0
  77. package/dist/types/usage/gemini.d.ts +2 -0
  78. package/dist/types/usage/github-copilot.d.ts +7 -0
  79. package/dist/types/usage/google-antigravity.d.ts +2 -0
  80. package/dist/types/usage/kimi.d.ts +2 -0
  81. package/dist/types/usage/minimax-code.d.ts +2 -0
  82. package/dist/types/usage/openai-codex.d.ts +3 -0
  83. package/dist/types/usage/shared.d.ts +1 -0
  84. package/dist/types/usage/zai.d.ts +2 -0
  85. package/dist/types/usage.d.ts +258 -0
  86. package/dist/types/utils/abort.d.ts +19 -0
  87. package/dist/types/utils/anthropic-auth.d.ts +31 -0
  88. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  89. package/dist/types/utils/discovery/codex.d.ts +38 -0
  90. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  91. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  92. package/dist/types/utils/discovery/index.d.ts +4 -0
  93. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  94. package/dist/types/utils/event-stream.d.ts +28 -0
  95. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  96. package/dist/types/utils/foundry.d.ts +1 -0
  97. package/dist/types/utils/h2-fetch.d.ts +22 -0
  98. package/dist/types/utils/http-inspector.d.ts +31 -0
  99. package/dist/types/utils/idle-iterator.d.ts +67 -0
  100. package/dist/types/utils/json-parse.d.ts +10 -0
  101. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  102. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  103. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  104. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  105. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  106. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  107. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  108. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  109. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  110. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  111. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  112. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  113. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  114. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  115. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  116. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  117. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  118. package/dist/types/utils/oauth/index.d.ts +38 -0
  119. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  120. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  121. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  122. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  123. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  124. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  125. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  126. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  127. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  128. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  129. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  130. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  131. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  132. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  133. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  134. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  135. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  136. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  137. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  138. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  139. package/dist/types/utils/oauth/together.d.ts +1 -0
  140. package/dist/types/utils/oauth/types.d.ts +44 -0
  141. package/dist/types/utils/oauth/venice.d.ts +18 -0
  142. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  143. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  144. package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
  145. package/dist/types/utils/oauth/zai.d.ts +18 -0
  146. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  147. package/dist/types/utils/overflow.d.ts +54 -0
  148. package/dist/types/utils/parse-bind.d.ts +23 -0
  149. package/dist/types/utils/provider-response.d.ts +3 -0
  150. package/dist/types/utils/retry-after.d.ts +3 -0
  151. package/dist/types/utils/retry.d.ts +26 -0
  152. package/dist/types/utils/schema/adapt.d.ts +24 -0
  153. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  154. package/dist/types/utils/schema/dereference.d.ts +11 -0
  155. package/dist/types/utils/schema/draft.d.ts +10 -0
  156. package/dist/types/utils/schema/equality.d.ts +4 -0
  157. package/dist/types/utils/schema/fields.d.ts +49 -0
  158. package/dist/types/utils/schema/index.d.ts +13 -0
  159. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  160. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  161. package/dist/types/utils/schema/normalize.d.ts +93 -0
  162. package/dist/types/utils/schema/spill.d.ts +8 -0
  163. package/dist/types/utils/schema/stamps.d.ts +25 -0
  164. package/dist/types/utils/schema/types.d.ts +4 -0
  165. package/dist/types/utils/schema/wire.d.ts +54 -0
  166. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  167. package/dist/types/utils/sse-debug.d.ts +10 -0
  168. package/dist/types/utils/tool-call-healing.d.ts +71 -0
  169. package/dist/types/utils/tool-choice.d.ts +50 -0
  170. package/dist/types/utils/validation.d.ts +17 -0
  171. package/dist/types/utils.d.ts +28 -0
  172. package/package.json +146 -0
  173. package/src/api-registry.ts +96 -0
  174. package/src/auth-broker/client.ts +358 -0
  175. package/src/auth-broker/index.ts +5 -0
  176. package/src/auth-broker/refresher.ts +127 -0
  177. package/src/auth-broker/remote-store.ts +623 -0
  178. package/src/auth-broker/server.ts +644 -0
  179. package/src/auth-broker/types.ts +127 -0
  180. package/src/auth-broker/wire-schemas.ts +200 -0
  181. package/src/auth-gateway/http.ts +194 -0
  182. package/src/auth-gateway/index.ts +3 -0
  183. package/src/auth-gateway/server.ts +717 -0
  184. package/src/auth-gateway/types.ts +134 -0
  185. package/src/auth-storage.ts +4104 -0
  186. package/src/cli.ts +262 -0
  187. package/src/index.ts +54 -0
  188. package/src/model-cache.ts +129 -0
  189. package/src/model-manager.ts +450 -0
  190. package/src/model-thinking.ts +691 -0
  191. package/src/models.json +73853 -0
  192. package/src/models.json.d.ts +9 -0
  193. package/src/models.ts +56 -0
  194. package/src/prompts/turn-aborted-guidance.md +4 -0
  195. package/src/provider-details.ts +90 -0
  196. package/src/provider-models/bundled-references.ts +38 -0
  197. package/src/provider-models/descriptors.ts +308 -0
  198. package/src/provider-models/google.ts +91 -0
  199. package/src/provider-models/index.ts +5 -0
  200. package/src/provider-models/ollama.ts +153 -0
  201. package/src/provider-models/openai-compat.ts +2275 -0
  202. package/src/provider-models/special.ts +67 -0
  203. package/src/providers/amazon-bedrock.ts +849 -0
  204. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  205. package/src/providers/anthropic-messages-server.ts +677 -0
  206. package/src/providers/anthropic.ts +2696 -0
  207. package/src/providers/aws-credentials.ts +501 -0
  208. package/src/providers/aws-eventstream.ts +185 -0
  209. package/src/providers/aws-sigv4.ts +218 -0
  210. package/src/providers/azure-openai-responses.ts +337 -0
  211. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  212. package/src/providers/cursor/proto/agent.proto +3526 -0
  213. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  214. package/src/providers/cursor/proto/buf.yaml +17 -0
  215. package/src/providers/cursor.ts +2561 -0
  216. package/src/providers/error-message.ts +21 -0
  217. package/src/providers/github-copilot-headers.ts +140 -0
  218. package/src/providers/gitlab-duo.ts +372 -0
  219. package/src/providers/google-auth.ts +252 -0
  220. package/src/providers/google-gemini-cli.ts +795 -0
  221. package/src/providers/google-gemini-headers.ts +41 -0
  222. package/src/providers/google-shared.ts +902 -0
  223. package/src/providers/google-types.ts +167 -0
  224. package/src/providers/google-vertex.ts +88 -0
  225. package/src/providers/google.ts +41 -0
  226. package/src/providers/grammar.ts +70 -0
  227. package/src/providers/kimi.ts +52 -0
  228. package/src/providers/mock.ts +500 -0
  229. package/src/providers/ollama.ts +544 -0
  230. package/src/providers/openai-anthropic-shim.ts +138 -0
  231. package/src/providers/openai-chat-server-schema.ts +243 -0
  232. package/src/providers/openai-chat-server.ts +628 -0
  233. package/src/providers/openai-codex/constants.ts +43 -0
  234. package/src/providers/openai-codex/request-transformer.ts +161 -0
  235. package/src/providers/openai-codex/response-handler.ts +81 -0
  236. package/src/providers/openai-codex-responses.ts +2598 -0
  237. package/src/providers/openai-completions-compat.ts +279 -0
  238. package/src/providers/openai-completions.ts +1853 -0
  239. package/src/providers/openai-responses-server-schema.ts +290 -0
  240. package/src/providers/openai-responses-server.ts +1183 -0
  241. package/src/providers/openai-responses-shared.ts +800 -0
  242. package/src/providers/openai-responses.ts +621 -0
  243. package/src/providers/pi-native-client.ts +228 -0
  244. package/src/providers/pi-native-server.ts +210 -0
  245. package/src/providers/register-builtins.ts +412 -0
  246. package/src/providers/synthetic.ts +50 -0
  247. package/src/providers/transform-messages.ts +309 -0
  248. package/src/providers/vision-guard.ts +31 -0
  249. package/src/rate-limit-utils.ts +84 -0
  250. package/src/stream.ts +895 -0
  251. package/src/types.ts +884 -0
  252. package/src/usage/claude.ts +431 -0
  253. package/src/usage/gemini.ts +250 -0
  254. package/src/usage/github-copilot.ts +421 -0
  255. package/src/usage/google-antigravity.ts +201 -0
  256. package/src/usage/kimi.ts +271 -0
  257. package/src/usage/minimax-code.ts +31 -0
  258. package/src/usage/openai-codex.ts +503 -0
  259. package/src/usage/shared.ts +10 -0
  260. package/src/usage/zai.ts +247 -0
  261. package/src/usage.ts +183 -0
  262. package/src/utils/abort.ts +51 -0
  263. package/src/utils/anthropic-auth.ts +87 -0
  264. package/src/utils/discovery/antigravity.ts +261 -0
  265. package/src/utils/discovery/codex.ts +371 -0
  266. package/src/utils/discovery/cursor.ts +306 -0
  267. package/src/utils/discovery/gemini.ts +248 -0
  268. package/src/utils/discovery/index.ts +4 -0
  269. package/src/utils/discovery/openai-compatible.ts +224 -0
  270. package/src/utils/event-stream.ts +142 -0
  271. package/src/utils/fireworks-model-id.ts +30 -0
  272. package/src/utils/foundry.ts +8 -0
  273. package/src/utils/h2-fetch.ts +60 -0
  274. package/src/utils/http-inspector.ts +176 -0
  275. package/src/utils/idle-iterator.ts +250 -0
  276. package/src/utils/json-parse.ts +148 -0
  277. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  278. package/src/utils/oauth/anthropic.ts +200 -0
  279. package/src/utils/oauth/api-key-login.ts +87 -0
  280. package/src/utils/oauth/api-key-validation.ts +92 -0
  281. package/src/utils/oauth/callback-server.ts +276 -0
  282. package/src/utils/oauth/cerebras.ts +16 -0
  283. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  284. package/src/utils/oauth/cursor.ts +157 -0
  285. package/src/utils/oauth/deepseek.ts +53 -0
  286. package/src/utils/oauth/firepass.ts +24 -0
  287. package/src/utils/oauth/fireworks.ts +15 -0
  288. package/src/utils/oauth/github-copilot.ts +362 -0
  289. package/src/utils/oauth/gitlab-duo.ts +123 -0
  290. package/src/utils/oauth/google-antigravity.ts +200 -0
  291. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  292. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  293. package/src/utils/oauth/huggingface.ts +62 -0
  294. package/src/utils/oauth/index.ts +444 -0
  295. package/src/utils/oauth/kagi.ts +47 -0
  296. package/src/utils/oauth/kilo.ts +87 -0
  297. package/src/utils/oauth/kimi.ts +254 -0
  298. package/src/utils/oauth/litellm.ts +47 -0
  299. package/src/utils/oauth/lm-studio.ts +38 -0
  300. package/src/utils/oauth/minimax-code.ts +78 -0
  301. package/src/utils/oauth/moonshot.ts +16 -0
  302. package/src/utils/oauth/nanogpt.ts +15 -0
  303. package/src/utils/oauth/nvidia.ts +70 -0
  304. package/src/utils/oauth/oauth.html +199 -0
  305. package/src/utils/oauth/ollama-cloud.ts +28 -0
  306. package/src/utils/oauth/ollama.ts +47 -0
  307. package/src/utils/oauth/openai-codex.ts +299 -0
  308. package/src/utils/oauth/opencode.ts +49 -0
  309. package/src/utils/oauth/parallel.ts +46 -0
  310. package/src/utils/oauth/perplexity.ts +206 -0
  311. package/src/utils/oauth/pkce.ts +18 -0
  312. package/src/utils/oauth/qianfan.ts +58 -0
  313. package/src/utils/oauth/qwen-portal.ts +60 -0
  314. package/src/utils/oauth/synthetic.ts +16 -0
  315. package/src/utils/oauth/tavily.ts +46 -0
  316. package/src/utils/oauth/together.ts +16 -0
  317. package/src/utils/oauth/types.ts +94 -0
  318. package/src/utils/oauth/venice.ts +59 -0
  319. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  320. package/src/utils/oauth/vllm.ts +40 -0
  321. package/src/utils/oauth/xiaomi.ts +137 -0
  322. package/src/utils/oauth/zai.ts +60 -0
  323. package/src/utils/oauth/zenmux.ts +15 -0
  324. package/src/utils/overflow.ts +137 -0
  325. package/src/utils/parse-bind.ts +54 -0
  326. package/src/utils/provider-response.ts +30 -0
  327. package/src/utils/retry-after.ts +110 -0
  328. package/src/utils/retry.ts +54 -0
  329. package/src/utils/schema/CONSTRAINTS.md +164 -0
  330. package/src/utils/schema/adapt.ts +36 -0
  331. package/src/utils/schema/compatibility.ts +435 -0
  332. package/src/utils/schema/dereference.ts +98 -0
  333. package/src/utils/schema/draft.ts +341 -0
  334. package/src/utils/schema/equality.ts +97 -0
  335. package/src/utils/schema/fields.ts +190 -0
  336. package/src/utils/schema/index.ts +13 -0
  337. package/src/utils/schema/json-schema-validator.ts +577 -0
  338. package/src/utils/schema/meta-validator.ts +167 -0
  339. package/src/utils/schema/normalize.ts +1588 -0
  340. package/src/utils/schema/spill.ts +43 -0
  341. package/src/utils/schema/stamps.ts +97 -0
  342. package/src/utils/schema/types.ts +11 -0
  343. package/src/utils/schema/wire.ts +213 -0
  344. package/src/utils/schema/zod-decontaminate.ts +331 -0
  345. package/src/utils/sse-debug.ts +289 -0
  346. package/src/utils/tool-call-healing.ts +271 -0
  347. package/src/utils/tool-choice.ts +99 -0
  348. package/src/utils/validation.ts +1019 -0
  349. package/src/utils.ts +166 -0
@@ -0,0 +1,2696 @@
1
+ import * as nodeCrypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import { scheduler } from "node:timers/promises";
4
+ import * as tls from "node:tls";
5
+ import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
6
+ import type {
7
+ ContentBlockParam,
8
+ MessageCreateParamsStreaming,
9
+ MessageParam,
10
+ RawMessageStreamEvent,
11
+ } from "@anthropic-ai/sdk/resources/messages";
12
+ import {
13
+ $env,
14
+ extractHttpStatusFromError,
15
+ isEnoent,
16
+ isRetryableError,
17
+ isUnexpectedSocketCloseMessage,
18
+ logger,
19
+ readSseEvents,
20
+ } from "@gajae-code/utils";
21
+ import { hasOpus47ApiRestrictions, mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
22
+ import { calculateCost } from "../models";
23
+ import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
24
+ import type {
25
+ Api,
26
+ AssistantMessage,
27
+ CacheRetention,
28
+ Context,
29
+ FetchImpl,
30
+ ImageContent,
31
+ Message,
32
+ Model,
33
+ ProviderSessionState,
34
+ RedactedThinkingContent,
35
+ ServiceTier,
36
+ SimpleStreamOptions,
37
+ StopReason,
38
+ StreamFunction,
39
+ StreamOptions,
40
+ TextContent,
41
+ ThinkingContent,
42
+ Tool,
43
+ ToolCall,
44
+ ToolResultMessage,
45
+ Usage,
46
+ } from "../types";
47
+ import { resolveServiceTier } from "../types";
48
+ import {
49
+ isAnthropicOAuthToken,
50
+ isRecord,
51
+ normalizeSystemPrompts,
52
+ normalizeToolCallId,
53
+ resolveCacheRetention,
54
+ } from "../utils";
55
+ import { createAbortSourceTracker } from "../utils/abort";
56
+ import { AssistantMessageEventStream } from "../utils/event-stream";
57
+ import { isFoundryEnabled } from "../utils/foundry";
58
+ import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
59
+ import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
60
+ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
61
+ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
62
+ import { notifyProviderResponse } from "../utils/provider-response";
63
+ import { isCopilotTransientModelError } from "../utils/retry";
64
+ import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
65
+ import { spillToDescription } from "../utils/schema/spill";
66
+ import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
67
+ import {
68
+ buildCopilotDynamicHeaders,
69
+ hasCopilotVisionInput,
70
+ resolveGitHubCopilotBaseUrl,
71
+ } from "./github-copilot-headers";
72
+ import { transformMessages } from "./transform-messages";
73
+ import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
74
+
75
+ export type AnthropicHeaderOptions = {
76
+ apiKey: string;
77
+ baseUrl?: string;
78
+ isOAuth?: boolean;
79
+ extraBetas?: string[];
80
+ stream?: boolean;
81
+ modelHeaders?: Record<string, string>;
82
+ isCloudflareAiGateway?: boolean;
83
+ };
84
+
85
+ export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
86
+ const trimmed = baseUrl?.trim();
87
+ if (!trimmed) {
88
+ return undefined;
89
+ }
90
+ const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
91
+ return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
92
+ }
93
+
94
+ // Build deduplicated beta header string
95
+ export function buildBetaHeader(baseBetas: string[], extraBetas: string[]): string {
96
+ const seen = new Set<string>();
97
+ const result: string[] = [];
98
+ for (const beta of [...baseBetas, ...extraBetas]) {
99
+ const trimmed = beta.trim();
100
+ if (trimmed && !seen.has(trimmed)) {
101
+ seen.add(trimmed);
102
+ result.push(trimmed);
103
+ }
104
+ }
105
+ return result.join(",");
106
+ }
107
+
108
+ const claudeCodeBetaDefaults = [
109
+ "claude-code-20250219",
110
+ "oauth-2025-04-20",
111
+ "context-management-2025-06-27",
112
+ "prompt-caching-scope-2026-01-05",
113
+ ];
114
+ const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
115
+ const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
116
+ const fastModeBeta = "fast-mode-2026-02-01";
117
+
118
+ function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined {
119
+ if (!headers) return undefined;
120
+ const normalizedName = headerName.toLowerCase();
121
+ for (const [key, value] of Object.entries(headers)) {
122
+ if (key.toLowerCase() === normalizedName) return value;
123
+ }
124
+ return undefined;
125
+ }
126
+
127
+ function isClaudeCodeClientUserAgent(userAgent: string | undefined): userAgent is string {
128
+ if (!userAgent) return false;
129
+ return userAgent.toLowerCase().startsWith("claude-cli");
130
+ }
131
+
132
+ function isAnthropicApiBaseUrl(baseUrl?: string): boolean {
133
+ if (!baseUrl) return true;
134
+ try {
135
+ const url = new URL(baseUrl);
136
+ return url.protocol.toLowerCase() === "https:" && url.hostname.toLowerCase() === "api.anthropic.com";
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ const sharedHeaders = {
143
+ "Accept-Encoding": "gzip, deflate, br, zstd",
144
+ Connection: "keep-alive",
145
+ "Content-Type": "application/json",
146
+ "Anthropic-Version": "2023-06-01",
147
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
148
+ "X-App": "cli",
149
+ };
150
+
151
+ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
152
+ const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
153
+ const extraBetas = options.extraBetas ?? [];
154
+ const stream = options.stream ?? false;
155
+ const betaHeader = buildBetaHeader(claudeCodeBetaDefaults, extraBetas);
156
+ const acceptHeader = stream ? "text/event-stream" : "application/json";
157
+ const modelHeaders = Object.fromEntries(
158
+ Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
159
+ );
160
+
161
+ if (options.isCloudflareAiGateway) {
162
+ return {
163
+ ...modelHeaders,
164
+ Accept: acceptHeader,
165
+ ...sharedHeaders,
166
+ "Anthropic-Beta": betaHeader,
167
+ "cf-aig-authorization": `Bearer ${options.apiKey}`,
168
+ };
169
+ }
170
+
171
+ if (oauthToken) {
172
+ const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
173
+ const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
174
+ ? incomingUserAgent
175
+ : `claude-cli/${claudeCodeVersion} (external, cli)`;
176
+ return {
177
+ ...modelHeaders,
178
+ ...claudeCodeHeaders,
179
+ Accept: acceptHeader,
180
+ Authorization: `Bearer ${options.apiKey}`,
181
+ ...sharedHeaders,
182
+ "Anthropic-Beta": betaHeader,
183
+ "User-Agent": userAgent,
184
+ };
185
+ } else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
186
+ return {
187
+ ...modelHeaders,
188
+ Accept: acceptHeader,
189
+ Authorization: `Bearer ${options.apiKey}`,
190
+ ...sharedHeaders,
191
+ "Anthropic-Beta": betaHeader,
192
+ };
193
+ } else {
194
+ return {
195
+ ...modelHeaders,
196
+ Accept: acceptHeader,
197
+ ...sharedHeaders,
198
+ "Anthropic-Beta": betaHeader,
199
+ "X-Api-Key": options.apiKey,
200
+ };
201
+ }
202
+ }
203
+
204
+ type AnthropicCacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
205
+
206
+ type AnthropicSamplingParams = MessageCreateParamsStreaming & {
207
+ top_p?: number;
208
+ top_k?: number;
209
+ };
210
+
211
+ const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
212
+ let warnedStopSequencesTrim = false;
213
+
214
+ /**
215
+ * Adaptive thinking `display` is supported starting with Anthropic model Opus 4.7.
216
+ * Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
217
+ */
218
+ function supportsAdaptiveThinkingDisplay(modelId: string): boolean {
219
+ const match = /claude-opus-(\d+)-(\d+)/.exec(modelId);
220
+ if (!match) return false;
221
+ const major = Number(match[1]);
222
+ const minor = Number(match[2]);
223
+ return major > 4 || (major === 4 && minor >= 7);
224
+ }
225
+
226
+ const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
227
+
228
+ type AnthropicProviderSessionState = ProviderSessionState & {
229
+ strictToolsDisabled: boolean;
230
+ fastModeDisabled: boolean;
231
+ };
232
+
233
+ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
234
+ const state: AnthropicProviderSessionState = {
235
+ strictToolsDisabled: false,
236
+ fastModeDisabled: false,
237
+ close: () => {
238
+ state.strictToolsDisabled = false;
239
+ state.fastModeDisabled = false;
240
+ },
241
+ };
242
+ return state;
243
+ }
244
+
245
+ function getAnthropicProviderSessionState(
246
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
247
+ ): AnthropicProviderSessionState | undefined {
248
+ if (!providerSessionState) return undefined;
249
+ const existing = providerSessionState.get(ANTHROPIC_PROVIDER_SESSION_STATE_KEY) as
250
+ | AnthropicProviderSessionState
251
+ | undefined;
252
+ if (existing) return existing;
253
+ const created = createAnthropicProviderSessionState();
254
+ providerSessionState.set(ANTHROPIC_PROVIDER_SESSION_STATE_KEY, created);
255
+ return created;
256
+ }
257
+
258
+ /**
259
+ * Clears the in-session "server rejected fast mode" sticky flag. Call when the
260
+ * caller is explicitly re-arming `serviceTier: "priority"` (e.g. user toggled
261
+ * `/fast on` after a previous turn auto-disabled it) so the next request
262
+ * actually carries `speed: "fast"` again. No-op when the map or state entry
263
+ * hasn't been materialized yet.
264
+ */
265
+ export function clearAnthropicFastModeFallback(
266
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
267
+ ): void {
268
+ if (!providerSessionState) return;
269
+ const state = providerSessionState.get(ANTHROPIC_PROVIDER_SESSION_STATE_KEY) as
270
+ | AnthropicProviderSessionState
271
+ | undefined;
272
+ if (state) state.fastModeDisabled = false;
273
+ }
274
+
275
+ function isAnthropicStrictGrammarTooLargeError(error: unknown): boolean {
276
+ if (extractHttpStatusFromError(error) !== 400) return false;
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ const isStrictGrammarTooLarge = /compiled grammar/i.test(message) && /too large/i.test(message);
279
+ const isSchemaCompilationTooComplex =
280
+ /schema/i.test(message) && /too complex/i.test(message) && /compil/i.test(message);
281
+ return /invalid_request_error/i.test(message) && (isStrictGrammarTooLarge || isSchemaCompilationTooComplex);
282
+ }
283
+
284
+ export function isAnthropicFastModeUnsupportedError(error: unknown): boolean {
285
+ const status = extractHttpStatusFromError(error);
286
+ if (status !== 400 && status !== 429) return false;
287
+ const message = error instanceof Error ? error.message : String(error);
288
+ // 400 invalid_request_error — model doesn't accept `speed` at all.
289
+ // Observed: "'Anthropic model-opus-4-5-20251101' does not support the `speed` parameter."
290
+ // Stay tolerant of phrasing drift ("is not supported", quoted vs backticked field).
291
+ if (
292
+ status === 400 &&
293
+ /invalid_request_error/i.test(message) &&
294
+ /\bspeed\b/i.test(message) &&
295
+ /not support/i.test(message)
296
+ ) {
297
+ return true;
298
+ }
299
+ // 429 rate_limit_error — account lacks the extra-usage entitlement fast mode requires.
300
+ // Observed: "Extra usage is required for fast mode."
301
+ if (status === 429 && /rate_limit_error/i.test(message) && /fast mode/i.test(message)) {
302
+ return true;
303
+ }
304
+ return false;
305
+ }
306
+
307
+ function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
308
+ const tools = params.tools as Array<{ strict?: unknown }> | undefined;
309
+ return tools?.some(tool => tool.strict === true) ?? false;
310
+ }
311
+
312
+ /**
313
+ * `speed` lives on `BetaMessageCreateParams` (client.beta.messages) but this
314
+ * provider posts via `client.messages.create`, whose param type doesn't
315
+ * include it. This alias narrows the cast to one place.
316
+ */
317
+ type ParamsWithSpeed = MessageCreateParamsStreaming & { speed?: "fast" };
318
+
319
+ function dropAnthropicFastMode(params: MessageCreateParamsStreaming): void {
320
+ delete (params as ParamsWithSpeed).speed;
321
+ }
322
+
323
+ function dropAnthropicStrictTools(params: MessageCreateParamsStreaming): void {
324
+ const tools = params.tools as Array<{ strict?: unknown }> | undefined;
325
+ if (!tools) return;
326
+ for (const tool of tools) {
327
+ delete tool.strict;
328
+ }
329
+ }
330
+
331
+ function getCacheControl(
332
+ model: Model<"anthropic-messages">,
333
+ baseUrl: string,
334
+ cacheRetention?: CacheRetention,
335
+ ): { retention: CacheRetention; cacheControl?: AnthropicCacheControl } {
336
+ const retention = resolveCacheRetention(cacheRetention);
337
+ if (retention === "none") {
338
+ return { retention };
339
+ }
340
+ const ttl =
341
+ retention === "long" && isAnthropicApiBaseUrl(baseUrl) && getAnthropicCompat(model).supportsLongCacheRetention
342
+ ? "1h"
343
+ : undefined;
344
+ return {
345
+ retention,
346
+ cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
347
+ };
348
+ }
349
+
350
+ // Stealth mode: Mimic Anthropic Code headers and tool prefixing.
351
+ export const claudeCodeVersion = "2.1.63";
352
+ export const claudeToolPrefix: string = "proxy_";
353
+ export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
354
+
355
+ export function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
356
+ switch (platform.toLowerCase()) {
357
+ case "darwin":
358
+ return "MacOS";
359
+ case "windows":
360
+ case "win32":
361
+ return "Windows";
362
+ case "linux":
363
+ return "Linux";
364
+ case "freebsd":
365
+ return "FreeBSD";
366
+ default:
367
+ return `Other::${platform.toLowerCase()}`;
368
+ }
369
+ }
370
+
371
+ export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
372
+ switch (arch.toLowerCase()) {
373
+ case "amd64":
374
+ case "x64":
375
+ return "x64";
376
+ case "arm64":
377
+ case "aarch64":
378
+ return "arm64";
379
+ case "386":
380
+ case "x86":
381
+ case "ia32":
382
+ return "x86";
383
+ default:
384
+ return `other::${arch.toLowerCase()}`;
385
+ }
386
+ }
387
+
388
+ export const claudeCodeHeaders = {
389
+ "X-Stainless-Retry-Count": "0",
390
+ "X-Stainless-Runtime-Version": "v24.3.0",
391
+ "X-Stainless-Package-Version": "0.74.0",
392
+ "X-Stainless-Runtime": "node",
393
+ "X-Stainless-Lang": "js",
394
+ "X-Stainless-Arch": mapStainlessArch(process.arch),
395
+ "X-Stainless-Os": mapStainlessOs(process.platform),
396
+ "X-Stainless-Timeout": "600",
397
+ } as const;
398
+
399
+ const enforcedHeaderKeys = new Set(
400
+ [
401
+ ...Object.keys(claudeCodeHeaders),
402
+ "Accept",
403
+ "Accept-Encoding",
404
+ "Connection",
405
+ "Content-Type",
406
+ "Anthropic-Version",
407
+ "Anthropic-Dangerous-Direct-Browser-Access",
408
+ "Anthropic-Beta",
409
+ "User-Agent",
410
+ "X-App",
411
+ "Authorization",
412
+ "X-Api-Key",
413
+ "cf-aig-authorization",
414
+ ].map(key => key.toLowerCase()),
415
+ );
416
+
417
+ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
418
+
419
+ function createClaudeBillingHeader(payload: unknown): string {
420
+ const payloadJson = JSON.stringify(payload) ?? "";
421
+ const cch = nodeCrypto.createHash("sha256").update(payloadJson).digest("hex").slice(0, 5);
422
+ const randomBytes = new Uint8Array(2);
423
+ crypto.getRandomValues(randomBytes);
424
+ const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
425
+ .join("")
426
+ .slice(0, 3);
427
+ return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint=cli; cch=${cch};`;
428
+ }
429
+
430
+ const CLAUDE_CLOAKING_USER_ID_REGEX =
431
+ /^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}$/;
432
+
433
+ export function isClaudeCloakingUserId(userId: string): boolean {
434
+ return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
435
+ }
436
+
437
+ /**
438
+ * Real Anthropic Code sends `metadata.user_id` as a JSON-stringified object of the
439
+ * shape `{ device_id, account_uuid, session_id, ...extra }` (see
440
+ * services/api/Anthropic model.ts → getAPIMetadata). Accept that shape so callers that
441
+ * supply a stable `session_id` aren't silently overwritten with fresh entropy
442
+ * on every request, which would inflate the backend session count.
443
+ */
444
+ function isClaudeJsonUserId(userId: string): boolean {
445
+ if (userId.length === 0 || userId[0] !== "{") return false;
446
+ let parsed: unknown;
447
+ try {
448
+ parsed = JSON.parse(userId);
449
+ } catch {
450
+ return false;
451
+ }
452
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
453
+ const obj = parsed as Record<string, unknown>;
454
+ return typeof obj.session_id === "string" && obj.session_id.length > 0;
455
+ }
456
+
457
+ export function generateClaudeCloakingUserId(): string {
458
+ const userHash = nodeCrypto.randomBytes(32).toString("hex");
459
+ const accountId = nodeCrypto.randomUUID().toLowerCase();
460
+ const sessionId = nodeCrypto.randomUUID().toLowerCase();
461
+ return `user_${userHash}_account_${accountId}_session_${sessionId}`;
462
+ }
463
+
464
+ function resolveAnthropicMetadataUserId(userId: unknown, isOAuthToken: boolean): string | undefined {
465
+ if (typeof userId === "string") {
466
+ if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
467
+ return userId;
468
+ }
469
+ }
470
+
471
+ if (!isOAuthToken) return undefined;
472
+ return generateClaudeCloakingUserId();
473
+ }
474
+ const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
475
+ export const applyClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
476
+ if (!prefixOverride) return name;
477
+ if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
478
+ const prefix = prefixOverride.toLowerCase();
479
+ if (name.toLowerCase().startsWith(prefix)) return name;
480
+ return `${prefixOverride}${name}`;
481
+ };
482
+
483
+ export const stripClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
484
+ if (!prefixOverride) return name;
485
+ const prefix = prefixOverride.toLowerCase();
486
+ if (!name.toLowerCase().startsWith(prefix)) return name;
487
+ return name.slice(prefixOverride.length);
488
+ };
489
+
490
+ /**
491
+ * Convert content blocks to Anthropic API format
492
+ */
493
+ function convertContentBlocks(
494
+ content: (TextContent | ImageContent)[],
495
+ supportsImages = true,
496
+ ):
497
+ | string
498
+ | Array<
499
+ | { type: "text"; text: string }
500
+ | {
501
+ type: "image";
502
+ source: {
503
+ type: "base64";
504
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
505
+ data: string;
506
+ };
507
+ }
508
+ > {
509
+ const textBlocks = content
510
+ .filter((block): block is TextContent => block.type === "text")
511
+ .map(block => block.text.toWellFormed())
512
+ .filter(text => text.trim().length > 0);
513
+ const imageBlocks = content.filter((block): block is ImageContent => block.type === "image");
514
+ const omittedImages = !supportsImages && imageBlocks.length > 0;
515
+ if (imageBlocks.length === 0 || !supportsImages) {
516
+ if (omittedImages) {
517
+ textBlocks.push(NON_VISION_IMAGE_PLACEHOLDER);
518
+ }
519
+ return textBlocks.join("\n").toWellFormed();
520
+ }
521
+
522
+ const blocks = [
523
+ ...textBlocks.map(text => ({
524
+ type: "text" as const,
525
+ text,
526
+ })),
527
+ ...imageBlocks.map(block => ({
528
+ type: "image" as const,
529
+ source: {
530
+ type: "base64" as const,
531
+ media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
532
+ data: block.data,
533
+ },
534
+ })),
535
+ ];
536
+
537
+ if (!textBlocks.length) {
538
+ blocks.unshift({
539
+ type: "text" as const,
540
+ text: "(see attached image)",
541
+ });
542
+ }
543
+
544
+ return blocks;
545
+ }
546
+
547
+ export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
548
+ export type AnthropicThinkingDisplay = "summarized" | "omitted";
549
+
550
+ export interface AnthropicOptions extends StreamOptions {
551
+ /**
552
+ * Enable extended thinking.
553
+ * For Opus 4.6+: uses adaptive thinking (Anthropic model decides when/how much to think).
554
+ * For older models: uses budget-based thinking with thinkingBudgetTokens.
555
+ */
556
+ thinkingEnabled?: boolean;
557
+ /**
558
+ * Token budget for extended thinking (older models only).
559
+ * Ignored for Opus 4.6+ which uses adaptive thinking.
560
+ */
561
+ thinkingBudgetTokens?: number;
562
+ /**
563
+ * Effort level for adaptive thinking (Opus 4.6+ only).
564
+ * Controls how much thinking Anthropic model allocates:
565
+ * - "max": Always thinks with no constraints
566
+ * - "high": Always thinks, deep reasoning (default)
567
+ * - "medium": Moderate thinking, may skip for simple queries
568
+ * - "low": Minimal thinking, skips for simple tasks
569
+ * Ignored for older models.
570
+ */
571
+ effort?: AnthropicEffort;
572
+ /**
573
+ * Optional reasoning level fallback for direct Anthropic provider usage.
574
+ * Converted to adaptive effort when effort is not explicitly provided.
575
+ */
576
+ reasoning?: SimpleStreamOptions["reasoning"];
577
+ /**
578
+ * Controls how Anthropic returns thinking content when the selected thinking
579
+ * transport supports a display option. Defaults to "summarized" where the
580
+ * API accepts it.
581
+ */
582
+ thinkingDisplay?: AnthropicThinkingDisplay;
583
+ interleavedThinking?: boolean;
584
+ toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
585
+ betas?: string[] | string;
586
+ /**
587
+ * Realization of `serviceTier: "priority"` on Anthropic models. When
588
+ * `"priority"`, sets `speed: "fast"` on the request and appends the
589
+ * `fast-mode-2026-02-01` beta header. Anthropic rejects unsupported models
590
+ * with `invalid_request_error`, which triggers an in-provider one-shot
591
+ * fallback (see `fastModeDisabled` provider state).
592
+ *
593
+ * Other `ServiceTier` values are currently ignored on this provider.
594
+ */
595
+ serviceTier?: ServiceTier;
596
+ /** Force OAuth bearer auth mode for proxy tokens that don't match Anthropic token prefixes. */
597
+ isOAuth?: boolean;
598
+ /**
599
+ * Pre-built Anthropic client instance. When provided, skips internal client
600
+ * construction entirely. Use this to inject alternative SDK clients such as
601
+ * `AnthropicVertex` that shares the same messaging API.
602
+ */
603
+ client?: Anthropic;
604
+ }
605
+
606
+ export type AnthropicClientOptionsArgs = {
607
+ model: Model<"anthropic-messages">;
608
+ apiKey: string;
609
+ extraBetas?: string[];
610
+ stream?: boolean;
611
+ interleavedThinking?: boolean;
612
+ headers?: Record<string, string>;
613
+ dynamicHeaders?: Record<string, string>;
614
+ isOAuth?: boolean;
615
+ hasTools?: boolean;
616
+ onSseEvent?: AnthropicOptions["onSseEvent"];
617
+ fetch?: FetchImpl;
618
+ };
619
+
620
+ export type AnthropicClientOptionsResult = {
621
+ isOAuthToken: boolean;
622
+ apiKey: string | null;
623
+ authToken?: string | null;
624
+ baseURL?: string;
625
+ maxRetries: number;
626
+ dangerouslyAllowBrowser: boolean;
627
+ defaultHeaders: Record<string, string>;
628
+ logLevel: AnthropicSdkClientOptions["logLevel"];
629
+ fetch?: AnthropicSdkClientOptions["fetch"];
630
+ fetchOptions?: AnthropicSdkClientOptions["fetchOptions"];
631
+ };
632
+
633
+ const CLAUDE_CODE_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
634
+
635
+ type FoundryTlsOptions = {
636
+ ca?: string | string[];
637
+ cert?: string;
638
+ key?: string;
639
+ };
640
+
641
+ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
642
+ if (model.provider === "github-copilot") {
643
+ return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
644
+ }
645
+ if (model.provider === "anthropic" && isFoundryEnabled()) {
646
+ const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
647
+ if (foundryBaseUrl) {
648
+ return foundryBaseUrl;
649
+ }
650
+ }
651
+ if (model.provider === "anthropic") {
652
+ return normalizeAnthropicBaseUrl(model.baseUrl) ?? "https://api.anthropic.com";
653
+ }
654
+ return normalizeAnthropicBaseUrl(model.baseUrl);
655
+ }
656
+
657
+ function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
658
+ const source = rawHeaders?.trim();
659
+ if (!source) return undefined;
660
+
661
+ const parsed: Record<string, string> = {};
662
+ for (const token of source.split(/\r?\n|,/)) {
663
+ const entry = token.trim();
664
+ if (!entry) continue;
665
+ const separatorIndex = entry.indexOf(":");
666
+ if (separatorIndex <= 0) continue;
667
+ const key = entry.slice(0, separatorIndex).trim();
668
+ const value = entry.slice(separatorIndex + 1).trim();
669
+ if (!key || !value) continue;
670
+ parsed[key] = value;
671
+ }
672
+
673
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
674
+ }
675
+
676
+ function resolveAnthropicCustomHeaders(model: Model<"anthropic-messages">): Record<string, string> | undefined {
677
+ if (model.provider !== "anthropic") return undefined;
678
+ if (!isFoundryEnabled()) return undefined;
679
+ return parseAnthropicCustomHeaders($env.ANTHROPIC_CUSTOM_HEADERS);
680
+ }
681
+
682
+ function looksLikeFilePath(value: string): boolean {
683
+ return value.includes("/") || value.includes("\\") || /\.(pem|crt|cer|key)$/i.test(value);
684
+ }
685
+
686
+ function resolvePemValue(value: string | undefined, name: string): string | undefined {
687
+ const trimmed = value?.trim();
688
+ if (!trimmed) return undefined;
689
+
690
+ const inline = trimmed.replace(/\\n/g, "\n");
691
+ if (inline.includes("-----BEGIN")) {
692
+ return inline;
693
+ }
694
+
695
+ if (looksLikeFilePath(trimmed)) {
696
+ try {
697
+ return fs.readFileSync(trimmed, "utf8");
698
+ } catch (error) {
699
+ if (isEnoent(error)) {
700
+ throw new Error(`${name} path does not exist: ${trimmed}`);
701
+ }
702
+ throw error;
703
+ }
704
+ }
705
+
706
+ return inline;
707
+ }
708
+
709
+ function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
710
+ if (model.provider !== "anthropic") return undefined;
711
+ if (!isFoundryEnabled()) return undefined;
712
+
713
+ const ca = resolvePemValue($env.NODE_EXTRA_CA_CERTS, "NODE_EXTRA_CA_CERTS");
714
+ const cert = resolvePemValue($env.CLAUDE_CODE_CLIENT_CERT, "CLAUDE_CODE_CLIENT_CERT");
715
+ const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
716
+
717
+ if ((cert && !key) || (!cert && key)) {
718
+ throw new Error("Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.");
719
+ }
720
+
721
+ const options: FoundryTlsOptions = {};
722
+ if (ca) options.ca = [...tls.rootCertificates, ca];
723
+ if (cert) options.cert = cert;
724
+ if (key) options.key = key;
725
+ return Object.keys(options).length > 0 ? options : undefined;
726
+ }
727
+
728
+ function buildClaudeCodeTlsFetchOptions(
729
+ model: Model<"anthropic-messages">,
730
+ baseUrl: string | undefined,
731
+ ): AnthropicSdkClientOptions["fetchOptions"] | undefined {
732
+ if (model.provider !== "anthropic") return undefined;
733
+ if (!baseUrl) return undefined;
734
+
735
+ let serverName: string;
736
+ try {
737
+ serverName = new URL(baseUrl).hostname;
738
+ } catch {
739
+ return undefined;
740
+ }
741
+
742
+ if (!serverName) return undefined;
743
+
744
+ const foundryTlsOptions = resolveFoundryTlsOptions(model);
745
+
746
+ return {
747
+ tls: {
748
+ rejectUnauthorized: true,
749
+ serverName,
750
+ ...(CLAUDE_CODE_TLS_CIPHERS ? { ciphers: CLAUDE_CODE_TLS_CIPHERS } : {}),
751
+ ...(foundryTlsOptions ?? {}),
752
+ },
753
+ };
754
+ }
755
+ function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
756
+ const merged: Record<string, string> = {};
757
+ for (const headers of headerSources) {
758
+ if (headers) {
759
+ Object.assign(merged, headers);
760
+ }
761
+ }
762
+ return merged;
763
+ }
764
+
765
+ // The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
766
+ // We surface the resulting provider error ourselves, so keep the SDK quiet.
767
+ const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
768
+
769
+ const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
770
+ "message_start",
771
+ "message_delta",
772
+ "message_stop",
773
+ "content_block_start",
774
+ "content_block_delta",
775
+ "content_block_stop",
776
+ ]);
777
+
778
+ async function* iterateAnthropicEvents(
779
+ response: Response,
780
+ signal?: AbortSignal,
781
+ onSseEvent?: AnthropicOptions["onSseEvent"],
782
+ ): AsyncGenerator<RawMessageStreamEvent> {
783
+ if (!response.body) {
784
+ throw new Error("Attempted to iterate over an Anthropic response with no body");
785
+ }
786
+
787
+ let sawMessageStart = false;
788
+ let sawMessageEnd = false;
789
+
790
+ for await (const sse of readSseEvents(response.body, signal)) {
791
+ notifyRawSseEvent(onSseEvent, sse);
792
+ if (sse.event === "error") {
793
+ throw new Error(sse.data);
794
+ }
795
+
796
+ if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) {
797
+ continue;
798
+ }
799
+
800
+ try {
801
+ const event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);
802
+ if (event.type === "message_start") {
803
+ sawMessageStart = true;
804
+ } else if (event.type === "message_stop") {
805
+ sawMessageEnd = true;
806
+ }
807
+ yield event;
808
+ } catch (error) {
809
+ const message = error instanceof Error ? error.message : String(error);
810
+ throw new Error(
811
+ `Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join("\\n")}`,
812
+ );
813
+ }
814
+ }
815
+
816
+ if (sawMessageStart && !sawMessageEnd) {
817
+ throw createAnthropicStreamEnvelopeError("stream ended before message_stop");
818
+ }
819
+ }
820
+
821
+ type AnthropicRawResponseRequest = {
822
+ asResponse(): Promise<Response>;
823
+ };
824
+
825
+ function hasAnthropicRawResponseRequest(request: unknown): request is AnthropicRawResponseRequest {
826
+ return isRecord(request) && typeof request.asResponse === "function";
827
+ }
828
+
829
+ type AnthropicStreamWithResponseRequest = {
830
+ withResponse(): Promise<{
831
+ data: AsyncIterable<RawMessageStreamEvent>;
832
+ response: Response;
833
+ request_id: string | null;
834
+ }>;
835
+ };
836
+
837
+ function hasAnthropicStreamWithResponseRequest(request: unknown): request is AnthropicStreamWithResponseRequest {
838
+ return isRecord(request) && typeof request.withResponse === "function";
839
+ }
840
+
841
+ async function getAnthropicStreamResponse(
842
+ request: unknown,
843
+ signal?: AbortSignal,
844
+ onSseEvent?: AnthropicOptions["onSseEvent"],
845
+ ): Promise<{ events: AsyncIterable<RawMessageStreamEvent>; response: Response; requestId: string | null }> {
846
+ if (hasAnthropicRawResponseRequest(request)) {
847
+ const response = await request.asResponse();
848
+ return {
849
+ events: iterateAnthropicEvents(response, signal, onSseEvent),
850
+ response,
851
+ requestId: response.headers.get("request-id"),
852
+ };
853
+ }
854
+ if (hasAnthropicStreamWithResponseRequest(request)) {
855
+ const { data, response, request_id } = await request.withResponse();
856
+ return { events: data, response, requestId: request_id };
857
+ }
858
+ throw new Error("Anthropic SDK request did not expose a stream response");
859
+ }
860
+
861
+ function getAnthropicCompat(
862
+ model: Model<"anthropic-messages">,
863
+ ): Required<NonNullable<Model<"anthropic-messages">["compat"]>> {
864
+ return {
865
+ disableStrictTools: model.compat?.disableStrictTools ?? false,
866
+ disableAdaptiveThinking: model.compat?.disableAdaptiveThinking ?? false,
867
+ supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
868
+ supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
869
+ };
870
+ }
871
+
872
+ const PROVIDER_MAX_RETRIES = 3;
873
+ const PROVIDER_BASE_DELAY_MS = 2000;
874
+
875
+ /**
876
+ * Check if an error from the Anthropic SDK is a rate-limit/transient error that
877
+ * should be retried before any content has been emitted.
878
+ *
879
+ * Includes malformed JSON stream-envelope parse errors seen from some
880
+ * Anthropic-compatible proxy endpoints.
881
+ */
882
+ /** Transient stream corruption errors where the response was truncated mid-JSON. */
883
+ function isTransientStreamParseError(error: unknown): boolean {
884
+ if (!(error instanceof Error)) return false;
885
+ return /json parse error|unterminated string|unexpected end of json input/i.test(error.message);
886
+ }
887
+
888
+ const ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX = "Anthropic stream envelope error:";
889
+
890
+ function createAnthropicStreamEnvelopeError(message: string): Error {
891
+ return new Error(`${ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX} ${message}`);
892
+ }
893
+
894
+ const ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES = new Set([
895
+ "content_block_start",
896
+ "content_block_delta",
897
+ "content_block_stop",
898
+ "message_delta",
899
+ "message_stop",
900
+ "message_start",
901
+ ]);
902
+
903
+ function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
904
+ if (typeof eventType !== "string") return false;
905
+ if (eventType === "ping") return true;
906
+ return !ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES.has(eventType);
907
+ }
908
+
909
+ function isTransientStreamEnvelopeError(error: unknown): boolean {
910
+ if (!(error instanceof Error)) return false;
911
+ return (
912
+ error.message.includes(ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX) ||
913
+ /stream event order|before message_start|before terminal stop signal/i.test(error.message)
914
+ );
915
+ }
916
+
917
+ function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
918
+ if (!(error instanceof Error)) return false;
919
+ return /stream event order|before message_start/i.test(error.message);
920
+ }
921
+
922
+ export function isProviderRetryableError(error: unknown, provider?: string): boolean {
923
+ if (!(error instanceof Error)) return false;
924
+ if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
925
+ const msg = error.message.toLowerCase();
926
+ if (
927
+ isUnexpectedSocketCloseMessage(msg) ||
928
+ /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(
929
+ msg,
930
+ ) ||
931
+ isTransientStreamParseError(error) ||
932
+ isProviderRetryableStreamEnvelopeError(error)
933
+ ) {
934
+ return true;
935
+ }
936
+ return isRetryableError(error);
937
+ }
938
+
939
+ function createEmptyUsage(premiumRequests?: number): Usage {
940
+ return {
941
+ input: 0,
942
+ output: 0,
943
+ cacheRead: 0,
944
+ cacheWrite: 0,
945
+ totalTokens: 0,
946
+ ...(premiumRequests === undefined ? {} : { premiumRequests }),
947
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
948
+ };
949
+ }
950
+
951
+ export type AnthropicUsageLike = {
952
+ cache_creation?: { ephemeral_5m_input_tokens?: number | null; ephemeral_1h_input_tokens?: number | null } | null;
953
+ server_tool_use?: { web_search_requests?: number | null; web_fetch_requests?: number | null } | null;
954
+ };
955
+
956
+ /**
957
+ * Capture Anthropic's optional cache-creation TTL breakdown and server-tool-use
958
+ * counters into the harness Usage shape. Only sets fields that were reported, so
959
+ * a `message_delta` that omits `cache_creation` does not clobber the breakdown
960
+ * established at `message_start`.
961
+ */
962
+ export function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLike): void {
963
+ const cacheCreation = source.cache_creation;
964
+ if (cacheCreation) {
965
+ const fiveMinute = cacheCreation.ephemeral_5m_input_tokens ?? 0;
966
+ const oneHour = cacheCreation.ephemeral_1h_input_tokens ?? 0;
967
+ if (fiveMinute > 0 || oneHour > 0) {
968
+ usage.cttl = {
969
+ ...(fiveMinute > 0 ? { ephemeral5m: fiveMinute } : {}),
970
+ ...(oneHour > 0 ? { ephemeral1h: oneHour } : {}),
971
+ };
972
+ }
973
+ }
974
+ const serverToolUse = source.server_tool_use;
975
+ if (serverToolUse) {
976
+ const webSearch = serverToolUse.web_search_requests ?? 0;
977
+ const webFetch = serverToolUse.web_fetch_requests ?? 0;
978
+ if (webSearch > 0 || webFetch > 0) {
979
+ usage.server = {
980
+ ...(webSearch > 0 ? { webSearch } : {}),
981
+ ...(webFetch > 0 ? { webFetch } : {}),
982
+ };
983
+ }
984
+ }
985
+ }
986
+
987
+ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
988
+ model: Model<"anthropic-messages">,
989
+ context: Context,
990
+ options?: AnthropicOptions,
991
+ ): AssistantMessageEventStream => {
992
+ const stream = new AssistantMessageEventStream();
993
+
994
+ (async () => {
995
+ const startTime = Date.now();
996
+ let firstTokenTime: number | undefined;
997
+
998
+ const copilotDynamicHeaders =
999
+ model.provider === "github-copilot"
1000
+ ? buildCopilotDynamicHeaders({
1001
+ messages: context.messages,
1002
+ hasImages: hasCopilotVisionInput(context.messages),
1003
+ premiumMultiplier: model.premiumMultiplier,
1004
+ headers: { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
1005
+ initiatorOverride: options?.initiatorOverride,
1006
+ })
1007
+ : undefined;
1008
+ const output: AssistantMessage = {
1009
+ role: "assistant",
1010
+ content: [],
1011
+ api: model.api as Api,
1012
+ provider: model.provider,
1013
+ model: model.id,
1014
+ usage: createEmptyUsage(copilotDynamicHeaders?.premiumRequests),
1015
+ stopReason: "stop",
1016
+ timestamp: Date.now(),
1017
+ };
1018
+ let rawRequestDump: RawHttpRequestDump | undefined;
1019
+ let activeAbortTracker = createAbortSourceTracker(options?.signal);
1020
+
1021
+ try {
1022
+ let client: Anthropic;
1023
+ let isOAuthToken: boolean;
1024
+
1025
+ if (options?.client) {
1026
+ client = options.client;
1027
+ isOAuthToken = false;
1028
+ } else {
1029
+ const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
1030
+
1031
+ const extraBetas = normalizeExtraBetas(options?.betas);
1032
+ const wantsAnthropicPriority = resolveServiceTier(options?.serviceTier, model.provider) === "priority";
1033
+ if (wantsAnthropicPriority && !extraBetas.includes(fastModeBeta)) {
1034
+ extraBetas.push(fastModeBeta);
1035
+ }
1036
+
1037
+ const created = createClient(model, {
1038
+ model,
1039
+ apiKey,
1040
+ extraBetas,
1041
+ stream: true,
1042
+ interleavedThinking: options?.interleavedThinking ?? true,
1043
+ headers: options?.headers,
1044
+ dynamicHeaders: copilotDynamicHeaders?.headers,
1045
+ isOAuth: options?.isOAuth,
1046
+ hasTools: !!context.tools?.length,
1047
+ onSseEvent: options?.onSseEvent,
1048
+ fetch: options?.fetch,
1049
+ });
1050
+ client = created.client;
1051
+ isOAuthToken = created.isOAuthToken;
1052
+ }
1053
+ const baseUrl =
1054
+ resolveAnthropicBaseUrl(model, options?.apiKey ?? getEnvApiKey(model.provider) ?? "") ??
1055
+ "https://api.anthropic.com";
1056
+ const providerSessionState = getAnthropicProviderSessionState(options?.providerSessionState);
1057
+ let disableStrictTools =
1058
+ (providerSessionState?.strictToolsDisabled ?? false) || (model.compat?.disableStrictTools ?? false);
1059
+ let strictFallbackErrorMessage: string | undefined;
1060
+ let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
1061
+ const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
1062
+ let nextParams = buildParams(model, baseUrl, context, isOAuthToken, options, disableStrictTools);
1063
+ if (disableStrictTools) {
1064
+ dropAnthropicStrictTools(nextParams);
1065
+ }
1066
+ if (dropFastMode) {
1067
+ dropAnthropicFastMode(nextParams);
1068
+ }
1069
+ const replacementPayload = await options?.onPayload?.(nextParams, model);
1070
+ if (replacementPayload !== undefined) {
1071
+ nextParams = replacementPayload as typeof nextParams;
1072
+ }
1073
+ rawRequestDump = {
1074
+ provider: model.provider,
1075
+ api: output.api,
1076
+ model: model.id,
1077
+ method: "POST",
1078
+ url: `${baseUrl}/v1/messages`,
1079
+ body: nextParams,
1080
+ };
1081
+ return nextParams;
1082
+ };
1083
+ let params = await prepareParams();
1084
+
1085
+ type Block = (
1086
+ | ThinkingContent
1087
+ | RedactedThinkingContent
1088
+ | TextContent
1089
+ | (ToolCall & { partialJson: string })
1090
+ ) & { index: number };
1091
+ const blocks = output.content as Block[];
1092
+ const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
1093
+ const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
1094
+ stream.push({ type: "start", partial: output });
1095
+ // Retry loop for transient errors from the stream.
1096
+ // Provider-level transport/rate-limit failures: only before any streamed content starts.
1097
+ // Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
1098
+ let providerRetryAttempt = 0;
1099
+ while (true) {
1100
+ activeAbortTracker = createAbortSourceTracker(options?.signal);
1101
+ const firstEventTimeoutAbortError = new Error(
1102
+ "Anthropic stream timed out while waiting for the first event",
1103
+ );
1104
+ const idleTimeoutAbortError = new Error("Anthropic stream stalled while waiting for the next event");
1105
+ const { requestSignal } = activeAbortTracker;
1106
+ const anthropicRequest = client.messages.create({ ...params, stream: true }, { signal: requestSignal });
1107
+ let streamedReplayUnsafeContent = false;
1108
+
1109
+ try {
1110
+ const {
1111
+ events: anthropicStream,
1112
+ response,
1113
+ requestId,
1114
+ } = await getAnthropicStreamResponse(
1115
+ anthropicRequest,
1116
+ requestSignal,
1117
+ options?.client ? event => options?.onSseEvent?.(event, model) : undefined,
1118
+ );
1119
+ await notifyProviderResponse(options, response, model, requestId);
1120
+ let sawEvent = false;
1121
+ let sawMessageStart = false;
1122
+ let sawTerminalEnvelope = false;
1123
+
1124
+ for await (const event of iterateWithIdleTimeout(anthropicStream, {
1125
+ idleTimeoutMs,
1126
+ firstItemTimeoutMs: firstEventTimeoutMs,
1127
+ errorMessage: idleTimeoutAbortError.message,
1128
+ firstItemErrorMessage: firstEventTimeoutAbortError.message,
1129
+ onIdle: () => activeAbortTracker.abortLocally(idleTimeoutAbortError),
1130
+ onFirstItemTimeout: () => activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
1131
+ abortSignal: options?.signal,
1132
+ })) {
1133
+ sawEvent = true;
1134
+
1135
+ if (event.type === "message_start") {
1136
+ if (sawMessageStart) {
1137
+ continue;
1138
+ }
1139
+ sawMessageStart = true;
1140
+ applyAnthropicUsageExtras(output.usage, event.message.usage);
1141
+ output.responseId = event.message.id;
1142
+ output.usage.input = event.message.usage.input_tokens || 0;
1143
+ output.usage.output = event.message.usage.output_tokens || 0;
1144
+ output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
1145
+ output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
1146
+ output.usage.totalTokens =
1147
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
1148
+ calculateCost(model, output.usage);
1149
+ continue;
1150
+ }
1151
+
1152
+ if (!sawMessageStart) {
1153
+ if (shouldIgnoreAnthropicPreambleEvent(event.type)) {
1154
+ continue;
1155
+ }
1156
+ throw createAnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
1157
+ }
1158
+
1159
+ if (event.type === "content_block_start") {
1160
+ if (!firstTokenTime) firstTokenTime = Date.now();
1161
+ if (event.content_block.type === "text") {
1162
+ streamedReplayUnsafeContent = true;
1163
+ const block: Block = {
1164
+ type: "text",
1165
+ text: "",
1166
+ index: event.index,
1167
+ };
1168
+ output.content.push(block);
1169
+ stream.push({
1170
+ type: "text_start",
1171
+ contentIndex: output.content.length - 1,
1172
+ partial: output,
1173
+ });
1174
+ } else if (event.content_block.type === "thinking") {
1175
+ const block: Block = {
1176
+ type: "thinking",
1177
+ thinking: "",
1178
+ thinkingSignature: "",
1179
+ index: event.index,
1180
+ };
1181
+ output.content.push(block);
1182
+ stream.push({
1183
+ type: "thinking_start",
1184
+ contentIndex: output.content.length - 1,
1185
+ partial: output,
1186
+ });
1187
+ } else if (event.content_block.type === "redacted_thinking") {
1188
+ const block: Block = {
1189
+ type: "redactedThinking",
1190
+ data: event.content_block.data,
1191
+ index: event.index,
1192
+ };
1193
+ output.content.push(block);
1194
+ } else if (event.content_block.type === "tool_use") {
1195
+ streamedReplayUnsafeContent = true;
1196
+ const block: Block = {
1197
+ type: "toolCall",
1198
+ id: event.content_block.id,
1199
+ name: isOAuthToken
1200
+ ? stripClaudeToolPrefix(event.content_block.name)
1201
+ : event.content_block.name,
1202
+ arguments: (event.content_block.input as Record<string, unknown>) ?? {},
1203
+ partialJson: "",
1204
+ index: event.index,
1205
+ };
1206
+ output.content.push(block);
1207
+ stream.push({
1208
+ type: "toolcall_start",
1209
+ contentIndex: output.content.length - 1,
1210
+ partial: output,
1211
+ });
1212
+ }
1213
+ } else if (event.type === "content_block_delta") {
1214
+ if (event.delta.type === "text_delta") {
1215
+ const index = blocks.findIndex(b => b.index === event.index);
1216
+ const block = blocks[index];
1217
+ if (block && block.type === "text") {
1218
+ block.text += event.delta.text;
1219
+ stream.push({
1220
+ type: "text_delta",
1221
+ contentIndex: index,
1222
+ delta: event.delta.text,
1223
+ partial: output,
1224
+ });
1225
+ }
1226
+ } else if (event.delta.type === "thinking_delta") {
1227
+ const index = blocks.findIndex(b => b.index === event.index);
1228
+ const block = blocks[index];
1229
+ if (block && block.type === "thinking") {
1230
+ block.thinking += event.delta.thinking;
1231
+ stream.push({
1232
+ type: "thinking_delta",
1233
+ contentIndex: index,
1234
+ delta: event.delta.thinking,
1235
+ partial: output,
1236
+ });
1237
+ }
1238
+ } else if (event.delta.type === "input_json_delta") {
1239
+ const index = blocks.findIndex(b => b.index === event.index);
1240
+ const block = blocks[index];
1241
+ if (block && block.type === "toolCall") {
1242
+ block.partialJson += event.delta.partial_json;
1243
+ block.arguments = parseStreamingJson(block.partialJson);
1244
+ stream.push({
1245
+ type: "toolcall_delta",
1246
+ contentIndex: index,
1247
+ delta: event.delta.partial_json,
1248
+ partial: output,
1249
+ });
1250
+ }
1251
+ } else if (event.delta.type === "signature_delta") {
1252
+ const index = blocks.findIndex(b => b.index === event.index);
1253
+ const block = blocks[index];
1254
+ if (block && block.type === "thinking") {
1255
+ block.thinkingSignature = block.thinkingSignature || "";
1256
+ block.thinkingSignature += event.delta.signature;
1257
+ }
1258
+ }
1259
+ } else if (event.type === "content_block_stop") {
1260
+ const index = blocks.findIndex(b => b.index === event.index);
1261
+ const block = blocks[index];
1262
+ if (block) {
1263
+ delete (block as { index?: number }).index;
1264
+ if (block.type === "text") {
1265
+ stream.push({
1266
+ type: "text_end",
1267
+ contentIndex: index,
1268
+ content: block.text,
1269
+ partial: output,
1270
+ });
1271
+ } else if (block.type === "thinking") {
1272
+ stream.push({
1273
+ type: "thinking_end",
1274
+ contentIndex: index,
1275
+ content: block.thinking,
1276
+ partial: output,
1277
+ });
1278
+ } else if (block.type === "toolCall") {
1279
+ block.arguments = parseStreamingJson(block.partialJson);
1280
+ delete (block as { partialJson?: string }).partialJson;
1281
+ stream.push({
1282
+ type: "toolcall_end",
1283
+ contentIndex: index,
1284
+ toolCall: block,
1285
+ partial: output,
1286
+ });
1287
+ }
1288
+ }
1289
+ } else if (event.type === "message_delta") {
1290
+ const rawStopReason = event.delta.stop_reason as string | null | undefined;
1291
+ if (rawStopReason) {
1292
+ output.stopReason = mapStopReason(rawStopReason);
1293
+ sawTerminalEnvelope = true;
1294
+ }
1295
+ const stopDetails = event.delta.stop_details;
1296
+ if (stopDetails && stopDetails.type === "refusal") {
1297
+ const explanation = stopDetails.explanation?.trim();
1298
+ const category = stopDetails.category;
1299
+ const label = category ? `Refusal (${category})` : "Refusal";
1300
+ output.errorMessage = explanation ? `${label}: ${explanation}` : label;
1301
+ } else if (output.stopReason === "error" && !output.errorMessage) {
1302
+ // Anthropic flagged an error-class stop (refusal / sensitive) without
1303
+ // populating stop_details. Surface the raw reason instead of falling
1304
+ // through to the generic "unknown error" string when we throw below.
1305
+ output.errorMessage =
1306
+ rawStopReason === "refusal"
1307
+ ? "Refusal (no details provided)"
1308
+ : rawStopReason === "sensitive"
1309
+ ? "Content flagged by safety filters"
1310
+ : `Anthropic stream ended with stop_reason: ${rawStopReason ?? "unknown"}`;
1311
+ }
1312
+ if (event.usage.input_tokens != null) {
1313
+ output.usage.input = event.usage.input_tokens;
1314
+ }
1315
+ if (event.usage.output_tokens != null) {
1316
+ output.usage.output = event.usage.output_tokens;
1317
+ }
1318
+ if (event.usage.cache_read_input_tokens != null) {
1319
+ output.usage.cacheRead = event.usage.cache_read_input_tokens;
1320
+ }
1321
+ if (event.usage.cache_creation_input_tokens != null) {
1322
+ output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
1323
+ }
1324
+ applyAnthropicUsageExtras(output.usage, event.usage);
1325
+ output.usage.totalTokens =
1326
+ output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
1327
+ calculateCost(model, output.usage);
1328
+ } else if (event.type === "message_stop") {
1329
+ sawTerminalEnvelope = true;
1330
+ }
1331
+ }
1332
+
1333
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
1334
+ if (firstEventTimeoutError) {
1335
+ throw firstEventTimeoutError;
1336
+ }
1337
+ if (activeAbortTracker.wasCallerAbort()) {
1338
+ throw new Error("Request was aborted");
1339
+ }
1340
+ if (!sawEvent || !sawMessageStart) {
1341
+ throw createAnthropicStreamEnvelopeError("stream ended before message_start");
1342
+ }
1343
+ if (!sawTerminalEnvelope) {
1344
+ throw createAnthropicStreamEnvelopeError("stream ended before terminal stop signal");
1345
+ }
1346
+
1347
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
1348
+ throw new Error(output.errorMessage ?? "An unknown error occurred");
1349
+ }
1350
+ break;
1351
+ } catch (streamError) {
1352
+ const streamFailure = activeAbortTracker.getLocalAbortReason() ?? streamError;
1353
+ if (
1354
+ !disableStrictTools &&
1355
+ firstTokenTime === undefined &&
1356
+ hasStrictAnthropicTools(params) &&
1357
+ isAnthropicStrictGrammarTooLargeError(streamFailure)
1358
+ ) {
1359
+ strictFallbackErrorMessage = await finalizeErrorMessage(streamFailure, rawRequestDump);
1360
+ output.errorMessage = strictFallbackErrorMessage;
1361
+ if (providerSessionState) {
1362
+ providerSessionState.strictToolsDisabled = true;
1363
+ }
1364
+ disableStrictTools = true;
1365
+ params = await prepareParams();
1366
+ providerRetryAttempt = 0;
1367
+ output.content.length = 0;
1368
+ output.responseId = undefined;
1369
+ output.providerPayload = undefined;
1370
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1371
+ output.stopReason = "stop";
1372
+ firstTokenTime = undefined;
1373
+ continue;
1374
+ }
1375
+ if (
1376
+ !dropFastMode &&
1377
+ resolveServiceTier(options?.serviceTier, model.provider) === "priority" &&
1378
+ firstTokenTime === undefined &&
1379
+ isAnthropicFastModeUnsupportedError(streamFailure)
1380
+ ) {
1381
+ logger.debug("anthropic: fast mode unsupported, retrying without speed", {
1382
+ model: model.id,
1383
+ error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
1384
+ });
1385
+ if (providerSessionState) {
1386
+ providerSessionState.fastModeDisabled = true;
1387
+ }
1388
+ dropFastMode = true;
1389
+ params = await prepareParams();
1390
+ providerRetryAttempt = 0;
1391
+ output.content.length = 0;
1392
+ output.responseId = undefined;
1393
+ output.providerPayload = undefined;
1394
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1395
+ output.stopReason = "stop";
1396
+ firstTokenTime = undefined;
1397
+ continue;
1398
+ }
1399
+ const isTransientEnvelopeFailure =
1400
+ isTransientStreamParseError(streamFailure) || isTransientStreamEnvelopeError(streamFailure);
1401
+ const canRetryTransientEnvelopeFailure = isTransientEnvelopeFailure && !streamedReplayUnsafeContent;
1402
+ const canRetryProviderFailure =
1403
+ firstTokenTime === undefined && isProviderRetryableError(streamFailure, model.provider);
1404
+ if (
1405
+ activeAbortTracker.wasCallerAbort() ||
1406
+ providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
1407
+ (!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
1408
+ ) {
1409
+ throw streamFailure;
1410
+ }
1411
+ providerRetryAttempt++;
1412
+ const delayMs = PROVIDER_BASE_DELAY_MS * 2 ** (providerRetryAttempt - 1);
1413
+ if (options?.providerRetryWait) {
1414
+ await options.providerRetryWait(delayMs, options.signal);
1415
+ } else {
1416
+ await scheduler.wait(delayMs, { signal: options?.signal });
1417
+ }
1418
+ output.content.length = 0;
1419
+ output.responseId = undefined;
1420
+ output.errorMessage = strictFallbackErrorMessage;
1421
+ output.providerPayload = undefined;
1422
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
1423
+ output.stopReason = "stop";
1424
+ firstTokenTime = undefined;
1425
+ }
1426
+ }
1427
+
1428
+ output.duration = Date.now() - startTime;
1429
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1430
+ if (dropFastMode && resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
1431
+ output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
1432
+ }
1433
+ stream.push({ type: "done", reason: output.stopReason, message: output });
1434
+ stream.end();
1435
+ } catch (error) {
1436
+ for (const block of output.content) {
1437
+ delete (block as { index?: number }).index;
1438
+ delete (block as { partialJson?: string }).partialJson;
1439
+ }
1440
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
1441
+ output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
1442
+ output.errorStatus = extractHttpStatusFromError(error);
1443
+ output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
1444
+ output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
1445
+ output.duration = Date.now() - startTime;
1446
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1447
+ stream.push({ type: "error", reason: output.stopReason, error: output });
1448
+ stream.end();
1449
+ }
1450
+ })();
1451
+
1452
+ return stream;
1453
+ };
1454
+
1455
+ export type AnthropicSystemBlock = {
1456
+ type: "text";
1457
+ text: string;
1458
+ cache_control?: AnthropicCacheControl;
1459
+ };
1460
+ type SystemBlockOptions = {
1461
+ includeClaudeCodeInstruction?: boolean;
1462
+ extraInstructions?: string[];
1463
+ billingPayload?: unknown;
1464
+ cacheControl?: AnthropicCacheControl;
1465
+ };
1466
+
1467
+ export function buildAnthropicSystemBlocks(
1468
+ systemPrompt: readonly string[] | undefined,
1469
+ options: SystemBlockOptions = {},
1470
+ ): AnthropicSystemBlock[] | undefined {
1471
+ const { includeClaudeCodeInstruction = false, extraInstructions = [], billingPayload, cacheControl } = options;
1472
+ const blocks: AnthropicSystemBlock[] = [];
1473
+ const sanitizedPrompts = normalizeSystemPrompts(systemPrompt);
1474
+ const trimmedInstructions = extraInstructions.map(instruction => instruction.trim()).filter(Boolean);
1475
+ const hasBillingHeader = sanitizedPrompts.some(prompt => prompt.includes(CLAUDE_BILLING_HEADER_PREFIX));
1476
+
1477
+ if (includeClaudeCodeInstruction && !hasBillingHeader) {
1478
+ const payloadSeed = billingPayload ?? {
1479
+ system: sanitizedPrompts,
1480
+ extraInstructions: trimmedInstructions,
1481
+ };
1482
+ blocks.push(
1483
+ { type: "text", text: createClaudeBillingHeader(payloadSeed) },
1484
+ {
1485
+ type: "text",
1486
+ text: claudeCodeSystemInstruction,
1487
+ },
1488
+ );
1489
+ }
1490
+
1491
+ for (const instruction of trimmedInstructions) {
1492
+ blocks.push({ type: "text", text: instruction });
1493
+ }
1494
+
1495
+ for (const systemPrompt of sanitizedPrompts) {
1496
+ blocks.push({ type: "text", text: systemPrompt });
1497
+ }
1498
+
1499
+ // Attach cache_control to the LAST emitted block only. Anthropic breakpoints are cumulative
1500
+ // prefix cuts, so a single trailing breakpoint covers every preceding block; spreading
1501
+ // cache_control across N blocks wastes slots against the 4-breakpoint cap.
1502
+ const lastIndex = blocks.length - 1;
1503
+ if (cacheControl && lastIndex >= 0) {
1504
+ blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
1505
+ }
1506
+
1507
+ return blocks.length > 0 ? blocks : undefined;
1508
+ }
1509
+
1510
+ export function normalizeExtraBetas(betas?: string[] | string): string[] {
1511
+ if (!betas) return [];
1512
+ const raw = Array.isArray(betas) ? betas : betas.split(",");
1513
+ return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
1514
+ }
1515
+
1516
+ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): AnthropicClientOptionsResult {
1517
+ const {
1518
+ model,
1519
+ apiKey,
1520
+ extraBetas = [],
1521
+ stream = true,
1522
+ interleavedThinking = true,
1523
+ headers,
1524
+ dynamicHeaders,
1525
+ hasTools = false,
1526
+ isOAuth,
1527
+ onSseEvent,
1528
+ } = args;
1529
+ const compat = getAnthropicCompat(model);
1530
+ const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinkingDisplay(model.id);
1531
+ const needsFineGrainedToolStreamingBeta = hasTools && !compat.supportsEagerToolInputStreaming;
1532
+ const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
1533
+ const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
1534
+ const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
1535
+ const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
1536
+ const baseFetch = args.fetch ?? fetch;
1537
+ const debugFetch = onSseEvent
1538
+ ? wrapFetchForSseDebug(baseFetch, event => onSseEvent(event, model))
1539
+ : args.fetch
1540
+ ? baseFetch
1541
+ : undefined;
1542
+ if (model.provider === "github-copilot") {
1543
+ const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
1544
+ const betaFeatures = [...extraBetas];
1545
+ if (needsFineGrainedToolStreamingBeta) {
1546
+ betaFeatures.push(fineGrainedToolStreamingBeta);
1547
+ }
1548
+ const defaultHeaders = mergeHeaders(
1549
+ {
1550
+ Accept: stream ? "text/event-stream" : "application/json",
1551
+ "Anthropic-Dangerous-Direct-Browser-Access": "true",
1552
+ Authorization: `Bearer ${copilotApiKey}`,
1553
+ ...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
1554
+ },
1555
+ model.headers,
1556
+ dynamicHeaders,
1557
+ headers,
1558
+ );
1559
+
1560
+ return {
1561
+ isOAuthToken: false,
1562
+ apiKey: null,
1563
+ authToken: copilotApiKey,
1564
+ baseURL: baseUrl,
1565
+ maxRetries: 5,
1566
+ dangerouslyAllowBrowser: true,
1567
+ defaultHeaders,
1568
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
1569
+ ...(debugFetch ? { fetch: debugFetch } : {}),
1570
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
1571
+ };
1572
+ }
1573
+
1574
+ const betaFeatures = [...extraBetas];
1575
+ if (needsFineGrainedToolStreamingBeta) {
1576
+ betaFeatures.push(fineGrainedToolStreamingBeta);
1577
+ }
1578
+ if (needsInterleavedBeta) {
1579
+ betaFeatures.push(interleavedThinkingBeta);
1580
+ }
1581
+
1582
+ const defaultHeaders = buildAnthropicHeaders({
1583
+ apiKey,
1584
+ baseUrl,
1585
+ isOAuth: oauthToken,
1586
+ extraBetas: betaFeatures,
1587
+ stream,
1588
+ modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
1589
+ isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
1590
+ });
1591
+
1592
+ if (model.provider === "cloudflare-ai-gateway") {
1593
+ return {
1594
+ isOAuthToken: false,
1595
+ apiKey: null,
1596
+ authToken: null,
1597
+ baseURL: baseUrl,
1598
+ maxRetries: 5,
1599
+ dangerouslyAllowBrowser: true,
1600
+ defaultHeaders,
1601
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
1602
+ ...(debugFetch ? { fetch: debugFetch } : {}),
1603
+ };
1604
+ }
1605
+
1606
+ return {
1607
+ isOAuthToken: oauthToken,
1608
+ apiKey: oauthToken ? null : apiKey,
1609
+ authToken: oauthToken ? apiKey : undefined,
1610
+ baseURL: baseUrl,
1611
+ maxRetries: 5,
1612
+ dangerouslyAllowBrowser: true,
1613
+ defaultHeaders,
1614
+ logLevel: ANTHROPIC_SDK_LOG_LEVEL,
1615
+ ...(debugFetch ? { fetch: debugFetch } : {}),
1616
+ ...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
1617
+ };
1618
+ }
1619
+
1620
+ function createClient(
1621
+ model: Model<"anthropic-messages">,
1622
+ args: AnthropicClientOptionsArgs,
1623
+ ): { client: Anthropic; isOAuthToken: boolean } {
1624
+ const { isOAuthToken: oauthToken, ...clientOptions } = buildAnthropicClientOptions({ ...args, model });
1625
+ const client = new Anthropic(clientOptions);
1626
+ return { client, isOAuthToken: oauthToken };
1627
+ }
1628
+
1629
+ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): void {
1630
+ const toolChoice = params.tool_choice;
1631
+ if (!toolChoice) return;
1632
+ if (toolChoice.type === "any" || toolChoice.type === "tool") {
1633
+ delete params.thinking;
1634
+ delete params.output_config;
1635
+ }
1636
+ }
1637
+
1638
+ function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
1639
+ const thinking = params.thinking;
1640
+ if (!thinking || thinking.type !== "enabled") return;
1641
+
1642
+ const budgetTokens = thinking.budget_tokens ?? 0;
1643
+ if (budgetTokens <= 0) return;
1644
+
1645
+ const maxTokens = params.max_tokens ?? 0;
1646
+ const requiredMaxTokens = budgetTokens + OUTPUT_FALLBACK_BUFFER;
1647
+ if (maxTokens < requiredMaxTokens) {
1648
+ params.max_tokens = Math.min(requiredMaxTokens, model.maxTokens);
1649
+ }
1650
+ }
1651
+
1652
+ type CacheControlBlock = {
1653
+ cache_control?: AnthropicCacheControl | null;
1654
+ };
1655
+
1656
+ function applyCacheControlToLastBlock<T extends CacheControlBlock>(
1657
+ blocks: T[],
1658
+ cacheControl: AnthropicCacheControl,
1659
+ ): void {
1660
+ if (blocks.length === 0) return;
1661
+ const lastIndex = blocks.length - 1;
1662
+ blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
1663
+ }
1664
+
1665
+ function applyCacheControlToLastTextBlock(
1666
+ blocks: Array<ContentBlockParam & CacheControlBlock>,
1667
+ cacheControl: AnthropicCacheControl,
1668
+ ): void {
1669
+ if (blocks.length === 0) return;
1670
+ for (let i = blocks.length - 1; i >= 0; i--) {
1671
+ if (blocks[i].type === "text") {
1672
+ blocks[i] = { ...blocks[i], cache_control: cacheControl };
1673
+ return;
1674
+ }
1675
+ }
1676
+ applyCacheControlToLastBlock(blocks, cacheControl);
1677
+ }
1678
+
1679
+ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?: AnthropicCacheControl): void {
1680
+ if (!cacheControl) return;
1681
+
1682
+ // Skip if cache_control breakpoints were already placed externally on messages.
1683
+ for (const message of params.messages) {
1684
+ if (Array.isArray(message.content)) {
1685
+ if ((message.content as Array<ContentBlockParam & CacheControlBlock>).some(b => b.cache_control != null))
1686
+ return;
1687
+ }
1688
+ }
1689
+
1690
+ const MAX_CACHE_BREAKPOINTS = 4;
1691
+ let cacheBreakpointsUsed = 0;
1692
+
1693
+ if (params.tools && params.tools.length > 0) {
1694
+ applyCacheControlToLastBlock(params.tools as Array<CacheControlBlock>, cacheControl);
1695
+ cacheBreakpointsUsed++;
1696
+ }
1697
+
1698
+ if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
1699
+
1700
+ if (params.system && Array.isArray(params.system) && params.system.length > 0) {
1701
+ applyCacheControlToLastBlock(params.system, cacheControl);
1702
+ cacheBreakpointsUsed++;
1703
+ }
1704
+
1705
+ if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
1706
+
1707
+ const userIndexes = params.messages
1708
+ .map((message, index) => (message.role === "user" ? index : -1))
1709
+ .filter(index => index >= 0);
1710
+
1711
+ if (userIndexes.length >= 2) {
1712
+ const penultimateUserIndex = userIndexes[userIndexes.length - 2];
1713
+ const penultimateUser = params.messages[penultimateUserIndex];
1714
+ if (penultimateUser) {
1715
+ if (typeof penultimateUser.content === "string") {
1716
+ const contentBlock: ContentBlockParam & CacheControlBlock = {
1717
+ type: "text",
1718
+ text: penultimateUser.content,
1719
+ cache_control: cacheControl,
1720
+ };
1721
+ penultimateUser.content = [contentBlock];
1722
+ cacheBreakpointsUsed++;
1723
+ } else if (Array.isArray(penultimateUser.content) && penultimateUser.content.length > 0) {
1724
+ applyCacheControlToLastTextBlock(
1725
+ penultimateUser.content as Array<ContentBlockParam & CacheControlBlock>,
1726
+ cacheControl,
1727
+ );
1728
+ cacheBreakpointsUsed++;
1729
+ }
1730
+ }
1731
+ }
1732
+
1733
+ if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
1734
+
1735
+ if (userIndexes.length >= 1) {
1736
+ const lastUserIndex = userIndexes[userIndexes.length - 1];
1737
+ const lastUser = params.messages[lastUserIndex];
1738
+ if (lastUser) {
1739
+ if (typeof lastUser.content === "string") {
1740
+ const contentBlock: ContentBlockParam & CacheControlBlock = {
1741
+ type: "text",
1742
+ text: lastUser.content,
1743
+ cache_control: cacheControl,
1744
+ };
1745
+ lastUser.content = [contentBlock];
1746
+ } else if (Array.isArray(lastUser.content) && lastUser.content.length > 0) {
1747
+ applyCacheControlToLastTextBlock(
1748
+ lastUser.content as Array<ContentBlockParam & CacheControlBlock>,
1749
+ cacheControl,
1750
+ );
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ function normalizeCacheControlBlockTtl(block: CacheControlBlock, seenFiveMinute: { value: boolean }): void {
1757
+ const cacheControl = block.cache_control;
1758
+ if (!cacheControl) return;
1759
+ if (cacheControl.ttl !== "1h") {
1760
+ seenFiveMinute.value = true;
1761
+ return;
1762
+ }
1763
+ if (seenFiveMinute.value) {
1764
+ delete cacheControl.ttl;
1765
+ }
1766
+ }
1767
+
1768
+ function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
1769
+ const seenFiveMinute = { value: false };
1770
+ if (params.tools) {
1771
+ for (const tool of params.tools as Array<Anthropic.Messages.Tool & CacheControlBlock>) {
1772
+ normalizeCacheControlBlockTtl(tool, seenFiveMinute);
1773
+ }
1774
+ }
1775
+ if (params.system && Array.isArray(params.system)) {
1776
+ for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
1777
+ normalizeCacheControlBlockTtl(block, seenFiveMinute);
1778
+ }
1779
+ }
1780
+ for (const message of params.messages) {
1781
+ if (!Array.isArray(message.content)) continue;
1782
+ for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
1783
+ normalizeCacheControlBlockTtl(block, seenFiveMinute);
1784
+ }
1785
+ }
1786
+ }
1787
+
1788
+ function findLastCacheControlIndex<T extends CacheControlBlock>(blocks: T[]): number {
1789
+ for (let index = blocks.length - 1; index >= 0; index--) {
1790
+ if (blocks[index]?.cache_control != null) return index;
1791
+ }
1792
+ return -1;
1793
+ }
1794
+
1795
+ function stripCacheControlExceptIndex<T extends CacheControlBlock>(
1796
+ blocks: T[],
1797
+ preserveIndex: number,
1798
+ excessCounter: { value: number },
1799
+ ): void {
1800
+ for (let index = 0; index < blocks.length && excessCounter.value > 0; index++) {
1801
+ if (index === preserveIndex) continue;
1802
+ if (!blocks[index]?.cache_control) continue;
1803
+ delete blocks[index].cache_control;
1804
+ excessCounter.value--;
1805
+ }
1806
+ }
1807
+
1808
+ function stripAllCacheControl<T extends CacheControlBlock>(blocks: T[], excessCounter: { value: number }): void {
1809
+ for (const block of blocks) {
1810
+ if (excessCounter.value <= 0) return;
1811
+ if (!block.cache_control) continue;
1812
+ delete block.cache_control;
1813
+ excessCounter.value--;
1814
+ }
1815
+ }
1816
+
1817
+ function stripMessageCacheControl(
1818
+ messages: MessageCreateParamsStreaming["messages"],
1819
+ excessCounter: { value: number },
1820
+ ): void {
1821
+ for (const message of messages) {
1822
+ if (excessCounter.value <= 0) return;
1823
+ if (!Array.isArray(message.content)) continue;
1824
+ for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
1825
+ if (excessCounter.value <= 0) return;
1826
+ if (!block.cache_control) continue;
1827
+ delete block.cache_control;
1828
+ excessCounter.value--;
1829
+ }
1830
+ }
1831
+ }
1832
+
1833
+ function countCacheControlBreakpoints(params: MessageCreateParamsStreaming): number {
1834
+ let total = 0;
1835
+ if (params.tools) {
1836
+ for (const tool of params.tools as Array<Anthropic.Messages.Tool & CacheControlBlock>) {
1837
+ if (tool.cache_control) total++;
1838
+ }
1839
+ }
1840
+ if (params.system && Array.isArray(params.system)) {
1841
+ for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
1842
+ if (block.cache_control) total++;
1843
+ }
1844
+ }
1845
+ for (const message of params.messages) {
1846
+ if (!Array.isArray(message.content)) continue;
1847
+ for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
1848
+ if (block.cache_control) total++;
1849
+ }
1850
+ }
1851
+ return total;
1852
+ }
1853
+
1854
+ function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreakpoints: number): void {
1855
+ const total = countCacheControlBreakpoints(params);
1856
+ if (total <= maxBreakpoints) return;
1857
+ const excessCounter = { value: total - maxBreakpoints };
1858
+ const systemBlocks =
1859
+ params.system && Array.isArray(params.system)
1860
+ ? (params.system as Array<AnthropicSystemBlock & CacheControlBlock>)
1861
+ : [];
1862
+ const toolBlocks = (params.tools ?? []) as Array<Anthropic.Messages.Tool & CacheControlBlock>;
1863
+ const lastSystemIndex = findLastCacheControlIndex(systemBlocks);
1864
+ const lastToolIndex = findLastCacheControlIndex(toolBlocks);
1865
+ if (systemBlocks.length > 0) {
1866
+ stripCacheControlExceptIndex(systemBlocks, lastSystemIndex, excessCounter);
1867
+ }
1868
+ if (excessCounter.value <= 0) return;
1869
+ if (toolBlocks.length > 0) {
1870
+ stripCacheControlExceptIndex(toolBlocks, lastToolIndex, excessCounter);
1871
+ }
1872
+ if (excessCounter.value <= 0) return;
1873
+ stripMessageCacheControl(params.messages, excessCounter);
1874
+ if (excessCounter.value <= 0) return;
1875
+ if (systemBlocks.length > 0) {
1876
+ stripAllCacheControl(systemBlocks, excessCounter);
1877
+ }
1878
+ if (excessCounter.value <= 0) return;
1879
+ if (toolBlocks.length > 0) {
1880
+ stripAllCacheControl(toolBlocks, excessCounter);
1881
+ }
1882
+ }
1883
+ function buildParams(
1884
+ model: Model<"anthropic-messages">,
1885
+ baseUrl: string,
1886
+ context: Context,
1887
+ isOAuthToken: boolean,
1888
+ options?: AnthropicOptions,
1889
+ disableStrictTools = false,
1890
+ ): MessageCreateParamsStreaming {
1891
+ const { cacheControl } = getCacheControl(model, baseUrl, options?.cacheRetention);
1892
+ const params: AnthropicSamplingParams = {
1893
+ model: model.id,
1894
+ messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
1895
+ max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
1896
+ stream: true,
1897
+ };
1898
+ if (options?.temperature !== undefined && !options?.thinkingEnabled) {
1899
+ params.temperature = options.temperature;
1900
+ }
1901
+
1902
+ if (options?.topP !== undefined) {
1903
+ params.top_p = options.topP;
1904
+ }
1905
+ if (options?.topK !== undefined) {
1906
+ params.top_k = options.topK;
1907
+ }
1908
+ if (options?.stopSequences?.length) {
1909
+ const seqs = options.stopSequences;
1910
+ if (seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX && !warnedStopSequencesTrim) {
1911
+ warnedStopSequencesTrim = true;
1912
+ logger.warn("anthropic: stop_sequences exceeds 4; extra entries dropped", {
1913
+ received: seqs.length,
1914
+ kept: ANTHROPIC_STOP_SEQUENCES_MAX,
1915
+ });
1916
+ }
1917
+ params.stop_sequences =
1918
+ seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX ? seqs.slice(0, ANTHROPIC_STOP_SEQUENCES_MAX) : seqs;
1919
+ }
1920
+
1921
+ // Opus 4.7+ rejects non-default sampling parameters with 400 error.
1922
+ if (hasOpus47ApiRestrictions(model.id)) {
1923
+ delete params.top_p;
1924
+ delete params.top_k;
1925
+ delete params.temperature;
1926
+ }
1927
+
1928
+ if (context.tools) {
1929
+ params.tools = convertTools(
1930
+ context.tools,
1931
+ isOAuthToken,
1932
+ disableStrictTools || model.provider === "github-copilot",
1933
+ getAnthropicCompat(model).supportsEagerToolInputStreaming,
1934
+ );
1935
+ }
1936
+
1937
+ if (model.reasoning) {
1938
+ if (options?.thinkingEnabled) {
1939
+ const mode = model.thinking?.mode;
1940
+ const requestedEffort = options.reasoning;
1941
+ const effort =
1942
+ options.effort ??
1943
+ (requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
1944
+
1945
+ const compat = getAnthropicCompat(model);
1946
+ if (mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
1947
+ // Starting with Anthropic model Opus 4.7, adaptive thinking content is omitted from the
1948
+ // response by default. Opt into summarized reasoning so thinking deltas keep
1949
+ // streaming with human-readable content for callers that rely on it.
1950
+ const adaptive: { type: "adaptive"; display?: AnthropicThinkingDisplay } = { type: "adaptive" };
1951
+ if (supportsAdaptiveThinkingDisplay(model.id)) {
1952
+ adaptive.display = options.thinkingDisplay ?? "summarized";
1953
+ }
1954
+ params.thinking = adaptive as typeof params.thinking;
1955
+ if (effort) {
1956
+ // SDK's OutputConfig.effort type is not yet widened to include the new "xhigh"
1957
+ // level introduced with Anthropic model Opus 4.7. Cast until the SDK catches up.
1958
+ params.output_config = { effort } as typeof params.output_config;
1959
+ }
1960
+ } else {
1961
+ params.thinking = {
1962
+ type: "enabled",
1963
+ budget_tokens: options.thinkingBudgetTokens || 1024,
1964
+ display: options.thinkingDisplay ?? "summarized",
1965
+ } as typeof params.thinking;
1966
+ if (mode === "anthropic-budget-effort" && effort) {
1967
+ params.output_config = { effort } as typeof params.output_config;
1968
+ }
1969
+ }
1970
+ } else if (options?.thinkingEnabled === false) {
1971
+ params.thinking = { type: "disabled" };
1972
+ }
1973
+ }
1974
+
1975
+ const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
1976
+ if (metadataUserId) {
1977
+ params.metadata = { user_id: metadataUserId };
1978
+ }
1979
+
1980
+ if (resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
1981
+ (params as ParamsWithSpeed).speed = "fast";
1982
+ }
1983
+
1984
+ if (options?.toolChoice) {
1985
+ if (typeof options.toolChoice === "string") {
1986
+ params.tool_choice = { type: options.toolChoice };
1987
+ } else if (isOAuthToken && options.toolChoice.name) {
1988
+ params.tool_choice = {
1989
+ ...options.toolChoice,
1990
+ name: applyClaudeToolPrefix(options.toolChoice.name),
1991
+ };
1992
+ } else {
1993
+ params.tool_choice = options.toolChoice;
1994
+ }
1995
+ }
1996
+
1997
+ const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
1998
+ const billingSystemPrompts = normalizeSystemPrompts(context.systemPrompt);
1999
+ const billingPayload = shouldInjectClaudeCodeInstruction
2000
+ ? {
2001
+ ...params,
2002
+ ...(billingSystemPrompts.length > 0 ? { system: billingSystemPrompts } : {}),
2003
+ }
2004
+ : undefined;
2005
+ const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
2006
+ includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
2007
+ billingPayload,
2008
+ });
2009
+ if (systemBlocks) {
2010
+ params.system = systemBlocks;
2011
+ }
2012
+ disableThinkingIfToolChoiceForced(params);
2013
+ ensureMaxTokensForThinking(params, model);
2014
+ applyPromptCaching(params, cacheControl);
2015
+ enforceCacheControlLimit(params, 4);
2016
+ normalizeCacheControlTtlOrdering(params);
2017
+
2018
+ return params;
2019
+ }
2020
+
2021
+ /**
2022
+ * Z.AI's Anthropic-compatible proxy at `api.z.ai/api/anthropic` deserializes
2023
+ * tool_result blocks into a Python class that accesses `.id`, even though
2024
+ * Anthropic's standard tool_result schema only carries `tool_use_id`. Detect
2025
+ * that endpoint so we can emit the non-standard alias for it without
2026
+ * polluting requests to api.anthropic.com or other compatible proxies.
2027
+ * See: https://github.com/can1357/gajae-code/issues/814
2028
+ */
2029
+ function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2030
+ if (model.provider === "zai") return true;
2031
+ const baseUrl = model.baseUrl;
2032
+ if (!baseUrl) return false;
2033
+ try {
2034
+ return new URL(baseUrl).hostname.toLowerCase() === "api.z.ai";
2035
+ } catch {
2036
+ return false;
2037
+ }
2038
+ }
2039
+
2040
+ /**
2041
+ * Returns true for providers whose Anthropic-compatible endpoints do NOT
2042
+ * implement signature-based thinking-chain integrity (DeepSeek, Z.AI, etc.).
2043
+ * For these providers, unsigned thinking blocks must be preserved as
2044
+ * `type: "thinking"` instead of being degraded to text.
2045
+ */
2046
+ function isNonSigningAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2047
+ // Known non-signing providers
2048
+ if (model.provider === "zai" || model.provider === "deepseek") return true;
2049
+ const baseUrl = model.baseUrl;
2050
+ if (!baseUrl) return false;
2051
+ try {
2052
+ const hostname = new URL(baseUrl).hostname.toLowerCase();
2053
+ return hostname === "api.deepseek.com" || hostname.endsWith(".deepseek.com");
2054
+ } catch {
2055
+ return false;
2056
+ }
2057
+ }
2058
+
2059
+ function buildToolResultBlock(model: Model<"anthropic-messages">, msg: ToolResultMessage): ContentBlockParam {
2060
+ const block: ContentBlockParam = {
2061
+ type: "tool_result",
2062
+ tool_use_id: msg.toolCallId,
2063
+ content: convertContentBlocks(msg.content, model.input.includes("image")),
2064
+ is_error: msg.isError,
2065
+ };
2066
+ if (isZaiAnthropicEndpoint(model)) {
2067
+ // Z.AI workaround (issue #814): include `id` aliased to `tool_use_id`.
2068
+ (block as unknown as Record<string, unknown>).id = msg.toolCallId;
2069
+ }
2070
+ return block;
2071
+ }
2072
+
2073
+ export function convertAnthropicMessages(
2074
+ messages: Message[],
2075
+ model: Model<"anthropic-messages">,
2076
+ isOAuthToken: boolean,
2077
+ ): MessageParam[] {
2078
+ const params: MessageParam[] = [];
2079
+
2080
+ const transformedMessages = transformMessages(messages, model, normalizeToolCallId);
2081
+
2082
+ for (let i = 0; i < transformedMessages.length; i++) {
2083
+ const msg = transformedMessages[i];
2084
+
2085
+ if (msg.role === "user" || msg.role === "developer") {
2086
+ if (!msg.content) continue;
2087
+
2088
+ if (typeof msg.content === "string") {
2089
+ if (msg.content.trim().length > 0) {
2090
+ params.push({
2091
+ role: "user",
2092
+ content: msg.content.toWellFormed(),
2093
+ });
2094
+ }
2095
+ } else {
2096
+ const contentBlocks = convertContentBlocks(msg.content, model.input.includes("image"));
2097
+ if (typeof contentBlocks === "string") {
2098
+ if (contentBlocks.trim().length === 0) continue;
2099
+ params.push({
2100
+ role: "user",
2101
+ content: contentBlocks,
2102
+ });
2103
+ continue;
2104
+ }
2105
+ if (contentBlocks.length === 0) continue;
2106
+ params.push({
2107
+ role: "user",
2108
+ content: contentBlocks,
2109
+ });
2110
+ }
2111
+ } else if (msg.role === "assistant") {
2112
+ const blocks: ContentBlockParam[] = [];
2113
+ const hasSignedThinking = msg.content.some(
2114
+ block =>
2115
+ block.type === "thinking" && !!block.thinkingSignature && block.thinkingSignature.trim().length > 0,
2116
+ );
2117
+
2118
+ for (const block of msg.content) {
2119
+ if (block.type === "text") {
2120
+ if (block.text.trim().length === 0) continue;
2121
+ blocks.push({
2122
+ type: "text",
2123
+ text: block.text.toWellFormed(),
2124
+ });
2125
+ } else if (block.type === "thinking") {
2126
+ if (hasSignedThinking) {
2127
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
2128
+ if (block.thinking.trim().length === 0) continue;
2129
+ blocks.push({
2130
+ type: "text",
2131
+ text: block.thinking.toWellFormed(),
2132
+ });
2133
+ continue;
2134
+ }
2135
+ blocks.push({
2136
+ type: "thinking",
2137
+ thinking: block.thinking,
2138
+ signature: block.thinkingSignature,
2139
+ });
2140
+ continue;
2141
+ }
2142
+ if (block.thinking.trim().length === 0) continue;
2143
+ if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
2144
+ if (isNonSigningAnthropicEndpoint(model)) {
2145
+ blocks.push({
2146
+ type: "thinking",
2147
+ thinking: block.thinking.toWellFormed(),
2148
+ signature: "",
2149
+ });
2150
+ } else {
2151
+ blocks.push({
2152
+ type: "text",
2153
+ text: block.thinking.toWellFormed(),
2154
+ });
2155
+ }
2156
+ } else {
2157
+ blocks.push({
2158
+ type: "thinking",
2159
+ thinking: block.thinking.toWellFormed(),
2160
+ signature: block.thinkingSignature,
2161
+ });
2162
+ }
2163
+ } else if (block.type === "redactedThinking") {
2164
+ if (block.data.trim().length === 0) continue;
2165
+ blocks.push({
2166
+ type: "redacted_thinking",
2167
+ data: block.data,
2168
+ });
2169
+ } else if (block.type === "toolCall") {
2170
+ blocks.push({
2171
+ type: "tool_use",
2172
+ id: block.id,
2173
+ name: isOAuthToken ? applyClaudeToolPrefix(block.name) : block.name,
2174
+ input: block.arguments ?? {},
2175
+ });
2176
+ }
2177
+ }
2178
+ if (blocks.length === 0) continue;
2179
+ params.push({
2180
+ role: "assistant",
2181
+ content: blocks,
2182
+ });
2183
+ } else if (msg.role === "toolResult") {
2184
+ // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
2185
+ const toolResults: ContentBlockParam[] = [];
2186
+
2187
+ // Add the current tool result
2188
+ toolResults.push(buildToolResultBlock(model, msg));
2189
+
2190
+ // Look ahead for consecutive toolResult messages
2191
+ let j = i + 1;
2192
+ while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
2193
+ const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
2194
+ toolResults.push(buildToolResultBlock(model, nextMsg));
2195
+ j++;
2196
+ }
2197
+
2198
+ // Skip the messages we've already processed
2199
+ i = j - 1;
2200
+
2201
+ // Add a single user message with all tool results
2202
+ params.push({
2203
+ role: "user",
2204
+ content: toolResults,
2205
+ });
2206
+ }
2207
+ }
2208
+
2209
+ if (params.length > 0 && params[params.length - 1]?.role === "assistant") {
2210
+ params.push({ role: "user", content: "Continue." });
2211
+ }
2212
+
2213
+ return params;
2214
+ }
2215
+
2216
+ /**
2217
+ * JSON Schema whitelist for Anthropic tool `input_schema` nodes.
2218
+ *
2219
+ * Mirrors the Anthropic Python SDK's `lib/_parse/_transform.py::transform_schema`:
2220
+ * we keep only structural/metadata keywords Anthropic's validator honors, and demote
2221
+ * anything else into the node's `description` as `\n\n{key: value, ...}` so the model
2222
+ * still sees the constraint as a natural-language hint.
2223
+ *
2224
+ * `Set` (not `Record<string, true>`) because membership is probed against arbitrary
2225
+ * user/Zod-derived schema keys: a literal Record would falsely match prototype names
2226
+ * like `"toString"` and silently strip valid properties.
2227
+ */
2228
+ const ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP = new Set([
2229
+ "$ref",
2230
+ "$defs",
2231
+ "$schema",
2232
+ "definitions",
2233
+ "type",
2234
+ "anyOf",
2235
+ "oneOf",
2236
+ "allOf",
2237
+ "enum",
2238
+ "const",
2239
+ "description",
2240
+ "title",
2241
+ "default",
2242
+ "nullable",
2243
+ ]);
2244
+ /** Keys preserved on `type: "object"` nodes (in addition to the universal set). */
2245
+ const ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP = new Set(["properties", "required", "additionalProperties"]);
2246
+ /** Keys preserved on `type: "array"` nodes; `minItems` only when its value is 0 or 1. */
2247
+ const ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP = new Set(["items", "prefixItems", "minItems"]);
2248
+ /** Keys preserved on `type: "string"` nodes; `format` only when its value is in the supported list. */
2249
+ const ANTHROPIC_TOOL_SCHEMA_STRING_KEEP = new Set(["format"]);
2250
+ /**
2251
+ * String `format` values Anthropic accepts; everything else (including `pattern`-style
2252
+ * format hints) gets demoted into `description`. Matches `SupportedStringFormats` in the
2253
+ * Anthropic SDK's `_transform.py`.
2254
+ */
2255
+ const ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS = new Set([
2256
+ "date-time",
2257
+ "time",
2258
+ "date",
2259
+ "duration",
2260
+ "email",
2261
+ "hostname",
2262
+ "uri",
2263
+ "ipv4",
2264
+ "ipv6",
2265
+ "uuid",
2266
+ ]);
2267
+ const ANTHROPIC_STRICT_TOOL_ALLOWLIST = new Set(["bash", "python", "edit", "find"]);
2268
+ const MAX_ANTHROPIC_STRICT_TOOLS = 20;
2269
+ const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
2270
+ const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
2271
+
2272
+ /** `minItems` / `maxItems` apply to arrays; Anthropic rejects them on `type: "object"` (including `minItems: 0`/`1`). */
2273
+ function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
2274
+ const t = schema.type;
2275
+ if (t === "array") return true;
2276
+ if (Array.isArray(t) && t.includes("array") && !t.includes("object")) return true;
2277
+ return false;
2278
+ }
2279
+
2280
+ function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean {
2281
+ if (isJsonSchemaArrayNode(schema)) return false;
2282
+ if (schema.type === "object") return true;
2283
+ if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
2284
+ if (isRecord(schema.properties)) return true;
2285
+ return false;
2286
+ }
2287
+
2288
+ /**
2289
+ * Pick the principal non-null scalar type from a `type` keyword. Anthropic accepts
2290
+ * `type` as either a single string or an array (e.g. `["number", "null"]` for a
2291
+ * nullable value); the SDK whitelist is keyed off the scalar type, with `"null"`
2292
+ * ignored so nullable variants are normalized as their underlying type.
2293
+ */
2294
+ function pickAnthropicScalarType(type: unknown): string | undefined {
2295
+ if (typeof type === "string") return type;
2296
+ if (Array.isArray(type)) {
2297
+ for (const entry of type) {
2298
+ if (typeof entry === "string" && entry !== "null") return entry;
2299
+ }
2300
+ }
2301
+ return undefined;
2302
+ }
2303
+
2304
+ function anthropicPerTypeKeep(scalarType: string | undefined): Set<string> | undefined {
2305
+ switch (scalarType) {
2306
+ case "object":
2307
+ return ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP;
2308
+ case "array":
2309
+ return ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP;
2310
+ case "string":
2311
+ return ANTHROPIC_TOOL_SCHEMA_STRING_KEEP;
2312
+ default:
2313
+ return undefined;
2314
+ }
2315
+ }
2316
+
2317
+ /**
2318
+ * Per-schema-object memoization slot for the normalized Anthropic tool form. We stamp
2319
+ * the result onto the host via a `Symbol` property (mirroring `utils/schema/stamps.ts`)
2320
+ * instead of using a `WeakMap`: it's a single hidden-class slot, so warm reads are
2321
+ * direct property access and write-once cycles resolve to the in-progress result.
2322
+ */
2323
+ const kAnthropicToolNormal = Symbol("pi.schema.anthropic.toolNormal");
2324
+
2325
+ /**
2326
+ * Normalize a JSON Schema node for Anthropic tool `input_schema`.
2327
+ *
2328
+ * Applies the full whitelist semantics from the Anthropic Python SDK's
2329
+ * `lib/_parse/_transform.py::transform_schema`:
2330
+ *
2331
+ * 1. Universal keys (`$ref`, `$defs`, `type`, `anyOf`/`oneOf`/`allOf`, `enum`, `const`,
2332
+ * `description`, `title`, `default`, `nullable`) are preserved on every node.
2333
+ * 2. Per-type keys are kept additively (object → `properties`/`required`/`additionalProperties`,
2334
+ * array → `items`/`prefixItems` plus `minItems` only when 0 or 1, string → `format`
2335
+ * only when in the supported value set).
2336
+ * 3. Everything else is demoted into the node's `description` as `\n\n{key: value, ...}`
2337
+ * so the model still sees the constraint as a natural-language hint.
2338
+ *
2339
+ * Object nodes default to `additionalProperties: false`, but explicit open-map
2340
+ * declarations (`additionalProperties: true` or a schema literal — Zod's
2341
+ * `z.record(z.string(), z.unknown())` produces `{}`) are preserved. The strict-mode
2342
+ * pass downstream demotes those shapes to non-strict instead of fabricating a closed
2343
+ * object, so callers like the resolve tool keep working open-map semantics.
2344
+ */
2345
+ export function normalizeAnthropicToolSchema(schema: unknown): unknown {
2346
+ if (Array.isArray(schema)) return schema.map(entry => normalizeAnthropicToolSchema(entry));
2347
+ if (!isRecord(schema)) return schema;
2348
+
2349
+ const slot = schema as Record<symbol, Record<string, unknown> | undefined>;
2350
+ const existing = slot[kAnthropicToolNormal];
2351
+ if (existing !== undefined) return existing;
2352
+
2353
+ const result: Record<string, unknown> = {};
2354
+ // Pre-stamp before recursion so cyclic schemas resolve to the in-progress object
2355
+ // (mirrors the WeakMap-set-before-recurse pattern the original implementation used).
2356
+ Object.defineProperty(schema, kAnthropicToolNormal, { value: result, writable: true, configurable: true });
2357
+
2358
+ const scalarType = pickAnthropicScalarType(schema.type);
2359
+ const perTypeKeep = anthropicPerTypeKeep(scalarType);
2360
+ const spill: Array<[string, unknown]> = [];
2361
+
2362
+ for (const key in schema) {
2363
+ if (!Object.hasOwn(schema, key)) continue;
2364
+ const value = schema[key];
2365
+ if (ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP.has(key) || perTypeKeep?.has(key)) {
2366
+ result[key] = value;
2367
+ } else {
2368
+ spill.push([key, value]);
2369
+ }
2370
+ }
2371
+
2372
+ // Per-type conditional keys: prune within the kept set.
2373
+ if (scalarType === "string") {
2374
+ const format = result.format;
2375
+ if (typeof format === "string" && !ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS.has(format)) {
2376
+ spill.push(["format", format]);
2377
+ delete result.format;
2378
+ }
2379
+ }
2380
+ if (scalarType === "array" && result.minItems !== undefined) {
2381
+ const minItems = result.minItems;
2382
+ if (!(typeof minItems === "number" && (minItems === 0 || minItems === 1))) {
2383
+ spill.push(["minItems", minItems]);
2384
+ delete result.minItems;
2385
+ }
2386
+ }
2387
+ if (scalarType === "object" && result.additionalProperties === undefined) {
2388
+ result.additionalProperties = false;
2389
+ }
2390
+
2391
+ // Recurse on structural keys.
2392
+ if (isRecord(result.properties)) {
2393
+ const normalizedProperties: Record<string, unknown> = {};
2394
+ const sourceProperties = result.properties as Record<string, unknown>;
2395
+ for (const propName in sourceProperties) {
2396
+ if (!Object.hasOwn(sourceProperties, propName)) continue;
2397
+ normalizedProperties[propName] = normalizeAnthropicToolSchema(sourceProperties[propName]);
2398
+ }
2399
+ result.properties = normalizedProperties;
2400
+ }
2401
+ if (isRecord(result.additionalProperties)) {
2402
+ const normalized = normalizeAnthropicToolSchema(result.additionalProperties);
2403
+ if (isRecord(normalized) && Object.keys(normalized).length === 0) {
2404
+ result.additionalProperties = true;
2405
+ } else {
2406
+ result.additionalProperties = normalized;
2407
+ }
2408
+ }
2409
+ if (Array.isArray(result.items)) {
2410
+ result.items = result.items.map(item => normalizeAnthropicToolSchema(item));
2411
+ } else if (isRecord(result.items)) {
2412
+ result.items = normalizeAnthropicToolSchema(result.items);
2413
+ }
2414
+ if (Array.isArray(result.prefixItems)) {
2415
+ result.prefixItems = result.prefixItems.map(item => normalizeAnthropicToolSchema(item));
2416
+ }
2417
+ for (const key of COMBINATOR_KEYS) {
2418
+ const variants = result[key];
2419
+ if (Array.isArray(variants)) {
2420
+ result[key] = variants.map(variant => normalizeAnthropicToolSchema(variant));
2421
+ }
2422
+ }
2423
+ for (const defsKey of ["$defs", "definitions"] as const) {
2424
+ const definitions = result[defsKey];
2425
+ if (!isRecord(definitions)) continue;
2426
+ const normalizedDefs: Record<string, unknown> = {};
2427
+ const sourceDefs = definitions as Record<string, unknown>;
2428
+ for (const name in sourceDefs) {
2429
+ if (!Object.hasOwn(sourceDefs, name)) continue;
2430
+ normalizedDefs[name] = normalizeAnthropicToolSchema(sourceDefs[name]);
2431
+ }
2432
+ result[defsKey] = normalizedDefs;
2433
+ }
2434
+
2435
+ spillToDescription(result, spill);
2436
+ return result;
2437
+ }
2438
+
2439
+ type AnthropicToolInputSchema = Anthropic.Messages.Tool["input_schema"];
2440
+
2441
+ type AnthropicToolSchemaPlan = {
2442
+ inputSchema: AnthropicToolInputSchema;
2443
+ strict: boolean;
2444
+ };
2445
+
2446
+ type AnthropicStrictBudget = {
2447
+ optionalRemaining: number;
2448
+ unionRemaining: number;
2449
+ optionalCount: number;
2450
+ unionCount: number;
2451
+ };
2452
+
2453
+ function hasAnthropicUnionType(schema: Record<string, unknown>): boolean {
2454
+ return Array.isArray(schema.type) || Array.isArray(schema.anyOf);
2455
+ }
2456
+
2457
+ function hasNullVariant(schema: Record<string, unknown>): boolean {
2458
+ if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
2459
+ return Array.isArray(schema.anyOf) && schema.anyOf.some(variant => isRecord(variant) && variant.type === "null");
2460
+ }
2461
+
2462
+ function makeAnthropicNullableSchema(schema: unknown, budget: AnthropicStrictBudget): unknown | undefined {
2463
+ if (isRecord(schema)) {
2464
+ if (hasNullVariant(schema)) return schema;
2465
+ if (Array.isArray(schema.anyOf)) {
2466
+ return { ...schema, anyOf: [...schema.anyOf, { type: "null" }] };
2467
+ }
2468
+ if (Array.isArray(schema.type)) {
2469
+ return { ...schema, type: [...schema.type, "null"] };
2470
+ }
2471
+ }
2472
+
2473
+ if (budget.unionRemaining <= 0) return undefined;
2474
+ budget.unionRemaining--;
2475
+ budget.unionCount++;
2476
+ return { anyOf: [schema, { type: "null" }] };
2477
+ }
2478
+
2479
+ function normalizeAnthropicStrictSchemaNode(
2480
+ schema: unknown,
2481
+ budget: AnthropicStrictBudget,
2482
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
2483
+ ): unknown | undefined {
2484
+ if (Array.isArray(schema)) {
2485
+ const result: unknown[] = [];
2486
+ for (const entry of schema) {
2487
+ const normalized = normalizeAnthropicStrictSchemaNode(entry, budget, cache);
2488
+ if (normalized === undefined) return undefined;
2489
+ result.push(normalized);
2490
+ }
2491
+ return result;
2492
+ }
2493
+
2494
+ if (!isRecord(schema)) return schema;
2495
+
2496
+ const cached = cache.get(schema);
2497
+ if (cached) return cached;
2498
+
2499
+ // Strict tool use only supports closed objects. Open maps stay available on
2500
+ // the non-strict schema plan instead of producing an Anthropic 400.
2501
+ if (isJsonSchemaObjectNode(schema) && schema.additionalProperties !== false) {
2502
+ return undefined;
2503
+ }
2504
+
2505
+ const result: Record<string, unknown> = { ...schema };
2506
+ cache.set(schema, result);
2507
+
2508
+ if (hasAnthropicUnionType(result)) {
2509
+ if (budget.unionRemaining <= 0) return undefined;
2510
+ budget.unionRemaining--;
2511
+ budget.unionCount++;
2512
+ }
2513
+
2514
+ if (isRecord(result.properties)) {
2515
+ const originalRequired = new Set(
2516
+ Array.isArray(result.required)
2517
+ ? result.required.filter((entry): entry is string => typeof entry === "string")
2518
+ : [],
2519
+ );
2520
+ const properties: Record<string, unknown> = {};
2521
+ const required: string[] = [];
2522
+
2523
+ for (const [propertyName, propertySchema] of Object.entries(result.properties)) {
2524
+ const normalizedProperty = normalizeAnthropicStrictSchemaNode(propertySchema, budget, cache);
2525
+ if (normalizedProperty === undefined) return undefined;
2526
+
2527
+ if (originalRequired.has(propertyName)) {
2528
+ properties[propertyName] = normalizedProperty;
2529
+ required.push(propertyName);
2530
+ continue;
2531
+ }
2532
+
2533
+ if (budget.optionalRemaining > 0) {
2534
+ budget.optionalRemaining--;
2535
+ budget.optionalCount++;
2536
+ properties[propertyName] = normalizedProperty;
2537
+ continue;
2538
+ }
2539
+
2540
+ const nullableProperty = makeAnthropicNullableSchema(normalizedProperty, budget);
2541
+ if (nullableProperty === undefined) return undefined;
2542
+ properties[propertyName] = nullableProperty;
2543
+ required.push(propertyName);
2544
+ }
2545
+
2546
+ result.properties = properties;
2547
+ result.required = required;
2548
+ }
2549
+
2550
+ if (Array.isArray(result.items)) {
2551
+ const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
2552
+ if (items === undefined) return undefined;
2553
+ result.items = items;
2554
+ } else if (isRecord(result.items)) {
2555
+ const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
2556
+ if (items === undefined) return undefined;
2557
+ result.items = items;
2558
+ }
2559
+ if (Array.isArray(result.prefixItems)) {
2560
+ const prefixItems = normalizeAnthropicStrictSchemaNode(result.prefixItems, budget, cache);
2561
+ if (prefixItems === undefined) return undefined;
2562
+ result.prefixItems = prefixItems;
2563
+ }
2564
+
2565
+ for (const key of COMBINATOR_KEYS) {
2566
+ const variants = result[key];
2567
+ if (!Array.isArray(variants)) continue;
2568
+ const normalizedVariants = normalizeAnthropicStrictSchemaNode(variants, budget, cache);
2569
+ if (normalizedVariants === undefined) return undefined;
2570
+ result[key] = normalizedVariants;
2571
+ }
2572
+
2573
+ for (const defsKey of ["$defs", "definitions"] as const) {
2574
+ const definitions = result[defsKey];
2575
+ if (!isRecord(definitions)) continue;
2576
+ const normalizedDefinitions: Record<string, unknown> = {};
2577
+ for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
2578
+ const normalizedDefinition = normalizeAnthropicStrictSchemaNode(definitionSchema, budget, cache);
2579
+ if (normalizedDefinition === undefined) return undefined;
2580
+ normalizedDefinitions[definitionName] = normalizedDefinition;
2581
+ }
2582
+ result[defsKey] = normalizedDefinitions;
2583
+ }
2584
+
2585
+ return result;
2586
+ }
2587
+
2588
+ function normalizeAnthropicStrictSchema(
2589
+ schema: Record<string, unknown>,
2590
+ optionalRemaining: number,
2591
+ unionRemaining: number,
2592
+ ): { schema: Record<string, unknown>; optionalCount: number; unionCount: number } | undefined {
2593
+ const budget: AnthropicStrictBudget = {
2594
+ optionalRemaining,
2595
+ unionRemaining,
2596
+ optionalCount: 0,
2597
+ unionCount: 0,
2598
+ };
2599
+ const normalized = normalizeAnthropicStrictSchemaNode(schema, budget, new WeakMap());
2600
+ if (!isRecord(normalized)) return undefined;
2601
+ return { schema: normalized, optionalCount: budget.optionalCount, unionCount: budget.unionCount };
2602
+ }
2603
+
2604
+ function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
2605
+ const jsonSchema = toolWireSchema(tool);
2606
+ return normalizeAnthropicToolSchema({
2607
+ ...jsonSchema,
2608
+ type: "object",
2609
+ properties: isRecord(jsonSchema.properties) ? jsonSchema.properties : {},
2610
+ required: Array.isArray(jsonSchema.required)
2611
+ ? jsonSchema.required.filter((entry): entry is string => typeof entry === "string")
2612
+ : [],
2613
+ }) as Record<string, unknown>;
2614
+ }
2615
+
2616
+ function buildAnthropicToolSchemaPlans(tools: Tool[], disableStrictTools = false): AnthropicToolSchemaPlan[] {
2617
+ const plans = tools.map(
2618
+ (tool): AnthropicToolSchemaPlan => ({
2619
+ inputSchema: buildAnthropicBaseToolInputSchema(tool) as AnthropicToolInputSchema,
2620
+ strict: false,
2621
+ }),
2622
+ );
2623
+ if (NO_STRICT || disableStrictTools) return plans;
2624
+
2625
+ const candidateIndexes = tools.flatMap((tool, index) => {
2626
+ if (!ANTHROPIC_STRICT_TOOL_ALLOWLIST.has(tool.name)) return [];
2627
+ return tool.strict === false ? [] : [index];
2628
+ });
2629
+
2630
+ let strictToolCount = 0;
2631
+ let strictOptionalParameterCount = 0;
2632
+ let strictUnionParameterCount = 0;
2633
+ for (const index of candidateIndexes) {
2634
+ if (strictToolCount >= MAX_ANTHROPIC_STRICT_TOOLS) break;
2635
+
2636
+ const strictResult = normalizeAnthropicStrictSchema(
2637
+ plans[index].inputSchema as Record<string, unknown>,
2638
+ MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS - strictOptionalParameterCount,
2639
+ MAX_ANTHROPIC_STRICT_UNION_PARAMETERS - strictUnionParameterCount,
2640
+ );
2641
+ if (!strictResult) continue;
2642
+
2643
+ plans[index] = {
2644
+ inputSchema: strictResult.schema as AnthropicToolInputSchema,
2645
+ strict: true,
2646
+ };
2647
+ strictToolCount++;
2648
+ strictOptionalParameterCount += strictResult.optionalCount;
2649
+ strictUnionParameterCount += strictResult.unionCount;
2650
+ }
2651
+
2652
+ return plans;
2653
+ }
2654
+
2655
+ function convertTools(
2656
+ tools: Tool[],
2657
+ isOAuthToken: boolean,
2658
+ disableStrictTools = false,
2659
+ supportsEagerToolInputStreaming = true,
2660
+ ): Anthropic.Messages.Tool[] {
2661
+ if (!tools) return [];
2662
+ const schemaPlans = buildAnthropicToolSchemaPlans(tools, disableStrictTools);
2663
+
2664
+ return tools.map((tool, index) => {
2665
+ const plan = schemaPlans[index];
2666
+ return {
2667
+ name: isOAuthToken ? applyClaudeToolPrefix(tool.name) : tool.name,
2668
+ description: tool.description || "",
2669
+ input_schema: plan.inputSchema,
2670
+ ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
2671
+ ...(plan.strict ? { strict: true } : {}),
2672
+ };
2673
+ });
2674
+ }
2675
+
2676
+ function mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason {
2677
+ switch (reason) {
2678
+ case "end_turn":
2679
+ return "stop";
2680
+ case "max_tokens":
2681
+ return "length";
2682
+ case "tool_use":
2683
+ return "toolUse";
2684
+ case "refusal":
2685
+ return "error";
2686
+ case "pause_turn": // Stop is good enough -> resubmit
2687
+ return "stop";
2688
+ case "stop_sequence":
2689
+ return "stop"; // We don't supply stop sequences, so this should never happen
2690
+ case "sensitive": // Content flagged by safety filters (not yet in SDK types)
2691
+ return "error";
2692
+ default:
2693
+ // Handle unknown stop reasons gracefully (API may add new values)
2694
+ throw new Error(`Unhandled stop reason: ${reason}`);
2695
+ }
2696
+ }