@linxiraos/pi-ai 1.0.0

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