@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,677 @@
1
+ import { logger } from "@gajae-code/utils";
2
+ import { captureRequestHeaders, resolvePromptCacheKey } from "../auth-gateway/http";
3
+ import type {
4
+ AssistantMessage,
5
+ AssistantMessageEventStream,
6
+ Message,
7
+ RedactedThinkingContent,
8
+ StopReason,
9
+ TextContent,
10
+ ThinkingContent,
11
+ Tool,
12
+ ToolCall,
13
+ ToolResultMessage,
14
+ UserMessage,
15
+ } from "../types";
16
+ import {
17
+ type AnthropicAssistantContentBlock,
18
+ type AnthropicMessage,
19
+ type AnthropicSystem,
20
+ type AnthropicTool,
21
+ type AnthropicToolChoice,
22
+ type AnthropicToolResultContent,
23
+ type AnthropicUserContentBlock,
24
+ anthropicMessagesRequestSchema,
25
+ } from "./anthropic-messages-server-schema";
26
+
27
+ /**
28
+ * Anthropic Messages API (https://docs.anthropic.com/en/api/messages) ↔ pi-ai
29
+ * gateway translation. Inbound: foreign HTTP body → gjc Context. Outbound:
30
+ * gjc AssistantMessage[Stream] → Anthropic-shaped JSON / SSE.
31
+ */
32
+
33
+ import type { AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types";
34
+
35
+ export type { ParsedRequest };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Inbound parsing
39
+ // ---------------------------------------------------------------------------
40
+
41
+ type ImageContentPart = { type: "image"; data: string; mimeType: string };
42
+
43
+ // Dedup noise from unknown-block-type warnings. Module-scoped so the warn
44
+ // fires once per (category, type) pair across the lifetime of the process.
45
+ const WARNED_UNKNOWN_BLOCK_TYPES = new Set<string>();
46
+ function warnUnknownBlockType(category: "user" | "assistant", blockType: string): void {
47
+ const key = `${category}:${blockType}`;
48
+ if (WARNED_UNKNOWN_BLOCK_TYPES.has(key)) return;
49
+ WARNED_UNKNOWN_BLOCK_TYPES.add(key);
50
+ logger.warn("anthropic-messages: unknown content block flattened to text placeholder", {
51
+ category,
52
+ blockType,
53
+ });
54
+ }
55
+
56
+ // pi-ai's `ImageContent` only carries base64 + mimeType. When the inbound
57
+ // uses `url` or `file_id` sources we surface a text placeholder so the
58
+ // downstream provider still sees a sane history; warn once per source kind.
59
+ const WARNED_NON_BASE64_IMAGE_SOURCES = new Set<string>();
60
+ function warnNonBase64ImageSource(sourceType: string): void {
61
+ if (WARNED_NON_BASE64_IMAGE_SOURCES.has(sourceType)) return;
62
+ WARNED_NON_BASE64_IMAGE_SOURCES.add(sourceType);
63
+ logger.warn("anthropic-messages: image source surfaced as text placeholder (pi-ai ImageContent lacks URL channel)", {
64
+ sourceType,
65
+ });
66
+ }
67
+
68
+ // Compact, log-safe stringification for unknown content blocks. Keeps the
69
+ // placeholder informative without dumping multi-KB structures into history.
70
+ function describeUnknownBlock(block: { type: string }): string {
71
+ try {
72
+ const json = JSON.stringify(block);
73
+ if (json !== undefined && json.length <= 200) return `[${block.type}: ${json}]`;
74
+ } catch {
75
+ // fall through
76
+ }
77
+ return `[${block.type}]`;
78
+ }
79
+
80
+ function buildSystemPrompt(raw: AnthropicSystem): string[] | undefined {
81
+ if (raw === undefined) return undefined;
82
+ if (typeof raw === "string") return raw.length > 0 ? [raw] : undefined;
83
+ const parts = raw.map(block => block.text).filter(text => text.length > 0);
84
+ return parts.length > 0 ? [parts.join("\n\n")] : undefined;
85
+ }
86
+
87
+ function makeUserMessage(parts: (TextContent | ImageContentPart)[], timestamp: number): UserMessage {
88
+ return {
89
+ role: "user",
90
+ content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts,
91
+ timestamp,
92
+ };
93
+ }
94
+
95
+ function toolResultPartsFromBlocks(
96
+ content: AnthropicToolResultContent[] | string | undefined,
97
+ ): (TextContent | ImageContentPart)[] {
98
+ if (content === undefined) return [];
99
+ if (typeof content === "string") return [{ type: "text", text: content }];
100
+ const out: (TextContent | ImageContentPart)[] = [];
101
+ for (const block of content) {
102
+ if (block.type === "text") {
103
+ out.push({ type: "text", text: block.text });
104
+ continue;
105
+ }
106
+ // block.type === "image" — schema only accepts base64 sources.
107
+ if (block.source.type === "base64") {
108
+ out.push({ type: "image", data: block.source.data, mimeType: block.source.media_type });
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
114
+ function walkUserContent(
115
+ blocks: string | AnthropicUserContentBlock[],
116
+ timestamp: number,
117
+ ): (UserMessage | ToolResultMessage)[] {
118
+ const messages: (UserMessage | ToolResultMessage)[] = [];
119
+ const userParts: (TextContent | ImageContentPart)[] = [];
120
+ const flush = () => {
121
+ if (userParts.length === 0) return;
122
+ messages.push(makeUserMessage(userParts.splice(0), timestamp));
123
+ };
124
+ if (typeof blocks === "string") {
125
+ if (blocks.length > 0) userParts.push({ type: "text", text: blocks });
126
+ flush();
127
+ return messages;
128
+ }
129
+ for (const block of blocks) {
130
+ if (block.type === "text") {
131
+ userParts.push({ type: "text", text: block.text });
132
+ } else if (block.type === "image") {
133
+ // SDK's typed source covers base64+url; our schema also accepts the
134
+ // forward-compat `file` variant. Narrow against a widened shape so
135
+ // every variant is handled at runtime regardless of SDK lag.
136
+ const source = block.source as {
137
+ type: string;
138
+ data?: string;
139
+ media_type?: string;
140
+ url?: string;
141
+ file_id?: string;
142
+ };
143
+ if (source.type === "base64" && source.data && source.media_type) {
144
+ userParts.push({ type: "image", data: source.data, mimeType: source.media_type });
145
+ } else {
146
+ warnNonBase64ImageSource(source.type);
147
+ const ref =
148
+ source.type === "url" ? (source.url ?? "") : source.type === "file" ? (source.file_id ?? "") : "";
149
+ userParts.push({ type: "text", text: `[image: ${ref}]` });
150
+ }
151
+ } else if (block.type === "tool_result") {
152
+ // Anthropic permits tool_result blocks to follow plain text/image
153
+ // siblings in the same user message. pi-ai's history is a flat
154
+ // sequence of typed messages, so flush the accumulated parts as a
155
+ // separate UserMessage before emitting the ToolResultMessage.
156
+ flush();
157
+ messages.push({
158
+ role: "toolResult",
159
+ toolCallId: block.tool_use_id,
160
+ // Anthropic tool_results don't carry the tool name; downstream can rehydrate.
161
+ toolName: "",
162
+ content: toolResultPartsFromBlocks(block.content as AnthropicToolResultContent[] | string | undefined),
163
+ isError: block.is_error === true,
164
+ timestamp,
165
+ });
166
+ } else {
167
+ // Unknown variant (server_tool_use, mcp_*, document, web_search_tool_result,
168
+ // container_upload, code_execution_*, …). Flatten to a text placeholder
169
+ // so the downstream provider still gets a coherent transcript.
170
+ const unknown = block as { type: string };
171
+ warnUnknownBlockType("user", unknown.type);
172
+ userParts.push({ type: "text", text: describeUnknownBlock(unknown) });
173
+ }
174
+ }
175
+ flush();
176
+ return messages;
177
+ }
178
+
179
+ function walkAssistantContent(
180
+ blocks: string | AnthropicAssistantContentBlock[],
181
+ ): (TextContent | ThinkingContent | RedactedThinkingContent | ToolCall)[] {
182
+ const out: (TextContent | ThinkingContent | RedactedThinkingContent | ToolCall)[] = [];
183
+ if (typeof blocks === "string") {
184
+ if (blocks.length > 0) out.push({ type: "text", text: blocks });
185
+ return out;
186
+ }
187
+ for (const block of blocks) {
188
+ switch (block.type) {
189
+ case "text":
190
+ out.push({ type: "text", text: block.text });
191
+ break;
192
+ case "thinking": {
193
+ const tc: ThinkingContent = { type: "thinking", thinking: block.thinking };
194
+ if (block.signature !== undefined) tc.thinkingSignature = block.signature;
195
+ out.push(tc);
196
+ break;
197
+ }
198
+ case "redacted_thinking":
199
+ out.push({ type: "redactedThinking", data: block.data });
200
+ break;
201
+ case "tool_use":
202
+ out.push({
203
+ type: "toolCall",
204
+ id: block.id,
205
+ name: block.name,
206
+ arguments: block.input ?? {},
207
+ });
208
+ break;
209
+ default: {
210
+ // Unknown assistant variant (server_tool_use, mcp_tool_use, …).
211
+ // Flatten to a text placeholder; warn once per unknown type.
212
+ const unknown = block as { type: string };
213
+ warnUnknownBlockType("assistant", unknown.type);
214
+ out.push({ type: "text", text: describeUnknownBlock(unknown) });
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ return out;
220
+ }
221
+
222
+ function walkTools(tools: AnthropicTool[] | undefined): Tool[] | undefined {
223
+ if (!tools) return undefined;
224
+ return tools.map(tool => ({
225
+ name: tool.name,
226
+ description: tool.description ?? "",
227
+ parameters: tool.input_schema as Record<string, unknown>,
228
+ }));
229
+ }
230
+
231
+ function mapToolChoice(choice: AnthropicToolChoice | undefined): ParsedRequest["options"]["toolChoice"] {
232
+ if (!choice) return undefined;
233
+ switch (choice.type) {
234
+ case "auto":
235
+ return "auto";
236
+ case "any":
237
+ return "required";
238
+ case "none":
239
+ return "none";
240
+ case "tool":
241
+ return { name: choice.name };
242
+ }
243
+ }
244
+
245
+ type AnthropicCacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
246
+ type HasCacheControl = { cache_control?: AnthropicCacheControl };
247
+
248
+ function readCacheControl(value: unknown): AnthropicCacheControl | undefined {
249
+ if (value === null || typeof value !== "object") return undefined;
250
+ const cc = (value as HasCacheControl).cache_control;
251
+ if (!cc || typeof cc !== "object" || cc.type !== "ephemeral") return undefined;
252
+ return cc;
253
+ }
254
+
255
+ /**
256
+ * Anthropic clients annotate caching breakpoints per block via
257
+ * `cache_control: { type: "ephemeral", ttl?: "1h"|"5m" }`. pi-ai's
258
+ * `cacheRetention` is per-request, not per-block, and its anthropic provider
259
+ * re-applies breakpoints itself on the rebuilt outbound wire. Scan every
260
+ * block once and return the strongest retention requested: any `ttl: "1h"`
261
+ * promotes the request to "long", anything else ephemeral maps to "short".
262
+ */
263
+ function deriveCacheRetention(data: {
264
+ system?: unknown;
265
+ messages: readonly unknown[];
266
+ tools?: readonly unknown[];
267
+ }): "short" | "long" | undefined {
268
+ let strongest: "short" | "long" | undefined;
269
+ const visit = (cc: AnthropicCacheControl | undefined): void => {
270
+ if (!cc) return;
271
+ if (cc.ttl === "1h") strongest = "long";
272
+ else strongest ??= "short";
273
+ };
274
+ if (Array.isArray(data.system)) {
275
+ for (const block of data.system) visit(readCacheControl(block));
276
+ }
277
+ for (const message of data.messages) {
278
+ if (message === null || typeof message !== "object") continue;
279
+ const content = (message as { content?: unknown }).content;
280
+ if (!Array.isArray(content)) continue;
281
+ for (const block of content) visit(readCacheControl(block));
282
+ }
283
+ if (data.tools) {
284
+ for (const tool of data.tools) visit(readCacheControl(tool));
285
+ }
286
+ return strongest;
287
+ }
288
+
289
+ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
290
+ const parsed = anthropicMessagesRequestSchema.safeParse(body);
291
+ if (!parsed.success) {
292
+ throw new Error(`anthropic-messages: ${parsed.error.message}`);
293
+ }
294
+ const data = parsed.data;
295
+
296
+ const now = Date.now();
297
+ const messages: Message[] = [];
298
+ for (const message of data.messages as AnthropicMessage[]) {
299
+ if (message.role === "user") {
300
+ for (const m of walkUserContent(message.content, now)) messages.push(m);
301
+ } else {
302
+ const assistant: AssistantMessage = {
303
+ role: "assistant",
304
+ content: walkAssistantContent(message.content),
305
+ api: "anthropic-messages",
306
+ provider: "anthropic",
307
+ model: data.model,
308
+ usage: emptyUsage(),
309
+ stopReason: "stop",
310
+ timestamp: now,
311
+ };
312
+ messages.push(assistant);
313
+ }
314
+ }
315
+
316
+ const options: ParsedRequest["options"] = {
317
+ maxOutputTokens: data.max_tokens,
318
+ };
319
+ if (data.temperature !== undefined) options.temperature = data.temperature;
320
+ if (data.top_p !== undefined) options.topP = data.top_p;
321
+ if (data.top_k !== undefined) options.topK = data.top_k;
322
+ if (data.stop_sequences) options.stopSequences = data.stop_sequences;
323
+ const toolChoice = mapToolChoice(data.tool_choice as AnthropicToolChoice | undefined);
324
+ if (toolChoice !== undefined) options.toolChoice = toolChoice;
325
+ // `disable_parallel_tool_use === true` means the client wants the model to
326
+ // emit at most one tool call per turn; map to pi-ai's negated boolean.
327
+ // Leave undefined when the field is absent or explicitly `false` so we
328
+ // don't override provider defaults.
329
+ if (data.tool_choice?.disable_parallel_tool_use === true) {
330
+ options.parallelToolCalls = false;
331
+ }
332
+ if (data.thinking) {
333
+ switch (data.thinking.type) {
334
+ case "enabled":
335
+ options.explicitThinkingBudgetTokens = data.thinking.budget_tokens;
336
+ break;
337
+ case "disabled":
338
+ options.disableReasoning = true;
339
+ break;
340
+ case "adaptive":
341
+ if (data.thinking.budget_tokens !== undefined) {
342
+ options.explicitThinkingBudgetTokens = data.thinking.budget_tokens;
343
+ }
344
+ break;
345
+ }
346
+ }
347
+ const cacheRetention = deriveCacheRetention(data);
348
+ if (cacheRetention !== undefined) options.cacheRetention = cacheRetention;
349
+ // Anthropic clients commonly send `metadata: { user_id }`; forward verbatim
350
+ // so downstream providers (and our anthropic-passthrough fast-path) can
351
+ // preserve abuse-tracking signal.
352
+ if (data.metadata !== undefined) {
353
+ options.metadata = data.metadata as Record<string, unknown>;
354
+ }
355
+ const cacheKey = resolvePromptCacheKey(body, headers);
356
+ if (cacheKey !== undefined) options.promptCacheKey = cacheKey;
357
+ // Allow-listed header capture. The gateway's `handleFormatEndpoint`
358
+ // already merges its own pre-capture under whatever the parser sets, but
359
+ // we populate here too so direct callers of `parseRequest` (tests, custom
360
+ // wrappers) see the same surface. `anthropic-version` is the most
361
+ // load-bearing — some downstream Anthropic-API targets reject requests
362
+ // missing it.
363
+ if (headers) {
364
+ const captured = captureRequestHeaders(headers);
365
+ if (Object.keys(captured).length > 0) options.headers = captured;
366
+ }
367
+
368
+ return {
369
+ modelId: data.model,
370
+ context: {
371
+ systemPrompt: buildSystemPrompt(data.system as AnthropicSystem),
372
+ messages,
373
+ tools: walkTools(data.tools as AnthropicTool[] | undefined),
374
+ },
375
+ stream: data.stream === true,
376
+ options,
377
+ };
378
+ }
379
+
380
+ function emptyUsage(): AssistantMessage["usage"] {
381
+ return {
382
+ input: 0,
383
+ output: 0,
384
+ cacheRead: 0,
385
+ cacheWrite: 0,
386
+ totalTokens: 0,
387
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
388
+ };
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Outbound encoding
393
+ // ---------------------------------------------------------------------------
394
+
395
+ function newMessageId(): string {
396
+ const hex = (globalThis.crypto?.randomUUID?.() ?? randomFallback()).replace(/-/g, "").slice(0, 24);
397
+ return `msg_${hex}`;
398
+ }
399
+
400
+ function randomFallback(): string {
401
+ // Sufficient for tests / environments without crypto.randomUUID
402
+ const buf = new Uint8Array(16);
403
+ for (let i = 0; i < 16; i++) buf[i] = Math.floor(Math.random() * 256);
404
+ const hex = Array.from(buf, b => b.toString(16).padStart(2, "0")).join("");
405
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
406
+ }
407
+
408
+ function mapStopReasonOut(reason: StopReason): "end_turn" | "max_tokens" | "tool_use" {
409
+ switch (reason) {
410
+ case "length":
411
+ return "max_tokens";
412
+ case "toolUse":
413
+ return "tool_use";
414
+ default:
415
+ return "end_turn";
416
+ }
417
+ }
418
+
419
+ function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>[] {
420
+ const blocks: Record<string, unknown>[] = [];
421
+ for (const c of message.content) {
422
+ switch (c.type) {
423
+ case "text":
424
+ blocks.push({ type: "text", text: c.text });
425
+ break;
426
+ case "thinking": {
427
+ const b: Record<string, unknown> = { type: "thinking", thinking: c.thinking };
428
+ if (c.thinkingSignature) b.signature = c.thinkingSignature;
429
+ blocks.push(b);
430
+ break;
431
+ }
432
+ case "redactedThinking":
433
+ blocks.push({ type: "redacted_thinking", data: c.data });
434
+ break;
435
+ case "toolCall":
436
+ blocks.push({ type: "tool_use", id: c.id, name: c.name, input: c.arguments ?? {} });
437
+ break;
438
+ }
439
+ }
440
+ return blocks;
441
+ }
442
+
443
+ function encodeUsage(message: AssistantMessage): Record<string, unknown> {
444
+ return {
445
+ input_tokens: message.usage.input,
446
+ output_tokens: message.usage.output,
447
+ cache_read_input_tokens: message.usage.cacheRead,
448
+ cache_creation_input_tokens: message.usage.cacheWrite,
449
+ };
450
+ }
451
+
452
+ export function encodeResponse(message: AssistantMessage, requestedModelId: string): Record<string, unknown> {
453
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
454
+ throw new Error(message.errorMessage ?? `anthropic-messages: upstream ${message.stopReason}`);
455
+ }
456
+ return {
457
+ id: message.responseId ?? newMessageId(),
458
+ type: "message",
459
+ role: "assistant",
460
+ model: requestedModelId,
461
+ content: encodeContentBlocks(message),
462
+ stop_reason: mapStopReasonOut(message.stopReason),
463
+ // TODO: surface the matched stop sequence once pi-ai's
464
+ // `AssistantMessage.stopReason` carries the matched string. Intentionally
465
+ // `null` for now (Anthropic schema allows it).
466
+ stop_sequence: null,
467
+ usage: encodeUsage(message),
468
+ };
469
+ }
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // Streaming encoder
473
+ // ---------------------------------------------------------------------------
474
+
475
+ const ENCODER = new TextEncoder();
476
+
477
+ function sseFrame(event: string, data: Record<string, unknown>): Uint8Array {
478
+ return ENCODER.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
479
+ }
480
+
481
+ type BlockKind = "text" | "thinking" | "tool_use";
482
+
483
+ interface OpenBlock {
484
+ index: number;
485
+ kind: BlockKind;
486
+ }
487
+
488
+ export function encodeStream(
489
+ events: AssistantMessageEventStream,
490
+ requestedModelId: string,
491
+ ): ReadableStream<Uint8Array> {
492
+ return new ReadableStream<Uint8Array>({
493
+ async start(controller) {
494
+ const messageId = newMessageId();
495
+ let started = false;
496
+ const open = new Map<number, OpenBlock>();
497
+
498
+ const ensureStart = (partial: AssistantMessage) => {
499
+ if (started) return;
500
+ started = true;
501
+ controller.enqueue(
502
+ sseFrame("message_start", {
503
+ type: "message_start",
504
+ message: {
505
+ id: messageId,
506
+ type: "message",
507
+ role: "assistant",
508
+ model: requestedModelId,
509
+ content: [],
510
+ stop_reason: null,
511
+ // TODO: same as encodeResponse — surface matched stop sequence
512
+ // once pi-ai propagates it.
513
+ stop_sequence: null,
514
+ usage: encodeUsage(partial),
515
+ },
516
+ }),
517
+ );
518
+ };
519
+
520
+ const closeBlock = (index: number) => {
521
+ if (!open.has(index)) return;
522
+ controller.enqueue(sseFrame("content_block_stop", { type: "content_block_stop", index }));
523
+ open.delete(index);
524
+ };
525
+
526
+ try {
527
+ for await (const ev of events) {
528
+ switch (ev.type) {
529
+ case "start":
530
+ ensureStart(ev.partial);
531
+ break;
532
+ case "text_start": {
533
+ ensureStart(ev.partial);
534
+ open.set(ev.contentIndex, { index: ev.contentIndex, kind: "text" });
535
+ controller.enqueue(
536
+ sseFrame("content_block_start", {
537
+ type: "content_block_start",
538
+ index: ev.contentIndex,
539
+ content_block: { type: "text", text: "" },
540
+ }),
541
+ );
542
+ break;
543
+ }
544
+ case "text_delta":
545
+ controller.enqueue(
546
+ sseFrame("content_block_delta", {
547
+ type: "content_block_delta",
548
+ index: ev.contentIndex,
549
+ delta: { type: "text_delta", text: ev.delta },
550
+ }),
551
+ );
552
+ break;
553
+ case "text_end":
554
+ closeBlock(ev.contentIndex);
555
+ break;
556
+ case "thinking_start": {
557
+ ensureStart(ev.partial);
558
+ open.set(ev.contentIndex, { index: ev.contentIndex, kind: "thinking" });
559
+ controller.enqueue(
560
+ sseFrame("content_block_start", {
561
+ type: "content_block_start",
562
+ index: ev.contentIndex,
563
+ content_block: { type: "thinking", thinking: "" },
564
+ }),
565
+ );
566
+ break;
567
+ }
568
+ case "thinking_delta":
569
+ controller.enqueue(
570
+ sseFrame("content_block_delta", {
571
+ type: "content_block_delta",
572
+ index: ev.contentIndex,
573
+ delta: { type: "thinking_delta", thinking: ev.delta },
574
+ }),
575
+ );
576
+ break;
577
+ case "thinking_end": {
578
+ const c = ev.partial.content[ev.contentIndex];
579
+ if (c?.type === "thinking" && c.thinkingSignature) {
580
+ controller.enqueue(
581
+ sseFrame("content_block_delta", {
582
+ type: "content_block_delta",
583
+ index: ev.contentIndex,
584
+ delta: { type: "signature_delta", signature: c.thinkingSignature },
585
+ }),
586
+ );
587
+ }
588
+ closeBlock(ev.contentIndex);
589
+ break;
590
+ }
591
+ case "toolcall_start": {
592
+ ensureStart(ev.partial);
593
+ const tc = ev.partial.content[ev.contentIndex] as ToolCall | undefined;
594
+ open.set(ev.contentIndex, { index: ev.contentIndex, kind: "tool_use" });
595
+ controller.enqueue(
596
+ sseFrame("content_block_start", {
597
+ type: "content_block_start",
598
+ index: ev.contentIndex,
599
+ content_block: {
600
+ type: "tool_use",
601
+ id: tc?.id ?? "",
602
+ name: tc?.name ?? "",
603
+ input: {},
604
+ },
605
+ }),
606
+ );
607
+ break;
608
+ }
609
+ case "toolcall_delta":
610
+ controller.enqueue(
611
+ sseFrame("content_block_delta", {
612
+ type: "content_block_delta",
613
+ index: ev.contentIndex,
614
+ delta: { type: "input_json_delta", partial_json: ev.delta },
615
+ }),
616
+ );
617
+ break;
618
+ case "toolcall_end":
619
+ closeBlock(ev.contentIndex);
620
+ break;
621
+ case "done": {
622
+ for (const idx of [...open.keys()]) closeBlock(idx);
623
+ controller.enqueue(
624
+ sseFrame("message_delta", {
625
+ type: "message_delta",
626
+ // TODO: surface matched stop sequence once pi-ai
627
+ // propagates it on the `done` event.
628
+ delta: { stop_reason: mapStopReasonOut(ev.reason), stop_sequence: null },
629
+ usage: encodeUsage(ev.message),
630
+ }),
631
+ );
632
+ controller.enqueue(sseFrame("message_stop", { type: "message_stop" }));
633
+ controller.close();
634
+ return;
635
+ }
636
+ case "error": {
637
+ const msg = ev.error.errorMessage ?? "stream error";
638
+ controller.enqueue(
639
+ sseFrame("error", { type: "error", error: { type: "api_error", message: msg } }),
640
+ );
641
+ controller.close();
642
+ return;
643
+ }
644
+ }
645
+ }
646
+ // stream ended without explicit done; close gracefully
647
+ for (const idx of [...open.keys()]) closeBlock(idx);
648
+ controller.enqueue(sseFrame("message_stop", { type: "message_stop" }));
649
+ controller.close();
650
+ } catch (err) {
651
+ controller.enqueue(
652
+ sseFrame("error", {
653
+ type: "error",
654
+ error: { type: "api_error", message: err instanceof Error ? err.message : String(err) },
655
+ }),
656
+ );
657
+ controller.close();
658
+ }
659
+ },
660
+ });
661
+ }
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // Error envelope
665
+ // ---------------------------------------------------------------------------
666
+
667
+ /**
668
+ * Anthropic error envelope: `{ type: "error", error: { type, message } }`.
669
+ * See https://docs.anthropic.com/en/api/errors. Returned as a `Response` so
670
+ * the gateway can hand it straight back to the client without extra wrapping.
671
+ */
672
+ export function formatError(status: number, type: string, message: string): Response {
673
+ return new Response(JSON.stringify({ type: "error", error: { type, message } }), {
674
+ status,
675
+ headers: { "Content-Type": "application/json" },
676
+ });
677
+ }