jeopi-ai 16.2.13

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 (598) hide show
  1. package/CHANGELOG.md +4347 -0
  2. package/README.md +1193 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +73 -0
  5. package/dist/types/auth-broker/discover.d.ts +35 -0
  6. package/dist/types/auth-broker/index.d.ts +7 -0
  7. package/dist/types/auth-broker/refresher.d.ts +25 -0
  8. package/dist/types/auth-broker/remote-store.d.ts +102 -0
  9. package/dist/types/auth-broker/server.d.ts +43 -0
  10. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  11. package/dist/types/auth-broker/types.d.ts +107 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +411 -0
  13. package/dist/types/auth-gateway/http.d.ts +39 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +36 -0
  16. package/dist/types/auth-gateway/types.d.ts +123 -0
  17. package/dist/types/auth-retry.d.ts +124 -0
  18. package/dist/types/auth-storage.d.ts +1026 -0
  19. package/dist/types/dialect/anthropic.d.ts +15 -0
  20. package/dist/types/dialect/catalog.d.ts +3 -0
  21. package/dist/types/dialect/coercion.d.ts +23 -0
  22. package/dist/types/dialect/deepseek.d.ts +14 -0
  23. package/dist/types/dialect/demotion.d.ts +23 -0
  24. package/dist/types/dialect/examples.d.ts +2 -0
  25. package/dist/types/dialect/factory.d.ts +3 -0
  26. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  27. package/dist/types/dialect/gemini.d.ts +17 -0
  28. package/dist/types/dialect/gemma.d.ts +15 -0
  29. package/dist/types/dialect/glm.d.ts +9 -0
  30. package/dist/types/dialect/harmony.d.ts +8 -0
  31. package/dist/types/dialect/hermes.d.ts +9 -0
  32. package/dist/types/dialect/history.d.ts +3 -0
  33. package/dist/types/dialect/index.d.ts +11 -0
  34. package/dist/types/dialect/inventory.d.ts +12 -0
  35. package/dist/types/dialect/kimi.d.ts +14 -0
  36. package/dist/types/dialect/minimax.d.ts +3 -0
  37. package/dist/types/dialect/owned-stream.d.ts +4 -0
  38. package/dist/types/dialect/qwen3.d.ts +9 -0
  39. package/dist/types/dialect/rendering.d.ts +45 -0
  40. package/dist/types/dialect/thinking.d.ts +6 -0
  41. package/dist/types/dialect/types.d.ts +69 -0
  42. package/dist/types/dialect/xml.d.ts +9 -0
  43. package/dist/types/error/abort.d.ts +14 -0
  44. package/dist/types/error/auth-classify.d.ts +16 -0
  45. package/dist/types/error/auth.d.ts +27 -0
  46. package/dist/types/error/aws.d.ts +23 -0
  47. package/dist/types/error/classes.d.ts +102 -0
  48. package/dist/types/error/finalize.d.ts +39 -0
  49. package/dist/types/error/flags.d.ts +79 -0
  50. package/dist/types/error/format.d.ts +20 -0
  51. package/dist/types/error/gateway.d.ts +20 -0
  52. package/dist/types/error/index.d.ts +13 -0
  53. package/dist/types/error/oauth.d.ts +43 -0
  54. package/dist/types/error/provider.d.ts +42 -0
  55. package/dist/types/error/rate-limit.d.ts +59 -0
  56. package/dist/types/error/retryable.d.ts +27 -0
  57. package/dist/types/error/validation.d.ts +32 -0
  58. package/dist/types/index.d.ts +49 -0
  59. package/dist/types/provider-details.d.ts +24 -0
  60. package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
  61. package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
  62. package/dist/types/providers/__tests__/openai-codex-error.test.d.ts +1 -0
  63. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  64. package/dist/types/providers/anthropic-client.d.ts +94 -0
  65. package/dist/types/providers/anthropic-messages-server-schema.d.ts +497 -0
  66. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  67. package/dist/types/providers/anthropic-wire.d.ts +318 -0
  68. package/dist/types/providers/anthropic.d.ts +248 -0
  69. package/dist/types/providers/aws-credentials.d.ts +53 -0
  70. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  71. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  72. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  73. package/dist/types/providers/cursor.d.ts +91 -0
  74. package/dist/types/providers/devin.d.ts +12 -0
  75. package/dist/types/providers/error-message.d.ts +27 -0
  76. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  77. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  78. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  79. package/dist/types/providers/google-auth.d.ts +32 -0
  80. package/dist/types/providers/google-gemini-cli.d.ts +118 -0
  81. package/dist/types/providers/google-interactions.d.ts +65 -0
  82. package/dist/types/providers/google-shared.d.ts +203 -0
  83. package/dist/types/providers/google-types.d.ts +155 -0
  84. package/dist/types/providers/google-vertex.d.ts +7 -0
  85. package/dist/types/providers/google.d.ts +4 -0
  86. package/dist/types/providers/grammar.d.ts +1 -0
  87. package/dist/types/providers/kimi.d.ts +27 -0
  88. package/dist/types/providers/mock.d.ts +178 -0
  89. package/dist/types/providers/ollama.d.ts +7 -0
  90. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  91. package/dist/types/providers/openai-chat-server-schema.d.ts +695 -0
  92. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  93. package/dist/types/providers/openai-chat-wire.d.ts +644 -0
  94. package/dist/types/providers/openai-codex/request-transformer.d.ts +54 -0
  95. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  96. package/dist/types/providers/openai-codex-responses.d.ts +108 -0
  97. package/dist/types/providers/openai-completions.d.ts +45 -0
  98. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  99. package/dist/types/providers/openai-responses-server-schema.d.ts +349 -0
  100. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  101. package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
  102. package/dist/types/providers/openai-responses.d.ts +126 -0
  103. package/dist/types/providers/openai-shared.d.ts +506 -0
  104. package/dist/types/providers/pi-native-client.d.ts +13 -0
  105. package/dist/types/providers/pi-native-server.d.ts +69 -0
  106. package/dist/types/providers/register-builtins.d.ts +32 -0
  107. package/dist/types/providers/synthetic.d.ts +26 -0
  108. package/dist/types/providers/transform-messages.d.ts +11 -0
  109. package/dist/types/providers/vision-guard.d.ts +20 -0
  110. package/dist/types/registry/aimlapi.d.ts +4 -0
  111. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  112. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  113. package/dist/types/registry/anthropic.d.ts +10 -0
  114. package/dist/types/registry/api-key-login.d.ts +42 -0
  115. package/dist/types/registry/api-key-validation.d.ts +43 -0
  116. package/dist/types/registry/azure.d.ts +4 -0
  117. package/dist/types/registry/cerebras.d.ts +7 -0
  118. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  119. package/dist/types/registry/coreweave.d.ts +7 -0
  120. package/dist/types/registry/cursor.d.ts +7 -0
  121. package/dist/types/registry/deepseek.d.ts +8 -0
  122. package/dist/types/registry/derived.d.ts +5 -0
  123. package/dist/types/registry/devin.d.ts +8 -0
  124. package/dist/types/registry/firepass.d.ts +16 -0
  125. package/dist/types/registry/fireworks.d.ts +7 -0
  126. package/dist/types/registry/github-copilot.d.ts +7 -0
  127. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  128. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  129. package/dist/types/registry/google-antigravity.d.ts +9 -0
  130. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  131. package/dist/types/registry/google-vertex.d.ts +5 -0
  132. package/dist/types/registry/google.d.ts +4 -0
  133. package/dist/types/registry/groq.d.ts +4 -0
  134. package/dist/types/registry/huggingface.d.ts +7 -0
  135. package/dist/types/registry/index.d.ts +4 -0
  136. package/dist/types/registry/kagi.d.ts +14 -0
  137. package/dist/types/registry/kilo.d.ts +7 -0
  138. package/dist/types/registry/kimi-code.d.ts +7 -0
  139. package/dist/types/registry/litellm.d.ts +13 -0
  140. package/dist/types/registry/llama-cpp.d.ts +8 -0
  141. package/dist/types/registry/lm-studio.d.ts +8 -0
  142. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  143. package/dist/types/registry/minimax-code.d.ts +6 -0
  144. package/dist/types/registry/minimax.d.ts +4 -0
  145. package/dist/types/registry/mistral.d.ts +4 -0
  146. package/dist/types/registry/moonshot.d.ts +7 -0
  147. package/dist/types/registry/nanogpt.d.ts +7 -0
  148. package/dist/types/registry/nvidia.d.ts +7 -0
  149. package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
  150. package/dist/types/registry/oauth/anthropic.d.ts +23 -0
  151. package/dist/types/registry/oauth/callback-server.d.ts +72 -0
  152. package/dist/types/registry/oauth/cursor.d.ts +15 -0
  153. package/dist/types/registry/oauth/devin.d.ts +5 -0
  154. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  155. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  156. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  157. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  158. package/dist/types/registry/oauth/google-gemini-cli.d.ts +10 -0
  159. package/dist/types/registry/oauth/google-oauth-shared.d.ts +22 -0
  160. package/dist/types/registry/oauth/index.d.ts +64 -0
  161. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  162. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  163. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  164. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  165. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  166. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  167. package/dist/types/registry/oauth/types.d.ts +56 -0
  168. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  169. package/dist/types/registry/oauth/xai-oauth.d.ts +52 -0
  170. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  171. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  172. package/dist/types/registry/ollama.d.ts +12 -0
  173. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  174. package/dist/types/registry/openai-codex.d.ts +9 -0
  175. package/dist/types/registry/openai.d.ts +4 -0
  176. package/dist/types/registry/opencode-go.d.ts +6 -0
  177. package/dist/types/registry/opencode-zen.d.ts +6 -0
  178. package/dist/types/registry/openrouter.d.ts +13 -0
  179. package/dist/types/registry/parallel.d.ts +14 -0
  180. package/dist/types/registry/perplexity.d.ts +7 -0
  181. package/dist/types/registry/qianfan.d.ts +7 -0
  182. package/dist/types/registry/qwen-portal.d.ts +7 -0
  183. package/dist/types/registry/registry.d.ts +303 -0
  184. package/dist/types/registry/sakana.d.ts +7 -0
  185. package/dist/types/registry/synthetic.d.ts +6 -0
  186. package/dist/types/registry/tavily.d.ts +14 -0
  187. package/dist/types/registry/together.d.ts +6 -0
  188. package/dist/types/registry/types.d.ts +51 -0
  189. package/dist/types/registry/umans.d.ts +7 -0
  190. package/dist/types/registry/venice.d.ts +13 -0
  191. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  192. package/dist/types/registry/vllm.d.ts +7 -0
  193. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  194. package/dist/types/registry/xai-oauth.d.ts +7 -0
  195. package/dist/types/registry/xai.d.ts +4 -0
  196. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  197. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  198. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  199. package/dist/types/registry/xiaomi.d.ts +6 -0
  200. package/dist/types/registry/zai.d.ts +7 -0
  201. package/dist/types/registry/zenmux.d.ts +7 -0
  202. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  203. package/dist/types/stream.d.ts +44 -0
  204. package/dist/types/types.d.ts +715 -0
  205. package/dist/types/usage/claude.d.ts +4 -0
  206. package/dist/types/usage/gemini.d.ts +2 -0
  207. package/dist/types/usage/github-copilot.d.ts +7 -0
  208. package/dist/types/usage/google-antigravity.d.ts +15 -0
  209. package/dist/types/usage/kimi.d.ts +2 -0
  210. package/dist/types/usage/minimax-code.d.ts +2 -0
  211. package/dist/types/usage/ollama.d.ts +5 -0
  212. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  213. package/dist/types/usage/openai-codex-reset.d.ts +79 -0
  214. package/dist/types/usage/openai-codex.d.ts +3 -0
  215. package/dist/types/usage/opencode-go.d.ts +2 -0
  216. package/dist/types/usage/shared.d.ts +1 -0
  217. package/dist/types/usage/zai.d.ts +2 -0
  218. package/dist/types/usage.d.ts +346 -0
  219. package/dist/types/utils/abort.d.ts +25 -0
  220. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  221. package/dist/types/utils/block-symbols.d.ts +20 -0
  222. package/dist/types/utils/deterministic-id.d.ts +16 -0
  223. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  224. package/dist/types/utils/event-stream.d.ts +30 -0
  225. package/dist/types/utils/foundry.d.ts +1 -0
  226. package/dist/types/utils/google-validation.d.ts +2 -0
  227. package/dist/types/utils/harmony-leak.d.ts +118 -0
  228. package/dist/types/utils/http-inspector.d.ts +30 -0
  229. package/dist/types/utils/idle-iterator.d.ts +137 -0
  230. package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
  231. package/dist/types/utils/openai-http.d.ts +54 -0
  232. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  233. package/dist/types/utils/parse-bind.d.ts +23 -0
  234. package/dist/types/utils/provider-response.d.ts +3 -0
  235. package/dist/types/utils/proxy.d.ts +29 -0
  236. package/dist/types/utils/request-debug.d.ts +29 -0
  237. package/dist/types/utils/retry-after.d.ts +4 -0
  238. package/dist/types/utils/retry.d.ts +14 -0
  239. package/dist/types/utils/schema/adapt.d.ts +24 -0
  240. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  241. package/dist/types/utils/schema/dereference.d.ts +11 -0
  242. package/dist/types/utils/schema/draft.d.ts +10 -0
  243. package/dist/types/utils/schema/equality.d.ts +4 -0
  244. package/dist/types/utils/schema/fields.d.ts +54 -0
  245. package/dist/types/utils/schema/index.d.ts +15 -0
  246. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  247. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  248. package/dist/types/utils/schema/normalize.d.ts +124 -0
  249. package/dist/types/utils/schema/spill.d.ts +8 -0
  250. package/dist/types/utils/schema/stamps.d.ts +17 -0
  251. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  252. package/dist/types/utils/schema/types.d.ts +4 -0
  253. package/dist/types/utils/schema/typescript.d.ts +18 -0
  254. package/dist/types/utils/schema/wire.d.ts +92 -0
  255. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  256. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  257. package/dist/types/utils/sse-debug.d.ts +5 -0
  258. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  259. package/dist/types/utils/thinking-loop.d.ts +102 -0
  260. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  261. package/dist/types/utils/tool-choice.d.ts +50 -0
  262. package/dist/types/utils/validation.d.ts +42 -0
  263. package/dist/types/utils.d.ts +24 -0
  264. package/package.json +139 -0
  265. package/src/api-registry.ts +109 -0
  266. package/src/auth-broker/client.ts +359 -0
  267. package/src/auth-broker/discover.ts +222 -0
  268. package/src/auth-broker/index.ts +7 -0
  269. package/src/auth-broker/refresher.ts +117 -0
  270. package/src/auth-broker/remote-store.ts +657 -0
  271. package/src/auth-broker/server.ts +646 -0
  272. package/src/auth-broker/snapshot-cache.ts +191 -0
  273. package/src/auth-broker/types.ts +130 -0
  274. package/src/auth-broker/wire-schemas.ts +249 -0
  275. package/src/auth-gateway/http.ts +194 -0
  276. package/src/auth-gateway/index.ts +3 -0
  277. package/src/auth-gateway/server.ts +802 -0
  278. package/src/auth-gateway/types.ts +151 -0
  279. package/src/auth-retry.ts +250 -0
  280. package/src/auth-storage.ts +5576 -0
  281. package/src/dialect/anthropic.md +31 -0
  282. package/src/dialect/anthropic.ts +608 -0
  283. package/src/dialect/catalog.ts +29 -0
  284. package/src/dialect/coercion.ts +136 -0
  285. package/src/dialect/deepseek.md +24 -0
  286. package/src/dialect/deepseek.ts +609 -0
  287. package/src/dialect/demotion.ts +36 -0
  288. package/src/dialect/examples.ts +33 -0
  289. package/src/dialect/factory.ts +34 -0
  290. package/src/dialect/fenced-thinking.ts +184 -0
  291. package/src/dialect/gemini.md +44 -0
  292. package/src/dialect/gemini.ts +597 -0
  293. package/src/dialect/gemma.md +33 -0
  294. package/src/dialect/gemma.ts +387 -0
  295. package/src/dialect/glm.md +32 -0
  296. package/src/dialect/glm.ts +456 -0
  297. package/src/dialect/harmony.md +31 -0
  298. package/src/dialect/harmony.ts +346 -0
  299. package/src/dialect/hermes.md +25 -0
  300. package/src/dialect/hermes.ts +206 -0
  301. package/src/dialect/history.ts +81 -0
  302. package/src/dialect/index.ts +15 -0
  303. package/src/dialect/inventory.ts +73 -0
  304. package/src/dialect/kimi.md +24 -0
  305. package/src/dialect/kimi.ts +340 -0
  306. package/src/dialect/minimax.md +31 -0
  307. package/src/dialect/minimax.ts +95 -0
  308. package/src/dialect/owned-stream.ts +470 -0
  309. package/src/dialect/prompt-template.md +12 -0
  310. package/src/dialect/qwen3.md +28 -0
  311. package/src/dialect/qwen3.ts +240 -0
  312. package/src/dialect/rendering.ts +249 -0
  313. package/src/dialect/thinking.ts +122 -0
  314. package/src/dialect/types.ts +57 -0
  315. package/src/dialect/xml.md +22 -0
  316. package/src/dialect/xml.ts +90 -0
  317. package/src/error/abort.ts +18 -0
  318. package/src/error/auth-classify.ts +30 -0
  319. package/src/error/auth.ts +48 -0
  320. package/src/error/aws.ts +31 -0
  321. package/src/error/classes.ts +186 -0
  322. package/src/error/finalize.ts +69 -0
  323. package/src/error/flags.ts +506 -0
  324. package/src/error/format.ts +45 -0
  325. package/src/error/gateway.ts +96 -0
  326. package/src/error/index.ts +13 -0
  327. package/src/error/oauth.ts +58 -0
  328. package/src/error/provider.ts +62 -0
  329. package/src/error/rate-limit.ts +161 -0
  330. package/src/error/retryable.ts +70 -0
  331. package/src/error/validation.ts +44 -0
  332. package/src/index.ts +49 -0
  333. package/src/provider-details.ts +90 -0
  334. package/src/providers/__tests__/google-auth.test.ts +144 -0
  335. package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
  336. package/src/providers/__tests__/openai-codex-error.test.ts +84 -0
  337. package/src/providers/amazon-bedrock.ts +1042 -0
  338. package/src/providers/anthropic-client.ts +295 -0
  339. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  340. package/src/providers/anthropic-messages-server.ts +756 -0
  341. package/src/providers/anthropic-wire.ts +318 -0
  342. package/src/providers/anthropic.ts +4078 -0
  343. package/src/providers/aws-credentials.ts +586 -0
  344. package/src/providers/aws-eventstream.ts +181 -0
  345. package/src/providers/aws-sigv4.ts +218 -0
  346. package/src/providers/azure-openai-responses.ts +382 -0
  347. package/src/providers/cursor/proto/agent.proto +3526 -0
  348. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  349. package/src/providers/cursor/proto/buf.yaml +17 -0
  350. package/src/providers/cursor.ts +2695 -0
  351. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  352. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  353. package/src/providers/devin/proto/buf.yaml +17 -0
  354. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  355. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  356. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  357. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  358. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  359. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  360. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  361. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  362. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  363. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  364. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  365. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  366. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  367. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  368. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  369. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  370. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  371. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  372. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  373. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  374. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  375. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  376. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  377. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  378. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  379. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  380. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  381. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  382. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  383. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  384. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  385. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  386. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  387. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  388. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  389. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  390. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  391. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  392. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  393. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  394. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  395. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  396. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  397. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  398. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  399. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  400. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  401. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  402. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  403. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  404. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  405. package/src/providers/devin.ts +577 -0
  406. package/src/providers/error-message.ts +21 -0
  407. package/src/providers/github-copilot-headers.ts +141 -0
  408. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  409. package/src/providers/gitlab-duo-workflow.ts +3058 -0
  410. package/src/providers/gitlab-duo.ts +395 -0
  411. package/src/providers/google-auth.ts +350 -0
  412. package/src/providers/google-gemini-cli.ts +1362 -0
  413. package/src/providers/google-interactions.ts +753 -0
  414. package/src/providers/google-shared.ts +1103 -0
  415. package/src/providers/google-types.ts +180 -0
  416. package/src/providers/google-vertex.ts +183 -0
  417. package/src/providers/google.ts +87 -0
  418. package/src/providers/grammar.ts +70 -0
  419. package/src/providers/kimi.ts +52 -0
  420. package/src/providers/mock.ts +507 -0
  421. package/src/providers/ollama.ts +773 -0
  422. package/src/providers/openai-anthropic-shim.ts +152 -0
  423. package/src/providers/openai-chat-server-schema.ts +242 -0
  424. package/src/providers/openai-chat-server.ts +715 -0
  425. package/src/providers/openai-chat-wire.ts +847 -0
  426. package/src/providers/openai-codex/request-transformer.ts +295 -0
  427. package/src/providers/openai-codex/response-handler.ts +102 -0
  428. package/src/providers/openai-codex-responses.ts +3468 -0
  429. package/src/providers/openai-completions.ts +2173 -0
  430. package/src/providers/openai-reasoning-fallback.ts +269 -0
  431. package/src/providers/openai-responses-reasoning-suppression.md +1 -0
  432. package/src/providers/openai-responses-server-schema.ts +282 -0
  433. package/src/providers/openai-responses-server.ts +1280 -0
  434. package/src/providers/openai-responses-wire.ts +6391 -0
  435. package/src/providers/openai-responses.ts +1022 -0
  436. package/src/providers/openai-shared.ts +2648 -0
  437. package/src/providers/pi-native-client.ts +266 -0
  438. package/src/providers/pi-native-server.ts +242 -0
  439. package/src/providers/register-builtins.ts +475 -0
  440. package/src/providers/synthetic.ts +50 -0
  441. package/src/providers/transform-messages.ts +787 -0
  442. package/src/providers/vision-guard.ts +54 -0
  443. package/src/registry/aimlapi.ts +6 -0
  444. package/src/registry/alibaba-coding-plan.ts +95 -0
  445. package/src/registry/amazon-bedrock.ts +22 -0
  446. package/src/registry/anthropic.ts +26 -0
  447. package/src/registry/api-key-login.ts +112 -0
  448. package/src/registry/api-key-validation.ts +161 -0
  449. package/src/registry/azure.ts +6 -0
  450. package/src/registry/cerebras.ts +23 -0
  451. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  452. package/src/registry/coreweave.ts +40 -0
  453. package/src/registry/cursor.ts +20 -0
  454. package/src/registry/deepseek.ts +46 -0
  455. package/src/registry/derived.ts +9 -0
  456. package/src/registry/devin.ts +15 -0
  457. package/src/registry/firepass.ts +32 -0
  458. package/src/registry/fireworks.ts +28 -0
  459. package/src/registry/github-copilot.ts +22 -0
  460. package/src/registry/gitlab-duo-workflow.ts +20 -0
  461. package/src/registry/gitlab-duo.ts +19 -0
  462. package/src/registry/google-antigravity.ts +22 -0
  463. package/src/registry/google-gemini-cli.ts +22 -0
  464. package/src/registry/google-vertex.ts +38 -0
  465. package/src/registry/google.ts +6 -0
  466. package/src/registry/groq.ts +6 -0
  467. package/src/registry/huggingface.ts +29 -0
  468. package/src/registry/index.ts +4 -0
  469. package/src/registry/kagi.ts +46 -0
  470. package/src/registry/kilo.ts +114 -0
  471. package/src/registry/kimi-code.ts +17 -0
  472. package/src/registry/litellm.ts +45 -0
  473. package/src/registry/llama-cpp.ts +35 -0
  474. package/src/registry/lm-studio.ts +31 -0
  475. package/src/registry/minimax-code-cn.ts +12 -0
  476. package/src/registry/minimax-code.ts +12 -0
  477. package/src/registry/minimax.ts +6 -0
  478. package/src/registry/mistral.ts +6 -0
  479. package/src/registry/moonshot.ts +22 -0
  480. package/src/registry/nanogpt.ts +22 -0
  481. package/src/registry/nvidia.ts +61 -0
  482. package/src/registry/oauth/__tests__/xai-oauth.test.ts +104 -0
  483. package/src/registry/oauth/anthropic.ts +311 -0
  484. package/src/registry/oauth/callback-server.ts +315 -0
  485. package/src/registry/oauth/cursor.ts +171 -0
  486. package/src/registry/oauth/devin.ts +124 -0
  487. package/src/registry/oauth/github-copilot.ts +369 -0
  488. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  489. package/src/registry/oauth/gitlab-duo.ts +222 -0
  490. package/src/registry/oauth/google-antigravity.ts +209 -0
  491. package/src/registry/oauth/google-gemini-cli.ts +273 -0
  492. package/src/registry/oauth/google-oauth-shared.ts +125 -0
  493. package/src/registry/oauth/index.ts +269 -0
  494. package/src/registry/oauth/kimi.ts +289 -0
  495. package/src/registry/oauth/minimax-code.ts +53 -0
  496. package/src/registry/oauth/oauth.html +311 -0
  497. package/src/registry/oauth/openai-codex.ts +364 -0
  498. package/src/registry/oauth/opencode.ts +50 -0
  499. package/src/registry/oauth/perplexity.ts +228 -0
  500. package/src/registry/oauth/pkce.ts +18 -0
  501. package/src/registry/oauth/types.ts +65 -0
  502. package/src/registry/oauth/wafer.ts +24 -0
  503. package/src/registry/oauth/xai-oauth.ts +394 -0
  504. package/src/registry/oauth/xiaomi.ts +211 -0
  505. package/src/registry/ollama-cloud.ts +36 -0
  506. package/src/registry/ollama.ts +43 -0
  507. package/src/registry/openai-codex-device.ts +18 -0
  508. package/src/registry/openai-codex.ts +19 -0
  509. package/src/registry/openai.ts +6 -0
  510. package/src/registry/opencode-go.ts +12 -0
  511. package/src/registry/opencode-zen.ts +12 -0
  512. package/src/registry/openrouter.ts +28 -0
  513. package/src/registry/parallel.ts +45 -0
  514. package/src/registry/perplexity.ts +13 -0
  515. package/src/registry/qianfan.ts +27 -0
  516. package/src/registry/qwen-portal.ts +50 -0
  517. package/src/registry/registry.ts +161 -0
  518. package/src/registry/sakana.ts +22 -0
  519. package/src/registry/synthetic.ts +21 -0
  520. package/src/registry/tavily.ts +45 -0
  521. package/src/registry/together.ts +22 -0
  522. package/src/registry/types.ts +56 -0
  523. package/src/registry/umans.ts +23 -0
  524. package/src/registry/venice.ts +33 -0
  525. package/src/registry/vercel-ai-gateway.ts +38 -0
  526. package/src/registry/vllm.ts +34 -0
  527. package/src/registry/wafer-serverless.ts +12 -0
  528. package/src/registry/xai-oauth.ts +17 -0
  529. package/src/registry/xai.ts +6 -0
  530. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  531. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  532. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  533. package/src/registry/xiaomi.ts +12 -0
  534. package/src/registry/zai.ts +27 -0
  535. package/src/registry/zenmux.ts +22 -0
  536. package/src/registry/zhipu-coding-plan.ts +27 -0
  537. package/src/stream.ts +1778 -0
  538. package/src/types.ts +856 -0
  539. package/src/usage/claude.ts +485 -0
  540. package/src/usage/gemini.ts +258 -0
  541. package/src/usage/github-copilot.ts +424 -0
  542. package/src/usage/google-antigravity.ts +497 -0
  543. package/src/usage/kimi.ts +271 -0
  544. package/src/usage/minimax-code.ts +30 -0
  545. package/src/usage/ollama.ts +41 -0
  546. package/src/usage/openai-codex-base-url.ts +35 -0
  547. package/src/usage/openai-codex-reset.ts +174 -0
  548. package/src/usage/openai-codex.ts +535 -0
  549. package/src/usage/opencode-go.ts +89 -0
  550. package/src/usage/shared.ts +10 -0
  551. package/src/usage/zai.ts +321 -0
  552. package/src/usage.ts +333 -0
  553. package/src/utils/abort.ts +67 -0
  554. package/src/utils/anthropic-auth.ts +93 -0
  555. package/src/utils/block-symbols.ts +32 -0
  556. package/src/utils/deterministic-id.ts +20 -0
  557. package/src/utils/empty-completion-retry.ts +159 -0
  558. package/src/utils/event-stream.ts +171 -0
  559. package/src/utils/foundry.ts +8 -0
  560. package/src/utils/google-validation.ts +25 -0
  561. package/src/utils/harmony-leak.ts +456 -0
  562. package/src/utils/http-inspector.ts +168 -0
  563. package/src/utils/idle-iterator.ts +473 -0
  564. package/src/utils/leaked-thinking-stream.ts +294 -0
  565. package/src/utils/openai-http.ts +122 -0
  566. package/src/utils/openrouter-headers.ts +12 -0
  567. package/src/utils/parse-bind.ts +56 -0
  568. package/src/utils/provider-response.ts +30 -0
  569. package/src/utils/proxy.ts +240 -0
  570. package/src/utils/request-debug.ts +351 -0
  571. package/src/utils/retry-after.ts +110 -0
  572. package/src/utils/retry.ts +59 -0
  573. package/src/utils/schema/CONSTRAINTS.md +166 -0
  574. package/src/utils/schema/adapt.ts +36 -0
  575. package/src/utils/schema/compatibility.ts +435 -0
  576. package/src/utils/schema/dereference.ts +98 -0
  577. package/src/utils/schema/draft.ts +341 -0
  578. package/src/utils/schema/equality.ts +97 -0
  579. package/src/utils/schema/fields.ts +207 -0
  580. package/src/utils/schema/index.ts +15 -0
  581. package/src/utils/schema/json-schema-validator.ts +595 -0
  582. package/src/utils/schema/meta-validator.ts +167 -0
  583. package/src/utils/schema/normalize.ts +1901 -0
  584. package/src/utils/schema/spill.ts +43 -0
  585. package/src/utils/schema/stamps.ts +109 -0
  586. package/src/utils/schema/strict-tool-validation.ts +117 -0
  587. package/src/utils/schema/types.ts +10 -0
  588. package/src/utils/schema/typescript.ts +198 -0
  589. package/src/utils/schema/wire.ts +789 -0
  590. package/src/utils/schema/zod-decontaminate.ts +331 -0
  591. package/src/utils/sdk-stream-timeout.ts +43 -0
  592. package/src/utils/sse-debug.ts +18 -0
  593. package/src/utils/stream-markup-healing.ts +247 -0
  594. package/src/utils/thinking-loop.ts +552 -0
  595. package/src/utils/tool-call-loop-guard.ts +107 -0
  596. package/src/utils/tool-choice.ts +99 -0
  597. package/src/utils/validation.ts +1507 -0
  598. package/src/utils.ts +171 -0
@@ -0,0 +1,2173 @@
1
+ import type { Effort } from "jeopi-catalog/effort";
2
+ import { isKimiModelId } from "jeopi-catalog/identity";
3
+ import { resolveWireModelId } from "jeopi-catalog/model-thinking";
4
+ import { calculateCost } from "jeopi-catalog/models";
5
+ import type { ResolvedOpenAICompat } from "jeopi-catalog/types";
6
+ import { $env, parseStreamingJson, parseStreamingJsonThrottled } from "jeopi-utils";
7
+ import { renderDemotedThinking } from "../dialect/demotion";
8
+ import * as AIError from "../error";
9
+ import { getKimiCommonHeaders } from "../registry/oauth/kimi";
10
+ import { getEnvApiKey } from "../stream";
11
+ import type {
12
+ AssistantMessage,
13
+ Context,
14
+ Message,
15
+ MessageAttribution,
16
+ Model,
17
+ ProviderSessionState,
18
+ RawSseEvent,
19
+ ServiceTier,
20
+ StopReason,
21
+ StreamFunction,
22
+ StreamOptions,
23
+ TextContent,
24
+ ThinkingContent,
25
+ Tool,
26
+ ToolCall,
27
+ ToolChoice,
28
+ ToolResultMessage,
29
+ } from "../types";
30
+ import { normalizeSystemPrompts } from "../utils";
31
+ import { createAbortSourceTracker } from "../utils/abort";
32
+ import { kStreamingLastParseLen } from "../utils/block-symbols";
33
+ import { hasVisibleAssistantContent, withEmptyCompletionRetry } from "../utils/empty-completion-retry";
34
+ import { AssistantMessageEventStream } from "../utils/event-stream";
35
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
36
+ import {
37
+ getOpenAIStreamFirstEventTimeoutMs,
38
+ getOpenAIStreamIdleTimeoutMs,
39
+ iterateWithIdleTimeout,
40
+ iterateWithTerminalGrace,
41
+ } from "../utils/idle-iterator";
42
+ import { OpenAIHttpError, postOpenAIStream } from "../utils/openai-http";
43
+ import { notifyProviderResponse } from "../utils/provider-response";
44
+ import { callWithCopilotModelRetry } from "../utils/retry";
45
+ import { adaptSchemaForStrict, NO_STRICT, normalizeSchemaForMoonshot, toolWireSchema } from "../utils/schema";
46
+ import {
47
+ type HealedToolCall,
48
+ StreamMarkupHealing,
49
+ type StreamMarkupHealingEvent,
50
+ } from "../utils/stream-markup-healing";
51
+ import { isForcedToolChoice, mapToOpenAICompletionsToolChoice } from "../utils/tool-choice";
52
+ import type {
53
+ ChatCompletionAssistantMessageParam,
54
+ ChatCompletionChunk,
55
+ ChatCompletionContentPart,
56
+ ChatCompletionContentPartImage,
57
+ ChatCompletionContentPartText,
58
+ ChatCompletionMessageParam,
59
+ ChatCompletionTool,
60
+ ChatCompletionToolMessageParam,
61
+ } from "./openai-chat-wire";
62
+ import {
63
+ applyOpenAIReasoningEffortFallback,
64
+ clearOpenAIReasoningEffortFallbackState,
65
+ createOpenAIReasoningEffortFallbackKey,
66
+ createOpenAIReasoningEffortFallbackState,
67
+ getOpenAIReasoningEffortFallback,
68
+ type OpenAIReasoningEffortFallback,
69
+ type OpenAIReasoningEffortFallbackState,
70
+ rememberOpenAIReasoningEffortFallback,
71
+ resolveOpenAIReasoningEffortFallback,
72
+ } from "./openai-reasoning-fallback";
73
+ import {
74
+ applyChatCompletionsCompatPolicy,
75
+ applyChatCompletionsToolStream,
76
+ applyOpenAIExtraBody,
77
+ applyOpenAIGatewayRouting,
78
+ applyOpenAIServiceTier,
79
+ applyWireModelIdTransform,
80
+ calculateOpenAIUsageAccounting,
81
+ clearOpenAIStrictToolsState,
82
+ createInitialResponsesAssistantMessage,
83
+ createOpenAIStrictToolsState,
84
+ disableStrictToolsForScope,
85
+ getOpenAIStrictToolsScope,
86
+ isCompiledGrammarTooLargeStrictError,
87
+ isOpenRouterAnthropicModel,
88
+ isStrictToolsDisabledForScope,
89
+ type OpenAICompatPolicy,
90
+ type OpenAICompletionsParams,
91
+ type OpenAIRequestSetup,
92
+ type OpenAIStrictToolsState,
93
+ parseAzureDeploymentNameMap,
94
+ resolveOpenAICompatPolicy,
95
+ resolveOpenAIOutputTokenParam,
96
+ resolveOpenAIRequestSetup,
97
+ resolveZaiReasoningOutputClamp,
98
+ shouldRetryWithoutStrictTools,
99
+ } from "./openai-shared";
100
+ import { transformMessages } from "./transform-messages";
101
+ import {
102
+ isDashscopeCompatibleModeTextOnlyQwen,
103
+ joinTextWithImagePlaceholder,
104
+ NON_VISION_IMAGE_PLACEHOLDER,
105
+ } from "./vision-guard";
106
+
107
+ export { applyOpenRouterRoutingVariant } from "./openai-shared";
108
+
109
+ type OpenAICompletionsReasoningField = NonNullable<ResolvedOpenAICompat["reasoningContentField"]>;
110
+
111
+ type ProviderAttributedChatCompletionChunk = ChatCompletionChunk & {
112
+ provider?: unknown;
113
+ };
114
+
115
+ type OpenAICompletionsChoiceUsage = ChatCompletionChunk.Choice & {
116
+ usage?: unknown;
117
+ };
118
+
119
+ type OpenAICompletionsDeltaWithReasoningDetails = ChatCompletionChunk.Choice["delta"] & {
120
+ reasoning_details?: unknown;
121
+ };
122
+
123
+ type OpenAICompletionsAssistantMessageParam = ChatCompletionAssistantMessageParam &
124
+ Partial<Record<OpenAICompletionsReasoningField, string>> & {
125
+ reasoning_details?: unknown[];
126
+ };
127
+
128
+ type OpenAICompletionsToolMessageParam = ChatCompletionToolMessageParam & {
129
+ name?: string;
130
+ };
131
+
132
+ type OpenAICompletionsUsageLike = {
133
+ completion_tokens?: unknown;
134
+ prompt_tokens?: unknown;
135
+ cached_tokens?: unknown;
136
+ prompt_cache_hit_tokens?: unknown;
137
+ prompt_cache_miss_tokens?: unknown;
138
+ prompt_tokens_details?: unknown;
139
+ completion_tokens_details?: unknown;
140
+ };
141
+
142
+ type OpenAICompletionsPromptTokenDetails = {
143
+ cached_tokens?: unknown;
144
+ cache_write_tokens?: unknown;
145
+ };
146
+
147
+ type OpenAICompletionsCompletionTokenDetails = {
148
+ reasoning_tokens?: unknown;
149
+ };
150
+
151
+ /**
152
+ * Normalize tool call ID for Mistral.
153
+ * Mistral requires tool IDs to be exactly 9 alphanumeric characters (a-z, A-Z, 0-9).
154
+ */
155
+ function normalizeMistralToolId(id: string, isMistral: boolean): string {
156
+ if (!isMistral) return id;
157
+ // Remove non-alphanumeric characters
158
+ let normalized = id.replace(/[^a-zA-Z0-9]/g, "");
159
+ // Mistral requires exactly 9 characters
160
+ if (normalized.length < 9) {
161
+ // Pad with deterministic characters based on original ID to ensure matching
162
+ const padding = "ABCDEFGHI";
163
+ normalized = normalized + padding.slice(0, 9 - normalized.length);
164
+ } else if (normalized.length > 9) {
165
+ normalized = normalized.slice(0, 9);
166
+ }
167
+ return normalized;
168
+ }
169
+ // Direct DeepSeek model ids on NanoGPT are routed via the default tools-capable
170
+ // path. We deliberately do NOT append `:tools` here: with `:tools`, NanoGPT
171
+ // performs server-side tool-call parsing on the upstream DeepSeek stream and
172
+ // 502s with `code: "malformed_tool_call"` on more complex tool schemas (issue
173
+ // #1488). The default route forwards `delta.content` (including DSML
174
+ // envelope leaks) which `StreamMarkupHealing` heals into a structured call
175
+ // client-side.
176
+ function resolveOpenAICompletionsRoutingEffort(
177
+ model: Model<"openai-completions">,
178
+ effort: Effort | undefined,
179
+ ): Effort | undefined {
180
+ if (!effort) return undefined;
181
+ if (model.thinking?.efforts.includes(effort)) return effort;
182
+ const compatMappedEffort = model.compat.reasoningEffortMap?.[effort] as Effort | undefined;
183
+ if (compatMappedEffort && model.thinking?.efforts.includes(compatMappedEffort)) return compatMappedEffort;
184
+ const thinkingMappedEffort = model.thinking?.effortMap?.[effort] as Effort | undefined;
185
+ if (thinkingMappedEffort && model.thinking?.efforts.includes(thinkingMappedEffort)) return thinkingMappedEffort;
186
+ return effort;
187
+ }
188
+
189
+ function resolveOpenAICompletionsModelId(
190
+ model: Model<"openai-completions">,
191
+ options: OpenAICompletionsOptions | undefined,
192
+ ): string {
193
+ // Effort-tier variants route per request effort (off → bare id, efforts →
194
+ // the thinking backing id); catalog variants (Copilot long-context `-1m`
195
+ // entries) pin via `requestModelId`; everything else serializes `model.id`.
196
+ const requestedEffort =
197
+ options?.reasoning && !options.disableReasoning && model.reasoning ? (options.reasoning as Effort) : undefined;
198
+ const effort = resolveOpenAICompletionsRoutingEffort(model, requestedEffort);
199
+ const wireId = resolveWireModelId(model, effort);
200
+ return applyWireModelIdTransform(wireId, model.compat.wireModelIdMode, options?.openrouterVariant);
201
+ }
202
+
203
+ /**
204
+ * Normalize OpenAI-compatible streaming `delta.content` into plain text.
205
+ * Most providers stream `delta.content` as a string, but some (notably Mistral
206
+ * Medium 3.5 / `mistral-medium-2604`) return an array of typed content parts
207
+ * — e.g. `[{ type: "text", text: "Hello" }]`. Without normalization those
208
+ * parts get string-coerced via `text += array`, producing the literal
209
+ * `[object Object]` sequences observed in issue #911.
210
+ *
211
+ * Returns the joined text. Non-text parts and unknown shapes are skipped so
212
+ * we never emit JS object sigils as visible output.
213
+ */
214
+ function normalizeStreamingContentText(content: unknown): string {
215
+ if (typeof content === "string") return content;
216
+ if (Array.isArray(content)) {
217
+ let out = "";
218
+ for (const part of content) {
219
+ if (typeof part === "string") {
220
+ out += part;
221
+ } else if (part && typeof part === "object") {
222
+ const obj = part as { type?: unknown; text?: unknown };
223
+ if ((obj.type === undefined || obj.type === "text") && typeof obj.text === "string") {
224
+ out += obj.text;
225
+ }
226
+ }
227
+ }
228
+ return out;
229
+ }
230
+ if (content && typeof content === "object") {
231
+ const obj = content as { type?: unknown; text?: unknown };
232
+ if ((obj.type === undefined || obj.type === "text") && typeof obj.text === "string") {
233
+ return obj.text;
234
+ }
235
+ }
236
+ return "";
237
+ }
238
+
239
+ function serializeToolArguments(value: unknown): string {
240
+ if (value && typeof value === "object" && !Array.isArray(value)) {
241
+ try {
242
+ return JSON.stringify(value);
243
+ } catch {
244
+ return "{}";
245
+ }
246
+ }
247
+
248
+ if (typeof value === "string") {
249
+ const trimmed = value.trim();
250
+ if (trimmed.length === 0) return "{}";
251
+ try {
252
+ const parsed = JSON.parse(trimmed);
253
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
254
+ return JSON.stringify(parsed);
255
+ }
256
+ } catch {}
257
+ return "{}";
258
+ }
259
+
260
+ return "{}";
261
+ }
262
+
263
+ function cloneStreamingArgumentValue(value: unknown): unknown {
264
+ if (Array.isArray(value)) {
265
+ return value.map(cloneStreamingArgumentValue);
266
+ }
267
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
268
+ return mergeStreamingArgumentObjects(undefined, value as Record<string, unknown>);
269
+ }
270
+ return value;
271
+ }
272
+
273
+ function streamingArgumentValuesEqual(left: unknown, right: unknown): boolean {
274
+ if (left === right) return true;
275
+ if (Array.isArray(left) && Array.isArray(right)) {
276
+ if (left.length !== right.length) return false;
277
+ for (let i = 0; i < left.length; i++) {
278
+ if (!streamingArgumentValuesEqual(left[i], right[i])) return false;
279
+ }
280
+ return true;
281
+ }
282
+ if (
283
+ left !== null &&
284
+ typeof left === "object" &&
285
+ !Array.isArray(left) &&
286
+ right !== null &&
287
+ typeof right === "object" &&
288
+ !Array.isArray(right)
289
+ ) {
290
+ const leftObject = left as Record<string, unknown>;
291
+ const rightObject = right as Record<string, unknown>;
292
+ let leftKeys = 0;
293
+ for (const key in leftObject) {
294
+ if (!Object.hasOwn(leftObject, key) || key === "__proto__" || key === "constructor" || key === "prototype")
295
+ continue;
296
+ leftKeys++;
297
+ if (!Object.hasOwn(rightObject, key) || !streamingArgumentValuesEqual(leftObject[key], rightObject[key])) {
298
+ return false;
299
+ }
300
+ }
301
+ let rightKeys = 0;
302
+ for (const key in rightObject) {
303
+ if (!Object.hasOwn(rightObject, key) || key === "__proto__" || key === "constructor" || key === "prototype")
304
+ continue;
305
+ rightKeys++;
306
+ }
307
+ return leftKeys === rightKeys;
308
+ }
309
+ return false;
310
+ }
311
+
312
+ function streamingArgumentArrayStartsWith(value: unknown[], prefix: unknown[]): boolean {
313
+ if (prefix.length > value.length) return false;
314
+ for (let i = 0; i < prefix.length; i++) {
315
+ if (!streamingArgumentValuesEqual(value[i], prefix[i])) return false;
316
+ }
317
+ return true;
318
+ }
319
+
320
+ function mergeStreamingArgumentArrays(prev: unknown[], fragment: unknown[]): unknown[] {
321
+ if (streamingArgumentArrayStartsWith(fragment, prev)) {
322
+ return fragment.map(cloneStreamingArgumentValue);
323
+ }
324
+ if (streamingArgumentArrayStartsWith(prev, fragment)) {
325
+ return prev.map(cloneStreamingArgumentValue);
326
+ }
327
+ const merged = prev.map(cloneStreamingArgumentValue);
328
+ for (const value of fragment) {
329
+ merged.push(cloneStreamingArgumentValue(value));
330
+ }
331
+ return merged;
332
+ }
333
+
334
+ function mergeStreamingArgumentValues(prev: unknown, fragment: unknown): unknown {
335
+ if (typeof prev === "string" && typeof fragment === "string") {
336
+ return fragment.startsWith(prev) ? fragment : prev + fragment;
337
+ }
338
+ if (Array.isArray(prev) && Array.isArray(fragment)) {
339
+ return mergeStreamingArgumentArrays(prev, fragment);
340
+ }
341
+ if (
342
+ prev !== null &&
343
+ typeof prev === "object" &&
344
+ !Array.isArray(prev) &&
345
+ fragment !== null &&
346
+ typeof fragment === "object" &&
347
+ !Array.isArray(fragment)
348
+ ) {
349
+ return mergeStreamingArgumentObjects(prev as Record<string, unknown>, fragment as Record<string, unknown>);
350
+ }
351
+ return cloneStreamingArgumentValue(fragment);
352
+ }
353
+
354
+ function mergeStreamingArgumentObjects(
355
+ prev: Record<string, unknown> | undefined,
356
+ fragment: Record<string, unknown>,
357
+ ): Record<string, unknown> {
358
+ const merged: Record<string, unknown> = {};
359
+ if (prev) {
360
+ for (const key in prev) {
361
+ if (!Object.hasOwn(prev, key) || key === "__proto__" || key === "constructor" || key === "prototype") continue;
362
+ merged[key] = cloneStreamingArgumentValue(prev[key]);
363
+ }
364
+ }
365
+ for (const key in fragment) {
366
+ if (!Object.hasOwn(fragment, key) || key === "__proto__" || key === "constructor" || key === "prototype")
367
+ continue;
368
+ merged[key] = Object.hasOwn(merged, key)
369
+ ? mergeStreamingArgumentValues(merged[key], fragment[key])
370
+ : cloneStreamingArgumentValue(fragment[key]);
371
+ }
372
+ return merged;
373
+ }
374
+
375
+ /**
376
+ * Check if conversation messages contain tool calls or tool results.
377
+ * This is needed because Anthropic (via proxy) requires the tools param
378
+ * to be present when messages include tool_calls or tool role messages.
379
+ */
380
+ function hasToolHistory(messages: Message[]): boolean {
381
+ for (const msg of messages) {
382
+ if (msg.role === "toolResult") {
383
+ return true;
384
+ }
385
+ if (msg.role === "assistant") {
386
+ if (msg.content.some(block => block.type === "toolCall")) {
387
+ return true;
388
+ }
389
+ }
390
+ }
391
+ return false;
392
+ }
393
+ /**
394
+ * Identify "real progress" stream chunks vs. keepalives, role-only preambles,
395
+ * and empty `{choices:[]}` no-ops emitted by some OpenAI-compatible endpoints.
396
+ * Without this filter, every keepalive resets `iterateWithIdleTimeout`'s
397
+ * deadline, so a provider that streams nothing but pings keeps the watchdog
398
+ * asleep indefinitely — observed against z.ai/GLM via OpenRouter where a
399
+ * subagent stalled for hours with no error surfaced.
400
+ *
401
+ * A chunk counts as progress when it carries terminal usage, a finish reason,
402
+ * or a model-produced delta (content / tool calls / reasoning / refusal).
403
+ * Role-only `delta: { role: "assistant" }` preambles do NOT count; we want the
404
+ * (longer) first-event timeout to keep governing until real output appears.
405
+ */
406
+ export function isOpenAICompletionsProgressChunk(chunk: unknown): boolean {
407
+ if (!chunk || typeof chunk !== "object") return false;
408
+ const record = chunk as {
409
+ usage?: unknown;
410
+ choices?: ReadonlyArray<{
411
+ finish_reason?: unknown;
412
+ usage?: unknown;
413
+ delta?: {
414
+ content?: unknown;
415
+ tool_calls?: unknown;
416
+ reasoning?: unknown;
417
+ reasoning_content?: unknown;
418
+ reasoning_text?: unknown;
419
+ refusal?: unknown;
420
+ };
421
+ }>;
422
+ };
423
+ if (record.usage) return true;
424
+ const choice = Array.isArray(record.choices) ? record.choices[0] : undefined;
425
+ if (!choice) return false;
426
+ if (choice.finish_reason) return true;
427
+ if (choice.usage) return true;
428
+ const delta = choice.delta;
429
+ if (!delta) return false;
430
+ const content = delta.content;
431
+ if (typeof content === "string" ? content.length > 0 : Array.isArray(content) && content.length > 0) return true;
432
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) return true;
433
+ if (typeof delta.reasoning === "string" && delta.reasoning.length > 0) return true;
434
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) return true;
435
+ if (typeof delta.reasoning_text === "string" && delta.reasoning_text.length > 0) return true;
436
+ if (typeof delta.refusal === "string" && delta.refusal.length > 0) return true;
437
+ return false;
438
+ }
439
+
440
+ export interface OpenAICompletionsOptions extends StreamOptions {
441
+ toolChoice?: ToolChoice;
442
+ reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
443
+ /** Force-disable reasoning where supported, or request the lowest effort on generic effort endpoints. */
444
+ disableReasoning?: boolean;
445
+ serviceTier?: ServiceTier;
446
+ /** @internal True when maxTokens came from the caller, not the model default. */
447
+ maxTokensExplicit?: boolean;
448
+ /**
449
+ * Routing-variant suffix appended to OpenRouter model IDs when none is
450
+ * already present (`anthropic/claude-haiku-latest` → `…:nitro`). Common
451
+ * values: `"nitro"`, `"floor"`, `"online"`, `"exacto"`. Ignored when the
452
+ * resolved `model.id` already contains a colon-suffix after the last
453
+ * provider segment (explicit `:nitro` in the selector or a catalog entry
454
+ * with the variant baked in).
455
+ */
456
+ openrouterVariant?: string;
457
+ }
458
+
459
+ type AppliedToolStrictMode = "mixed" | "all_strict" | "none";
460
+ type ToolStrictModeOverride = Exclude<ResolvedOpenAICompat["toolStrictMode"], "mixed"> | undefined;
461
+
462
+ type BuiltOpenAICompletionTools = {
463
+ tools: ChatCompletionTool[];
464
+ toolStrictMode: AppliedToolStrictMode;
465
+ /** True when at least one wire tool was sent with `strict: true`. */
466
+ strictToolsApplied: boolean;
467
+ };
468
+
469
+ const OPENAI_COMPLETIONS_PROVIDER_SESSION_STATE_PREFIX = "openai-completions:";
470
+
471
+ type OpenAICompletionsProviderSessionState = ProviderSessionState &
472
+ OpenAIStrictToolsState &
473
+ OpenAIReasoningEffortFallbackState;
474
+
475
+ function createOpenAICompletionsProviderSessionState(): OpenAICompletionsProviderSessionState {
476
+ const strictToolsState = createOpenAIStrictToolsState();
477
+ const reasoningEffortFallbackState = createOpenAIReasoningEffortFallbackState();
478
+ const state: OpenAICompletionsProviderSessionState = {
479
+ ...strictToolsState,
480
+ ...reasoningEffortFallbackState,
481
+ close: () => {
482
+ clearOpenAIStrictToolsState(state);
483
+ clearOpenAIReasoningEffortFallbackState(state);
484
+ },
485
+ };
486
+ return state;
487
+ }
488
+
489
+ function getOpenAICompletionsProviderSessionState(
490
+ model: Model<"openai-completions">,
491
+ baseUrl: string | undefined,
492
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
493
+ ): OpenAICompletionsProviderSessionState | undefined {
494
+ if (!providerSessionState) return undefined;
495
+ const key = `${OPENAI_COMPLETIONS_PROVIDER_SESSION_STATE_PREFIX}${model.provider}:${baseUrl ?? ""}:${model.id}`;
496
+ const existing = providerSessionState.get(key) as OpenAICompletionsProviderSessionState | undefined;
497
+ if (existing) return existing;
498
+ const created = createOpenAICompletionsProviderSessionState();
499
+ providerSessionState.set(key, created);
500
+ return created;
501
+ }
502
+
503
+ // DeepSeek models leak chat-template special tokens (e.g. `<|tool_calls_begin|>`,
504
+ // `<|DSML|tool_calls|>`) into visible `content` deltas when hosted behind providers
505
+ // (such as NVIDIA NIM) that don't strip them server-side. The structured `tool_calls`
506
+ // payload is still emitted correctly — we only need to filter the leaked markers from
507
+ // user-visible text. Tokens use either fullwidth pipes (|, U+FF5C) or ASCII pipes.
508
+ // Body is restricted to identifier-like chars (with the DeepSeek tokenizer's `▁`),
509
+ // capped at a sane length to avoid swallowing legitimate angle-bracket text.
510
+ const DEEPSEEK_SPECIAL_TOKEN_REGEX = /<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>/g;
511
+ const DEEPSEEK_SPECIAL_TOKEN_AT_START_REGEX = /^\s*<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>/;
512
+ const DEEPSEEK_SPECIAL_TOKEN_AT_END_REGEX = /<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>\s*$/;
513
+ const DEEPSEEK_OPEN_DELIMS = ["<|", "<|"] as const;
514
+
515
+ function stripDeepseekSpecialTokens(text: string): string {
516
+ const stripped = text.replace(DEEPSEEK_SPECIAL_TOKEN_REGEX, "");
517
+ if (stripped === text) return text;
518
+
519
+ let normalized = stripped;
520
+ if (DEEPSEEK_SPECIAL_TOKEN_AT_START_REGEX.test(text)) normalized = normalized.replace(/^\s+/u, "");
521
+ if (DEEPSEEK_SPECIAL_TOKEN_AT_END_REGEX.test(text)) normalized = normalized.replace(/\s+$/u, "");
522
+ return normalized;
523
+ }
524
+
525
+ // Find a trailing partial `<|...` (or `<|...`) that has not yet been closed by a
526
+ // matching `|>`/`|>`, so it can be held back until the next chunk arrives. A solo
527
+ // trailing `<` is also held in case it is the start of a new token.
528
+ function getTrailingPartialDeepseekToken(text: string): string {
529
+ let bestIdx = -1;
530
+ for (const delim of DEEPSEEK_OPEN_DELIMS) {
531
+ const idx = text.lastIndexOf(delim);
532
+ if (idx > bestIdx) bestIdx = idx;
533
+ }
534
+ if (bestIdx === -1) {
535
+ return text.endsWith("<") ? "<" : "";
536
+ }
537
+ const tail = text.slice(bestIdx);
538
+ if (tail.includes("|>") || tail.includes("|>")) return "";
539
+ // Cap the held-back length so a stray `<|` in normal prose can't grow unboundedly.
540
+ if (tail.length > 256) return "";
541
+ return tail;
542
+ }
543
+ const OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE =
544
+ "OpenAI completions stream timed out while waiting for the first event";
545
+ // How long to keep draining the stream after a `finish_reason` chunk arrived.
546
+ // Compliant hosts follow it (almost) immediately with an optional usage-only
547
+ // chunk and the `[DONE]` sentinel, so the window only ever elapses on hosts
548
+ // that hold the connection open after the response logically completed —
549
+ // without it the turn parks on `iterator.next()` until the idle watchdog
550
+ // converts the already-successful response into a timeout error.
551
+ const OPENAI_COMPLETIONS_POST_FINISH_GRACE_MS = 2_500;
552
+
553
+ const streamOpenAICompletionsOnce = (
554
+ model: Model<"openai-completions">,
555
+ context: Context,
556
+ options?: OpenAICompletionsOptions,
557
+ ): AssistantMessageEventStream => {
558
+ const stream = new AssistantMessageEventStream();
559
+
560
+ (async () => {
561
+ const startTime = performance.now();
562
+ let firstTokenTime: number | undefined;
563
+ const policy = resolveOpenAICompatForRequest(model, options);
564
+
565
+ const output: AssistantMessage = createInitialResponsesAssistantMessage(model.api, model.provider, model.id);
566
+ let rawRequestDump: RawHttpRequestDump | undefined;
567
+ const abortTracker = createAbortSourceTracker(options?.signal);
568
+ const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
569
+ OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE,
570
+ );
571
+ const { requestAbortController, requestSignal } = abortTracker;
572
+ const onSseEvent = options?.onSseEvent;
573
+ const rawSseObserver = onSseEvent
574
+ ? (event: RawSseEvent) => {
575
+ if (!event.event && event.data && event.data !== "[DONE]") {
576
+ try {
577
+ const parsed = JSON.parse(event.data);
578
+ const resolvedEvent =
579
+ typeof parsed.type === "string"
580
+ ? parsed.type
581
+ : typeof parsed.object === "string"
582
+ ? parsed.object
583
+ : null;
584
+ if (resolvedEvent) {
585
+ event.event = resolvedEvent;
586
+ event.raw = [`event: ${resolvedEvent}`, ...event.raw];
587
+ }
588
+ } catch {}
589
+ }
590
+ onSseEvent(event, model);
591
+ }
592
+ : undefined;
593
+ // Assigned once the block helpers exist (they are scoped to the `try`);
594
+ // the catch handler uses it to close open blocks before emitting the
595
+ // terminal error so both exit paths obey the same block lifecycle.
596
+ let finishOpenBlocksOnError: () => void = () => {};
597
+
598
+ try {
599
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
600
+ const idleTimeoutFallbackMs = model.compat.streamIdleTimeoutMs;
601
+ const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(idleTimeoutFallbackMs);
602
+ const firstEventTimeoutMs =
603
+ options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
604
+ const requestTimeoutMs =
605
+ firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
606
+ const { copilotPremiumRequests, baseUrl, headers, query, requestHeaders } = createRequestSetup(
607
+ model,
608
+ context,
609
+ apiKey,
610
+ options?.headers,
611
+ options?.initiatorOverride,
612
+ );
613
+ const premiumRequestsTotal = copilotPremiumRequests;
614
+ let appliedStrictTools = false;
615
+ const requestReasoningEffortFallbacks = new Map<string, OpenAIReasoningEffortFallback>();
616
+ const attemptedReasoningEffortFallbacks = new Set<string>();
617
+ let activeReasoningEffortFallbackKey: string | undefined;
618
+ let activeRequestParams: OpenAICompletionsParams | undefined;
619
+ const providerSessionState = getOpenAICompletionsProviderSessionState(
620
+ model,
621
+ baseUrl,
622
+ options?.providerSessionState,
623
+ );
624
+ const strictToolsScope = getOpenAIStrictToolsScope(model, baseUrl);
625
+ let disableStrictTools = isStrictToolsDisabledForScope(providerSessionState, strictToolsScope);
626
+ const trimmedBaseUrl = baseUrl.replace(/\/+$/, "");
627
+ const completionsUrl = query
628
+ ? `${trimmedBaseUrl}/chat/completions?${new URLSearchParams(query)}`
629
+ : `${trimmedBaseUrl}/chat/completions`;
630
+ const createCompletionsStream = async (toolStrictModeOverride?: ToolStrictModeOverride) => {
631
+ const effectiveToolStrictModeOverride = disableStrictTools ? "none" : toolStrictModeOverride;
632
+ const { params, strictToolsApplied } = buildParams(
633
+ model,
634
+ context,
635
+ options,
636
+ effectiveToolStrictModeOverride,
637
+ );
638
+ appliedStrictTools = strictToolsApplied;
639
+ const reasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
640
+ "chat-completions",
641
+ trimmedBaseUrl,
642
+ params.model,
643
+ );
644
+ const requestReasoningEffortFallback = requestReasoningEffortFallbacks.has(reasoningEffortFallbackKey)
645
+ ? requestReasoningEffortFallbacks.get(reasoningEffortFallbackKey)
646
+ : getOpenAIReasoningEffortFallback(providerSessionState, reasoningEffortFallbackKey);
647
+ if (requestReasoningEffortFallback !== undefined) {
648
+ applyOpenAIReasoningEffortFallback(params, requestReasoningEffortFallback);
649
+ }
650
+ activeReasoningEffortFallbackKey = reasoningEffortFallbackKey;
651
+ activeRequestParams = params;
652
+ options?.onPayload?.(params);
653
+ rawRequestDump = {
654
+ provider: model.provider,
655
+ api: output.api,
656
+ model: model.id,
657
+ method: "POST",
658
+ url: completionsUrl,
659
+ headers: requestHeaders,
660
+ body: params,
661
+ };
662
+ let requestTimeout: NodeJS.Timeout | undefined;
663
+ if (requestTimeoutMs !== undefined) {
664
+ requestTimeout = setTimeout(
665
+ () => abortTracker.abortLocally(firstEventTimeoutAbortError),
666
+ requestTimeoutMs,
667
+ );
668
+ }
669
+ try {
670
+ const headersWithTimeout = { ...headers };
671
+ if (requestTimeoutMs !== undefined) {
672
+ headersWithTimeout["X-Stainless-Timeout"] = Math.floor(requestTimeoutMs / 1000).toString();
673
+ }
674
+ const { events, response, requestId } = await postOpenAIStream<ChatCompletionChunk>({
675
+ url: completionsUrl,
676
+ headers: headersWithTimeout,
677
+ body: params,
678
+ signal: requestSignal,
679
+ fetch: options?.fetch,
680
+ // With a first-event watchdog armed, transport retries must
681
+ // not silently extend the deadline (old SDK `maxRetries: 0`).
682
+ maxAttempts: requestTimeoutMs === undefined ? undefined : 1,
683
+ onSseEvent: rawSseObserver,
684
+ });
685
+ await notifyProviderResponse(options, response, model, requestId);
686
+ return events;
687
+ } finally {
688
+ // Headers arrived (or the request failed); from here the
689
+ // first-event deadline is enforced by `iterateWithIdleTimeout`.
690
+ if (requestTimeout !== undefined) clearTimeout(requestTimeout);
691
+ }
692
+ };
693
+ let openaiStream: AsyncIterable<ChatCompletionChunk>;
694
+ try {
695
+ openaiStream = await callWithCopilotModelRetry(() => createCompletionsStream(), {
696
+ provider: model.provider,
697
+ signal: requestSignal,
698
+ });
699
+ } catch (error) {
700
+ const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
701
+ const reasoningEffortFallback =
702
+ activeReasoningEffortFallbackKey && activeRequestParams && !requestSignal.aborted
703
+ ? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, activeRequestParams, {
704
+ explicitDisable: options?.disableReasoning === true && options.reasoning === undefined,
705
+ })
706
+ : undefined;
707
+ if (reasoningEffortFallback !== undefined && activeReasoningEffortFallbackKey) {
708
+ const retryMarker = `${activeReasoningEffortFallbackKey}:${String(reasoningEffortFallback)}`;
709
+ if (attemptedReasoningEffortFallbacks.has(retryMarker)) throw error;
710
+ attemptedReasoningEffortFallbacks.add(retryMarker);
711
+ requestReasoningEffortFallbacks.set(activeReasoningEffortFallbackKey, reasoningEffortFallback);
712
+ openaiStream = await createCompletionsStream();
713
+ rememberOpenAIReasoningEffortFallback(
714
+ providerSessionState,
715
+ activeReasoningEffortFallbackKey,
716
+ reasoningEffortFallback,
717
+ );
718
+ } else if (
719
+ isOpenRouterAnthropicModel(model) &&
720
+ !disableStrictTools &&
721
+ isCompiledGrammarTooLargeStrictError(error, capturedErrorResponse)
722
+ ) {
723
+ disableStrictToolsForScope(providerSessionState, strictToolsScope);
724
+ disableStrictTools = true;
725
+ openaiStream = await createCompletionsStream("none");
726
+ } else {
727
+ if (!shouldRetryWithoutStrictTools(error, capturedErrorResponse, appliedStrictTools, context.tools)) {
728
+ throw error;
729
+ }
730
+ // Remember the rejection for the rest of the session so every
731
+ // subsequent request doesn't pay a strict-400 + retry round-trip.
732
+ disableStrictToolsForScope(providerSessionState, strictToolsScope);
733
+ disableStrictTools = true;
734
+ openaiStream = await createCompletionsStream("none");
735
+ }
736
+ }
737
+ if (premiumRequestsTotal !== undefined) {
738
+ output.usage.premiumRequests = premiumRequestsTotal;
739
+ }
740
+ stream.push({ type: "start", partial: output });
741
+
742
+ // Some OpenAI-compatible DeepSeek hosts (including NVIDIA NIM and DeepSeek's
743
+ // native API) leak chat-template tool-call markers in `delta.content` even
744
+ // though tool calls are also surfaced structurally. Strip the leaked markers
745
+ // so users don't see raw `<|...|>` tokens.
746
+ const stripDeepseekChatTemplateTokens = policy.stream.stripSpecialTokens === "deepseek";
747
+ type ToolCallStreamBlock = ToolCall & {
748
+ partialArgs?: string | Record<string, unknown>;
749
+ streamIndex?: number;
750
+ [kStreamingLastParseLen]?: number;
751
+ };
752
+ type OpenAIStreamBlock = TextContent | ThinkingContent | ToolCallStreamBlock;
753
+ const pendingToolCallBlocks: ToolCallStreamBlock[] = [];
754
+ const toolCallBlockByIndex = new Map<number, ToolCallStreamBlock>();
755
+ let currentBlock: OpenAIStreamBlock | undefined;
756
+ const blockIndex = (block: OpenAIStreamBlock | undefined): number => {
757
+ if (!block) return Math.max(0, output.content.length - 1);
758
+ return output.content.indexOf(block);
759
+ };
760
+ const finishToolCallBlock = (block: ToolCallStreamBlock): void => {
761
+ if (block.partialArgs === undefined) return;
762
+ const contentIndex = blockIndex(block);
763
+ if (contentIndex < 0) return;
764
+ // Object-shaped `partialArgs` came from MiniMax-compatible hosts that stream
765
+ // `function.arguments` as an object. The per-chunk handler holds them with an
766
+ // empty wire delta (see the object branch below) because emitting each chunk's
767
+ // `JSON.stringify(rawArgs)` would feed concat-based downstream consumers
768
+ // (proxy.ts, openai-chat-server, openai-responses-server, anthropic-messages-server)
769
+ // an invalid concatenation like `{"input":"a"}{"input":"b"}`. Flush the final
770
+ // merged object as one concat-safe delta now so those consumers reconstruct the
771
+ // args correctly before observing `toolcall_end`.
772
+ if (typeof block.partialArgs === "object" && !Array.isArray(block.partialArgs)) {
773
+ const fullJson = JSON.stringify(block.partialArgs);
774
+ if (fullJson.length > 0 && fullJson !== "{}") {
775
+ stream.push({ type: "toolcall_delta", contentIndex, delta: fullJson, partial: output });
776
+ }
777
+ }
778
+ block.arguments =
779
+ typeof block.partialArgs === "string" ? parseStreamingJson(block.partialArgs) : block.partialArgs;
780
+ delete block.partialArgs;
781
+ if (block.streamIndex !== undefined) {
782
+ toolCallBlockByIndex.delete(block.streamIndex);
783
+ delete block.streamIndex;
784
+ }
785
+ const pendingIndex = pendingToolCallBlocks.indexOf(block);
786
+ if (pendingIndex >= 0) pendingToolCallBlocks.splice(pendingIndex, 1);
787
+ stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
788
+ };
789
+ const finishPendingToolCallBlocks = (): void => {
790
+ for (const block of [...pendingToolCallBlocks]) {
791
+ finishToolCallBlock(block);
792
+ }
793
+ };
794
+ const finishCurrentBlock = (block: OpenAIStreamBlock | undefined): void => {
795
+ if (!block) return;
796
+ const contentIndex = blockIndex(block);
797
+ if (contentIndex < 0) return;
798
+ if (block.type === "text") {
799
+ stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
800
+ return;
801
+ }
802
+ if (block.type === "thinking") {
803
+ stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
804
+ return;
805
+ }
806
+ finishToolCallBlock(block);
807
+ };
808
+ finishOpenBlocksOnError = () => {
809
+ if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
810
+ finishPendingToolCallBlocks();
811
+ };
812
+ const appendText = (
813
+ message: AssistantMessage,
814
+ eventStream: AssistantMessageEventStream,
815
+ text: string,
816
+ ): void => {
817
+ if (currentBlock?.type !== "text") {
818
+ // Leave toolCall blocks pending across text transitions: chunks after
819
+ // the first typically carry only `index`, so a finished (de-registered)
820
+ // call would be reborn as a nameless phantom block when its arguments
821
+ // resume. The stream-end sweep finalizes pending calls.
822
+ if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
823
+ currentBlock = { type: "text", text: "" };
824
+ message.content.push(currentBlock);
825
+ eventStream.push({ type: "text_start", contentIndex: blockIndex(currentBlock), partial: message });
826
+ }
827
+ currentBlock.text += text;
828
+ eventStream.push({
829
+ type: "text_delta",
830
+ contentIndex: blockIndex(currentBlock),
831
+ delta: text,
832
+ partial: message,
833
+ });
834
+ };
835
+ const appendThinking = (
836
+ message: AssistantMessage,
837
+ eventStream: AssistantMessageEventStream,
838
+ thinking: string,
839
+ signature?: string,
840
+ ): void => {
841
+ if (
842
+ currentBlock?.type !== "thinking" ||
843
+ (signature !== undefined && currentBlock.thinkingSignature !== signature)
844
+ ) {
845
+ // Same as appendText: leave toolCall blocks pending so index-only
846
+ // continuation deltas can still find them.
847
+ if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
848
+ currentBlock = { type: "thinking", thinking: "", thinkingSignature: signature };
849
+ message.content.push(currentBlock);
850
+ eventStream.push({
851
+ type: "thinking_start",
852
+ contentIndex: blockIndex(currentBlock),
853
+ partial: message,
854
+ });
855
+ }
856
+ if (signature !== undefined && !currentBlock.thinkingSignature) {
857
+ currentBlock.thinkingSignature = signature;
858
+ }
859
+ currentBlock.thinking += thinking;
860
+ eventStream.push({
861
+ type: "thinking_delta",
862
+ contentIndex: blockIndex(currentBlock),
863
+ delta: thinking,
864
+ partial: message,
865
+ });
866
+ };
867
+
868
+ const appendTextDelta = (text: string): void => {
869
+ if (!text) return;
870
+ if (!firstTokenTime) firstTokenTime = performance.now();
871
+ appendText(output, stream, text);
872
+ };
873
+ // Tracks the last full cumulative reasoning snapshot per signature (the
874
+ // reasoning field name) so dedup survives block transitions. Required
875
+ // for MiniMax-M3: once `</think>` and visible text arrive, currentBlock
876
+ // flips to "text", but later chunks keep carrying the same cumulative
877
+ // `reasoning_content` snapshot. Without an external tracker the guard
878
+ // below misses and the snapshot gets re-emitted as a fresh thinking
879
+ // block after the answer has started.
880
+ const lastCumulativeReasoningBySignature = new Map<string, string>();
881
+ const appendThinkingDelta = (
882
+ thinking: string,
883
+ signature?: string,
884
+ source: "delta" | "cumulative" = "delta",
885
+ ): void => {
886
+ if (!thinking) return;
887
+ let emittedThinking = thinking;
888
+ if (source === "cumulative") {
889
+ const key = signature ?? "";
890
+ const lastSnapshot = lastCumulativeReasoningBySignature.get(key) ?? "";
891
+ if (thinking.startsWith(lastSnapshot)) {
892
+ emittedThinking = thinking.slice(lastSnapshot.length);
893
+ }
894
+ lastCumulativeReasoningBySignature.set(key, thinking);
895
+ if (!emittedThinking) return;
896
+ }
897
+ if (!firstTokenTime) firstTokenTime = performance.now();
898
+ appendThinking(output, stream, emittedThinking, signature);
899
+ };
900
+
901
+ let deepseekStripBuffer = "";
902
+ const flushDeepseekStripBuffer = (final: boolean): void => {
903
+ if (deepseekStripBuffer.length === 0) return;
904
+ let flushable: string;
905
+ if (final) {
906
+ flushable = deepseekStripBuffer;
907
+ deepseekStripBuffer = "";
908
+ } else {
909
+ const trailing = getTrailingPartialDeepseekToken(deepseekStripBuffer);
910
+ flushable = deepseekStripBuffer.slice(0, deepseekStripBuffer.length - trailing.length);
911
+ deepseekStripBuffer = trailing;
912
+ }
913
+ const stripped = stripDeepseekSpecialTokens(flushable);
914
+ if (stripped && (stripped === flushable || stripped.trim().length > 0)) appendTextDelta(stripped);
915
+ };
916
+ const appendProcessedText = (processedText: string): void => {
917
+ if (processedText.length === 0) return;
918
+ if (stripDeepseekChatTemplateTokens) {
919
+ deepseekStripBuffer += processedText;
920
+ flushDeepseekStripBuffer(false);
921
+ } else {
922
+ appendTextDelta(processedText);
923
+ }
924
+ };
925
+ const streamMarkupHealingPattern = policy.stream.markupHealingPattern;
926
+ const streamMarkupHealing = streamMarkupHealingPattern
927
+ ? new StreamMarkupHealing({ pattern: streamMarkupHealingPattern })
928
+ : undefined;
929
+ const explicitReasoningDeltasMayBeCumulative = policy.stream.reasoningDeltasMayBeCumulative;
930
+ let suppressHealedThinking = false;
931
+ let healedToolCallEmitted = false;
932
+ const emitHealedToolCall = (call: HealedToolCall): void => {
933
+ finishCurrentBlock(currentBlock);
934
+ const block: ToolCall & { partialArgs: string } = {
935
+ type: "toolCall",
936
+ id: call.id,
937
+ name: call.name,
938
+ arguments: {},
939
+ partialArgs: call.arguments,
940
+ };
941
+ block.arguments = parseStreamingJson(call.arguments);
942
+ currentBlock = block;
943
+ output.content.push(block);
944
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(block), partial: output });
945
+ stream.push({
946
+ type: "toolcall_delta",
947
+ contentIndex: blockIndex(block),
948
+ delta: call.arguments,
949
+ partial: output,
950
+ });
951
+ finishCurrentBlock(block);
952
+ currentBlock = undefined;
953
+ healedToolCallEmitted = true;
954
+ };
955
+ const emitHealingEvent = (event: StreamMarkupHealingEvent, suppressThinking: boolean): void => {
956
+ if (event.type === "text") {
957
+ appendProcessedText(event.text);
958
+ } else if (event.type === "thinking") {
959
+ if (!suppressThinking) appendThinkingDelta(event.thinking);
960
+ } else {
961
+ emitHealedToolCall(event.call);
962
+ }
963
+ };
964
+ const flushHealedToolCalls = (): void => {
965
+ if (!streamMarkupHealing) return;
966
+ const calls = streamMarkupHealing.drainCompleted();
967
+ for (const call of calls) emitHealedToolCall(call);
968
+ };
969
+
970
+ // Terminal-chunk bookkeeping for the post-finish grace window below.
971
+ // `streamFinishedAt` flips when a chunk carries `finish_reason`;
972
+ // `sawUsagePayload` flips when a usage payload was parsed.
973
+ let streamFinishedAt: number | undefined;
974
+ let sawUsagePayload = false;
975
+ const timedOpenaiStream = iterateWithIdleTimeout(openaiStream, {
976
+ idleTimeoutMs,
977
+ firstItemTimeoutMs: firstEventTimeoutMs,
978
+ firstItemErrorMessage: OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE,
979
+ errorMessage: "OpenAI completions stream stalled while waiting for the next event",
980
+ onIdle: () => requestAbortController.abort(),
981
+ onFirstItemTimeout: () => abortTracker.abortLocally(firstEventTimeoutAbortError),
982
+ abortSignal: options?.signal,
983
+ isProgressItem: isOpenAICompletionsProgressChunk,
984
+ });
985
+ const terminalAwareStream = iterateWithTerminalGrace(timedOpenaiStream, {
986
+ finishedAtMs: () => streamFinishedAt,
987
+ graceMs: OPENAI_COMPLETIONS_POST_FINISH_GRACE_MS,
988
+ // The inner idle-timeout generator is parked mid-`next()` when the
989
+ // grace window closes, so abort the transport to settle that read
990
+ // and release the socket immediately (a queued `.return()` alone
991
+ // would wait on the never-arriving next chunk).
992
+ onGraceEnd: () => requestAbortController.abort(),
993
+ });
994
+ for await (const chunk of terminalAwareStream) {
995
+ if (!chunk || typeof chunk !== "object") continue;
996
+
997
+ // OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
998
+ // and each chunk in a streamed completion carries the same id.
999
+ output.responseId ||= chunk.id;
1000
+
1001
+ // Aggregators (OpenRouter, Vercel AI Gateway, …) report the upstream
1002
+ // provider that actually served the request via a top-level `provider`
1003
+ // field present on every chunk. Capture the first non-empty value so
1004
+ // callers can attribute routing without re-parsing the raw stream.
1005
+ if (!output.upstreamProvider) {
1006
+ const upstreamProvider = (chunk as ProviderAttributedChatCompletionChunk).provider;
1007
+ output.upstreamProvider =
1008
+ typeof upstreamProvider === "string" && upstreamProvider.length > 0 ? upstreamProvider : undefined;
1009
+ }
1010
+
1011
+ if (chunk.usage) {
1012
+ output.usage = parseChunkUsage(chunk.usage, model, premiumRequestsTotal);
1013
+ sawUsagePayload = true;
1014
+ }
1015
+
1016
+ const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
1017
+ if (!choice) {
1018
+ // Trailing usage-only chunk (`stream_options.include_usage`) after
1019
+ // `finish_reason`: the response is complete — stop pulling instead
1020
+ // of waiting for `[DONE]`/close from hosts that never send either.
1021
+ if (streamFinishedAt !== undefined && sawUsagePayload) break;
1022
+ continue;
1023
+ }
1024
+
1025
+ if (!chunk.usage) {
1026
+ const choiceUsage = (choice as OpenAICompletionsChoiceUsage).usage;
1027
+ if (typeof choiceUsage === "object" && choiceUsage !== null) {
1028
+ output.usage = parseChunkUsage(choiceUsage, model, premiumRequestsTotal);
1029
+ sawUsagePayload = true;
1030
+ }
1031
+ }
1032
+
1033
+ if (choice.finish_reason) {
1034
+ const finishReasonResult = mapStopReason(choice.finish_reason);
1035
+ output.stopReason = finishReasonResult.stopReason;
1036
+ if (finishReasonResult.errorMessage) {
1037
+ output.errorMessage = finishReasonResult.errorMessage;
1038
+ }
1039
+ streamFinishedAt ??= Date.now();
1040
+ }
1041
+
1042
+ if (choice.delta) {
1043
+ // Some endpoints return reasoning in reasoning_content (llama.cpp),
1044
+ // or reasoning (other openai compatible endpoints). Use the first
1045
+ // non-empty reasoning field to avoid duplication when a chunk carries
1046
+ // multiple aliases for the same reasoning text.
1047
+ const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
1048
+ const deltaRecord = choice.delta as Record<string, unknown>;
1049
+ let foundReasoningField: string | undefined;
1050
+ let foundReasoningDelta = "";
1051
+ for (const field of reasoningFields) {
1052
+ const reasoningDelta = deltaRecord[field];
1053
+ if (typeof reasoningDelta === "string" && reasoningDelta.length > 0) {
1054
+ foundReasoningField = field;
1055
+ foundReasoningDelta = reasoningDelta;
1056
+ break;
1057
+ }
1058
+ }
1059
+
1060
+ if (foundReasoningField) {
1061
+ appendThinkingDelta(
1062
+ foundReasoningDelta,
1063
+ foundReasoningField,
1064
+ explicitReasoningDeltasMayBeCumulative ? "cumulative" : "delta",
1065
+ );
1066
+ suppressHealedThinking = true;
1067
+ }
1068
+
1069
+ const normalizedDeltaText = normalizeStreamingContentText(choice.delta.content);
1070
+ if (normalizedDeltaText.length > 0) {
1071
+ if (!firstTokenTime) firstTokenTime = performance.now();
1072
+ const hasStructuredToolCalls =
1073
+ Array.isArray(choice.delta.tool_calls) && choice.delta.tool_calls.length > 0;
1074
+
1075
+ if (streamMarkupHealing) {
1076
+ const healingEvents = hasStructuredToolCalls
1077
+ ? streamMarkupHealing.feedEventsWithoutCalls(normalizedDeltaText)
1078
+ : streamMarkupHealing.feedEvents(normalizedDeltaText);
1079
+ for (const event of healingEvents) {
1080
+ emitHealingEvent(event, suppressHealedThinking);
1081
+ }
1082
+ } else {
1083
+ appendProcessedText(normalizedDeltaText);
1084
+ }
1085
+ }
1086
+
1087
+ if (choice?.delta?.tool_calls && choice.delta.tool_calls.length > 0) {
1088
+ for (const toolCall of choice.delta.tool_calls) {
1089
+ const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
1090
+ let block = streamIndex !== undefined ? toolCallBlockByIndex.get(streamIndex) : undefined;
1091
+ if (!block && toolCall.id) {
1092
+ block = pendingToolCallBlocks.find(candidate => candidate.id === toolCall.id);
1093
+ }
1094
+ if (
1095
+ !block &&
1096
+ currentBlock?.type === "toolCall" &&
1097
+ (!toolCall.id || currentBlock.id === toolCall.id)
1098
+ ) {
1099
+ block = currentBlock;
1100
+ }
1101
+
1102
+ if (!block) {
1103
+ if (currentBlock?.type !== "toolCall") {
1104
+ finishCurrentBlock(currentBlock);
1105
+ }
1106
+ block = {
1107
+ type: "toolCall",
1108
+ id: toolCall.id || "",
1109
+ name: toolCall.function?.name || "",
1110
+ arguments: {},
1111
+ partialArgs: "",
1112
+ streamIndex,
1113
+ };
1114
+ if (streamIndex !== undefined) toolCallBlockByIndex.set(streamIndex, block);
1115
+ pendingToolCallBlocks.push(block);
1116
+ currentBlock = block;
1117
+ output.content.push(block);
1118
+ stream.push({
1119
+ type: "toolcall_start",
1120
+ contentIndex: blockIndex(block),
1121
+ partial: output,
1122
+ });
1123
+ } else {
1124
+ // Resuming a pending call after interleaved text/thinking:
1125
+ // close the text/thinking block we drifted into.
1126
+ if (currentBlock !== block && currentBlock && currentBlock.type !== "toolCall") {
1127
+ finishCurrentBlock(currentBlock);
1128
+ }
1129
+ currentBlock = block;
1130
+ if (streamIndex !== undefined && block.streamIndex === undefined) {
1131
+ block.streamIndex = streamIndex;
1132
+ toolCallBlockByIndex.set(streamIndex, block);
1133
+ }
1134
+ }
1135
+
1136
+ if (toolCall.id) block.id = toolCall.id;
1137
+ if (toolCall.function?.name) block.name = toolCall.function.name;
1138
+ let delta = "";
1139
+ // The OpenAI SDK types `function.arguments` as a JSON string, but MiniMax-compatible
1140
+ // hosts stream a fully-formed object instead. Model both shapes so the branches below
1141
+ // narrow honestly rather than widening through `unknown`.
1142
+ const rawArgs = toolCall.function?.arguments as string | Record<string, unknown> | undefined;
1143
+ if (typeof rawArgs === "string") {
1144
+ if (rawArgs.length > 0) {
1145
+ delta = rawArgs;
1146
+ const prev = typeof block.partialArgs === "string" ? block.partialArgs : "";
1147
+ block.partialArgs = prev + rawArgs;
1148
+ const throttled = parseStreamingJsonThrottled(
1149
+ block.partialArgs,
1150
+ block[kStreamingLastParseLen] ?? 0,
1151
+ );
1152
+ if (throttled) {
1153
+ block.arguments = throttled.value;
1154
+ block[kStreamingLastParseLen] = throttled.parsedLen;
1155
+ }
1156
+ }
1157
+ } else if (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)) {
1158
+ // MiniMax-compatible hosts stream `function.arguments` as an object instead of the
1159
+ // OpenAI JSON-string contract. Most chunks carry the complete object in one delta,
1160
+ // but cannot rely on that: replacing per-chunk drops earlier keys (and earlier
1161
+ // string content for the same key) when the host fragments the args across deltas.
1162
+ // Deep-merge into the accumulated object. Strings and arrays detect
1163
+ // cumulative-vs-delta semantics by prefix, nested objects merge by key, and
1164
+ // prototype-polluting keys are ignored before storing or comparing values.
1165
+ //
1166
+ // `delta` stays empty here: emitting `JSON.stringify(rawArgs)` per chunk feeds
1167
+ // downstream concat-based accumulators (proxy.ts, openai-chat-server,
1168
+ // openai-responses-server, anthropic-messages-server) an invalid sequence like
1169
+ // `{"input":"a"}{"input":"b"}`. The merged object is flushed as a single
1170
+ // concat-safe delta in `finishToolCallBlock` before `toolcall_end` instead.
1171
+ const prev =
1172
+ block.partialArgs !== null &&
1173
+ typeof block.partialArgs === "object" &&
1174
+ !Array.isArray(block.partialArgs)
1175
+ ? (block.partialArgs as Record<string, unknown>)
1176
+ : undefined;
1177
+ const merged = mergeStreamingArgumentObjects(prev, rawArgs);
1178
+ block.partialArgs = merged;
1179
+ block.arguments = merged;
1180
+ }
1181
+ stream.push({
1182
+ type: "toolcall_delta",
1183
+ contentIndex: blockIndex(block),
1184
+ delta,
1185
+ partial: output,
1186
+ });
1187
+ }
1188
+ }
1189
+
1190
+ const reasoningDetails = (choice.delta as OpenAICompletionsDeltaWithReasoningDetails).reasoning_details;
1191
+ if (Array.isArray(reasoningDetails)) {
1192
+ for (const detail of reasoningDetails) {
1193
+ if (!detail || typeof detail !== "object") continue;
1194
+ const detailObject = detail as { type?: unknown; id?: unknown; data?: unknown };
1195
+ if (detailObject.type === "reasoning.encrypted" && detailObject.id && detailObject.data) {
1196
+ const matchingToolCall = output.content.find(
1197
+ b => b.type === "toolCall" && b.id === detailObject.id,
1198
+ ) as ToolCall | undefined;
1199
+ if (matchingToolCall) {
1200
+ matchingToolCall.thoughtSignature = JSON.stringify(detailObject);
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+
1207
+ // `finish_reason` + usage both observed: the chat-completions
1208
+ // contract has nothing left to deliver. Break instead of waiting
1209
+ // for `[DONE]`/connection close so hosts that hold the socket open
1210
+ // can't park the turn until the idle watchdog errors it out.
1211
+ if (streamFinishedAt !== undefined && sawUsagePayload) break;
1212
+ }
1213
+
1214
+ if (streamMarkupHealing) {
1215
+ for (const event of streamMarkupHealing.flushEvents()) {
1216
+ emitHealingEvent(event, suppressHealedThinking);
1217
+ }
1218
+ flushHealedToolCalls();
1219
+ if (healedToolCallEmitted && output.stopReason === "stop") {
1220
+ // Hosts that leak tool-call templates often still report
1221
+ // `finish_reason: stop` for the surrounding turn. Promote
1222
+ // only that natural-completion finish — leave `error`,
1223
+ // `length`, `aborted`, etc. untouched.
1224
+ output.stopReason = "toolUse";
1225
+ }
1226
+ }
1227
+
1228
+ if (stripDeepseekChatTemplateTokens) {
1229
+ flushDeepseekStripBuffer(true);
1230
+ }
1231
+
1232
+ if (currentBlock?.type === "toolCall") {
1233
+ finishPendingToolCallBlocks();
1234
+ } else {
1235
+ finishCurrentBlock(currentBlock);
1236
+ finishPendingToolCallBlocks();
1237
+ }
1238
+
1239
+ // Some OpenAI-compatible hosts stream structured `tool_calls` but report
1240
+ // `finish_reason: "stop"` instead of `"tool_calls"`. In the OpenAI contract a
1241
+ // tool call always means "execute and continue", so promote that
1242
+ // natural-completion finish to `toolUse` whenever the turn produced tool-call
1243
+ // blocks — the agent loop gates execution on the stop reason. `error`,
1244
+ // `length`, and `aborted` are intentionally left untouched. (Anthropic's
1245
+ // distinct `end_turn`-with-tool-calls "abandon" semantics live in its own
1246
+ // provider and correctly keep `stop`.)
1247
+ if (output.stopReason === "stop" && output.content.some(b => b.type === "toolCall")) {
1248
+ output.stopReason = "toolUse";
1249
+ }
1250
+
1251
+ if (
1252
+ policy.stream.emptyLengthFinishIsContextError &&
1253
+ output.stopReason === "length" &&
1254
+ !hasVisibleAssistantContent(output)
1255
+ ) {
1256
+ output.stopReason = "error";
1257
+ output.errorMessage = EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE;
1258
+ }
1259
+ const localAbortReason = abortTracker.getLocalAbortReason();
1260
+ if (localAbortReason) {
1261
+ throw localAbortReason;
1262
+ }
1263
+ if (abortTracker.wasCallerAbort()) {
1264
+ throw new AIError.AbortError();
1265
+ }
1266
+
1267
+ if (output.stopReason === "aborted") {
1268
+ throw new AIError.AbortError();
1269
+ }
1270
+ if (output.stopReason === "error") {
1271
+ throw new AIError.ProviderResponseError(output.errorMessage || "Provider returned an error stop reason", {
1272
+ provider: model.provider,
1273
+ kind: "runtime",
1274
+ });
1275
+ }
1276
+
1277
+ output.errorMessage = undefined;
1278
+ output.duration = performance.now() - startTime;
1279
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1280
+ stream.push({ type: "done", reason: output.stopReason, message: output });
1281
+ stream.end();
1282
+ } catch (error) {
1283
+ // Close open blocks first so consumers tracking text_/thinking_/toolcall_
1284
+ // lifecycles never see orphaned starts on the error path. Best-effort: a
1285
+ // throw here must not prevent the terminal error event below.
1286
+ try {
1287
+ finishOpenBlocksOnError();
1288
+ } catch {}
1289
+ const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
1290
+ const result = await AIError.finalize(error, {
1291
+ api: model.api,
1292
+ provider: model.provider,
1293
+ abortTracker,
1294
+ rawRequestDump,
1295
+ capturedErrorResponse,
1296
+ });
1297
+ output.stopReason = result.stopReason;
1298
+ output.errorStatus = result.status;
1299
+ output.errorId = result.id;
1300
+ output.errorMessage = result.message;
1301
+ // Some providers via OpenRouter include extra details here.
1302
+ const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
1303
+ if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
1304
+ output.duration = performance.now() - startTime;
1305
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1306
+ stream.push({ type: "error", reason: output.stopReason, error: output });
1307
+ stream.end();
1308
+ }
1309
+ })();
1310
+
1311
+ return stream;
1312
+ };
1313
+
1314
+ /**
1315
+ * Public entry: wrap the single-attempt streamer with bounded empty-completion
1316
+ * retries — flaky gateways occasionally 200 with `delta: {}` + `finish_reason:
1317
+ * "stop"` and no usage, which would otherwise stall the agent loop. Shared with
1318
+ * the Anthropic provider via `withEmptyCompletionRetry`.
1319
+ */
1320
+ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (model, context, options) =>
1321
+ withEmptyCompletionRetry(model, context, options, streamOpenAICompletionsOnce);
1322
+
1323
+ function createRequestSetup(
1324
+ model: Model<"openai-completions">,
1325
+ context: Context,
1326
+ apiKey?: string,
1327
+ extraHeaders?: Record<string, string>,
1328
+ initiatorOverride?: MessageAttribution,
1329
+ ): OpenAIRequestSetup & { baseUrl: string } {
1330
+ const apiVersion = $env.AZURE_OPENAI_API_VERSION || "2024-10-21";
1331
+ const deploymentName = parseAzureDeploymentNameMap($env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).get(model.id) ?? model.id;
1332
+ const setup = resolveOpenAIRequestSetup(model, {
1333
+ apiKey,
1334
+ extraHeaders,
1335
+ initiatorOverride,
1336
+ messages: context.messages,
1337
+ defaultBaseUrl: "https://api.openai.com/v1",
1338
+ // Provider auth/header overlay: Kimi-code hosts require shared client
1339
+ // attribution headers prepended before caller headers. Kept here (not in
1340
+ // the shared helper) because it is provider-specific request setup.
1341
+ prependHeaders: model.provider === "kimi-code" ? getKimiCommonHeaders : undefined,
1342
+ alibabaCodingPlanAuth: true,
1343
+ azureChatCompletions: { apiVersion, deploymentName },
1344
+ });
1345
+ if (!setup.baseUrl) {
1346
+ throw new AIError.ConfigurationError("OpenAI request setup did not resolve a base URL");
1347
+ }
1348
+ return setup as OpenAIRequestSetup & { baseUrl: string };
1349
+ }
1350
+
1351
+ function resolveOpenAICompatForRequest(
1352
+ model: Model<"openai-completions">,
1353
+ options: OpenAICompletionsOptions | undefined,
1354
+ ): OpenAICompatPolicy {
1355
+ return resolveOpenAICompatPolicy(model, {
1356
+ endpoint: "chat-completions",
1357
+ reasoning: options?.reasoning,
1358
+ disableReasoning: options?.disableReasoning,
1359
+ toolChoice: mapToOpenAICompletionsToolChoice(options?.toolChoice),
1360
+ });
1361
+ }
1362
+
1363
+ function dropOpenRouterKimiForcedToolReasoning(
1364
+ params: OpenAICompletionsParams,
1365
+ model: Model<"openai-completions">,
1366
+ policy: OpenAICompatPolicy,
1367
+ ): void {
1368
+ if (
1369
+ policy.reasoning.disableReason === "forced-tool-choice" &&
1370
+ policy.reasoning.disableMode === "openrouter-enabled-false" &&
1371
+ policy.compat.isOpenRouterHost &&
1372
+ isKimiModelId(model.id)
1373
+ ) {
1374
+ delete params.reasoning;
1375
+ }
1376
+ }
1377
+
1378
+ function buildParams(
1379
+ model: Model<"openai-completions">,
1380
+ context: Context,
1381
+ options: OpenAICompletionsOptions | undefined,
1382
+ toolStrictModeOverride?: ToolStrictModeOverride,
1383
+ ): { params: OpenAICompletionsParams; toolStrictMode: AppliedToolStrictMode; strictToolsApplied: boolean } {
1384
+ const initialPolicy = resolveOpenAICompatForRequest(model, options);
1385
+ const initialCompat = initialPolicy.compat as ResolvedOpenAICompat;
1386
+
1387
+ const requestModelId = resolveOpenAICompletionsModelId(model, options);
1388
+ const params: OpenAICompletionsParams = {
1389
+ model: requestModelId,
1390
+ messages: [],
1391
+ stream: true,
1392
+ };
1393
+ let toolStrictMode: AppliedToolStrictMode = "none";
1394
+ let strictToolsApplied = false;
1395
+
1396
+ if (initialCompat.supportsUsageInStreaming !== false) {
1397
+ params.stream_options = { include_usage: true };
1398
+ }
1399
+
1400
+ if (initialCompat.supportsStore) {
1401
+ params.store = false;
1402
+ }
1403
+
1404
+ if (options?.temperature !== undefined) {
1405
+ params.temperature = options.temperature;
1406
+ }
1407
+ if (options?.topP !== undefined) {
1408
+ params.top_p = options.topP;
1409
+ }
1410
+ if (options?.topK !== undefined) {
1411
+ params.top_k = options.topK;
1412
+ }
1413
+ if (options?.minP !== undefined) {
1414
+ params.min_p = options.minP;
1415
+ }
1416
+ if (options?.presencePenalty !== undefined) {
1417
+ params.presence_penalty = options.presencePenalty;
1418
+ }
1419
+ if (options?.repetitionPenalty !== undefined) {
1420
+ params.repetition_penalty = options.repetitionPenalty;
1421
+ }
1422
+ if (options?.stopSequences?.length) {
1423
+ const seqs = options.stopSequences;
1424
+ params.stop = seqs.length === 1 ? seqs[0] : seqs.slice(0, 4);
1425
+ }
1426
+ if (options?.frequencyPenalty !== undefined) {
1427
+ params.frequency_penalty = options.frequencyPenalty;
1428
+ }
1429
+ applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
1430
+
1431
+ if (context.tools?.length) {
1432
+ const builtTools = convertTools(context.tools, initialCompat, toolStrictModeOverride);
1433
+ params.tools = builtTools.tools;
1434
+ toolStrictMode = builtTools.toolStrictMode;
1435
+ strictToolsApplied = builtTools.strictToolsApplied;
1436
+ } else if (context.tools === undefined && hasToolHistory(context.messages)) {
1437
+ // Anthropic (via LiteLLM/proxy) requires the `tools` param when the conversation
1438
+ // contains tool_calls/tool_results, even when no tools are offered this turn.
1439
+ // Only inject the sentinel when the caller passed `context.tools = undefined`
1440
+ // (i.e. tools were not specified at all). An explicit `context.tools = []` means
1441
+ // the caller opted out of tools for this turn (as /btw and IRC background replies
1442
+ // do via AgentSession.runEphemeralTurn) — honour that intent and emit nothing,
1443
+ // so LiteLLM → Bedrock never sees an empty `toolConfig` block.
1444
+ params.tools = [];
1445
+ }
1446
+
1447
+ if (options?.toolChoice && initialCompat.supportsToolChoice) {
1448
+ params.tool_choice = mapToOpenAICompletionsToolChoice(options.toolChoice);
1449
+ }
1450
+ if (
1451
+ typeof params.tool_choice === "object" &&
1452
+ params.tool_choice !== null &&
1453
+ !initialCompat.supportsNamedToolChoice
1454
+ ) {
1455
+ params.tool_choice = "required";
1456
+ }
1457
+ if (isForcedToolChoice(params.tool_choice) && !initialCompat.supportsForcedToolChoice) {
1458
+ // Some thinking-required OpenAI-compatible models reject forced
1459
+ // `tool_choice` while still accepting tools with the default auto
1460
+ // selector. Keep the tool available and let the model choose it.
1461
+ params.tool_choice = "auto";
1462
+ }
1463
+
1464
+ if (params.tool_choice === "none" && (!Array.isArray(params.tools) || params.tools.length === 0)) {
1465
+ // `tool_choice: "none"` with no tools to gate is redundant and also
1466
+ // trips LiteLLM → Bedrock: the proxy serializes the directive into a
1467
+ // `toolConfig` block, and Bedrock requires `toolConfig.tools` to be
1468
+ // non-empty whenever the conversation already holds `toolUse`/`toolResult`
1469
+ // content. Drop it whenever the resolved tools list is missing or empty.
1470
+ // Side-channel turns hit this: `/btw` and IRC background replies route
1471
+ // through `AgentSession.runEphemeralTurn`, which sets `context.tools = []`
1472
+ // and `toolChoice: "none"` (see packages/coding-agent/src/session/agent-session.ts).
1473
+ delete params.tool_choice;
1474
+ }
1475
+
1476
+ const forcedToolName =
1477
+ typeof params.tool_choice === "object" && params.tool_choice !== null && "function" in params.tool_choice
1478
+ ? params.tool_choice.function.name
1479
+ : undefined;
1480
+ if (
1481
+ forcedToolName !== undefined &&
1482
+ (!Array.isArray(params.tools) ||
1483
+ !params.tools.some(tool => tool.type === "function" && tool.function.name === forcedToolName))
1484
+ ) {
1485
+ // A forced named tool_choice is only valid when the same request offers
1486
+ // that function in `tools`. Active-tool filtering normally enforces this
1487
+ // before provider dispatch; this guard keeps raw provider callers from
1488
+ // emitting a self-inconsistent OpenAI-compatible payload.
1489
+ delete params.tool_choice;
1490
+ }
1491
+
1492
+ const finalPolicy = resolveOpenAICompatPolicy(model, {
1493
+ endpoint: "chat-completions",
1494
+ reasoning: options?.reasoning,
1495
+ disableReasoning: options?.disableReasoning,
1496
+ toolChoice: params.tool_choice,
1497
+ });
1498
+ const compat = finalPolicy.compat as ResolvedOpenAICompat;
1499
+ const messages = convertMessages(model, context, compat);
1500
+ maybeAddAnthropicCacheControl(compat, messages);
1501
+ params.messages = messages;
1502
+ const outputToken = resolveOpenAIOutputTokenParam({
1503
+ field: compat.maxTokensField,
1504
+ maxTokens: options?.maxTokens,
1505
+ maxTokensExplicit: options?.maxTokensExplicit ?? options?.maxTokens !== undefined,
1506
+ modelMaxTokens: model.maxTokens,
1507
+ omitMaxOutputTokens: model.omitMaxOutputTokens ?? false,
1508
+ isOpenRouterHost: compat.isOpenRouterHost,
1509
+ alwaysSendMaxTokens: compat.alwaysSendMaxTokens,
1510
+ providerOutputClamp: resolveZaiReasoningOutputClamp(model, compat),
1511
+ });
1512
+ if (outputToken) {
1513
+ if (outputToken.field === "max_tokens") {
1514
+ params.max_tokens = outputToken.value;
1515
+ } else if (outputToken.field === "max_completion_tokens") {
1516
+ params.max_completion_tokens = outputToken.value;
1517
+ }
1518
+ }
1519
+ applyChatCompletionsToolStream(params, model, compat);
1520
+
1521
+ applyChatCompletionsCompatPolicy(params, finalPolicy);
1522
+ dropOpenRouterKimiForcedToolReasoning(params, model, finalPolicy);
1523
+
1524
+ applyOpenAIGatewayRouting(params, compat);
1525
+
1526
+ applyOpenAIExtraBody(params, compat.extraBody, {
1527
+ dropThinkingWhenReasoningEffort: compat.dropThinkingWhenReasoningEffort,
1528
+ });
1529
+
1530
+ return { params, toolStrictMode, strictToolsApplied };
1531
+ }
1532
+
1533
+ export function parseChunkUsage(
1534
+ rawUsage: object,
1535
+ model: Model<"openai-completions">,
1536
+ premiumRequests: number | undefined,
1537
+ ): AssistantMessage["usage"] {
1538
+ const usageLike = rawUsage as OpenAICompletionsUsageLike;
1539
+ const rawPromptTokenDetails = usageLike.prompt_tokens_details;
1540
+ const promptTokenDetails =
1541
+ typeof rawPromptTokenDetails === "object" && rawPromptTokenDetails !== null
1542
+ ? (rawPromptTokenDetails as OpenAICompletionsPromptTokenDetails)
1543
+ : undefined;
1544
+ const rawCompletionTokenDetails = usageLike.completion_tokens_details;
1545
+ const completionTokenDetails =
1546
+ typeof rawCompletionTokenDetails === "object" && rawCompletionTokenDetails !== null
1547
+ ? (rawCompletionTokenDetails as OpenAICompletionsCompletionTokenDetails)
1548
+ : undefined;
1549
+ const completionTokens = usageLike.completion_tokens;
1550
+ const promptTokens = usageLike.prompt_tokens;
1551
+ const cachedTokens = usageLike.cached_tokens;
1552
+ const promptCacheHitTokens = usageLike.prompt_cache_hit_tokens;
1553
+ const promptCacheMissTokens = usageLike.prompt_cache_miss_tokens;
1554
+ const promptTokenCachedTokens = promptTokenDetails?.cached_tokens;
1555
+ const completionReasoningTokens = completionTokenDetails?.reasoning_tokens;
1556
+ const cacheWriteTokens = promptTokenDetails?.cache_write_tokens;
1557
+ const outputTokens = typeof completionTokens === "number" ? completionTokens : 0;
1558
+ const accounting = calculateOpenAIUsageAccounting({
1559
+ promptTokens: typeof promptTokens === "number" ? promptTokens : 0,
1560
+ outputTokens,
1561
+ cachedTokens:
1562
+ (typeof cachedTokens === "number" ? cachedTokens : undefined) ??
1563
+ (typeof promptCacheHitTokens === "number" ? promptCacheHitTokens : undefined) ??
1564
+ (typeof promptTokenCachedTokens === "number" ? promptTokenCachedTokens : undefined) ??
1565
+ 0,
1566
+ reasoningTokens: typeof completionReasoningTokens === "number" ? completionReasoningTokens : 0,
1567
+ cacheWriteOpenRouter: typeof cacheWriteTokens === "number" ? cacheWriteTokens : undefined,
1568
+ cacheWriteDeepSeek: typeof promptCacheMissTokens === "number" ? promptCacheMissTokens : undefined,
1569
+ hasDeepSeekCacheHitAndMiss: typeof promptCacheHitTokens === "number" && typeof promptCacheMissTokens === "number",
1570
+ });
1571
+ const usage: AssistantMessage["usage"] = {
1572
+ ...accounting,
1573
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1574
+ ...(premiumRequests !== undefined ? { premiumRequests } : {}),
1575
+ };
1576
+ calculateCost(model, usage);
1577
+ return usage;
1578
+ }
1579
+
1580
+ function maybeAddAnthropicCacheControl(compat: ResolvedOpenAICompat, messages: ChatCompletionMessageParam[]): void {
1581
+ if (compat.cacheControlFormat !== "anthropic") return;
1582
+ // Anthropic-style caching requires cache_control on a text part. Add a breakpoint
1583
+ // on the last user/assistant message (walking backwards until we find text content).
1584
+ for (let i = messages.length - 1; i >= 0; i--) {
1585
+ const msg = messages[i];
1586
+ if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "developer") continue;
1587
+
1588
+ const content = msg.content;
1589
+ if (typeof content === "string") {
1590
+ if (content.trim().length === 0) continue;
1591
+ msg.content = [
1592
+ Object.assign({ type: "text" as const, text: content }, { cache_control: { type: "ephemeral" } }),
1593
+ ];
1594
+ return;
1595
+ }
1596
+
1597
+ if (!Array.isArray(content)) continue;
1598
+
1599
+ // Find last non-empty text part and add cache_control. Empty assistant
1600
+ // content is valid for tool-call replay, but Anthropic/OpenRouter reject
1601
+ // empty text blocks once cache_control turns it into structured content.
1602
+ for (let j = content.length - 1; j >= 0; j--) {
1603
+ const part = content[j];
1604
+ if (part?.type === "text" && part.text.trim().length > 0) {
1605
+ Object.assign(part, { cache_control: { type: "ephemeral" } });
1606
+ return;
1607
+ }
1608
+ }
1609
+ }
1610
+ }
1611
+
1612
+ export function convertMessages(
1613
+ model: Model<"openai-completions">,
1614
+ context: Context,
1615
+ compat: ResolvedOpenAICompat,
1616
+ ): ChatCompletionMessageParam[] {
1617
+ const params: ChatCompletionMessageParam[] = [];
1618
+
1619
+ const maxNormalizedToolCallIdLength = compat.requiresMistralToolIds
1620
+ ? 9
1621
+ : compat.usesOpenAIToolCallIdLimit
1622
+ ? 40
1623
+ : undefined;
1624
+ const duplicateToolCallIdSuffixPrefix = compat.requiresMistralToolIds ? "dup" : undefined;
1625
+ const normalizeToolCallId = (id: string): string => {
1626
+ if (compat.requiresMistralToolIds) return normalizeMistralToolId(id, true);
1627
+
1628
+ // Handle pipe-separated IDs from OpenAI Responses API
1629
+ // Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
1630
+ // These come from providers like github-copilot, openai-codex, opencode
1631
+ // Extract just the call_id part and normalize it
1632
+ if (id.includes("|")) {
1633
+ const [callId] = id.split("|");
1634
+ // Sanitize to allowed chars and truncate to 40 chars (OpenAI limit)
1635
+ return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
1636
+ }
1637
+
1638
+ if (compat.usesOpenAIToolCallIdLimit) return id.length > 40 ? id.slice(0, 40) : id;
1639
+ return id;
1640
+ };
1641
+ const transformedMessages = transformMessages(
1642
+ context.messages,
1643
+ model,
1644
+ id => normalizeToolCallId(id),
1645
+ maxNormalizedToolCallIdLength,
1646
+ duplicateToolCallIdSuffixPrefix,
1647
+ compat,
1648
+ );
1649
+
1650
+ const remappedToolCallIds = new Map<string, string[]>();
1651
+ let generatedToolCallIdCounter = 0;
1652
+
1653
+ const generateFallbackToolCallId = (seed: string): string => {
1654
+ generatedToolCallIdCounter += 1;
1655
+ const hash = Bun.hash(`${model.provider}:${model.id}:${seed}:${generatedToolCallIdCounter}`).toString(36);
1656
+ return `call_${hash}`;
1657
+ };
1658
+
1659
+ const rememberToolCallId = (originalId: string, normalizedId: string): void => {
1660
+ const queue = remappedToolCallIds.get(originalId);
1661
+ if (queue) {
1662
+ queue.push(normalizedId);
1663
+ return;
1664
+ }
1665
+ remappedToolCallIds.set(originalId, [normalizedId]);
1666
+ };
1667
+
1668
+ const consumeToolCallId = (originalId: string): string | null => {
1669
+ const queue = remappedToolCallIds.get(originalId);
1670
+ if (!queue || queue.length === 0) return null;
1671
+ const nextId = queue.shift() ?? null;
1672
+ if (queue.length === 0) remappedToolCallIds.delete(originalId);
1673
+ return nextId;
1674
+ };
1675
+
1676
+ const ensureToolCallId = (rawId: string, seed: string): string => {
1677
+ const normalized = normalizeToolCallId(rawId);
1678
+ if (normalized.trim().length > 0) return normalized;
1679
+ return generateFallbackToolCallId(seed);
1680
+ };
1681
+
1682
+ const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
1683
+ if (systemPrompts.length > 0) {
1684
+ const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
1685
+ const role = useDeveloperRole ? "developer" : "system";
1686
+ // Default to one block per ordered system prompt so the leading prefix
1687
+ // stays byte-identical between turns and the provider's KV cache can
1688
+ // reuse it. Hosts whose chat templates reject follow-up system messages
1689
+ // (Qwen via vLLM, MiniMax, Alibaba Dashscope, Qwen Portal, …) opt out
1690
+ // via `compat.supportsMultipleSystemMessages = false`; in that mode we
1691
+ // coalesce into a single message joined by `\n\n`.
1692
+ if (compat.supportsMultipleSystemMessages) {
1693
+ for (const systemPrompt of systemPrompts) {
1694
+ params.push({ role, content: systemPrompt });
1695
+ }
1696
+ } else {
1697
+ params.push({ role, content: systemPrompts.join("\n\n") });
1698
+ }
1699
+ }
1700
+
1701
+ let lastRole: string | null = null;
1702
+
1703
+ for (let i = 0; i < transformedMessages.length; i++) {
1704
+ const msg = transformedMessages[i];
1705
+ // Some providers (e.g. Mistral/Devstral) don't allow user messages directly after tool results
1706
+ // Insert a synthetic assistant message to bridge the gap
1707
+ if (
1708
+ compat.requiresAssistantAfterToolResult &&
1709
+ lastRole === "toolResult" &&
1710
+ (msg.role === "user" || msg.role === "developer")
1711
+ ) {
1712
+ params.push({
1713
+ role: "assistant",
1714
+ content: "I have processed the tool results.",
1715
+ });
1716
+ }
1717
+
1718
+ const devAsUser = !compat.supportsDeveloperRole;
1719
+ if (msg.role === "user" || msg.role === "developer") {
1720
+ const role = !devAsUser && msg.role === "developer" ? "developer" : "user";
1721
+ if (typeof msg.content === "string") {
1722
+ const text = msg.content.toWellFormed();
1723
+ if (text.trim().length === 0) continue;
1724
+ params.push({
1725
+ role: role,
1726
+ content: text,
1727
+ });
1728
+ } else {
1729
+ const supportsImages = model.input.includes("image") && !isDashscopeCompatibleModeTextOnlyQwen(model);
1730
+ const content: ChatCompletionContentPart[] = [];
1731
+ let omittedImages = false;
1732
+ for (const item of msg.content) {
1733
+ if (item.type === "text") {
1734
+ const text = item.text.toWellFormed();
1735
+ if (text.trim().length === 0) continue;
1736
+ content.push({
1737
+ type: "text",
1738
+ text,
1739
+ } satisfies ChatCompletionContentPartText);
1740
+ } else if (supportsImages) {
1741
+ content.push({
1742
+ type: "image_url",
1743
+ image_url: {
1744
+ url: `data:${item.mimeType};base64,${item.data}`,
1745
+ // Chat Completions has no "original"; omit it (provider default).
1746
+ ...(item.detail && item.detail !== "original" ? { detail: item.detail } : {}),
1747
+ },
1748
+ } satisfies ChatCompletionContentPartImage);
1749
+ } else {
1750
+ omittedImages = true;
1751
+ }
1752
+ }
1753
+ if (omittedImages) {
1754
+ content.push({
1755
+ type: "text",
1756
+ text: NON_VISION_IMAGE_PLACEHOLDER,
1757
+ } satisfies ChatCompletionContentPartText);
1758
+ }
1759
+ if (content.length === 0) continue;
1760
+ params.push({
1761
+ role: "user",
1762
+ content,
1763
+ });
1764
+ }
1765
+ } else if (msg.role === "assistant") {
1766
+ const assistantMsg: OpenAICompletionsAssistantMessageParam = {
1767
+ role: "assistant",
1768
+ content: null,
1769
+ };
1770
+
1771
+ const textBlocks = msg.content.filter(b => b.type === "text") as TextContent[];
1772
+ // Filter out empty text blocks to avoid API validation errors
1773
+ const nonEmptyTextBlocks = textBlocks.filter(b => b.text && b.text.trim().length > 0);
1774
+ if (nonEmptyTextBlocks.length > 0) {
1775
+ // Always send assistant content as a plain string. Some OpenAI-compatible
1776
+ // backends mirror array-of-text-block payloads back to the model literally,
1777
+ // causing recursive nested content in subsequent turns.
1778
+ assistantMsg.content = nonEmptyTextBlocks.map(b => b.text.toWellFormed()).join("");
1779
+ }
1780
+
1781
+ // Handle thinking blocks
1782
+ const thinkingBlocks = msg.content.filter(b => b.type === "thinking") as ThinkingContent[];
1783
+ // Filter out empty thinking blocks to avoid API validation errors
1784
+ const nonEmptyThinkingBlocks = thinkingBlocks.filter(b => b.thinking && b.thinking.trim().length > 0);
1785
+ if (nonEmptyThinkingBlocks.length > 0) {
1786
+ if (compat.requiresThinkingAsText) {
1787
+ const thinkingText = nonEmptyThinkingBlocks
1788
+ .map(b => renderDemotedThinking(model.id, b.thinking))
1789
+ .join("");
1790
+ // `content` is a plain string at this point (set above) or null —
1791
+ // never an array. Prepend the demoted thinking to the string form.
1792
+ assistantMsg.content =
1793
+ typeof assistantMsg.content === "string" && assistantMsg.content.length > 0
1794
+ ? `${thinkingText}${assistantMsg.content}`
1795
+ : thinkingText;
1796
+ } else if (compat.requiresReasoningContentForToolCalls) {
1797
+ // Use the streamed signature when the backend accepts whichever
1798
+ // recognized field name was emitted (allowsSynthetic=true). Backends
1799
+ // like opencode-kimi-with-thinking and DeepSeek demand the exact
1800
+ // configured `reasoningContentField` instead, so honor that here
1801
+ // rather than echoing the upstream field name.
1802
+ const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
1803
+ const wireField =
1804
+ compat.allowsSyntheticReasoningContentForToolCalls &&
1805
+ (signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text")
1806
+ ? signature
1807
+ : signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text"
1808
+ ? (compat.reasoningContentField ?? "reasoning_content")
1809
+ : undefined;
1810
+ if (wireField) {
1811
+ assistantMsg[wireField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
1812
+ }
1813
+ } else if (compat.thinkingFormat === "zai" && model.reasoning) {
1814
+ // Z.AI / Zhipu / Moonshot Kimi (native) / Xiaomi MiMo accept
1815
+ // `reasoning_content` as a continuation hint even when they don't
1816
+ // strictly require it. Surfacing the preserved thinking text here
1817
+ // keeps cross-API replays (Z.AI Anthropic → Z.AI OpenAI, etc.)
1818
+ // shipping reasoning as structured `reasoning_content` rather than
1819
+ // folded into conversation text (#3434). Signature is irrelevant on
1820
+ // this path: `transform-messages` strips the source wire-format
1821
+ // signature on cross-API replays before the block reaches us.
1822
+ const reasoningField = compat.reasoningContentField ?? "reasoning_content";
1823
+ assistantMsg[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
1824
+ } else if (compat.replayReasoningContent) {
1825
+ // Local llama.cpp-style servers (llama.cpp, LM Studio, vLLM, Ollama
1826
+ // in openai-completions mode, custom providers pointed at a
1827
+ // loopback baseUrl) re-tokenize the entire prompt every request.
1828
+ // Qwen3 / DeepSeek-R1 / GLM chat templates reconstruct the prior
1829
+ // assistant turn's `<think>` block from `reasoning_content`; if we
1830
+ // drop the field the template re-renders the assistant turn
1831
+ // without thinking content, the rendered tokens diverge from the
1832
+ // slot's existing KV cache, and llama.cpp falls back to full
1833
+ // prompt re-processing (#3528). Honor the streamed signature when
1834
+ // it identifies a recognized wire field so a model that emitted
1835
+ // `reasoning` (some llama.cpp builds) round-trips to the same
1836
+ // field; otherwise fall back to the configured
1837
+ // `reasoningContentField`. Gated by the new compat flag rather
1838
+ // than the existing `requires*` flags because local servers
1839
+ // accept but don't validate the field — they just need it to
1840
+ // preserve cache locality.
1841
+ const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
1842
+ const reasoningField: OpenAICompletionsReasoningField =
1843
+ signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text"
1844
+ ? signature
1845
+ : (compat.reasoningContentField ?? "reasoning_content");
1846
+ assistantMsg[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
1847
+ }
1848
+ }
1849
+
1850
+ if (compat.requiresReasoningContentForToolCalls) {
1851
+ const streamedReasoningField = nonEmptyThinkingBlocks[0]?.thinkingSignature;
1852
+ const reasoningField =
1853
+ compat.allowsSyntheticReasoningContentForToolCalls &&
1854
+ (streamedReasoningField === "reasoning_content" ||
1855
+ streamedReasoningField === "reasoning" ||
1856
+ streamedReasoningField === "reasoning_text")
1857
+ ? streamedReasoningField
1858
+ : (compat.reasoningContentField ?? "reasoning_content");
1859
+ const reasoningContent = assistantMsg[reasoningField];
1860
+ if (!reasoningContent) {
1861
+ const reasoning = assistantMsg.reasoning;
1862
+ const reasoningText = assistantMsg.reasoning_text;
1863
+ if (reasoning && reasoningField !== "reasoning") {
1864
+ assistantMsg[reasoningField] = reasoning;
1865
+ } else if (reasoningText && reasoningField !== "reasoning_text") {
1866
+ assistantMsg[reasoningField] = reasoningText;
1867
+ } else if (nonEmptyThinkingBlocks.length > 0) {
1868
+ assistantMsg[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
1869
+ }
1870
+ }
1871
+ }
1872
+
1873
+ const toolCalls = msg.content.filter(b => b.type === "toolCall") as ToolCall[];
1874
+ // Replay reasoning_content on assistant turns for backends that validate
1875
+ // thinking-mode history. DeepSeek V4 requires reasoning_content on EVERY
1876
+ // assistant turn once a prior turn included it — not just tool-call turns.
1877
+ // The replay logic has three tiers:
1878
+ // 1. Recover from thinking blocks with valid signatures (covers same-model replay
1879
+ // where nonEmptyThinkingBlocks may have filtered out empty-text blocks)
1880
+ // 2. For providers that require the field but returned no reasoning at all
1881
+ // (e.g. proxy-stripped reasoning_content), emit an empty string
1882
+ // 3. For providers that accept synthetic placeholders (Kimi, OpenRouter), emit "."
1883
+ // DeepSeek V4 rejects synthetic "." placeholders — it validates the exact value —
1884
+ // so the allowsSyntheticReasoningContentForToolCalls flag controls tier 3.
1885
+ const canUseSyntheticReasoningContent =
1886
+ compat.requiresReasoningContentForToolCalls &&
1887
+ compat.allowsSyntheticReasoningContentForToolCalls &&
1888
+ (compat.thinkingFormat === "openai" ||
1889
+ compat.thinkingFormat === "openrouter" ||
1890
+ compat.thinkingFormat === "zai");
1891
+ // DeepSeek-compatible reasoning models require reasoning_content on all
1892
+ // assistant turns. Providers that allow placeholders only need it on
1893
+ // tool-call turns.
1894
+ const needsReasoningOnAllTurns = compat.requiresReasoningContentForAllAssistantTurns;
1895
+ const needsReasoningField = needsReasoningOnAllTurns || toolCalls.length > 0;
1896
+ let hasReasoningField =
1897
+ assistantMsg.reasoning_content !== undefined ||
1898
+ assistantMsg.reasoning !== undefined ||
1899
+ assistantMsg.reasoning_text !== undefined;
1900
+ // Tier 1: Recover reasoning_content from ALL thinking blocks (including empty-text
1901
+ // ones) when the provider requires exact replay and rejects synthetic placeholders.
1902
+ // This covers the case where thinking blocks have valid signatures but were excluded
1903
+ // by the nonEmptyThinkingBlocks filter above, or where thinking text is empty but
1904
+ // the signature identifies the correct field name for replay.
1905
+ // Only recognized OpenAI-compat reasoning field names qualify — opaque signatures
1906
+ // from other providers (Anthropic encrypted, OpenAI Responses JSON, etc.) are not
1907
+ // valid property names for the wire message.
1908
+ if (
1909
+ needsReasoningField &&
1910
+ !hasReasoningField &&
1911
+ compat.requiresReasoningContentForToolCalls &&
1912
+ !compat.allowsSyntheticReasoningContentForToolCalls
1913
+ ) {
1914
+ const allThinkingBlocks = msg.content.filter(b => b.type === "thinking") as ThinkingContent[];
1915
+ if (allThinkingBlocks.length > 0) {
1916
+ const signature = allThinkingBlocks[0].thinkingSignature;
1917
+ if (signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text") {
1918
+ const reasoningField = compat.reasoningContentField ?? "reasoning_content";
1919
+ assistantMsg[reasoningField] = allThinkingBlocks.map(b => b.thinking).join("\n");
1920
+ hasReasoningField = true;
1921
+ }
1922
+ }
1923
+ }
1924
+ // Tier 2: When the provider requires reasoning_content but there are genuinely no
1925
+ // thinking blocks at all (e.g. proxy stripped reasoning_content from the response),
1926
+ // emit an empty string. The field must be present; an empty string is the most honest
1927
+ // representation of "no reasoning was captured."
1928
+ if (
1929
+ needsReasoningField &&
1930
+ !hasReasoningField &&
1931
+ compat.requiresReasoningContentForToolCalls &&
1932
+ !compat.allowsSyntheticReasoningContentForToolCalls
1933
+ ) {
1934
+ const reasoningField = compat.reasoningContentField ?? "reasoning_content";
1935
+ assistantMsg[reasoningField] = "";
1936
+ hasReasoningField = true;
1937
+ }
1938
+ // Tier 3: For providers that accept synthetic placeholders (Kimi, OpenRouter).
1939
+ if (toolCalls.length > 0 && canUseSyntheticReasoningContent && !hasReasoningField) {
1940
+ const reasoningField = compat.reasoningContentField ?? "reasoning_content";
1941
+ assistantMsg[reasoningField] = ".";
1942
+ hasReasoningField = true;
1943
+ }
1944
+ if (toolCalls.length > 0) {
1945
+ assistantMsg.tool_calls = toolCalls.map((tc, toolCallIndex) => {
1946
+ const toolCallId = ensureToolCallId(tc.id, `${i}:${toolCallIndex}:${tc.name}`);
1947
+ rememberToolCallId(tc.id, toolCallId);
1948
+ return {
1949
+ id: normalizeMistralToolId(toolCallId, compat.requiresMistralToolIds),
1950
+ type: "function" as const,
1951
+ function: {
1952
+ name: tc.name,
1953
+ arguments: serializeToolArguments(tc.arguments),
1954
+ },
1955
+ };
1956
+ });
1957
+ const reasoningDetails = toolCalls
1958
+ .filter(tc => tc.thoughtSignature)
1959
+ .map(tc => {
1960
+ try {
1961
+ const parsed: unknown = JSON.parse(tc.thoughtSignature!);
1962
+ return parsed;
1963
+ } catch {
1964
+ return null;
1965
+ }
1966
+ })
1967
+ .filter(Boolean);
1968
+ if (reasoningDetails.length > 0) {
1969
+ assistantMsg.reasoning_details = reasoningDetails;
1970
+ }
1971
+ }
1972
+ // Some OpenAI-compatible backends concatenate assistant content as a
1973
+ // string even for tool-call replay. OpenAI accepts an empty string here;
1974
+ // null trips strict/proxy implementations before the tool result is read.
1975
+ if (assistantMsg.content === null && (hasReasoningField || assistantMsg.tool_calls)) {
1976
+ assistantMsg.content = "";
1977
+ }
1978
+ // Skip assistant messages that have no content, no tool calls, and no reasoning payload.
1979
+ // Some OpenAI-compatible backends require replaying reasoning-only assistant turns
1980
+ // so follow-up requests preserve the provider-specific reasoning field name.
1981
+ const content = assistantMsg.content;
1982
+ const hasContent =
1983
+ content !== null &&
1984
+ content !== undefined &&
1985
+ (typeof content === "string" ? content.length > 0 : content.length > 0);
1986
+ if (!hasContent && assistantMsg.tool_calls && compat.requiresAssistantContentForToolCalls) {
1987
+ assistantMsg.content = ".";
1988
+ }
1989
+ if (!hasContent && !assistantMsg.tool_calls && !hasReasoningField) {
1990
+ continue;
1991
+ }
1992
+ params.push(assistantMsg);
1993
+ } else if (msg.role === "toolResult") {
1994
+ // Batch consecutive tool results and collect all images
1995
+ const imageBlocks: Array<{ type: "image_url"; image_url: { url: string } }> = [];
1996
+ let j = i;
1997
+
1998
+ for (; j < transformedMessages.length && transformedMessages[j].role === "toolResult"; j++) {
1999
+ const toolMsg = transformedMessages[j] as ToolResultMessage;
2000
+
2001
+ // Extract text and image content
2002
+ const textResult = toolMsg.content
2003
+ .filter(c => c.type === "text")
2004
+ .map(c => (c as TextContent).text)
2005
+ .join("\n");
2006
+ const supportsImages = model.input.includes("image") && !isDashscopeCompatibleModeTextOnlyQwen(model);
2007
+ const hasImages = toolMsg.content.some(c => c.type === "image");
2008
+ const omittedImages = hasImages && !supportsImages;
2009
+
2010
+ // Always send tool result with text (or placeholder if only images)
2011
+ const hasText = textResult.length > 0;
2012
+ const remappedToolCallId = consumeToolCallId(toolMsg.toolCallId);
2013
+ const resolvedToolCallId =
2014
+ remappedToolCallId ?? ensureToolCallId(toolMsg.toolCallId, `${j}:${toolMsg.toolName ?? "tool"}`);
2015
+ const toolResultContent = omittedImages
2016
+ ? joinTextWithImagePlaceholder(textResult, true)
2017
+ : hasText
2018
+ ? textResult
2019
+ : hasImages
2020
+ ? "(see attached image)"
2021
+ : "";
2022
+ const toolResultMsg: OpenAICompletionsToolMessageParam = {
2023
+ role: "tool",
2024
+ content: toolResultContent.toWellFormed(),
2025
+ tool_call_id: normalizeMistralToolId(resolvedToolCallId, compat.requiresMistralToolIds),
2026
+ };
2027
+ if (compat.requiresToolResultName && toolMsg.toolName) {
2028
+ toolResultMsg.name = toolMsg.toolName;
2029
+ }
2030
+ params.push(toolResultMsg);
2031
+
2032
+ if (hasImages && supportsImages) {
2033
+ for (const block of toolMsg.content) {
2034
+ if (block.type === "image") {
2035
+ imageBlocks.push({
2036
+ type: "image_url",
2037
+ image_url: {
2038
+ url: `data:${block.mimeType};base64,${block.data}`,
2039
+ },
2040
+ });
2041
+ }
2042
+ }
2043
+ }
2044
+ }
2045
+
2046
+ i = j - 1;
2047
+
2048
+ // After all consecutive tool results, add a single user message with all images
2049
+ if (imageBlocks.length > 0) {
2050
+ if (compat.requiresAssistantAfterToolResult) {
2051
+ params.push({
2052
+ role: "assistant",
2053
+ content: "I have processed the tool results.",
2054
+ });
2055
+ }
2056
+
2057
+ params.push({
2058
+ role: "user",
2059
+ content: [
2060
+ {
2061
+ type: "text",
2062
+ text: "Attached image(s) from tool result:",
2063
+ },
2064
+ ...imageBlocks,
2065
+ ],
2066
+ });
2067
+ lastRole = "user";
2068
+ } else {
2069
+ lastRole = "toolResult";
2070
+ }
2071
+ continue;
2072
+ }
2073
+
2074
+ lastRole =
2075
+ msg.role === "developer"
2076
+ ? model.reasoning && compat.supportsDeveloperRole
2077
+ ? "developer"
2078
+ : "system"
2079
+ : msg.role;
2080
+ }
2081
+
2082
+ return params;
2083
+ }
2084
+
2085
+ function convertTools(
2086
+ tools: Tool[],
2087
+ compat: ResolvedOpenAICompat,
2088
+ toolStrictModeOverride?: ToolStrictModeOverride,
2089
+ ): BuiltOpenAICompletionTools {
2090
+ const adaptedTools = tools.map(tool => {
2091
+ const strict = !NO_STRICT && compat.supportsStrictMode !== false && tool.strict !== false;
2092
+ const baseParameters = toolWireSchema(tool);
2093
+ const adapted = adaptSchemaForStrict(baseParameters, strict);
2094
+ return {
2095
+ tool,
2096
+ baseParameters,
2097
+ parameters: adapted.schema,
2098
+ strict: adapted.strict,
2099
+ };
2100
+ });
2101
+
2102
+ const requestedStrictMode = toolStrictModeOverride ?? compat.toolStrictMode;
2103
+ const toolStrictMode =
2104
+ requestedStrictMode === "none"
2105
+ ? "none"
2106
+ : requestedStrictMode === "all_strict"
2107
+ ? adaptedTools.every(tool => tool.strict)
2108
+ ? "all_strict"
2109
+ : "none"
2110
+ : "mixed";
2111
+
2112
+ return {
2113
+ tools: adaptedTools.map(({ tool, baseParameters, parameters, strict }) => {
2114
+ const includeStrict = toolStrictMode === "all_strict" || (toolStrictMode === "mixed" && strict);
2115
+ const wireParameters = includeStrict ? parameters : baseParameters;
2116
+ return {
2117
+ type: "function",
2118
+ function: {
2119
+ name: tool.name,
2120
+ description: tool.description || "",
2121
+ // Moonshot/Kimi native hosts validate against the stricter MFJS subset
2122
+ // (const→enum, typed enums, no validators) and 400 otherwise.
2123
+ parameters:
2124
+ compat.toolSchemaFlavor === "moonshot-mfjs"
2125
+ ? (normalizeSchemaForMoonshot(wireParameters) as Record<string, unknown>)
2126
+ : wireParameters,
2127
+ // Only include strict if provider supports it. Some reject unknown fields.
2128
+ ...(includeStrict && { strict: true }),
2129
+ },
2130
+ };
2131
+ }),
2132
+ toolStrictMode,
2133
+ strictToolsApplied:
2134
+ tools.length > 0 &&
2135
+ (toolStrictMode === "all_strict" || (toolStrictMode === "mixed" && adaptedTools.some(tool => tool.strict))),
2136
+ };
2137
+ }
2138
+
2139
+ const EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE =
2140
+ "Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.";
2141
+
2142
+ function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | string): {
2143
+ stopReason: StopReason;
2144
+ errorMessage?: string;
2145
+ } {
2146
+ if (reason === null) return { stopReason: "stop" };
2147
+ switch (reason) {
2148
+ case "stop":
2149
+ case "end":
2150
+ return { stopReason: "stop" };
2151
+ case "length":
2152
+ return { stopReason: "length" };
2153
+ case "function_call":
2154
+ case "tool_calls":
2155
+ return { stopReason: "toolUse" };
2156
+ case "content_filter":
2157
+ return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
2158
+ case "network_error":
2159
+ return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
2160
+ case "error":
2161
+ // Gateways (OpenRouter, Vercel AI Gateway, …) report upstream model
2162
+ // failures as a bare `finish_reason: "error"` with no detail. These are
2163
+ // almost always transient (e.g. Gemini MALFORMED_FUNCTION_CALL), so word
2164
+ // the message to match the session retry classifier's transient-transport
2165
+ // pattern (`provider.?returned.?error`) and get the turn auto-retried.
2166
+ return { stopReason: "error", errorMessage: "Provider returned error finish_reason" };
2167
+ default:
2168
+ return {
2169
+ stopReason: "error",
2170
+ errorMessage: `Provider finish_reason: ${reason}`,
2171
+ };
2172
+ }
2173
+ }