@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,3500 @@
1
+ import type { Effort } from "@linxiraos/pi-catalog/effort";
2
+ import { toFirepassWireModelId, toFireworksWireModelId } from "@linxiraos/pi-catalog/fireworks-model-id";
3
+ import { isGlm52ReasoningEffortModelId, isKimiK3ModelId } from "@linxiraos/pi-catalog/identity";
4
+ import { getSupportedEfforts } from "@linxiraos/pi-catalog/model-thinking";
5
+ import { calculateCost } from "@linxiraos/pi-catalog/models";
6
+ import type {
7
+ OpenAICompat,
8
+ OpenAIReasoningDisableMode,
9
+ OpenAIStreamMarkupHealingPattern,
10
+ OpenRouterRouting,
11
+ ResolvedOpenAICompat,
12
+ ResolvedOpenAIResponsesCompat,
13
+ ResolvedOpenAISharedCompat,
14
+ VercelGatewayRouting,
15
+ } from "@linxiraos/pi-catalog/types";
16
+ import { parseAlibabaTokenPlanCredential } from "@linxiraos/pi-catalog/wire/alibaba-token-plan";
17
+ import {
18
+ COREWEAVE_PROJECT_HEADER,
19
+ coreWeaveProjectHeaders,
20
+ hasCoreWeaveProjectHeader,
21
+ removeBlankCoreWeaveProjectHeaders,
22
+ } from "@linxiraos/pi-catalog/wire/coreweave";
23
+ import { parseGitHubCopilotApiKey } from "@linxiraos/pi-catalog/wire/github-copilot";
24
+ import {
25
+ $env,
26
+ classifyJsonPrefix,
27
+ extractHttpStatusFromError,
28
+ logger,
29
+ parseImageMetadata,
30
+ parseStreamingJson,
31
+ parseStreamingJsonThrottled,
32
+ stringifyJson,
33
+ structuredCloneJSON,
34
+ } from "@linxiraos/pi-utils";
35
+ import * as AIError from "../error";
36
+ import {
37
+ type Api,
38
+ type AssistantMessage,
39
+ type CacheRetention,
40
+ type ComputerAction,
41
+ type ComputerToolCallMetadata,
42
+ type Context,
43
+ type ImageContent,
44
+ type Message,
45
+ type MessageAttribution,
46
+ type Model,
47
+ OPENAI_MAX_OUTPUT_TOKENS,
48
+ type ServiceTier,
49
+ type StopReason,
50
+ type StreamOptions,
51
+ shouldSendServiceTier,
52
+ type TextContent,
53
+ type TextSignatureV1,
54
+ type ThinkingContent,
55
+ type Tool,
56
+ type ToolCall,
57
+ type ToolResultMessage,
58
+ type Usage,
59
+ } from "../types";
60
+
61
+ export type { OpenAIPromptCacheOptions } from "../types";
62
+
63
+ import {
64
+ getOpenAIResponsesHistoryItems,
65
+ getOpenAIResponsesHistoryPayload,
66
+ normalizeResponsesToolCallId,
67
+ normalizeSystemPrompts,
68
+ resolveCacheRetention,
69
+ sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
70
+ sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
71
+ sanitizeOpenAIResponsesHistoryItemsForReplay,
72
+ stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay,
73
+ } from "../utils";
74
+ import {
75
+ clearStreamingPartialJson,
76
+ kStreamingArgumentsDone,
77
+ kStreamingLastParseLen,
78
+ kStreamingPartialJson,
79
+ } from "../utils/block-symbols";
80
+ import type { AssistantMessageEventStream } from "../utils/event-stream";
81
+ import {
82
+ escapeHarmonyControlTokens,
83
+ escapeHarmonyControlTokensInJson,
84
+ isHarmonyDialectModel,
85
+ } from "../utils/harmony-leak";
86
+ import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
87
+ import { getOpenRouterHeaders } from "../utils/openrouter-headers";
88
+ import { isForcedToolChoice } from "../utils/tool-choice";
89
+ import {
90
+ buildCopilotDynamicHeaders,
91
+ hasCopilotVisionInput,
92
+ resolveGitHubCopilotBaseUrl,
93
+ } from "./github-copilot-headers";
94
+ import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
95
+ import type { InputItem } from "./openai-codex/request-transformer";
96
+ import type {
97
+ Response as OpenAIResponse,
98
+ ResponseComputerToolCall,
99
+ ResponseContentPartAddedEvent,
100
+ ResponseCreateParamsStreaming,
101
+ ResponseCustomToolCall,
102
+ ResponseFunctionToolCall,
103
+ ResponseInput,
104
+ ResponseInputContent,
105
+ ResponseInputImage,
106
+ ResponseInputItem,
107
+ ResponseInputText,
108
+ ResponseOutputItem,
109
+ ResponseOutputMessage,
110
+ ResponseReasoningItem,
111
+ ResponseStatus,
112
+ ResponseStreamEvent,
113
+ } from "./openai-responses-wire";
114
+ import { transformMessages } from "./transform-messages";
115
+ import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
116
+
117
+ /**
118
+ * Keyless-provider sentinel. Custom providers configured with `auth: none`
119
+ * (models.yml) have no credential, so the coding-agent resolves their API key
120
+ * to this literal instead of a real secret. Providers must treat it as "no
121
+ * credential" and suppress any credential-bearing header (e.g. `Authorization:
122
+ * Bearer …`) rather than forwarding the sentinel on the wire. See #6188; the
123
+ * google-vertex and amazon-bedrock transports apply the same guard inline.
124
+ */
125
+ export const NO_AUTH_SENTINEL = "N/A";
126
+
127
+ export interface OpenAIModelIdentity {
128
+ provider: string;
129
+ id: string;
130
+ baseUrl?: string;
131
+ }
132
+
133
+ export interface OpenAIStrictToolsScope {
134
+ provider: string;
135
+ baseUrl: string | undefined;
136
+ modelId: string;
137
+ }
138
+
139
+ export interface OpenAIStrictToolsState {
140
+ strictTools: {
141
+ disabledModelScopes: Set<string>;
142
+ };
143
+ }
144
+
145
+ export interface OpenAIRequestSetupModel extends OpenAIModelIdentity {
146
+ headers?: Record<string, string>;
147
+ premiumMultiplier?: number;
148
+ compat?: Pick<ResolvedOpenAISharedCompat, "promptCacheSessionHeader">;
149
+ }
150
+
151
+ /** Cache identity controls shared by OpenAI-family transports. */
152
+ export interface OpenAICacheOptions {
153
+ cacheRetention?: CacheRetention;
154
+ sessionId?: string;
155
+ promptCacheKey?: string;
156
+ }
157
+
158
+ export interface OpenAIRequestSetupOptions {
159
+ apiKey?: string;
160
+ extraHeaders?: Record<string, string>;
161
+ initiatorOverride?: MessageAttribution;
162
+ messages: Message[];
163
+ defaultBaseUrl?: string;
164
+ prependHeaders?: () => Record<string, string>;
165
+ alibabaCodingPlanAuth?: boolean;
166
+ azureChatCompletions?: {
167
+ apiVersion: string;
168
+ deploymentName: string;
169
+ };
170
+ openAISessionId?: string;
171
+ promptCacheSessionId?: string;
172
+ }
173
+
174
+ export interface OpenAIRequestSetup {
175
+ copilotPremiumRequests: number | undefined;
176
+ baseUrl: string | undefined;
177
+ headers: Record<string, string>;
178
+ query: Record<string, string> | undefined;
179
+ requestHeaders: Record<string, string>;
180
+ }
181
+
182
+ function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
183
+ const value = baseUrl?.trim();
184
+ if (!value) return undefined;
185
+ const normalized = value.replace(/\/+$/, "");
186
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
187
+ }
188
+
189
+ function resolveSakanaRequestBaseUrl(): string | undefined {
190
+ return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
191
+ }
192
+
193
+ function applyCoreWeaveProjectHeader(headers: Record<string, string>): void {
194
+ removeBlankCoreWeaveProjectHeaders(headers);
195
+ if (hasCoreWeaveProjectHeader(headers)) {
196
+ return;
197
+ }
198
+ const projectHeaders = coreWeaveProjectHeaders($env);
199
+ if (projectHeaders) {
200
+ headers[COREWEAVE_PROJECT_HEADER] = projectHeaders[COREWEAVE_PROJECT_HEADER];
201
+ }
202
+ }
203
+
204
+ function setHeaderIfAbsent(headers: Record<string, string>, name: string, value: string): void {
205
+ const normalizedName = name.toLowerCase();
206
+ for (const existingName in headers) {
207
+ if (existingName.toLowerCase() === normalizedName) return;
208
+ }
209
+ headers[name] = value;
210
+ }
211
+
212
+ export function resolveOpenAIRequestSetup(
213
+ model: OpenAIRequestSetupModel,
214
+ options: OpenAIRequestSetupOptions,
215
+ ): OpenAIRequestSetup {
216
+ let apiKey = options.apiKey;
217
+ if (!apiKey) {
218
+ if (!$env.OPENAI_API_KEY) {
219
+ throw new AIError.MissingApiKeyError(
220
+ undefined,
221
+ "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.",
222
+ );
223
+ }
224
+ apiKey = $env.OPENAI_API_KEY;
225
+ }
226
+ const rawApiKey = apiKey;
227
+ let headers = { ...(model.headers ?? {}) };
228
+ if (model.provider === "openrouter") {
229
+ Object.assign(headers, getOpenRouterHeaders());
230
+ }
231
+ Object.assign(headers, options.extraHeaders);
232
+ if (model.provider === "coreweave") {
233
+ applyCoreWeaveProjectHeader(headers);
234
+ }
235
+ if (options.prependHeaders) {
236
+ headers = { ...options.prependHeaders(), ...headers };
237
+ }
238
+
239
+ let copilotPremiumRequests: number | undefined;
240
+ let baseUrl = model.baseUrl;
241
+ if (model.provider === "moonshot") {
242
+ // Bundled `moonshot` catalog models hardcode the international endpoint
243
+ // (`api.moonshot.ai`). MOONSHOT_BASE_URL lets users redirect the provider
244
+ // at the China platform (`api.moonshot.cn`), which only accepts China keys
245
+ // and rejects the international host. (#2883)
246
+ const moonshotBaseUrl = $env.MOONSHOT_BASE_URL?.trim();
247
+ if (moonshotBaseUrl) {
248
+ baseUrl = moonshotBaseUrl;
249
+ }
250
+ }
251
+ if (model.provider === "sakana") {
252
+ const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
253
+ if (sakanaBaseUrl) {
254
+ baseUrl = sakanaBaseUrl;
255
+ }
256
+ }
257
+ if (model.provider === "github-copilot") {
258
+ apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
259
+ const copilot = buildCopilotDynamicHeaders({
260
+ messages: options.messages,
261
+ hasImages: hasCopilotVisionInput(options.messages),
262
+ premiumMultiplier: model.premiumMultiplier,
263
+ headers,
264
+ initiatorOverride: options.initiatorOverride,
265
+ });
266
+ Object.assign(headers, copilot.headers);
267
+ copilotPremiumRequests = copilot.premiumRequests;
268
+ baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
269
+ }
270
+
271
+ if (model.provider === "alibaba-token-plan") {
272
+ // Require an explicitly resolved Token Plan credential. The generic
273
+ // `$env.OPENAI_API_KEY` fallback above matches the broad `sk-*` token
274
+ // grammar and would otherwise be sent to QwenCloud as bearer material.
275
+ if (!options.apiKey) {
276
+ throw new AIError.MissingApiKeyError("alibaba-token-plan");
277
+ }
278
+ const credential = parseAlibabaTokenPlanCredential(rawApiKey);
279
+ if (!credential) throw new AIError.ConfigurationError("Invalid QwenCloud Token Plan credential");
280
+ apiKey = credential.token;
281
+ if (credential.baseUrl) baseUrl = credential.baseUrl;
282
+ }
283
+
284
+ if (options.alibabaCodingPlanAuth && model.provider === "alibaba-coding-plan") {
285
+ try {
286
+ const parsed = JSON.parse(rawApiKey);
287
+ if (typeof parsed?.token === "string") {
288
+ apiKey = parsed.token;
289
+ }
290
+ if (typeof parsed?.enterpriseUrl === "string") {
291
+ baseUrl = parsed.enterpriseUrl;
292
+ }
293
+ } catch {
294
+ // Not JSON — use raw apiKey and catalog baseUrl.
295
+ }
296
+ }
297
+
298
+ let query: Record<string, string> | undefined;
299
+ if (options.azureChatCompletions && baseUrl?.includes(".openai.azure.com")) {
300
+ if (!baseUrl.includes("/deployments/")) {
301
+ baseUrl = `${baseUrl}/deployments/${options.azureChatCompletions.deploymentName}`;
302
+ }
303
+ query = { "api-version": options.azureChatCompletions.apiVersion };
304
+ }
305
+
306
+ if (options.openAISessionId && model.provider === "openai") {
307
+ setHeaderIfAbsent(headers, "session_id", options.openAISessionId);
308
+ setHeaderIfAbsent(headers, "x-client-request-id", options.openAISessionId);
309
+ }
310
+ if (options.promptCacheSessionId && model.compat?.promptCacheSessionHeader) {
311
+ setHeaderIfAbsent(headers, model.compat.promptCacheSessionHeader, options.promptCacheSessionId);
312
+ }
313
+
314
+ if (options.defaultBaseUrl !== undefined) {
315
+ baseUrl = baseUrl ?? ($env.OPENAI_BASE_URL?.trim() || options.defaultBaseUrl);
316
+ }
317
+ const requestHeaders = { ...headers };
318
+ // A keyless provider (`auth: none` in models.yml) resolves to the `N/A`
319
+ // sentinel rather than a real key. Injecting `Authorization: Bearer N/A`
320
+ // breaks custom endpoints that authenticate via their own headers (e.g.
321
+ // `headers.x-api-key`) and reject the bogus bearer — mirror the sentinel
322
+ // guards in google-vertex / amazon-bedrock and send no Authorization here
323
+ // (#6188). A caller-supplied Authorization in `model.headers` still wins.
324
+ if (apiKey !== NO_AUTH_SENTINEL) {
325
+ headers.Authorization ??= `Bearer ${apiKey}`;
326
+ }
327
+ return { copilotPremiumRequests, baseUrl, headers, query, requestHeaders };
328
+ }
329
+
330
+ export function applyOpenAIServiceTier(
331
+ params: { service_tier?: ServiceTier | null | undefined },
332
+ serviceTier: ServiceTier | null | undefined,
333
+ model: Pick<Model, "provider" | "api" | "id">,
334
+ ): void {
335
+ if (!shouldSendServiceTier(serviceTier, model)) return;
336
+ params.service_tier = serviceTier;
337
+ }
338
+
339
+ /**
340
+ * Standard OpenAI Responses service-tier cost multipliers. The non-Codex
341
+ * Responses path bills the tier it was served (or requested): Flex processing is
342
+ * half price; Priority is a 2x premium. Codex bills the same tiers with its own
343
+ * table (Priority is 2.5x on gpt-5.5) and applies that separately.
344
+ */
345
+ function getOpenAIResponsesServiceTierCostMultiplier(tier: string | null | undefined): number {
346
+ switch (tier) {
347
+ case "flex":
348
+ return 0.5;
349
+ case "priority":
350
+ return 2;
351
+ default:
352
+ return 1;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Adjust resolved cost by the service tier OpenAI actually billed — parity with
358
+ * Codex (`applyCodexServiceTierPricing`), but with the standard (non-Codex)
359
+ * multipliers. The served tier comes from the response echo, falling back to the
360
+ * resolved request tier. Scoped to `provider: "openai"` (the only standard
361
+ * Responses biller) so an echoed `service_tier` from an Azure/OpenRouter/Copilot
362
+ * proxy can never skew those costs.
363
+ */
364
+ export function applyOpenAIResponsesServiceTierCost(
365
+ model: Pick<Model, "provider">,
366
+ usage: AssistantMessage["usage"],
367
+ responseServiceTier: unknown,
368
+ requestServiceTier: ServiceTier | null | undefined,
369
+ ): void {
370
+ if (model.provider !== "openai") return;
371
+ // The response echo is authoritative when present (OpenAI may downgrade a
372
+ // requested priority/flex turn to default under load); only fall back to the
373
+ // requested tier when the response omits the echo entirely.
374
+ const served = typeof responseServiceTier === "string" ? responseServiceTier : (requestServiceTier ?? undefined);
375
+ const multiplier = getOpenAIResponsesServiceTierCostMultiplier(served);
376
+ if (multiplier === 1) return;
377
+ usage.cost.input *= multiplier;
378
+ usage.cost.output *= multiplier;
379
+ usage.cost.cacheRead *= multiplier;
380
+ usage.cost.cacheWrite *= multiplier;
381
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
382
+ }
383
+
384
+ /** Reconcile token-price estimates with OpenRouter's authoritative account charge. */
385
+ export function applyOpenRouterReportedCost(model: Pick<Model, "provider">, usage: Usage, rawUsage: unknown): void {
386
+ if (model.provider !== "openrouter" || typeof rawUsage !== "object" || rawUsage === null) return;
387
+ const reportedCost = Reflect.get(rawUsage, "cost");
388
+ if (typeof reportedCost !== "number" || !Number.isFinite(reportedCost) || reportedCost < 0) return;
389
+
390
+ const estimatedCost = usage.cost.total;
391
+ if (Number.isFinite(estimatedCost) && estimatedCost > 0) {
392
+ const scale = reportedCost / estimatedCost;
393
+ usage.cost.input *= scale;
394
+ usage.cost.output *= scale;
395
+ usage.cost.cacheRead *= scale;
396
+ usage.cost.cacheWrite *= scale;
397
+ } else {
398
+ // Keep legacy component-only aggregators additive when catalog pricing is unavailable.
399
+ usage.cost.input = reportedCost;
400
+ usage.cost.output = 0;
401
+ usage.cost.cacheRead = 0;
402
+ usage.cost.cacheWrite = 0;
403
+ }
404
+ usage.cost.total = reportedCost;
405
+ }
406
+
407
+ export interface OpenAIUsageAccountingInput {
408
+ promptTokens: number;
409
+ outputTokens: number;
410
+ cachedTokens: number;
411
+ reasoningTokens: number;
412
+ cacheWriteOpenRouter: number | undefined;
413
+ cacheWriteDeepSeek: number | undefined;
414
+ hasDeepSeekCacheHitAndMiss: boolean;
415
+ }
416
+
417
+ export interface OpenAIUsageAccounting {
418
+ input: number;
419
+ output: number;
420
+ cacheRead: number;
421
+ cacheWrite: number;
422
+ totalTokens: number;
423
+ reasoningTokens?: number;
424
+ orchestration?: Usage["orchestration"];
425
+ }
426
+
427
+ export function calculateOpenAIUsageAccounting(accounting: OpenAIUsageAccountingInput): OpenAIUsageAccounting {
428
+ const cacheWriteTokens = accounting.cacheWriteOpenRouter ?? accounting.cacheWriteDeepSeek ?? 0;
429
+ const isDeepSeekUsage =
430
+ accounting.hasDeepSeekCacheHitAndMiss &&
431
+ accounting.cacheWriteOpenRouter === undefined &&
432
+ (accounting.cacheWriteDeepSeek ?? 0) > 0;
433
+ const input = isDeepSeekUsage
434
+ ? Math.max(0, accounting.promptTokens - accounting.cachedTokens)
435
+ : Math.max(0, accounting.promptTokens - accounting.cachedTokens - cacheWriteTokens);
436
+ const cacheWrite = isDeepSeekUsage ? 0 : cacheWriteTokens;
437
+ return {
438
+ input,
439
+ output: accounting.outputTokens,
440
+ cacheRead: accounting.cachedTokens,
441
+ cacheWrite,
442
+ totalTokens: input + accounting.outputTokens + accounting.cachedTokens + cacheWrite,
443
+ ...(accounting.reasoningTokens > 0 ? { reasoningTokens: accounting.reasoningTokens } : {}),
444
+ };
445
+ }
446
+
447
+ /** Normalize a cache identity to the wire limit accepted by OpenAI-family providers. */
448
+ export function normalizeOpenAIPromptCacheKey(sessionId: string | undefined): string | undefined {
449
+ return normalizeOpenAIStableId(sessionId, 64, "pc_");
450
+ }
451
+
452
+ export function normalizeOpenRouterResponsesSessionId(sessionId: string | undefined): string | undefined {
453
+ return normalizeOpenAIStableId(sessionId, 256, "session_");
454
+ }
455
+
456
+ /** Resolve a prompt-cache identity, falling back to the provider session unless caching is disabled. */
457
+ export function getOpenAIPromptCacheKey(options: OpenAICacheOptions | undefined): string | undefined {
458
+ if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
459
+ return normalizeOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
460
+ }
461
+
462
+ export function getOpenAIResponsesRoutingSessionId(
463
+ options: Pick<OpenAICacheOptions, "cacheRetention" | "sessionId"> | undefined,
464
+ ): string | undefined {
465
+ if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
466
+ return normalizeOpenAIPromptCacheKey(options?.sessionId);
467
+ }
468
+
469
+ export function getOpenRouterResponsesSessionId(
470
+ options: Pick<OpenAICacheOptions, "cacheRetention" | "sessionId"> | undefined,
471
+ ): string | undefined {
472
+ if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
473
+ return normalizeOpenRouterResponsesSessionId(options?.sessionId);
474
+ }
475
+
476
+ export function parseAzureDeploymentNameMap(value: string | undefined): Map<string, string> {
477
+ const map = new Map<string, string>();
478
+ if (!value) return map;
479
+ for (const entry of value.split(",")) {
480
+ const trimmed = entry.trim();
481
+ if (!trimmed) continue;
482
+ const [modelId, deploymentName] = trimmed.split("=", 2);
483
+ if (!modelId || !deploymentName) continue;
484
+ map.set(modelId.trim(), deploymentName.trim());
485
+ }
486
+ return map;
487
+ }
488
+
489
+ export function createOpenAIStrictToolsState(): OpenAIStrictToolsState {
490
+ return {
491
+ strictTools: {
492
+ disabledModelScopes: new Set<string>(),
493
+ },
494
+ };
495
+ }
496
+
497
+ export function clearOpenAIStrictToolsState(state: OpenAIStrictToolsState): void {
498
+ state.strictTools.disabledModelScopes.clear();
499
+ }
500
+
501
+ export function getOpenAIStrictToolsScope(
502
+ model: OpenAIModelIdentity,
503
+ resolvedBaseUrl: string | undefined,
504
+ ): OpenAIStrictToolsScope {
505
+ return {
506
+ provider: model.provider,
507
+ baseUrl: resolvedBaseUrl ?? model.baseUrl,
508
+ modelId: model.id,
509
+ };
510
+ }
511
+
512
+ export function isStrictToolsDisabledForScope(
513
+ state: OpenAIStrictToolsState | undefined,
514
+ scope: OpenAIStrictToolsScope | undefined,
515
+ ): boolean {
516
+ if (!scope) return false;
517
+ return (
518
+ state?.strictTools.disabledModelScopes.has(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`) ?? false
519
+ );
520
+ }
521
+
522
+ export function disableStrictToolsForScope(
523
+ state: OpenAIStrictToolsState | undefined,
524
+ scope: OpenAIStrictToolsScope | undefined,
525
+ ): void {
526
+ if (!scope) return;
527
+ state?.strictTools.disabledModelScopes.add(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`);
528
+ }
529
+
530
+ export function isOpenRouterAnthropicModel(model: OpenAIModelIdentity): boolean {
531
+ return model.provider === "openrouter" && model.id.toLowerCase().startsWith("anthropic/");
532
+ }
533
+
534
+ /**
535
+ * Append an OpenRouter routing-variant suffix (e.g. `:nitro`, `:floor`, `:online`, `:exacto`)
536
+ * to a model id when no explicit variant is already present. A variant is considered
537
+ * "already present" when `modelId` contains a colon after the last `/` separator —
538
+ * which covers both user-typed selectors (`anthropic/claude-haiku:nitro`) and catalog
539
+ * entries that bake the variant in (`deepseek/deepseek-v3.1-terminus:exacto`).
540
+ */
541
+ export function applyOpenRouterRoutingVariant(modelId: string, variant: string | undefined): string {
542
+ if (!variant) return modelId;
543
+ const lastSlash = modelId.lastIndexOf("/");
544
+ const lastColon = modelId.lastIndexOf(":");
545
+ if (lastColon > lastSlash) return modelId;
546
+ return `${modelId}:${variant}`;
547
+ }
548
+
549
+ export function applyWireModelIdTransform(
550
+ baseId: string,
551
+ mode: ResolvedOpenAISharedCompat["wireModelIdMode"],
552
+ openrouterVariant?: string,
553
+ ): string {
554
+ switch (mode) {
555
+ case "firepass":
556
+ return toFirepassWireModelId(baseId);
557
+ case "fireworks":
558
+ return toFireworksWireModelId(baseId);
559
+ case "openrouter":
560
+ return applyOpenRouterRoutingVariant(baseId, openrouterVariant);
561
+ default:
562
+ return baseId;
563
+ }
564
+ }
565
+
566
+ export interface OpenAIOutputTokenParam {
567
+ field: "max_tokens" | "max_completion_tokens" | "max_output_tokens";
568
+ value: number;
569
+ }
570
+
571
+ export interface ResolveOpenAIOutputTokenInput {
572
+ /** Wire field the endpoint expects for the output cap. */
573
+ field: OpenAIOutputTokenParam["field"];
574
+ /** Caller-supplied output cap (model-defaulted by `stream.ts`, or null/undefined on direct provider calls). */
575
+ maxTokens: number | null | undefined;
576
+ /** Whether the caller explicitly set `maxTokens` (routing omission only applies when false). */
577
+ maxTokensExplicit: boolean;
578
+ /** Model output cap (`model.maxTokens`). */
579
+ modelMaxTokens: number | null | undefined;
580
+ /** Drop the field entirely — proxies with unknown upstream caps (Ollama via `model.omitMaxOutputTokens`). */
581
+ omitMaxOutputTokens: boolean;
582
+ /** The model sits behind OpenRouter (catalog default caps are omitted so each upstream self-caps). */
583
+ isOpenRouterHost: boolean;
584
+ /** Endpoint always needs a cap (Kimi-family TPM math); supplies the model default when the caller did not. */
585
+ alwaysSendMaxTokens: boolean;
586
+ /** Hard provider clamp; defaults to {@link OPENAI_MAX_OUTPUT_TOKENS}. */
587
+ providerOutputClamp?: number;
588
+ }
589
+
590
+ /**
591
+ * Resolve the single output-token wire parameter shared by Chat Completions
592
+ * (`max_tokens`/`max_completion_tokens`) and the Responses family
593
+ * (`max_output_tokens`). Centralizes the provider exceptions that previously
594
+ * lived inline in both `buildParams`:
595
+ * - `alwaysSendMaxTokens`: Kimi-family endpoints derive TPM limits from the
596
+ * cap and require one on every call, so default from the model cap (or
597
+ * {@link OPENAI_MAX_OUTPUT_TOKENS}) when the caller omitted it.
598
+ * - OpenRouter routing omission: OpenRouter fans out to upstreams whose output
599
+ * caps differ from the catalog value, so a catalog default above the routed
600
+ * upstream's cap makes OpenRouter skip that upstream. Omit catalog defaults
601
+ * (explicit caller caps still win) so `provider.order`/`only` is honored.
602
+ * - model/provider clamp: never exceed `model.maxTokens` or the provider clamp
603
+ * (`OPENAI_MAX_OUTPUT_TOKENS`, raised for GLM-5.2 reasoning by the caller).
604
+ * - `omitMaxOutputTokens`: proxies (Ollama) with unknown upstream caps drop it.
605
+ */
606
+ export function resolveOpenAIOutputTokenParam(
607
+ input: ResolveOpenAIOutputTokenInput,
608
+ ): OpenAIOutputTokenParam | undefined {
609
+ if (input.omitMaxOutputTokens) return undefined;
610
+ const requested =
611
+ input.maxTokens ?? (input.alwaysSendMaxTokens ? (input.modelMaxTokens ?? OPENAI_MAX_OUTPUT_TOKENS) : undefined);
612
+ if (requested === undefined) return undefined;
613
+ if (input.isOpenRouterHost && !input.alwaysSendMaxTokens && !input.maxTokensExplicit) return undefined;
614
+ const value = Math.min(
615
+ requested,
616
+ input.modelMaxTokens ?? Number.POSITIVE_INFINITY,
617
+ input.providerOutputClamp ?? OPENAI_MAX_OUTPUT_TOKENS,
618
+ );
619
+ if (!(value > 0)) return undefined;
620
+ return { field: input.field, value };
621
+ }
622
+
623
+ export interface OpenAIGatewayRoutingParams {
624
+ provider?: OpenRouterRouting;
625
+ providerOptions?: { gateway?: Pick<VercelGatewayRouting, "only" | "order" | "caching"> };
626
+ }
627
+
628
+ export interface OpenAIGatewayRoutingCompat {
629
+ isOpenRouterHost: boolean;
630
+ openRouterRouting?: OpenRouterRouting;
631
+ isVercelGatewayHost?: boolean;
632
+ vercelGatewayRouting?: VercelGatewayRouting;
633
+ }
634
+
635
+ /**
636
+ * Apply gateway routing preferences to the request body. OpenRouter routes via
637
+ * the top-level `provider` field; the Vercel AI Gateway routes Chat
638
+ * Completions through `providerOptions.gateway`.
639
+ */
640
+ export function applyOpenAIGatewayRouting(
641
+ params: OpenAIGatewayRoutingParams,
642
+ compat: OpenAIGatewayRoutingCompat,
643
+ cacheEnabled = true,
644
+ ): void {
645
+ if (compat.isOpenRouterHost && compat.openRouterRouting) {
646
+ params.provider = compat.openRouterRouting;
647
+ }
648
+ if (compat.isVercelGatewayHost && compat.vercelGatewayRouting) {
649
+ const routing = compat.vercelGatewayRouting;
650
+ if (routing.only || routing.order || (cacheEnabled && routing.caching)) {
651
+ const gatewayOptions: Pick<VercelGatewayRouting, "only" | "order" | "caching"> = {};
652
+ if (routing.only) gatewayOptions.only = routing.only;
653
+ if (routing.order) gatewayOptions.order = routing.order;
654
+ if (cacheEnabled && routing.caching) gatewayOptions.caching = routing.caching;
655
+ params.providerOptions = { gateway: gatewayOptions };
656
+ }
657
+ }
658
+ }
659
+
660
+ export interface VercelResponsesCacheParams {
661
+ caching?: "auto";
662
+ cache_anchor_items?: number;
663
+ cache_ttl?: "5m" | "1h";
664
+ providerOptions?: { gateway?: Pick<VercelGatewayRouting, "only" | "order"> };
665
+ }
666
+
667
+ export interface VercelResponsesCacheCompat {
668
+ isVercelGatewayHost: boolean;
669
+ vercelGatewayRouting?: VercelGatewayRouting;
670
+ }
671
+
672
+ /**
673
+ * Apply Vercel AI Gateway's Responses-only automatic cache controls and
674
+ * provider routing. Cache settings are top-level Responses fields, while
675
+ * `only` and `order` remain under `providerOptions.gateway`.
676
+ */
677
+ export function applyVercelResponsesCacheControls(
678
+ params: VercelResponsesCacheParams,
679
+ compat: VercelResponsesCacheCompat,
680
+ cacheRetention: CacheRetention = "short",
681
+ ): void {
682
+ const routing = compat.vercelGatewayRouting;
683
+ if (!compat.isVercelGatewayHost) return;
684
+
685
+ if (routing?.only || routing?.order) {
686
+ const gateway: Pick<VercelGatewayRouting, "only" | "order"> = {};
687
+ if (routing.only) gateway.only = routing.only;
688
+ if (routing.order) gateway.order = routing.order;
689
+ params.providerOptions = { gateway };
690
+ }
691
+
692
+ if (cacheRetention === "none" || routing?.caching !== "auto") return;
693
+
694
+ params.caching = "auto";
695
+ if (routing.cacheAnchorItems !== undefined) params.cache_anchor_items = routing.cacheAnchorItems;
696
+ // A configured 1h TTL is capped by resolved retention; default and short intentionally omit it.
697
+ if (routing.cacheTtl !== undefined && (routing.cacheTtl !== "1h" || cacheRetention === "long")) {
698
+ params.cache_ttl = routing.cacheTtl;
699
+ }
700
+ }
701
+
702
+ export interface OpenAIExtraBodyOptions {
703
+ /**
704
+ * Fireworks rejects DeepSeek-style `thinking` toggles alongside OpenAI-style
705
+ * `reasoning_effort`; drop `thinking` when the effort field carries the level.
706
+ */
707
+ dropThinkingWhenReasoningEffort?: boolean;
708
+ }
709
+
710
+ /**
711
+ * Merge a compat/options `extraBody` blob into the request params. When
712
+ * `dropThinkingWhenReasoningEffort` is set and `reasoning_effort` is present,
713
+ * delete the conflicting `thinking` toggle (Fireworks rejects both together).
714
+ */
715
+ export function applyOpenAIExtraBody<P extends object>(
716
+ params: P,
717
+ extraBody: Record<string, unknown> | undefined,
718
+ options?: OpenAIExtraBodyOptions,
719
+ ): void {
720
+ if (!extraBody) return;
721
+ Object.assign(params, extraBody);
722
+ if (options?.dropThinkingWhenReasoningEffort) {
723
+ const shaped = params as { reasoning_effort?: unknown; thinking?: unknown };
724
+ if (shaped.reasoning_effort !== undefined) {
725
+ delete shaped.thinking;
726
+ }
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Chat Completions streaming request body shaped by the OpenAI-family providers.
732
+ * Extends the vendored SDK params with the compat dialect fields pi-ai emits
733
+ * (binary `thinking`, Qwen `enable_thinking`/`chat_template_kwargs`, nested
734
+ * `reasoning`, gateway `provider`/`providerOptions`, sampling extras). Lives in
735
+ * the shared module beside the request-shaping helpers that mutate it.
736
+ */
737
+ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming, "reasoning_effort" | "service_tier"> & {
738
+ top_k?: number;
739
+ min_p?: number;
740
+ repetition_penalty?: number;
741
+ thinking?: { type: "enabled" | "disabled"; effort?: string; keep?: "all" };
742
+ enable_thinking?: boolean;
743
+ preserve_thinking?: boolean;
744
+ chat_template_kwargs?: { enable_thinking?: boolean; preserve_thinking?: boolean };
745
+ reasoning?: { effort?: string } | { enabled: false };
746
+ reasoning_effort?: string | null;
747
+ service_tier?: ServiceTier;
748
+ tool_stream?: boolean;
749
+ provider?: OpenAICompat["openRouterRouting"];
750
+ providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
751
+ };
752
+
753
+ /** Reasoning-relevant slice of caller options the Chat Completions dialect dispatch reads. */
754
+ export interface ChatCompletionsReasoningOptions {
755
+ reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
756
+ disableReasoning?: boolean;
757
+ }
758
+
759
+ export type OpenAICompatEndpoint = "chat-completions" | "responses";
760
+
761
+ export type OpenAIReasoningDisableReason = "caller" | "forced-tool-choice" | "tool-choice" | "not-requested";
762
+
763
+ export type OpenAICompatPolicyCompat = ResolvedOpenAISharedCompat &
764
+ Partial<ResolvedOpenAICompat> &
765
+ Partial<ResolvedOpenAIResponsesCompat>;
766
+
767
+ export interface ResolveOpenAICompatPolicyOptions {
768
+ endpoint: OpenAICompatEndpoint;
769
+ compat?: OpenAICompatPolicyCompat;
770
+ reasoning?: string;
771
+ disableReasoning?: boolean;
772
+ toolChoice?: unknown;
773
+ strictResponsesPairing?: boolean;
774
+ includeEncryptedReasoning?: boolean;
775
+ filterReasoningHistory?: boolean;
776
+ omitReasoningEffort?: boolean;
777
+ }
778
+
779
+ export interface OpenAICompatPolicy {
780
+ endpoint: OpenAICompatEndpoint;
781
+ compat: OpenAICompatPolicyCompat;
782
+ reasoning: {
783
+ modelSupported: boolean;
784
+ supportsParams: boolean;
785
+ requestedEffort?: string;
786
+ wireEffort?: string;
787
+ enabled: boolean;
788
+ disabled: boolean;
789
+ disableReason?: OpenAIReasoningDisableReason;
790
+ dialect: ResolvedOpenAISharedCompat["thinkingFormat"];
791
+ disableMode: OpenAIReasoningDisableMode;
792
+ omitReasoningEffort: boolean;
793
+ includeEncryptedReasoning: boolean;
794
+ filterReasoningHistory: boolean;
795
+ requiresReasoningContentForToolCalls: boolean;
796
+ requiresReasoningContentForAllAssistantTurns: boolean;
797
+ allowsSyntheticReasoningContentForToolCalls: boolean;
798
+ reasoningContentField?: OpenAICompat["reasoningContentField"];
799
+ requiresThinkingAsText: boolean;
800
+ };
801
+ tools: {
802
+ strictResponsesPairing: boolean;
803
+ toolCallIdKind: "default" | "openai-40" | "mistral-9-alnum";
804
+ };
805
+ messages: {
806
+ systemRole: "system" | "developer";
807
+ supportsDeveloperRole: boolean;
808
+ supportsMultipleSystemMessages: boolean;
809
+ };
810
+ stream: {
811
+ stripSpecialTokens: "deepseek" | false;
812
+ markupHealingPattern?: OpenAIStreamMarkupHealingPattern;
813
+ reasoningDeltasMayBeCumulative: boolean;
814
+ emptyLengthFinishIsContextError: boolean;
815
+ };
816
+ }
817
+
818
+ /**
819
+ * Map a user-facing effort to the provider wire value: explicit compat
820
+ * override first, then the model's baked `thinking.effortMap`, else identity.
821
+ * Shared by the chat-completions/Responses policy resolver and the Codex
822
+ * request transformer.
823
+ */
824
+ export function mapOpenAIReasoningEffort(
825
+ model: Pick<Model, "thinking">,
826
+ compat: { reasoningEffortMap?: Partial<Record<Effort, string>> } | undefined,
827
+ effort: string,
828
+ ): string {
829
+ const level = effort as Effort;
830
+ return compat?.reasoningEffortMap?.[level] ?? model.thinking?.effortMap?.[level] ?? effort;
831
+ }
832
+
833
+ function isImplicitDisableWhenNotRequested(disableMode: OpenAIReasoningDisableMode): boolean {
834
+ return (
835
+ disableMode === "zai-thinking-disabled" ||
836
+ disableMode === "qwen-enable-thinking-false" ||
837
+ disableMode === "qwen-template-false"
838
+ );
839
+ }
840
+
841
+ export function resolveOpenAICompatPolicy<TApi extends Api>(
842
+ model: Model<TApi>,
843
+ options: ResolveOpenAICompatPolicyOptions,
844
+ ): OpenAICompatPolicy {
845
+ const baseCompat = (options.compat ?? model.compat) as OpenAICompatPolicyCompat;
846
+ const requestedEffort = options.reasoning;
847
+ const modelSupported = Boolean(model.reasoning);
848
+ const forcedToolChoiceSuppressesReasoning =
849
+ baseCompat.disableReasoningOnForcedToolChoice &&
850
+ baseCompat.supportsForcedToolChoice &&
851
+ isForcedToolChoice(options.toolChoice);
852
+ const anyToolChoiceSuppressesReasoning =
853
+ !forcedToolChoiceSuppressesReasoning &&
854
+ baseCompat.disableReasoningOnToolChoice &&
855
+ options.toolChoice !== undefined;
856
+ const requestedAndAllowed = requestedEffort !== undefined && !options.disableReasoning && modelSupported;
857
+ const conflictDisableReason: OpenAIReasoningDisableReason | undefined = forcedToolChoiceSuppressesReasoning
858
+ ? "forced-tool-choice"
859
+ : anyToolChoiceSuppressesReasoning
860
+ ? "tool-choice"
861
+ : undefined;
862
+ const disableReason: OpenAIReasoningDisableReason | undefined = options.disableReasoning
863
+ ? "caller"
864
+ : conflictDisableReason;
865
+ const enabledBeforeThinkingVariant = requestedAndAllowed && disableReason === undefined;
866
+ const baseWireEffort =
867
+ enabledBeforeThinkingVariant && requestedEffort !== undefined
868
+ ? mapOpenAIReasoningEffort(model, baseCompat, requestedEffort)
869
+ : undefined;
870
+ const disabledByNoneEffort =
871
+ enabledBeforeThinkingVariant &&
872
+ baseCompat.reasoningDisableMode === "zai-thinking-disabled" &&
873
+ baseWireEffort === "none";
874
+ const enabled = enabledBeforeThinkingVariant && !disabledByNoneEffort;
875
+ const compat =
876
+ enabled && baseCompat.whenThinking ? (baseCompat.whenThinking as OpenAICompatPolicyCompat) : baseCompat;
877
+ const omitReasoningEffort =
878
+ options.omitReasoningEffort ?? (compat.omitReasoningEffort || !compat.supportsReasoningEffort);
879
+ const disableMode = compat.reasoningDisableMode;
880
+ let wireEffort =
881
+ enabled && requestedEffort !== undefined ? mapOpenAIReasoningEffort(model, compat, requestedEffort) : undefined;
882
+ const disabledWithoutRequest =
883
+ modelSupported &&
884
+ requestedEffort === undefined &&
885
+ !options.disableReasoning &&
886
+ isImplicitDisableWhenNotRequested(disableMode);
887
+ const disabled =
888
+ (modelSupported && disableReason === "caller") ||
889
+ conflictDisableReason !== undefined ||
890
+ (modelSupported && disabledWithoutRequest) ||
891
+ disabledByNoneEffort;
892
+ if (
893
+ disabled &&
894
+ disableReason === "caller" &&
895
+ requestedEffort === undefined &&
896
+ disableMode === "lowest-effort" &&
897
+ compat.supportsReasoningEffort &&
898
+ !omitReasoningEffort
899
+ ) {
900
+ const minEffort = getSupportedEfforts(model)[0];
901
+ if (minEffort === undefined) {
902
+ throw new AIError.ConfigurationError(`Model ${model.provider}/${model.id} has no supported reasoning efforts`);
903
+ }
904
+ wireEffort = mapOpenAIReasoningEffort(model, compat, minEffort);
905
+ }
906
+
907
+ return {
908
+ endpoint: options.endpoint,
909
+ compat,
910
+ reasoning: {
911
+ modelSupported,
912
+ supportsParams: compat.supportsReasoningParams,
913
+ requestedEffort,
914
+ wireEffort,
915
+ enabled,
916
+ disabled,
917
+ disableReason: disableReason ?? (disabledWithoutRequest || disabledByNoneEffort ? "not-requested" : undefined),
918
+ dialect: compat.thinkingFormat,
919
+ requiresReasoningContentForToolCalls: compat.requiresReasoningContentForToolCalls,
920
+ requiresReasoningContentForAllAssistantTurns: compat.requiresReasoningContentForAllAssistantTurns,
921
+ allowsSyntheticReasoningContentForToolCalls: compat.allowsSyntheticReasoningContentForToolCalls,
922
+ reasoningContentField: compat.reasoningContentField,
923
+ requiresThinkingAsText: compat.requiresThinkingAsText,
924
+ disableMode,
925
+ omitReasoningEffort,
926
+ includeEncryptedReasoning: options.includeEncryptedReasoning ?? compat.includeEncryptedReasoning,
927
+ filterReasoningHistory: options.filterReasoningHistory ?? compat.filterReasoningHistory,
928
+ },
929
+ tools: {
930
+ strictResponsesPairing: options.strictResponsesPairing ?? compat.strictResponsesPairing ?? false,
931
+ toolCallIdKind: compat.requiresMistralToolIds
932
+ ? "mistral-9-alnum"
933
+ : compat.usesOpenAIToolCallIdLimit
934
+ ? "openai-40"
935
+ : "default",
936
+ },
937
+ messages: {
938
+ systemRole: modelSupported && compat.supportsDeveloperRole ? "developer" : "system",
939
+ supportsDeveloperRole: compat.supportsDeveloperRole,
940
+ supportsMultipleSystemMessages: compat.supportsMultipleSystemMessages ?? true,
941
+ },
942
+ stream: {
943
+ stripSpecialTokens: compat.stripDeepseekSpecialTokens ? "deepseek" : false,
944
+ markupHealingPattern: compat.streamMarkupHealingPattern,
945
+ reasoningDeltasMayBeCumulative: compat.reasoningDeltasMayBeCumulative,
946
+ emptyLengthFinishIsContextError: compat.emptyLengthFinishIsContextError,
947
+ },
948
+ };
949
+ }
950
+
951
+ function encodeChatCompletionsDisabledReasoning(
952
+ params: OpenAICompletionsParams,
953
+ disableMode: OpenAIReasoningDisableMode,
954
+ ): void {
955
+ delete params.reasoning_effort;
956
+ switch (disableMode) {
957
+ case "zai-thinking-disabled":
958
+ params.thinking = { type: "disabled" };
959
+ break;
960
+ case "qwen-enable-thinking-false":
961
+ params.enable_thinking = false;
962
+ break;
963
+ case "qwen-template-false":
964
+ params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: false };
965
+ break;
966
+ case "openrouter-enabled-false":
967
+ (params as typeof params & { reasoning?: { effort?: string } | { enabled: false } }).reasoning = {
968
+ enabled: false,
969
+ };
970
+ break;
971
+ default:
972
+ delete params.reasoning;
973
+ break;
974
+ }
975
+ }
976
+
977
+ export function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void {
978
+ // `preserve_thinking` is a chat-template HISTORY knob, not a per-turn
979
+ // thinking switch — it controls whether OLDER assistant turns render
980
+ // with `<think>...</think>` on Qwen3.6+. Emit it BEFORE the reasoning
981
+ // state branches and EVERY early-return below, because the wire shape
982
+ // must carry the kwarg in three cases the auto-detected
983
+ // `qwenPreserveThinking` flag covers but `reasoning.enabled` does not:
984
+ //
985
+ // 1. Discovered local Qwen models. `discoverOpenAICompatibleModels`
986
+ // stamps `reasoning: false` on every spec built from a generic
987
+ // `/v1/models` endpoint (the upstream doesn't advertise the
988
+ // capability), so `model.reasoning === false` → `reasoning.enabled
989
+ // === false`, the body wouldn't otherwise see the kwarg, and the
990
+ // encoder's `replayReasoningContent` branch would keep shipping
991
+ // `reasoning_content` only for the template to strip `<think>` from
992
+ // older turns anyway. Exactly the #3528 / #3541 symptom on every
993
+ // discovered Qwen build.
994
+ // 2. Caller-disabled reasoning. The slot's KV cache still holds prior
995
+ // `<think>...</think>` tokens from earlier thinking turns; the
996
+ // template must keep rendering them or cache invalidates at the
997
+ // first historic `<think>`.
998
+ // 3. Forced-tool-choice / DeepSeek-style auto-disable. Same reasoning
999
+ // as (2) — historic thinking blocks have to survive history replay
1000
+ // even when the current turn cannot think.
1001
+ //
1002
+ // Non-Qwen templates ignore the parameter (jinja `is defined` check
1003
+ // silently no-ops), so emitting it unconditionally for the Qwen-family
1004
+ // + local-cache compat flag is safe.
1005
+ if (policy.compat.qwenPreserveThinking) {
1006
+ // Mirror the dialect split that gates `enable_thinking`. The
1007
+ // `qwen` dialect rides the top-level field (the only place
1008
+ // llama.cpp's `--jinja` hook AND Alibaba Cloud Model Studio's
1009
+ // compatible-mode look) while the `qwen-chat-template` dialect
1010
+ // (NVIDIA NIM, vLLM/SGLang's chat-template-kwargs path) MUST
1011
+ // ride only the kwargs copy — NIM's request schema is
1012
+ // `additionalProperties: false` and rejects every unknown
1013
+ // top-level field, the very reason `enable_thinking` is
1014
+ // route-split this way (#2299, see `catalog/src/compat/openai.ts`
1015
+ // thinkingFormat comment).
1016
+ if (policy.compat.thinkingFormat === "qwen") {
1017
+ params.preserve_thinking = true;
1018
+ }
1019
+ params.chat_template_kwargs = { ...params.chat_template_kwargs, preserve_thinking: true };
1020
+ }
1021
+
1022
+ const reasoning = policy.reasoning;
1023
+ if ((!reasoning.modelSupported && !reasoning.disabled) || !reasoning.supportsParams) return;
1024
+ if (reasoning.enabled) {
1025
+ switch (reasoning.disableMode) {
1026
+ case "zai-thinking-disabled":
1027
+ if (reasoning.wireEffort === "none") {
1028
+ encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
1029
+ return;
1030
+ }
1031
+ if (reasoning.dialect === "kimi" && reasoning.wireEffort !== undefined) {
1032
+ params.thinking = { type: "enabled", effort: reasoning.wireEffort };
1033
+ if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
1034
+ break;
1035
+ }
1036
+ params.thinking = { type: "enabled" };
1037
+ if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
1038
+ if (policy.compat.supportsReasoningEffort && reasoning.wireEffort !== undefined) {
1039
+ params.reasoning_effort = reasoning.wireEffort as Effort;
1040
+ }
1041
+ break;
1042
+ case "qwen-enable-thinking-false":
1043
+ params.enable_thinking = true;
1044
+ break;
1045
+ case "qwen-template-false":
1046
+ // Spread so the `preserve_thinking` kwarg hoisted above
1047
+ // survives the merge — a bare `{ enable_thinking: true }`
1048
+ // would clobber it.
1049
+ params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: true };
1050
+ break;
1051
+ case "openrouter-enabled-false":
1052
+ if (reasoning.wireEffort !== undefined) {
1053
+ (params as typeof params & { reasoning?: { effort?: string } }).reasoning = {
1054
+ effort: reasoning.wireEffort,
1055
+ };
1056
+ }
1057
+ break;
1058
+ default:
1059
+ if (!reasoning.omitReasoningEffort && reasoning.wireEffort !== undefined) {
1060
+ params.reasoning_effort = reasoning.wireEffort as Effort;
1061
+ }
1062
+ break;
1063
+ }
1064
+ return;
1065
+ }
1066
+ if (!reasoning.disabled) return;
1067
+ if (
1068
+ reasoning.disableReason === "caller" &&
1069
+ reasoning.requestedEffort === undefined &&
1070
+ reasoning.disableMode === "lowest-effort" &&
1071
+ reasoning.wireEffort !== undefined
1072
+ ) {
1073
+ params.reasoning_effort = reasoning.wireEffort as Effort;
1074
+ return;
1075
+ }
1076
+ encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
1077
+ }
1078
+
1079
+ export function applyChatCompletionsReasoningParams(
1080
+ params: OpenAICompletionsParams,
1081
+ model: Model<"openai-completions">,
1082
+ compat: ResolvedOpenAICompat,
1083
+ options: (ChatCompletionsReasoningOptions & { toolChoice?: unknown }) | undefined,
1084
+ ): void {
1085
+ applyChatCompletionsCompatPolicy(
1086
+ params,
1087
+ resolveOpenAICompatPolicy(model, {
1088
+ endpoint: "chat-completions",
1089
+ compat,
1090
+ reasoning: options?.reasoning,
1091
+ disableReasoning: options?.disableReasoning,
1092
+ toolChoice: options?.toolChoice,
1093
+ }),
1094
+ );
1095
+ }
1096
+
1097
+ export function disableChatCompletionsReasoningForDialect(
1098
+ params: OpenAICompletionsParams,
1099
+ compat: ResolvedOpenAICompat,
1100
+ ): void {
1101
+ encodeChatCompletionsDisabledReasoning(params, compat.reasoningDisableMode);
1102
+ }
1103
+
1104
+ /**
1105
+ * Z.AI/GLM-5.2 reasoning-effort dialect predicate. GLM-5.2 models served on a
1106
+ * Z.AI-format host (thinkingFormat "zai") accept `reasoning_effort`, stream tool
1107
+ * calls via `tool_stream`, and clamp output to the model cap. Moonshot Kimi and
1108
+ * Xiaomi MiMo also resolve to thinkingFormat "zai" with supportsReasoningEffort
1109
+ * true but are NOT GLM-5.2, so the model-id check is load-bearing — never swap it
1110
+ * for `compat.supportsReasoningEffort`.
1111
+ */
1112
+ function isZaiReasoningEffortDialect(model: Model<"openai-completions">, compat: ResolvedOpenAICompat): boolean {
1113
+ return compat.thinkingFormat === "zai" && isGlm52ReasoningEffortModelId(model.id);
1114
+ }
1115
+
1116
+ /**
1117
+ * Provider-specific Chat Completions output clamp.
1118
+ *
1119
+ * Most OpenAI-compatible endpoints retain the conservative 64k ceiling from
1120
+ * {@link resolveOpenAIOutputTokenParam}. Z.AI/GLM-5.2 reasoning and native
1121
+ * Moonshot K3 explicitly accept their full advertised model caps, so those
1122
+ * routes clamp to `model.maxTokens` instead.
1123
+ */
1124
+ export function resolveOpenAICompletionsOutputClamp(
1125
+ model: Model<"openai-completions">,
1126
+ compat: ResolvedOpenAICompat,
1127
+ ): number | undefined {
1128
+ if (isZaiReasoningEffortDialect(model, compat)) {
1129
+ return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
1130
+ }
1131
+ if (model.provider === "moonshot" && isKimiK3ModelId(model.id)) {
1132
+ return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
1133
+ }
1134
+ return undefined;
1135
+ }
1136
+
1137
+ /**
1138
+ * Provider-specific Responses API output clamp.
1139
+ *
1140
+ * Meta documents a 131,072-token output limit for Muse Spark 1.1, so native
1141
+ * Meta requests may use the model's full advertised cap instead of the
1142
+ * conservative 64k OpenAI-compatible default.
1143
+ */
1144
+ export function resolveOpenAIResponsesOutputClamp(model: Pick<Model, "provider" | "maxTokens">): number | undefined {
1145
+ if (model.provider === "meta") {
1146
+ return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
1147
+ }
1148
+ return undefined;
1149
+ }
1150
+
1151
+ /**
1152
+ * Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present
1153
+ * (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so).
1154
+ */
1155
+ export function applyChatCompletionsToolStream(
1156
+ params: OpenAICompletionsParams,
1157
+ model: Model<"openai-completions">,
1158
+ compat: ResolvedOpenAICompat,
1159
+ ): void {
1160
+ if (
1161
+ isZaiReasoningEffortDialect(model, compat) &&
1162
+ compat.supportsReasoningEffort &&
1163
+ Array.isArray(params.tools) &&
1164
+ params.tools.length > 0
1165
+ ) {
1166
+ params.tool_stream = true;
1167
+ }
1168
+ }
1169
+
1170
+ export function isCompiledGrammarTooLargeStrictError(
1171
+ error: unknown,
1172
+ capturedErrorResponse: CapturedHttpErrorResponse | undefined,
1173
+ ): boolean {
1174
+ const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
1175
+ if (status !== 400) return false;
1176
+ const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
1177
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
1178
+ .join("\n");
1179
+ return (
1180
+ /invalid_request_error/i.test(messageParts) &&
1181
+ /compiled grammar/i.test(messageParts) &&
1182
+ /too large/i.test(messageParts)
1183
+ );
1184
+ }
1185
+
1186
+ interface StrictToolsRetryContext {
1187
+ model: OpenAIModelIdentity;
1188
+ strictToolsApplied: boolean;
1189
+ tools: Tool[] | undefined;
1190
+ }
1191
+
1192
+ /** Decide whether an OpenAI-family request should retry once with non-strict tools. */
1193
+ export function shouldRetryWithoutStrictTools(
1194
+ error: unknown,
1195
+ capturedErrorResponse: CapturedHttpErrorResponse | undefined,
1196
+ context: StrictToolsRetryContext,
1197
+ ): boolean {
1198
+ const { model, strictToolsApplied, tools } = context;
1199
+ if (!tools || tools.length === 0 || !strictToolsApplied) return false;
1200
+ const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
1201
+ if (status !== 400 && status !== 422) return false;
1202
+ const errorMessage = error instanceof Error ? error.message.trim() : "";
1203
+ const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
1204
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
1205
+ .join("\n");
1206
+ if (
1207
+ /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function|structured[_ -]?outputs?\b[^\n]*(?:not (?:supported|available|enabled)|unsupported)|(?:not support|unsupported)[^\n]*structured[_ -]?outputs?\b/i.test(
1208
+ messageParts,
1209
+ )
1210
+ ) {
1211
+ return true;
1212
+ }
1213
+ if (model.provider !== "openrouter" || !/^(?:400\s+)?Provider returned error$/i.test(errorMessage)) return false;
1214
+ const body = capturedErrorResponse?.bodyJson;
1215
+ if (body && typeof body === "object" && "error" in body) {
1216
+ const errorBody = body.error;
1217
+ if (errorBody && typeof errorBody === "object" && "metadata" in errorBody) {
1218
+ const metadata = errorBody.metadata;
1219
+ if (metadata && typeof metadata === "object" && "raw" in metadata) {
1220
+ const raw = metadata.raw;
1221
+ if (typeof raw === "string" ? raw.trim().length > 0 : raw != null) return false;
1222
+ }
1223
+ }
1224
+ }
1225
+ return true;
1226
+ }
1227
+
1228
+ function normalizeOpenAIStableId(value: string | undefined, maxLength: number, hashPrefix: string): string | undefined {
1229
+ if (!value || value.length === 0) return undefined;
1230
+ const wellFormed = value.toWellFormed();
1231
+ if (wellFormed.length <= maxLength) return wellFormed;
1232
+ return `${hashPrefix}${Bun.hash(wellFormed).toString(36)}`;
1233
+ }
1234
+
1235
+ export const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string> = new Set([
1236
+ "response.created",
1237
+ "response.output_item.added",
1238
+ "response.reasoning_summary_part.added",
1239
+ "response.reasoning_summary_text.delta",
1240
+ "response.reasoning_summary_text.done",
1241
+ "response.reasoning_summary_part.done",
1242
+ "response.reasoning_text.delta",
1243
+ "response.content_part.added",
1244
+ "response.output_text.delta",
1245
+ "response.refusal.delta",
1246
+ "response.function_call_arguments.delta",
1247
+ "response.function_call_arguments.done",
1248
+ "response.custom_tool_call_input.delta",
1249
+ "response.custom_tool_call_input.done",
1250
+ "response.output_item.done",
1251
+ "response.completed",
1252
+ "response.incomplete",
1253
+ "response.failed",
1254
+ "error",
1255
+ ]);
1256
+
1257
+ export function isOpenAIResponsesProgressEvent(event: unknown): boolean {
1258
+ if (!event || typeof event !== "object") return false;
1259
+ const type = (event as { type?: unknown }).type;
1260
+ return typeof type === "string" && OPENAI_RESPONSES_PROGRESS_EVENT_TYPES.has(type);
1261
+ }
1262
+
1263
+ export function encodeTextSignatureV1(id: string, phase?: TextSignatureV1["phase"]): string {
1264
+ const payload: TextSignatureV1 = { v: 1, id };
1265
+ if (phase) payload.phase = phase;
1266
+ return JSON.stringify(payload);
1267
+ }
1268
+
1269
+ export function parseTextSignature(
1270
+ signature: string | undefined,
1271
+ ): { id: string; phase?: TextSignatureV1["phase"] } | undefined {
1272
+ if (!signature) return undefined;
1273
+ if (signature.startsWith("{")) {
1274
+ try {
1275
+ const parsed = JSON.parse(signature) as Partial<TextSignatureV1>;
1276
+ if (parsed.v === 1 && typeof parsed.id === "string") {
1277
+ if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
1278
+ return { id: parsed.id, phase: parsed.phase };
1279
+ }
1280
+ return { id: parsed.id };
1281
+ }
1282
+ } catch {
1283
+ // Fall through to legacy plain-string handling.
1284
+ }
1285
+ }
1286
+ return { id: signature };
1287
+ }
1288
+
1289
+ export function encodeResponsesToolCallId(callId: string, itemId: string | null | undefined): string {
1290
+ const stableItemId = itemId && itemId.length > 0 ? itemId : `fc_${Bun.hash(callId).toString(36)}`;
1291
+ return `${callId}|${stableItemId}`;
1292
+ }
1293
+
1294
+ export function normalizeResponsesToolCallIdForTransform(
1295
+ id: string,
1296
+ model?: Model<Api>,
1297
+ source?: AssistantMessage,
1298
+ ): string {
1299
+ if (!id.includes("|")) return id;
1300
+ const isForeignToolCall =
1301
+ source != null && model != null && (source.provider !== model.provider || source.api !== model.api);
1302
+ if (isForeignToolCall) {
1303
+ const [callId, itemId] = id.split("|");
1304
+ const normalizeIdPart = (part: string): string => {
1305
+ const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
1306
+ const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
1307
+ return truncated.replace(/_+$/, "");
1308
+ };
1309
+ const normalizedCallId = normalizeIdPart(callId);
1310
+ let normalizedItemId = `fc_${Bun.hash(itemId).toString(36)}`;
1311
+ if (normalizedItemId.length > 64) normalizedItemId = normalizedItemId.slice(0, 64);
1312
+ return `${normalizedCallId}|${normalizedItemId}`;
1313
+ }
1314
+ const normalized = normalizeResponsesToolCallId(id);
1315
+ return `${normalized.callId}|${normalized.itemId}`;
1316
+ }
1317
+
1318
+ type ResponsesToolCallKind = "function" | "custom" | "computer";
1319
+
1320
+ function responsesToolCallKind(type: unknown): ResponsesToolCallKind | undefined {
1321
+ if (type === "function_call") return "function";
1322
+ if (type === "custom_tool_call") return "custom";
1323
+ if (type === "computer_call") return "computer";
1324
+ return undefined;
1325
+ }
1326
+
1327
+ function responsesToolOutputKind(type: unknown): ResponsesToolCallKind | undefined {
1328
+ if (type === "function_call_output") return "function";
1329
+ if (type === "custom_tool_call_output") return "custom";
1330
+ if (type === "computer_call_output") return "computer";
1331
+ return undefined;
1332
+ }
1333
+ function responseInputCallId(item: ResponseInput[number]): string | undefined {
1334
+ if (!("call_id" in item)) return undefined;
1335
+ return typeof item.call_id === "string" ? item.call_id : undefined;
1336
+ }
1337
+
1338
+ export function collectKnownCallIds(messages: ResponseInput): Set<string> {
1339
+ const knownCallIds = new Set<string>();
1340
+ for (const item of messages) {
1341
+ if (responsesToolCallKind(item.type) === undefined) continue;
1342
+ const callId = responseInputCallId(item);
1343
+ if (callId) knownCallIds.add(callId);
1344
+ }
1345
+ return knownCallIds;
1346
+ }
1347
+
1348
+ /** Scan replay items for call_ids that were originally custom tool calls. */
1349
+ export function collectCustomCallIds(messages: ResponseInput): Set<string> {
1350
+ const customCallIds = new Set<string>();
1351
+ for (const item of messages) {
1352
+ if (item.type !== "custom_tool_call") continue;
1353
+ const callId = responseInputCallId(item);
1354
+ if (callId) customCallIds.add(callId);
1355
+ }
1356
+ return customCallIds;
1357
+ }
1358
+
1359
+ /** Scan replay items for call_ids that were originally native computer calls. */
1360
+ export function collectComputerCallIds(messages: ResponseInput): Set<string> {
1361
+ const computerCallIds = new Set<string>();
1362
+ for (const item of messages) {
1363
+ if (item.type !== "computer_call") continue;
1364
+ const callId = responseInputCallId(item);
1365
+ if (callId) computerCallIds.add(callId);
1366
+ }
1367
+ return computerCallIds;
1368
+ }
1369
+
1370
+ /**
1371
+ * Convert orphan `function_call_output` / `custom_tool_call_output` items —
1372
+ * those whose `call_id` has no matching preceding `function_call` /
1373
+ * `custom_tool_call` in the same input — into assistant text notes.
1374
+ *
1375
+ * The Responses API rejects unpaired outputs with
1376
+ * `400 No tool call found for function call output with call_id …`. Orphans
1377
+ * sneak in through two paths today:
1378
+ *
1379
+ * - A previous turn's `providerPayload` snapshot replaces the input array via
1380
+ * the `dt: false` splice (see {@link convertConversationMessages}), wiping
1381
+ * the matching `function_call` while leaving the matching
1382
+ * `function_call_output` queued in a later `toolResult`.
1383
+ * - A locally-rejected tool call (argument-validation failure, hook reject,
1384
+ * aborted turn before the call streamed) produces a tool result without a
1385
+ * `function_call` ever landing in any persisted provider payload.
1386
+ *
1387
+ * Dropping the result loses information the model needs to recover; sending
1388
+ * it as-is 400s the request. Folding it into an assistant `message` preserves
1389
+ * the payload (call_id + truncated output) while staying within the Responses
1390
+ * input grammar. Matches the behavior of {@link transformRequestBody} in the
1391
+ * codex provider — issue #1351 / regression of #472.
1392
+ */
1393
+ export function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput {
1394
+ const precedingCalls = new Set<string>();
1395
+ let repaired: ResponseInput | undefined;
1396
+ for (let index = 0; index < input.length; index++) {
1397
+ const item = input[index];
1398
+ const callKind = responsesToolCallKind(item.type);
1399
+ const callId = responseInputCallId(item);
1400
+ if (callKind && callId) precedingCalls.add(`${callKind}\0${callId}`);
1401
+
1402
+ const outputKind = responsesToolOutputKind(item.type);
1403
+ if (!outputKind || !callId || precedingCalls.has(`${outputKind}\0${callId}`)) {
1404
+ repaired?.push(item);
1405
+ continue;
1406
+ }
1407
+
1408
+ if (!repaired) repaired = input.slice(0, index);
1409
+ const toolName = outputKind === "computer" ? "computer" : "tool";
1410
+ const rawOutput = "output" in item ? item.output : undefined;
1411
+ let text: string;
1412
+ if (typeof rawOutput === "string") text = rawOutput;
1413
+ else if (rawOutput == null) text = "";
1414
+ else {
1415
+ try {
1416
+ text = JSON.stringify(rawOutput);
1417
+ } catch {
1418
+ text = String(rawOutput);
1419
+ }
1420
+ }
1421
+ const ORPHAN_OUTPUT_LIMIT = 16_000;
1422
+ if (text.length > ORPHAN_OUTPUT_LIMIT) text = `${text.slice(0, ORPHAN_OUTPUT_LIMIT)}\n...[truncated]`;
1423
+ repaired.push({
1424
+ type: "message",
1425
+ role: "assistant",
1426
+ content: `[Orphan ${toolName} result; call_id=${callId}]: ${text}`,
1427
+ } as ResponseInput[number]);
1428
+ }
1429
+ return repaired ?? input;
1430
+ }
1431
+
1432
+ /** Placeholder output for a tool call whose result is absent from the input. */
1433
+ const ORPHAN_TOOL_CALL_PLACEHOLDER =
1434
+ "[No tool output recorded: the tool call was interrupted before it produced a result.]";
1435
+
1436
+ /**
1437
+ * Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
1438
+ * for every `function_call` / `custom_tool_call` whose `call_id` has no matching
1439
+ * output later in the same input. The Responses API rejects an unpaired call
1440
+ * with `400 No tool output found for function call …`.
1441
+ *
1442
+ * Orphan calls surface when the user branches/navigates the session tree to a
1443
+ * node that ends on a tool call (the tool-result child is excluded from the
1444
+ * reconstructed history) or when a turn is aborted/crashes after the call
1445
+ * streamed but before its result persisted. Dropping the call would erase the
1446
+ * assistant's action; a placeholder output keeps the call visible so the model
1447
+ * can recover (e.g. re-issue the call). Symmetric to
1448
+ * {@link repairOrphanResponsesToolOutputs}.
1449
+ */
1450
+ export function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput {
1451
+ const laterOutputs = new Set<string>();
1452
+ const orphanIndexes = new Set<number>();
1453
+ for (let index = input.length - 1; index >= 0; index--) {
1454
+ const item = input[index];
1455
+ const callId = responseInputCallId(item);
1456
+ const outputKind = responsesToolOutputKind(item.type);
1457
+ if (outputKind && callId) laterOutputs.add(`${outputKind}\0${callId}`);
1458
+
1459
+ const callKind = responsesToolCallKind(item.type);
1460
+ if (callKind && callId && !laterOutputs.has(`${callKind}\0${callId}`)) orphanIndexes.add(index);
1461
+ }
1462
+ if (orphanIndexes.size === 0) return input;
1463
+
1464
+ const repaired: ResponseInput = [];
1465
+ for (let index = 0; index < input.length; index++) {
1466
+ const item = input[index];
1467
+ if (!orphanIndexes.has(index)) {
1468
+ repaired.push(item);
1469
+ continue;
1470
+ }
1471
+ const kind = responsesToolCallKind(item.type);
1472
+ const callId = responseInputCallId(item);
1473
+ if (!kind || !callId) {
1474
+ repaired.push(item);
1475
+ continue;
1476
+ }
1477
+ if (kind === "computer") {
1478
+ repaired.push({
1479
+ type: "message",
1480
+ role: "assistant",
1481
+ content: `[Computer call interrupted before a screenshot was recorded; call_id=${callId}]`,
1482
+ } as ResponseInput[number]);
1483
+ continue;
1484
+ }
1485
+ repaired.push(item);
1486
+ repaired.push({
1487
+ type: kind === "custom" ? "custom_tool_call_output" : "function_call_output",
1488
+ call_id: callId,
1489
+ output: ORPHAN_TOOL_CALL_PLACEHOLDER,
1490
+ } as ResponseInput[number]);
1491
+ }
1492
+ return repaired;
1493
+ }
1494
+
1495
+ /**
1496
+ * Some Responses backends (notably GitHub Copilot) reject the OpenAI image
1497
+ * `detail: "original"` value with a 400. When the model does not advertise
1498
+ * support for it, degrade `"original"` to `"auto"` so the request still goes
1499
+ * through with the closest valid fidelity instead of failing outright. See #2822.
1500
+ */
1501
+ function clampResponsesImageDetail(
1502
+ detail: ImageContent["detail"],
1503
+ supportsImageDetailOriginal: boolean,
1504
+ ): ResponseInputImage["detail"] {
1505
+ const resolved = detail ?? "auto";
1506
+ return resolved === "original" && !supportsImageDetailOriginal ? "auto" : resolved;
1507
+ }
1508
+
1509
+ export function convertResponsesInputContent(
1510
+ content: string | Array<TextContent | ImageContent>,
1511
+ supportsImages: boolean,
1512
+ supportsImageDetailOriginal: boolean,
1513
+ escapeControlTokens = false,
1514
+ ): ResponseInputContent[] | undefined {
1515
+ if (typeof content === "string") {
1516
+ if (content.trim().length === 0) return undefined;
1517
+ const text = content.toWellFormed();
1518
+ return [
1519
+ {
1520
+ type: "input_text",
1521
+ text: escapeControlTokens ? escapeHarmonyControlTokens(text) : text,
1522
+ } satisfies ResponseInputText,
1523
+ ];
1524
+ }
1525
+
1526
+ const { textBlocks, imageBlocks, omittedImages } = partitionVisionContent(content, supportsImages);
1527
+ const normalizedContent: ResponseInputContent[] = [];
1528
+ for (const item of textBlocks) {
1529
+ const raw = item.text.toWellFormed();
1530
+ const text = escapeControlTokens ? escapeHarmonyControlTokens(raw) : raw;
1531
+ if (text.trim().length === 0) continue;
1532
+ normalizedContent.push({
1533
+ type: "input_text",
1534
+ text,
1535
+ } satisfies ResponseInputText);
1536
+ }
1537
+ for (const item of imageBlocks) {
1538
+ normalizedContent.push({
1539
+ type: "input_image",
1540
+ detail: clampResponsesImageDetail(item.detail, supportsImageDetailOriginal),
1541
+ image_url: `data:${item.mimeType};base64,${item.data}`,
1542
+ } satisfies ResponseInputImage);
1543
+ }
1544
+ if (omittedImages) {
1545
+ normalizedContent.push({
1546
+ type: "input_text",
1547
+ text: NON_VISION_IMAGE_PLACEHOLDER,
1548
+ } satisfies ResponseInputText);
1549
+ }
1550
+ return normalizedContent.length > 0 ? normalizedContent : undefined;
1551
+ }
1552
+
1553
+ /**
1554
+ * Map freeform custom-tool wire names back to the internal tool name for
1555
+ * providers that only accept function_call / function_call_output.
1556
+ * Built once per request; `apply_patch` → `edit` is the OMP default.
1557
+ */
1558
+ function buildCustomToolWireNameMap(tools: readonly Tool[] | undefined): ReadonlyMap<string, string> | undefined {
1559
+ if (!tools?.length) return undefined;
1560
+ const map = new Map<string, string>();
1561
+ for (const tool of tools) {
1562
+ if (tool.customWireName) map.set(tool.customWireName, tool.name);
1563
+ }
1564
+ return map.size > 0 ? map : undefined;
1565
+ }
1566
+
1567
+ function resolveReplayCustomToolName(wireName: string, wireNameMap: ReadonlyMap<string, string> | undefined): string {
1568
+ return wireNameMap?.get(wireName) ?? (wireName === "apply_patch" ? "edit" : wireName);
1569
+ }
1570
+
1571
+ /**
1572
+ * Downgrade OpenAI-only custom tool items when the target model does not
1573
+ * advertise freeform custom tools (`applyPatchToolType === "freeform"`).
1574
+ * No-op (returns the same array reference) when freeform is supported.
1575
+ */
1576
+ function adaptResponsesReplayItemsForModel(
1577
+ input: ResponseInput,
1578
+ supportsCustomToolCalls: boolean,
1579
+ wireNameMap: ReadonlyMap<string, string> | undefined,
1580
+ supportsComputerUse: boolean,
1581
+ ): ResponseInput {
1582
+ if (supportsCustomToolCalls && supportsComputerUse) return input;
1583
+
1584
+ let changed = false;
1585
+ const adapted: ResponseInput = [];
1586
+ for (const item of input) {
1587
+ if (!supportsCustomToolCalls && item.type === "custom_tool_call") {
1588
+ changed = true;
1589
+ adapted.push({
1590
+ type: "function_call",
1591
+ ...(item.id ? { id: item.id } : {}),
1592
+ call_id: item.call_id,
1593
+ name: resolveReplayCustomToolName(item.name, wireNameMap),
1594
+ arguments: JSON.stringify({ input: item.input }),
1595
+ ...(item.namespace ? { namespace: item.namespace } : {}),
1596
+ });
1597
+ continue;
1598
+ }
1599
+ if (!supportsCustomToolCalls && item.type === "custom_tool_call_output") {
1600
+ changed = true;
1601
+ adapted.push({
1602
+ type: "function_call_output",
1603
+ call_id: item.call_id,
1604
+ output: item.output,
1605
+ });
1606
+ continue;
1607
+ }
1608
+ if (!supportsComputerUse && (item.type === "computer_call" || item.type === "computer_call_output")) {
1609
+ changed = true;
1610
+ const callId = responseInputCallId(item) ?? "unknown";
1611
+ adapted.push({
1612
+ type: "message",
1613
+ role: "assistant",
1614
+ content: `[Previous computer ${item.type === "computer_call" ? "call" : "result"}; call_id=${callId}]: ${stringifyJson(item) ?? ""}`,
1615
+ } as ResponseInput[number]);
1616
+ continue;
1617
+ }
1618
+ adapted.push(item);
1619
+ }
1620
+ return changed ? adapted : input;
1621
+ }
1622
+
1623
+ export interface BuildResponsesInputOptions<TApi extends Api> {
1624
+ model: Model<TApi>;
1625
+ context: Context;
1626
+ strictResponsesPairing: boolean;
1627
+ supportsImageDetailOriginal: boolean;
1628
+ systemRole?: "system" | "developer";
1629
+ nativeHistory?: {
1630
+ replay: boolean;
1631
+ filterReasoning: boolean;
1632
+ };
1633
+ includeThinkingSignatures?: boolean;
1634
+ developerStringContent?: boolean;
1635
+ repairOrphanOutputs?: boolean;
1636
+ /** Preserve assistant message item IDs from text signatures during fallback replay. */
1637
+ preserveAssistantMessageIds?: boolean;
1638
+ }
1639
+
1640
+ /**
1641
+ * Escape reserved Harmony control tokens in the free-text fields of replayed
1642
+ * Responses input items: user/developer/system text, tool-result output,
1643
+ * assistant message text, and tool-call payloads.
1644
+ *
1645
+ * Tool-call items are covered deliberately. The original #6913 fix skipped
1646
+ * model-owned items on the theory that they carry no client data — but a model
1647
+ * legitimately writing *about* Harmony samples `<|channel|>` etc. into its own
1648
+ * `function_call.arguments`, and a full-transcript replay (stale or blocked
1649
+ * previous_response_id, provider fallback) feeds those bytes back as input,
1650
+ * which gpt-5.x reject with invalid_prompt / "Request blocked", permanently
1651
+ * poisoning the session. `arguments` is a JSON document, so it uses
1652
+ * {@link escapeHarmonyControlTokensInJson} to stay parseable. Reasoning items
1653
+ * are left untouched: `encrypted_content` is opaque and plaintext summaries
1654
+ * are never rendered back into the prompt.
1655
+ *
1656
+ * Native history replay pushes stored `providerPayload` items straight onto the
1657
+ * wire, bypassing {@link convertResponsesInputContent}; without this a stored
1658
+ * `input_text` carrying `<|channel|>analysis` still reaches gpt-5.x raw (#6913).
1659
+ * Callers gate on {@link isHarmonyDialectModel}. Items are copied, not mutated.
1660
+ */
1661
+ export function escapeReplayedControlTokens(items: ResponseInput): ResponseInput {
1662
+ return items.map(item => {
1663
+ if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
1664
+ return typeof item.output === "string" ? { ...item, output: escapeHarmonyControlTokens(item.output) } : item;
1665
+ }
1666
+ if (item.type === "function_call") {
1667
+ return typeof item.arguments === "string"
1668
+ ? { ...item, arguments: escapeHarmonyControlTokensInJson(item.arguments) }
1669
+ : item;
1670
+ }
1671
+ if (item.type === "custom_tool_call") {
1672
+ return typeof item.input === "string" ? { ...item, input: escapeHarmonyControlTokens(item.input) } : item;
1673
+ }
1674
+ // EasyInputMessage may omit `type` (`{ role, content }`); the responses
1675
+ // server persists it verbatim, so treat missing type as a message too.
1676
+ const isTypedMessage = item.type === "message" || item.type === undefined;
1677
+ if (!isTypedMessage || !("role" in item) || !("content" in item)) return item;
1678
+ if (item.role === "assistant") {
1679
+ // Assistant output text is model-owned but equally capable of carrying
1680
+ // control tokens as data. `status` discriminates ResponseOutputMessage.
1681
+ if ("status" in item && Array.isArray(item.content)) {
1682
+ return {
1683
+ ...item,
1684
+ content: item.content.map(part =>
1685
+ part.type === "output_text"
1686
+ ? { ...part, text: escapeHarmonyControlTokens(part.text) }
1687
+ : part.type === "refusal"
1688
+ ? { ...part, refusal: escapeHarmonyControlTokens(part.refusal) }
1689
+ : part,
1690
+ ),
1691
+ };
1692
+ }
1693
+ return item;
1694
+ }
1695
+ const content = item.content;
1696
+ if (typeof content === "string") {
1697
+ return { ...item, content: escapeHarmonyControlTokens(content) };
1698
+ }
1699
+ if (Array.isArray(content)) {
1700
+ return {
1701
+ ...item,
1702
+ content: content.map(part =>
1703
+ part.type === "input_text" ? { ...part, text: escapeHarmonyControlTokens(part.text) } : part,
1704
+ ),
1705
+ };
1706
+ }
1707
+ return item;
1708
+ });
1709
+ }
1710
+
1711
+ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput {
1712
+ const messages: ResponseInput = [];
1713
+ const systemPrompts = options.systemRole ? normalizeSystemPrompts(options.context.systemPrompt) : [];
1714
+ for (const systemPrompt of systemPrompts) {
1715
+ messages.push({ role: options.systemRole as "system" | "developer", content: systemPrompt });
1716
+ }
1717
+
1718
+ // Compat is resolved by the catalog (e.g. Copilot / xai-oauth reject
1719
+ // `detail: "original"`). Do not re-branch on provider id here.
1720
+ const supportsImageDetailOriginal = options.supportsImageDetailOriginal;
1721
+ // Freeform custom tools (`custom_tool_call`) only when the catalog says so;
1722
+ // same gate as tool conversion (`applyPatchToolType === "freeform"`).
1723
+ const supportsCustomToolCalls = options.model.applyPatchToolType === "freeform";
1724
+ const customToolWireNameMap = supportsCustomToolCalls
1725
+ ? undefined
1726
+ : buildCustomToolWireNameMap(options.context.tools);
1727
+ let knownCallIds = new Set<string>();
1728
+ const customCallIds = new Set<string>();
1729
+ const computerCallIds = new Set<string>();
1730
+ const transformedMessages = transformMessages(
1731
+ options.context.messages,
1732
+ options.model,
1733
+ normalizeResponsesToolCallIdForTransform,
1734
+ );
1735
+ const filterReasoning = <T extends { type?: string }>(items: T[]): T[] =>
1736
+ options.nativeHistory?.filterReasoning ? items.filter(item => item?.type !== "reasoning") : items;
1737
+ const includeThinkingSignatures = options.includeThinkingSignatures ?? options.nativeHistory?.replay ?? true;
1738
+ // Harmony-server models (gpt-5.x) reject requests whose input data reproduces
1739
+ // reserved control-token spellings; escape the transport copy of untrusted
1740
+ // user/tool text so ordinary docs, code, or grep results cannot poison the
1741
+ // session (#6913). The persisted transcript is never touched.
1742
+ const escapeControlTokens = isHarmonyDialectModel(options.model);
1743
+
1744
+ let msgIndex = 0;
1745
+ for (const msg of transformedMessages) {
1746
+ if (msg.role === "user" || msg.role === "developer") {
1747
+ const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
1748
+ const historyItems = options.nativeHistory
1749
+ ? getOpenAIResponsesHistoryItems(providerPayload, options.model.provider)
1750
+ : undefined;
1751
+ const shouldReplayPayloadItems =
1752
+ options.nativeHistory?.replay ||
1753
+ (historyItems?.some(item => {
1754
+ if (!item || typeof item !== "object") return false;
1755
+ const candidate = item as { type?: unknown };
1756
+ return candidate.type === "compaction" || candidate.type === "compaction_summary";
1757
+ }) ??
1758
+ false);
1759
+ if (historyItems && shouldReplayPayloadItems) {
1760
+ const sanitizedItems = sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems), {
1761
+ supportsImageDetailOriginal,
1762
+ supportsComputerUse: options.model.supportsComputerUse === true,
1763
+ });
1764
+ const replayItems = adaptResponsesReplayItemsForModel(
1765
+ sanitizedItems,
1766
+ supportsCustomToolCalls,
1767
+ customToolWireNameMap,
1768
+ options.model.supportsComputerUse === true,
1769
+ );
1770
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
1771
+ knownCallIds = collectKnownCallIds(messages);
1772
+ for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
1773
+ for (const id of collectComputerCallIds(messages)) computerCallIds.add(id);
1774
+ msgIndex++;
1775
+ continue;
1776
+ }
1777
+ const content = convertResponsesInputContent(
1778
+ msg.content,
1779
+ options.model.input.includes("image"),
1780
+ supportsImageDetailOriginal,
1781
+ escapeControlTokens,
1782
+ );
1783
+ if (!content) continue;
1784
+ const developerText =
1785
+ options.developerStringContent && msg.role === "developer" && typeof msg.content === "string"
1786
+ ? msg.content.toWellFormed()
1787
+ : undefined;
1788
+ messages.push({
1789
+ role: "user",
1790
+ content:
1791
+ developerText !== undefined
1792
+ ? escapeControlTokens
1793
+ ? escapeHarmonyControlTokens(developerText)
1794
+ : developerText
1795
+ : content,
1796
+ });
1797
+ } else if (msg.role === "assistant") {
1798
+ const assistantMsg = msg as AssistantMessage;
1799
+ // Providers replay stale native items even when the current request has
1800
+ // disabled native replay (cold session state, filter policy). Consult
1801
+ // the payload sanitizer directly so hidden-empty turns are recognized
1802
+ // on both the warm and cold paths.
1803
+ const providerPayload =
1804
+ assistantMsg.api === options.model.api && assistantMsg.model === options.model.id
1805
+ ? getOpenAIResponsesHistoryPayload(
1806
+ assistantMsg.providerPayload,
1807
+ options.model.provider,
1808
+ assistantMsg.provider,
1809
+ )
1810
+ : undefined;
1811
+ const nativeReplayEnabled = options.nativeHistory?.replay === true;
1812
+ const historyItems = providerPayload?.items;
1813
+ let suppressHiddenEmptyFallback = false;
1814
+ if (historyItems) {
1815
+ const rawSanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
1816
+ filterReasoning(historyItems),
1817
+ {
1818
+ supportsImageDetailOriginal,
1819
+ supportsComputerUse: options.model.supportsComputerUse === true,
1820
+ },
1821
+ );
1822
+ const sanitizedHistoryItems = rawSanitizedHistoryItems
1823
+ ? adaptResponsesReplayItemsForModel(
1824
+ rawSanitizedHistoryItems,
1825
+ supportsCustomToolCalls,
1826
+ customToolWireNameMap,
1827
+ options.model.supportsComputerUse === true,
1828
+ )
1829
+ : undefined;
1830
+ if (nativeReplayEnabled && sanitizedHistoryItems) {
1831
+ // Model-owned replay items can carry reserved control-token
1832
+ // spellings as data (the model writing *about* Harmony); escape the
1833
+ // transport copy just like client turns.
1834
+ const wireItems = escapeControlTokens
1835
+ ? escapeReplayedControlTokens(sanitizedHistoryItems)
1836
+ : sanitizedHistoryItems;
1837
+ if (providerPayload?.dt) {
1838
+ messages.push(...wireItems);
1839
+ } else {
1840
+ messages.splice(0, messages.length, ...wireItems);
1841
+ customCallIds.clear();
1842
+ computerCallIds.clear();
1843
+ }
1844
+ knownCallIds = collectKnownCallIds(messages);
1845
+ for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
1846
+ for (const id of collectComputerCallIds(messages)) computerCallIds.add(id);
1847
+ msgIndex++;
1848
+ continue;
1849
+ }
1850
+ if (!sanitizedHistoryItems) suppressHiddenEmptyFallback = true;
1851
+ }
1852
+
1853
+ const convertedOutputItems = convertResponsesAssistantMessage(
1854
+ assistantMsg,
1855
+ options.model,
1856
+ msgIndex,
1857
+ knownCallIds,
1858
+ suppressHiddenEmptyFallback ? false : includeThinkingSignatures,
1859
+ customCallIds,
1860
+ options.preserveAssistantMessageIds,
1861
+ supportsCustomToolCalls,
1862
+ customToolWireNameMap,
1863
+ computerCallIds,
1864
+ );
1865
+ const outputItems = suppressHiddenEmptyFallback
1866
+ ? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
1867
+ : convertedOutputItems;
1868
+ if (outputItems.length === 0) continue;
1869
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
1870
+ } else if (msg.role === "toolResult") {
1871
+ appendResponsesToolResultMessages(
1872
+ messages,
1873
+ msg,
1874
+ options.model,
1875
+ options.strictResponsesPairing,
1876
+ supportsImageDetailOriginal,
1877
+ knownCallIds,
1878
+ customCallIds,
1879
+ supportsCustomToolCalls,
1880
+ computerCallIds,
1881
+ );
1882
+ }
1883
+ msgIndex++;
1884
+ }
1885
+
1886
+ const withRepairedOutputs = options.repairOrphanOutputs ? repairOrphanResponsesToolOutputs(messages) : messages;
1887
+ const withRepairedCalls = repairOrphanResponsesToolCalls(withRepairedOutputs);
1888
+ return stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay(withRepairedCalls);
1889
+ }
1890
+
1891
+ type ResponsesReplayAssistantMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
1892
+
1893
+ function parseResponseReasoningReplayItem(signature: string | undefined): ResponseReasoningItem | undefined {
1894
+ if (!signature) return undefined;
1895
+ try {
1896
+ const parsed = JSON.parse(signature) as unknown;
1897
+ if (!parsed || typeof parsed !== "object") return undefined;
1898
+ if (!("type" in parsed) || parsed.type !== "reasoning") return undefined;
1899
+ if (!("id" in parsed) || typeof parsed.id !== "string") return undefined;
1900
+ return parsed as ResponseReasoningItem;
1901
+ } catch {
1902
+ return undefined;
1903
+ }
1904
+ }
1905
+
1906
+ export function convertResponsesAssistantMessage<TApi extends Api>(
1907
+ assistantMsg: AssistantMessage,
1908
+ model: Model<TApi>,
1909
+ msgIndex: number,
1910
+ knownCallIds: Set<string>,
1911
+ includeThinkingSignatures = true,
1912
+ customCallIds?: Set<string>,
1913
+ preserveMessageIds = false,
1914
+ supportsCustomToolCalls = true,
1915
+ customToolWireNameMap?: ReadonlyMap<string, string>,
1916
+ computerCallIds?: Set<string>,
1917
+ ): ResponseInput {
1918
+ const outputItems: ResponseInput = [];
1919
+ let unsignedTextBlocks = 0;
1920
+ const hasReplayableReasoningItem =
1921
+ includeThinkingSignatures &&
1922
+ assistantMsg.stopReason !== "error" &&
1923
+ assistantMsg.content.some(
1924
+ block => block.type === "thinking" && parseResponseReasoningReplayItem(block.thinkingSignature) !== undefined,
1925
+ );
1926
+ const isDifferentModel =
1927
+ assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
1928
+
1929
+ for (const block of assistantMsg.content) {
1930
+ if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
1931
+ if (!includeThinkingSignatures) {
1932
+ continue;
1933
+ }
1934
+ const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
1935
+ if (reasoningItem) outputItems.push(reasoningItem);
1936
+ continue;
1937
+ }
1938
+
1939
+ if (block.type === "text") {
1940
+ const parsedSignature = parseTextSignature(block.textSignature);
1941
+ let msgId = parsedSignature?.id;
1942
+ if (!msgId) {
1943
+ if (hasReplayableReasoningItem) {
1944
+ // Distinct ids per unsigned block: several text blocks in one message
1945
+ // (cross-provider replay downgrades thinking → text) must not share an id.
1946
+ msgId = unsignedTextBlocks === 0 ? `msg_${msgIndex}` : `msg_${msgIndex}_${unsignedTextBlocks}`;
1947
+ unsignedTextBlocks += 1;
1948
+ }
1949
+ } else if (!preserveMessageIds && !hasReplayableReasoningItem) {
1950
+ // Without the matching reasoning item the server rejects replayed
1951
+ // item ids (#4173) — drop them regardless of shape, including
1952
+ // legacy plain-string signatures that would otherwise fall into
1953
+ // the >64-char hash branch and fabricate a bogus msg_ id.
1954
+ msgId = undefined;
1955
+ } else if (msgId.length > 64) {
1956
+ msgId = `msg_${Bun.hash(msgId).toString(36)}`;
1957
+ }
1958
+ const messageItem: ResponsesReplayAssistantMessage = {
1959
+ type: "message",
1960
+ role: "assistant",
1961
+ content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
1962
+ status: "completed",
1963
+ ...(msgId ? { id: msgId } : {}),
1964
+ ...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}),
1965
+ };
1966
+ outputItems.push(messageItem as ResponseInput[number]);
1967
+ continue;
1968
+ }
1969
+
1970
+ if (block.type !== "toolCall") {
1971
+ continue;
1972
+ }
1973
+
1974
+ if (block.providerMetadata?.type === "computer") {
1975
+ if (model.supportsComputerUse !== true) {
1976
+ const callId = normalizeResponsesToolCallId(block.id, "ctc").callId;
1977
+ outputItems.push({
1978
+ type: "message",
1979
+ role: "assistant",
1980
+ content: `[Previous computer call; call_id=${callId}]: ${stringifyJson(block.providerMetadata.actions) ?? ""}`,
1981
+ } as ResponseInput[number]);
1982
+ continue;
1983
+ }
1984
+ const normalized = normalizeResponsesToolCallId(block.id, "ctc");
1985
+ knownCallIds.add(normalized.callId);
1986
+ computerCallIds?.add(normalized.callId);
1987
+ outputItems.push({
1988
+ type: "computer_call",
1989
+ id: block.providerMetadata.providerItemId,
1990
+ call_id: normalized.callId,
1991
+ actions: structuredCloneJSON(block.providerMetadata.actions),
1992
+ pending_safety_checks: structuredCloneJSON(block.providerMetadata.pendingSafetyChecks),
1993
+ status: "completed",
1994
+ } as ResponseInput[number]);
1995
+ continue;
1996
+ }
1997
+ const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
1998
+ let itemId: string | undefined = normalized.itemId;
1999
+ if (
2000
+ !hasReplayableReasoningItem &&
2001
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
2002
+ ) {
2003
+ itemId = undefined;
2004
+ } else if (
2005
+ isDifferentModel &&
2006
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
2007
+ ) {
2008
+ itemId = undefined;
2009
+ }
2010
+ knownCallIds.add(normalized.callId);
2011
+ if (block.customWireName && supportsCustomToolCalls) {
2012
+ const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
2013
+ customCallIds?.add(normalized.callId);
2014
+ outputItems.push({
2015
+ type: "custom_tool_call",
2016
+ ...(itemId ? { id: itemId } : {}),
2017
+ call_id: normalized.callId,
2018
+ name: block.customWireName,
2019
+ input: rawInput,
2020
+ } as ResponseInput[number]);
2021
+ continue;
2022
+ }
2023
+ const functionName =
2024
+ block.customWireName && !supportsCustomToolCalls
2025
+ ? resolveReplayCustomToolName(block.customWireName, customToolWireNameMap)
2026
+ : block.name;
2027
+ outputItems.push({
2028
+ type: "function_call",
2029
+ ...(itemId ? { id: itemId } : {}),
2030
+ call_id: normalized.callId,
2031
+ name: functionName,
2032
+ arguments: stringifyJson(block.arguments) ?? "null",
2033
+ });
2034
+ }
2035
+
2036
+ return outputItems;
2037
+ }
2038
+
2039
+ const syntheticToolImageMessages = new WeakSet<object>();
2040
+
2041
+ function insertResponsesToolOutput(messages: ResponseInput, output: ResponseInput[number]): void {
2042
+ let index = messages.length;
2043
+ while (index > 0) {
2044
+ const previous = messages[index - 1];
2045
+ if (typeof previous !== "object" || previous === null || !syntheticToolImageMessages.has(previous)) {
2046
+ break;
2047
+ }
2048
+ index -= 1;
2049
+ }
2050
+ messages.splice(index, 0, output);
2051
+ }
2052
+
2053
+ /** Appends one tool result while keeping consecutive outputs ahead of its synthetic image messages. */
2054
+ export function appendResponsesToolResultMessages<TApi extends Api>(
2055
+ messages: ResponseInput,
2056
+ toolResult: ToolResultMessage,
2057
+ model: Model<TApi>,
2058
+ strictResponsesPairing: boolean,
2059
+ supportsImageDetailOriginal: boolean,
2060
+ knownCallIds: ReadonlySet<string>,
2061
+ customCallIds?: ReadonlySet<string>,
2062
+ supportsCustomToolCalls = true,
2063
+ computerCallIds?: ReadonlySet<string>,
2064
+ ): void {
2065
+ const supportsImages = model.input.includes("image");
2066
+ const textResult = toolResult.content
2067
+ .filter((block): block is TextContent => block.type === "text")
2068
+ .map(block => block.text)
2069
+ .join("\n");
2070
+ const hasImages = toolResult.content.some((block): block is ImageContent => block.type === "image");
2071
+ const omittedImages = hasImages && !supportsImages;
2072
+ const normalized = normalizeResponsesToolCallId(toolResult.toolCallId);
2073
+ // "(see attached image)" is only truthful when the result actually carries
2074
+ // images (they ride as a separate user message on the Responses API). A
2075
+ // genuinely empty text result (empty file read, silent tool) must stay
2076
+ // empty — the placeholder sent models chasing an attachment that never
2077
+ // existed.
2078
+ const rawOutput = (
2079
+ omittedImages
2080
+ ? joinTextWithImagePlaceholder(textResult, true)
2081
+ : textResult.length > 0
2082
+ ? textResult
2083
+ : hasImages
2084
+ ? "(see attached image)"
2085
+ : ""
2086
+ ).toWellFormed();
2087
+ // Harmony-server models reject reserved control-token spellings even as tool
2088
+ // data; escape the transport copy so a grep/read result cannot poison the
2089
+ // session (#6913). Covers every downstream branch that consumes `output`.
2090
+ const output = isHarmonyDialectModel(model) ? escapeHarmonyControlTokens(rawOutput) : rawOutput;
2091
+ if (toolResult.providerMetadata?.type === "computer" && model.supportsComputerUse !== true) {
2092
+ messages.push({
2093
+ type: "message",
2094
+ role: "assistant",
2095
+ content: `[Previous computer result; call_id=${normalized.callId}]: ${stringifyJson(toolResult.providerMetadata.screenshot) ?? ""}`,
2096
+ } as ResponseInput[number]);
2097
+ return;
2098
+ }
2099
+ if (computerCallIds?.has(normalized.callId)) {
2100
+ if (toolResult.providerMetadata?.type !== "computer") {
2101
+ const limit = 16_000;
2102
+ const noteText = output.length > limit ? `${output.slice(0, limit)}\n...[truncated]` : output;
2103
+ messages.push({
2104
+ type: "message",
2105
+ role: "assistant",
2106
+ content: `[Computer tool failed before a screenshot was produced; call_id=${normalized.callId}]: ${noteText}`,
2107
+ } as ResponseInput[number]);
2108
+ return;
2109
+ }
2110
+ if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
2111
+ messages.push({
2112
+ type: "message",
2113
+ role: "assistant",
2114
+ content: `[Orphan computer result; call_id=${normalized.callId}]`,
2115
+ } as ResponseInput[number]);
2116
+ return;
2117
+ }
2118
+ insertResponsesToolOutput(messages, {
2119
+ type: "computer_call_output",
2120
+ call_id: normalized.callId,
2121
+ output: structuredCloneJSON(toolResult.providerMetadata.screenshot),
2122
+ acknowledged_safety_checks: structuredCloneJSON(toolResult.providerMetadata.acknowledgedSafetyChecks),
2123
+ } as ResponseInput[number]);
2124
+ return;
2125
+ }
2126
+ if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
2127
+ // Strict backends (Azure, Copilot) reject unpaired outputs outright, but
2128
+ // silently dropping the result loses information the model needs. Fold it
2129
+ // into an assistant note instead (same shape as repairOrphanResponsesToolOutputs).
2130
+ const limit = 16_000;
2131
+ const noteText = output.length > limit ? `${output.slice(0, limit)}\n...[truncated]` : output;
2132
+ messages.push({
2133
+ type: "message",
2134
+ role: "assistant",
2135
+ content: `[Orphan ${toolResult.toolName || "tool"} result; call_id=${normalized.callId}]: ${noteText}`,
2136
+ } as ResponseInput[number]);
2137
+ return;
2138
+ }
2139
+ if (supportsCustomToolCalls && customCallIds?.has(normalized.callId)) {
2140
+ insertResponsesToolOutput(messages, {
2141
+ type: "custom_tool_call_output",
2142
+ call_id: normalized.callId,
2143
+ output,
2144
+ } as ResponseInput[number]);
2145
+ } else {
2146
+ insertResponsesToolOutput(messages, {
2147
+ type: "function_call_output",
2148
+ call_id: normalized.callId,
2149
+ output,
2150
+ });
2151
+ }
2152
+
2153
+ if (!hasImages || !supportsImages) {
2154
+ return;
2155
+ }
2156
+
2157
+ const contentParts: ResponseInputContent[] = [
2158
+ { type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText,
2159
+ ];
2160
+ for (const block of toolResult.content) {
2161
+ if (block.type === "image") {
2162
+ contentParts.push({
2163
+ type: "input_image",
2164
+ detail: clampResponsesImageDetail(block.detail, supportsImageDetailOriginal),
2165
+ image_url: `data:${block.mimeType};base64,${block.data}`,
2166
+ } satisfies ResponseInputImage);
2167
+ }
2168
+ }
2169
+ const imageMessage = { role: "user", content: contentParts } satisfies ResponseInput[number];
2170
+ syntheticToolImageMessages.add(imageMessage);
2171
+ messages.push(imageMessage);
2172
+ }
2173
+
2174
+ /**
2175
+ * Per-block accumulation helpers shared by the two Responses decode loops —
2176
+ * {@link processResponsesStream} (generic Responses) and the Codex stream
2177
+ * handler in `openai-codex-responses.ts`. Each endpoint keeps its own
2178
+ * item-routing, terminal handling, and transport bookkeeping; these own only
2179
+ * the leaf mutations on an already-resolved open block, so the
2180
+ * append/parse/finalize logic lives in exactly one place. The caller passes the
2181
+ * `contentIndex` its router resolved (generic uses `output.content.indexOf`;
2182
+ * Codex uses the open item's recorded index) so the emitted stream events match
2183
+ * each decoder's existing behavior byte-for-byte.
2184
+ */
2185
+ type ResponsesToolCallBlock = ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number };
2186
+
2187
+ export function appendReasoningSummaryPart(
2188
+ item: ResponseReasoningItem,
2189
+ part: ResponseReasoningItem["summary"][number],
2190
+ ): void {
2191
+ item.summary = item.summary || [];
2192
+ item.summary.push(part);
2193
+ }
2194
+
2195
+ /**
2196
+ * Response-global accumulator for the sequential-cutoff summary contract.
2197
+ *
2198
+ * Summary indices are cumulative across ALL reasoning items in a response:
2199
+ * each new reasoning item replays the previous item's last completed section
2200
+ * (`.done` at index N-1) before streaming its own, and replay-only items may
2201
+ * add nothing new. Folding per item would re-emit every replayed section, so
2202
+ * the canonical summary and the emitted text span items and live here.
2203
+ */
2204
+ export interface SequentialCutoffSummaryState {
2205
+ /** Latest full text per response-global summary index. */
2206
+ summary: ResponseReasoningItem["summary"];
2207
+ /** Canonical summary text already emitted as thinking deltas across all blocks. */
2208
+ emitted: string;
2209
+ }
2210
+
2211
+ export function createSequentialCutoffSummaryState(): SequentialCutoffSummaryState {
2212
+ return { summary: [], emitted: "" };
2213
+ }
2214
+
2215
+ // Sequential-cutoff streams may repeat the full canonical summary as later parts.
2216
+ function foldReasoningSummary(parts: ResponseReasoningItem["summary"] | undefined): string {
2217
+ if (!parts) return "";
2218
+ let canonical = "";
2219
+ for (const part of parts) {
2220
+ const text = part.text;
2221
+ if (!text || text === canonical) continue;
2222
+ const extendsCanonical = text.startsWith(canonical) && text[canonical.length] === "\n";
2223
+ canonical = !canonical || extendsCanonical ? text : `${canonical}\n\n${text}`;
2224
+ }
2225
+ return canonical;
2226
+ }
2227
+
2228
+ /** Chooses final reasoning text without making sequential-cutoff results disagree with emitted deltas. */
2229
+ export function finalizeReasoningThinking(
2230
+ item: ResponseReasoningItem,
2231
+ streamedThinking: string,
2232
+ cutoff?: SequentialCutoffSummaryState,
2233
+ ): string {
2234
+ if (cutoff) return finalizeCutoffReasoningThinking(item, streamedThinking, cutoff);
2235
+ const summaryThinking = item.summary?.map(part => part.text).join("\n\n") ?? "";
2236
+ if (summaryThinking) return summaryThinking;
2237
+ const contentThinking = item.content?.[0]?.type === "reasoning_text" ? (item.content[0].text ?? "") : "";
2238
+ return contentThinking || streamedThinking || "";
2239
+ }
2240
+
2241
+ function finalizeCutoffReasoningThinking(
2242
+ item: ResponseReasoningItem,
2243
+ streamedThinking: string,
2244
+ cutoff: SequentialCutoffSummaryState,
2245
+ ): string {
2246
+ // The block's streamed deltas are authoritative: final text must never
2247
+ // disagree with what delta consumers already rendered.
2248
+ if (streamedThinking) return streamedThinking;
2249
+ const summaryThinking = foldReasoningSummary(item.summary);
2250
+ if (summaryThinking) {
2251
+ // The done payload carries the response-cumulative summary. Emit only
2252
+ // what no earlier block already emitted; replay-only items finalize empty.
2253
+ if (cutoff.emitted.startsWith(summaryThinking)) return "";
2254
+ if (!cutoff.emitted || summaryThinking.startsWith(cutoff.emitted)) {
2255
+ const suffix = summaryThinking.slice(cutoff.emitted.length).replace(/^\n+/, "");
2256
+ // Adopt the payload as canonical so later items cannot replay this text.
2257
+ cutoff.summary = item.summary?.map(part => ({ ...part })) ?? [];
2258
+ cutoff.emitted = summaryThinking;
2259
+ return suffix;
2260
+ }
2261
+ // Diverged from streamed text — the deltas already shown win.
2262
+ return "";
2263
+ }
2264
+ return item.content?.[0]?.type === "reasoning_text" ? (item.content[0].text ?? "") : "";
2265
+ }
2266
+
2267
+ export function appendReasoningSummaryTextDelta(
2268
+ item: ResponseReasoningItem,
2269
+ block: ThinkingContent,
2270
+ delta: string,
2271
+ stream: AssistantMessageEventStream,
2272
+ output: AssistantMessage,
2273
+ contentIndex: number,
2274
+ ): void {
2275
+ item.summary = item.summary || [];
2276
+ const lastPart = item.summary[item.summary.length - 1];
2277
+ if (!lastPart) return;
2278
+ block.thinking += delta;
2279
+ lastPart.text += delta;
2280
+ stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
2281
+ }
2282
+
2283
+ export function appendReasoningSummaryPartDone(
2284
+ item: ResponseReasoningItem,
2285
+ block: ThinkingContent,
2286
+ stream: AssistantMessageEventStream,
2287
+ output: AssistantMessage,
2288
+ contentIndex: number,
2289
+ ): void {
2290
+ item.summary = item.summary || [];
2291
+ const lastPart = item.summary[item.summary.length - 1];
2292
+ if (!lastPart) return;
2293
+ block.thinking += "\n\n";
2294
+ lastPart.text += "\n\n";
2295
+ stream.push({ type: "thinking_delta", contentIndex, delta: "\n\n", partial: output });
2296
+ }
2297
+
2298
+ /**
2299
+ * Applies an atomic `response.reasoning_summary_text.done` snapshot.
2300
+ *
2301
+ * Sequential-cutoff summary indices are response-global: later reasoning items
2302
+ * replay earlier sections, resend the accumulated summary as one part, or
2303
+ * complete without new sections. The canonical summary is rebuilt in `state`
2304
+ * (spanning items) and only its append-only suffix is emitted into the current
2305
+ * block. Divergent corrections stay buffered until finalization so delta
2306
+ * consumers never receive suffixes based on unseen replacement text.
2307
+ */
2308
+ export function applyReasoningSummaryDone(
2309
+ state: SequentialCutoffSummaryState,
2310
+ block: ThinkingContent,
2311
+ text: string,
2312
+ summaryIndex: number,
2313
+ stream: AssistantMessageEventStream,
2314
+ output: AssistantMessage,
2315
+ contentIndex: number,
2316
+ ): void {
2317
+ while (state.summary.length <= summaryIndex) {
2318
+ state.summary.push({ type: "summary_text", text: "" });
2319
+ }
2320
+ state.summary[summaryIndex].text = text;
2321
+ const after = foldReasoningSummary(state.summary);
2322
+ if (!after.startsWith(state.emitted)) return;
2323
+ let delta = after.slice(state.emitted.length);
2324
+ if (!delta) return;
2325
+ state.emitted = after;
2326
+ // A fresh block starts a new section: drop the inter-section separator so
2327
+ // each thinking block stands alone.
2328
+ if (!block.thinking) delta = delta.replace(/^\n+/, "");
2329
+ if (!delta) return;
2330
+ block.thinking += delta;
2331
+ stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
2332
+ }
2333
+
2334
+ export function appendMessageContentPart(
2335
+ item: ResponseOutputMessage,
2336
+ part: ResponseContentPartAddedEvent["part"] | undefined,
2337
+ ): void {
2338
+ item.content = item.content || [];
2339
+ if (part && (part.type === "output_text" || part.type === "refusal")) {
2340
+ item.content.push(part);
2341
+ }
2342
+ }
2343
+
2344
+ export function appendMessageTextDelta(
2345
+ item: ResponseOutputMessage,
2346
+ block: TextContent,
2347
+ delta: string,
2348
+ stream: AssistantMessageEventStream,
2349
+ output: AssistantMessage,
2350
+ contentIndex: number,
2351
+ partType: "output_text" | "refusal",
2352
+ ): void {
2353
+ item.content = item.content || [];
2354
+ let lastPart = item.content[item.content.length - 1];
2355
+ if (lastPart?.type !== partType) {
2356
+ // `content_part.added` never arrived (lossy proxy) — synthesize the part
2357
+ // so live text still streams instead of freezing until output_item.done.
2358
+ lastPart =
2359
+ partType === "output_text"
2360
+ ? { type: "output_text", text: "", annotations: [] }
2361
+ : { type: "refusal", refusal: "" };
2362
+ item.content.push(lastPart);
2363
+ }
2364
+ block.text += delta;
2365
+ if (lastPart.type === "output_text") {
2366
+ lastPart.text += delta;
2367
+ } else {
2368
+ lastPart.refusal += delta;
2369
+ }
2370
+ stream.push({ type: "text_delta", contentIndex, delta, partial: output });
2371
+ }
2372
+ /** Chooses final message text while treating non-empty terminal content as authoritative. */
2373
+ export function finalizeMessageText(item: ResponseOutputMessage, streamedText: string): string {
2374
+ if (!item.content?.length) return streamedText || "";
2375
+ return item.content.map(part => (part.type === "output_text" ? (part.text ?? "") : (part.refusal ?? ""))).join("");
2376
+ }
2377
+
2378
+ export function accumulateToolCallArgumentsDelta(
2379
+ block: ResponsesToolCallBlock,
2380
+ delta: string,
2381
+ stream: AssistantMessageEventStream,
2382
+ output: AssistantMessage,
2383
+ contentIndex: number,
2384
+ ): void {
2385
+ block[kStreamingPartialJson] += delta;
2386
+ const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
2387
+ if (throttled) {
2388
+ block.arguments = throttled.value;
2389
+ block[kStreamingLastParseLen] = throttled.parsedLen;
2390
+ }
2391
+ stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
2392
+ }
2393
+
2394
+ /**
2395
+ * Finalize streamed function-call arguments from the authoritative `.done`
2396
+ * payload. The caller owns the `argumentsDone` flag (generic Responses sets it;
2397
+ * Codex's block shape has no such field), so this only rewrites `arguments` and
2398
+ * drops the transient accumulation fields.
2399
+ */
2400
+ export function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void {
2401
+ block[kStreamingPartialJson] = args;
2402
+ block.arguments = parseStreamingJson(block[kStreamingPartialJson]);
2403
+ clearStreamingPartialJson(block);
2404
+ }
2405
+
2406
+ export function accumulateCustomToolCallInputDelta(
2407
+ block: ResponsesToolCallBlock,
2408
+ delta: string,
2409
+ stream: AssistantMessageEventStream,
2410
+ output: AssistantMessage,
2411
+ contentIndex: number,
2412
+ ): void {
2413
+ block[kStreamingPartialJson] += delta;
2414
+ block.arguments = { input: block[kStreamingPartialJson] };
2415
+ stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
2416
+ }
2417
+
2418
+ export function finalizeCustomToolCallInputDone(block: ResponsesToolCallBlock, input: string): void {
2419
+ block.arguments = { input };
2420
+ }
2421
+
2422
+ type OpenAIResponsesTerminalStreamEvent =
2423
+ | Extract<ResponseStreamEvent, { type: "response.completed" | "response.incomplete" }>
2424
+ | { type: "response.done"; response?: Partial<OpenAIResponse> };
2425
+
2426
+ function getOpenAIResponsesTerminalEvent(event: ResponseStreamEvent): OpenAIResponsesTerminalStreamEvent | undefined {
2427
+ const type = (event as { type?: unknown }).type;
2428
+ return type === "response.completed" || type === "response.incomplete" || type === "response.done"
2429
+ ? (event as OpenAIResponsesTerminalStreamEvent)
2430
+ : undefined;
2431
+ }
2432
+
2433
+ export interface ProcessResponsesStreamOptions {
2434
+ onFirstToken?: () => void;
2435
+ onOutputItemDone?: (item: ResponseOutputItem) => void;
2436
+ /**
2437
+ * Called when a terminal `response.completed`, `response.incomplete`, or
2438
+ * `response.done` event is successfully processed. Only invoked on the
2439
+ * successful-completion path; thrown failure (`response.failed`) and
2440
+ * cancellation paths never call this.
2441
+ * Used by callers to detect premature stream closure (i.e. the stream ended
2442
+ * without a recognized terminal event).
2443
+ */
2444
+ onCompleted?: () => void;
2445
+ /**
2446
+ * Caller-requested service tier, used to bill the served tier when the
2447
+ * response omits the `service_tier` echo. Only applied for `provider: "openai"`.
2448
+ */
2449
+ requestServiceTier?: ServiceTier;
2450
+ }
2451
+
2452
+ export function computerCallMetadata(item: ResponseComputerToolCall): ComputerToolCallMetadata {
2453
+ const actions = item.actions?.length ? item.actions : item.action ? [item.action] : [];
2454
+ return {
2455
+ type: "computer",
2456
+ providerItemId: item.id,
2457
+ actions: structuredCloneJSON(actions) as ComputerAction[],
2458
+ pendingSafetyChecks: structuredCloneJSON(item.pending_safety_checks ?? []),
2459
+ };
2460
+ }
2461
+
2462
+ /** Append a native Responses image result and emit its completion event. */
2463
+ export function appendResponsesImageResult(
2464
+ output: AssistantMessage,
2465
+ stream: AssistantMessageEventStream,
2466
+ result: string,
2467
+ ): void {
2468
+ const image: ImageContent = {
2469
+ type: "image",
2470
+ data: result,
2471
+ mimeType: parseImageMetadata(Buffer.from(result, "base64"))?.mimeType ?? "image/png",
2472
+ };
2473
+ output.content.push(image);
2474
+ stream.push({
2475
+ type: "image_end",
2476
+ contentIndex: output.content.length - 1,
2477
+ content: image,
2478
+ partial: output,
2479
+ });
2480
+ }
2481
+
2482
+ export async function processResponsesStream<TApi extends Api>(
2483
+ openaiStream: AsyncIterable<ResponseStreamEvent>,
2484
+ output: AssistantMessage,
2485
+ stream: AssistantMessageEventStream,
2486
+ model: Model<TApi>,
2487
+ options?: ProcessResponsesStreamOptions,
2488
+ ): Promise<void> {
2489
+ type StreamingToolCallBlock = ToolCall & {
2490
+ [kStreamingPartialJson]: string;
2491
+ [kStreamingLastParseLen]?: number;
2492
+ [kStreamingArgumentsDone]?: boolean;
2493
+ };
2494
+ interface StreamingItem {
2495
+ item:
2496
+ | ResponseReasoningItem
2497
+ | ResponseOutputMessage
2498
+ | ResponseFunctionToolCall
2499
+ | ResponseCustomToolCall
2500
+ | ResponseComputerToolCall;
2501
+ block: ThinkingContent | TextContent | StreamingToolCallBlock;
2502
+ }
2503
+
2504
+ // Multiple items (parallel function_calls in particular) can be open at the same
2505
+ // time. OpenAI's spec routes every per-item event by `output_index`/`item_id`;
2506
+ // see https://github.com/can1357/oh-my-pi/issues/1880 — llama.cpp emits parallel
2507
+ // function_call deltas interleaved, and a singleton `current` reference would
2508
+ // fold them into the wrong block and drop arguments on every call but the last.
2509
+ //
2510
+ // OpenAI-compatible hosts can compound this by omitting `item.id` and
2511
+ // `output_index` on `output_item.added` while routing later argument deltas to
2512
+ // either the bare `call_id` or a synthesized `fc_<call_id>` item id. Register
2513
+ // both keys so each delta reaches its own block instead of falling back to the
2514
+ // most recently added parallel call.
2515
+ const openItemsByOutputIndex = new Map<number, StreamingItem>();
2516
+ const openItemsByItemId = new Map<string, StreamingItem>();
2517
+ const openItemsByPrefixedCallId = new Map<string, StreamingItem>();
2518
+ let lastOpenItem: StreamingItem | null = null;
2519
+ const openItemsInOrder: StreamingItem[] = [];
2520
+
2521
+ const prefixedFunctionCallItemKey = (callId: string | undefined): string | undefined =>
2522
+ callId ? `fc_${callId}` : undefined;
2523
+
2524
+ const registerOpenItem = (
2525
+ outputIndex: number | undefined,
2526
+ itemId: string | undefined,
2527
+ entry: StreamingItem,
2528
+ alternateItemKey?: string,
2529
+ prefixedAlternateItemKey?: string,
2530
+ ): void => {
2531
+ if (typeof outputIndex === "number") openItemsByOutputIndex.set(outputIndex, entry);
2532
+ if (itemId) openItemsByItemId.set(itemId, entry);
2533
+ if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.set(alternateItemKey, entry);
2534
+ if (
2535
+ prefixedAlternateItemKey &&
2536
+ prefixedAlternateItemKey !== itemId &&
2537
+ prefixedAlternateItemKey !== alternateItemKey
2538
+ ) {
2539
+ openItemsByPrefixedCallId.set(prefixedAlternateItemKey, entry);
2540
+ }
2541
+ openItemsInOrder.push(entry);
2542
+ lastOpenItem = entry;
2543
+ };
2544
+ const lookupOpenItem = (event: { output_index?: number; item_id?: string }): StreamingItem | undefined => {
2545
+ const hasKey = typeof event.output_index === "number" || event.item_id !== undefined;
2546
+ if (typeof event.output_index === "number") {
2547
+ const found = openItemsByOutputIndex.get(event.output_index);
2548
+ if (found) return found;
2549
+ }
2550
+ if (event.item_id) {
2551
+ const found = openItemsByItemId.get(event.item_id);
2552
+ if (found) return found;
2553
+ }
2554
+ // Keyed events whose item already closed are stale; drop them instead of
2555
+ // routing to a sibling. Only fully identifierless mock/proxy events use the
2556
+ // legacy singleton fallback.
2557
+ return hasKey ? undefined : (lastOpenItem ?? undefined);
2558
+ };
2559
+ const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
2560
+ typeof event.output_index === "number" || event.item_id !== undefined;
2561
+ const startsJsonObjectDelta = (delta: unknown): boolean => {
2562
+ if (typeof delta !== "string") return false;
2563
+ for (let index = 0; index < delta.length; index++) {
2564
+ const code = delta.charCodeAt(index);
2565
+ if (code === 0x09 || code === 0x0a || code === 0x0d || code === 0x20) continue;
2566
+ return code === 0x7b;
2567
+ }
2568
+ return false;
2569
+ };
2570
+ const shouldAdvanceIdentifierlessFunctionDelta = (
2571
+ event: { output_index?: number; item_id?: string; delta?: unknown },
2572
+ candidate: StreamingItem,
2573
+ ): boolean => {
2574
+ const delta = event.delta;
2575
+ if (
2576
+ hasOpenItemKey(event) ||
2577
+ typeof delta !== "string" ||
2578
+ !startsJsonObjectDelta(delta) ||
2579
+ candidate.item.type !== "function_call" ||
2580
+ candidate.block.type !== "toolCall"
2581
+ ) {
2582
+ return false;
2583
+ }
2584
+ const partial = candidate.block[kStreamingPartialJson];
2585
+ if (partial.trim().length === 0) return false;
2586
+ // A `{`-starting identifierless delta is ambiguous: the opening of a new
2587
+ // sibling call, or continuation bytes inside the candidate's own argument
2588
+ // JSON (`{"command":"echo ` + `{1..3}"}`). Advance only when the candidate
2589
+ // cannot absorb the delta: its buffer is already one complete JSON value,
2590
+ // already unsalvageable (lossy hosts abandon buffers mid-string, leaving
2591
+ // raw control characters strict JSON forbids), or the concatenation would
2592
+ // break it. Otherwise the delta is a legal continuation and must stay.
2593
+ const state = classifyJsonPrefix(partial);
2594
+ if (state !== "prefix") return true;
2595
+ return classifyJsonPrefix(partial + delta) === "invalid";
2596
+ };
2597
+ const hasLaterUnfinishedFunctionCall = (start: number): boolean => {
2598
+ for (let index = start + 1; index < openItemsInOrder.length; index++) {
2599
+ const candidate = openItemsInOrder[index];
2600
+ if (
2601
+ candidate?.item.type === "function_call" &&
2602
+ candidate.block.type === "toolCall" &&
2603
+ !candidate.block[kStreamingArgumentsDone]
2604
+ ) {
2605
+ return true;
2606
+ }
2607
+ }
2608
+ return false;
2609
+ };
2610
+
2611
+ let identifierlessFunctionDeltaTarget: StreamingItem | undefined;
2612
+
2613
+ const lookupOpenToolCallAlias = (
2614
+ event: { output_index?: number; item_id?: string },
2615
+ type: "function_call" | "custom_tool_call",
2616
+ ): StreamingItem | undefined => {
2617
+ if (typeof event.output_index === "number") {
2618
+ const byOutputIndex = openItemsByOutputIndex.get(event.output_index);
2619
+ if (byOutputIndex) return byOutputIndex;
2620
+ // A lossy host (llama.cpp/Ollama, issue #2015) can omit `output_index` on
2621
+ // `output_item.added` while still stamping the spec-required field on the
2622
+ // delta. The index was never registered, so fall through to the prefixed
2623
+ // alias / exact item-id maps instead of dropping to `lastOpenItem`.
2624
+ }
2625
+ if (event.item_id) {
2626
+ // Prefixed call-id aliases share the same wire namespace as real call ids.
2627
+ // Argument/input events can use the prefixed form, while final
2628
+ // output_item.done events below use exact call ids; keep aliases in a
2629
+ // separate map so a real `call_id: "fc_x"` cannot overwrite the alias
2630
+ // for `call_id: "x"`.
2631
+ const alias = openItemsByPrefixedCallId.get(event.item_id);
2632
+ if (alias?.item.type === type) return alias;
2633
+ const exact = openItemsByItemId.get(event.item_id);
2634
+ if (exact) return exact;
2635
+ }
2636
+ return lookupOpenItem(event);
2637
+ };
2638
+ const lookupOpenFunctionCallItem = (event: {
2639
+ output_index?: number;
2640
+ item_id?: string;
2641
+ delta?: unknown;
2642
+ }): StreamingItem | undefined => {
2643
+ if (hasOpenItemKey(event)) return lookupOpenToolCallAlias(event, "function_call");
2644
+ const canContinuePreviousIdentifierlessDelta = typeof event.delta === "string";
2645
+ if (canContinuePreviousIdentifierlessDelta && identifierlessFunctionDeltaTarget) {
2646
+ const targetIndex = openItemsInOrder.indexOf(identifierlessFunctionDeltaTarget);
2647
+ const target = targetIndex >= 0 ? openItemsInOrder[targetIndex] : undefined;
2648
+ if (
2649
+ target?.item.type === "function_call" &&
2650
+ target.block.type === "toolCall" &&
2651
+ !target.block[kStreamingArgumentsDone]
2652
+ ) {
2653
+ const shouldAdvanceFromTarget =
2654
+ shouldAdvanceIdentifierlessFunctionDelta(event, target) && hasLaterUnfinishedFunctionCall(targetIndex);
2655
+ if (!shouldAdvanceFromTarget) return target;
2656
+ } else {
2657
+ identifierlessFunctionDeltaTarget = undefined;
2658
+ }
2659
+ }
2660
+ let skippedStartedCandidate = false;
2661
+ for (let index = 0; index < openItemsInOrder.length; index++) {
2662
+ const candidate = openItemsInOrder[index]!;
2663
+ if (
2664
+ candidate.item.type === "function_call" &&
2665
+ candidate.block.type === "toolCall" &&
2666
+ !candidate.block[kStreamingArgumentsDone]
2667
+ ) {
2668
+ if (shouldAdvanceIdentifierlessFunctionDelta(event, candidate) && hasLaterUnfinishedFunctionCall(index)) {
2669
+ skippedStartedCandidate = true;
2670
+ continue;
2671
+ }
2672
+ if (canContinuePreviousIdentifierlessDelta) identifierlessFunctionDeltaTarget = candidate;
2673
+ return candidate;
2674
+ }
2675
+ }
2676
+ if (skippedStartedCandidate && startsJsonObjectDelta(event.delta)) return undefined;
2677
+ return lastOpenItem?.item.type === "function_call" ? lastOpenItem : undefined;
2678
+ };
2679
+ const closeOpenItem = (
2680
+ outputIndex: number | undefined,
2681
+ itemId: string | undefined,
2682
+ entry: StreamingItem | undefined,
2683
+ alternateItemKey?: string,
2684
+ prefixedAlternateItemKey?: string,
2685
+ ): void => {
2686
+ if (typeof outputIndex === "number") openItemsByOutputIndex.delete(outputIndex);
2687
+ if (itemId) openItemsByItemId.delete(itemId);
2688
+ if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.delete(alternateItemKey);
2689
+ if (
2690
+ prefixedAlternateItemKey &&
2691
+ prefixedAlternateItemKey !== itemId &&
2692
+ prefixedAlternateItemKey !== alternateItemKey &&
2693
+ openItemsByPrefixedCallId.get(prefixedAlternateItemKey) === entry
2694
+ ) {
2695
+ openItemsByPrefixedCallId.delete(prefixedAlternateItemKey);
2696
+ }
2697
+ if (entry) {
2698
+ const index = openItemsInOrder.indexOf(entry);
2699
+ if (index >= 0) openItemsInOrder.splice(index, 1);
2700
+ }
2701
+ if (entry && identifierlessFunctionDeltaTarget === entry) identifierlessFunctionDeltaTarget = undefined;
2702
+ if (entry && lastOpenItem === entry) lastOpenItem = null;
2703
+ };
2704
+ const contentIndexOf = (block: ThinkingContent | TextContent | StreamingToolCallBlock): number =>
2705
+ output.content.indexOf(block);
2706
+
2707
+ let sawFirstToken = false;
2708
+
2709
+ for await (const event of openaiStream) {
2710
+ const terminalEvent = getOpenAIResponsesTerminalEvent(event);
2711
+ if (event.type === "response.created") {
2712
+ output.responseId = event.response.id;
2713
+ } else if (event.type === "response.output_item.added") {
2714
+ if (!sawFirstToken) {
2715
+ sawFirstToken = true;
2716
+ options?.onFirstToken?.();
2717
+ }
2718
+ const item = event.item;
2719
+ if (item.type === "reasoning") {
2720
+ const block: ThinkingContent = { type: "thinking", thinking: "", itemId: item.id };
2721
+ output.content.push(block);
2722
+ registerOpenItem(event.output_index, item.id, { item, block });
2723
+ stream.push({ type: "thinking_start", contentIndex: contentIndexOf(block), partial: output });
2724
+ } else if (item.type === "message") {
2725
+ const block: TextContent = {
2726
+ type: "text",
2727
+ text: "",
2728
+ textSignature: encodeTextSignatureV1(item.id, item.phase ?? undefined),
2729
+ };
2730
+ output.content.push(block);
2731
+ registerOpenItem(event.output_index, item.id, { item, block });
2732
+ stream.push({ type: "text_start", contentIndex: contentIndexOf(block), partial: output });
2733
+ } else if (item.type === "function_call") {
2734
+ const block: StreamingToolCallBlock = {
2735
+ type: "toolCall",
2736
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2737
+ name: item.name,
2738
+ arguments: {},
2739
+ [kStreamingPartialJson]: item.arguments || "",
2740
+ };
2741
+ output.content.push(block);
2742
+ registerOpenItem(
2743
+ event.output_index,
2744
+ item.id,
2745
+ { item, block },
2746
+ item.call_id,
2747
+ prefixedFunctionCallItemKey(item.call_id),
2748
+ );
2749
+ stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
2750
+ } else if (item.type === "computer_call") {
2751
+ const block: StreamingToolCallBlock = {
2752
+ type: "toolCall",
2753
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2754
+ name: "computer",
2755
+ arguments: {},
2756
+ providerMetadata: computerCallMetadata(item),
2757
+ [kStreamingPartialJson]: "",
2758
+ };
2759
+ output.content.push(block);
2760
+ registerOpenItem(event.output_index, item.id, { item, block }, item.call_id);
2761
+ stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
2762
+ } else if (item.type === "custom_tool_call") {
2763
+ const block: StreamingToolCallBlock = {
2764
+ type: "toolCall",
2765
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2766
+ // Preserve the raw wire name (e.g. `apply_patch`). The agent-loop
2767
+ // dispatcher matches it against both `Tool.name` and
2768
+ // `Tool.customWireName`, so this stays wire-accurate through
2769
+ // history replay while still routing to the right handler.
2770
+ name: item.name,
2771
+ arguments: { input: item.input ?? "" },
2772
+ customWireName: item.name,
2773
+ // Custom tools stream a raw string, but we reuse `partialJson` as the
2774
+ // accumulation buffer so later code that inspects the field still works.
2775
+ [kStreamingPartialJson]: item.input ?? "",
2776
+ };
2777
+ output.content.push(block);
2778
+ registerOpenItem(
2779
+ event.output_index,
2780
+ item.id,
2781
+ { item, block },
2782
+ item.call_id,
2783
+ prefixedFunctionCallItemKey(item.call_id),
2784
+ );
2785
+ stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
2786
+ }
2787
+ } else if (event.type === "response.reasoning_summary_part.added") {
2788
+ const entry = lookupOpenItem(event);
2789
+ if (entry?.item.type === "reasoning") appendReasoningSummaryPart(entry.item, event.part);
2790
+ } else if (event.type === "response.reasoning_summary_text.delta") {
2791
+ const entry = lookupOpenItem(event);
2792
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
2793
+ appendReasoningSummaryTextDelta(
2794
+ entry.item,
2795
+ entry.block,
2796
+ event.delta,
2797
+ stream,
2798
+ output,
2799
+ contentIndexOf(entry.block),
2800
+ );
2801
+ }
2802
+ } else if (event.type === "response.reasoning_summary_part.done") {
2803
+ const entry = lookupOpenItem(event);
2804
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
2805
+ appendReasoningSummaryPartDone(entry.item, entry.block, stream, output, contentIndexOf(entry.block));
2806
+ }
2807
+ } else if (event.type === "response.reasoning_text.delta") {
2808
+ // Raw reasoning text delta from local providers that stream thinking
2809
+ // directly rather than via the OpenAI summary tracking protocol.
2810
+ const entry = lookupOpenItem(event);
2811
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
2812
+ entry.block.thinking += event.delta;
2813
+ stream.push({
2814
+ type: "thinking_delta",
2815
+ contentIndex: contentIndexOf(entry.block),
2816
+ delta: event.delta,
2817
+ partial: output,
2818
+ });
2819
+ }
2820
+ } else if (event.type === "response.content_part.added") {
2821
+ const entry = lookupOpenItem(event);
2822
+ if (entry?.item.type === "message") appendMessageContentPart(entry.item, event.part);
2823
+ } else if (event.type === "response.output_text.delta") {
2824
+ const entry = lookupOpenItem(event);
2825
+ if (entry?.item.type === "message" && entry.block.type === "text") {
2826
+ appendMessageTextDelta(
2827
+ entry.item,
2828
+ entry.block,
2829
+ event.delta,
2830
+ stream,
2831
+ output,
2832
+ contentIndexOf(entry.block),
2833
+ "output_text",
2834
+ );
2835
+ }
2836
+ } else if (event.type === "response.refusal.delta") {
2837
+ const entry = lookupOpenItem(event);
2838
+ if (entry?.item.type === "message" && entry.block.type === "text") {
2839
+ appendMessageTextDelta(
2840
+ entry.item,
2841
+ entry.block,
2842
+ event.delta,
2843
+ stream,
2844
+ output,
2845
+ contentIndexOf(entry.block),
2846
+ "refusal",
2847
+ );
2848
+ }
2849
+ } else if (event.type === "response.function_call_arguments.delta") {
2850
+ const entry = lookupOpenFunctionCallItem(event);
2851
+ if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
2852
+ accumulateToolCallArgumentsDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
2853
+ }
2854
+ } else if (event.type === "response.function_call_arguments.done") {
2855
+ const entry = lookupOpenFunctionCallItem(event);
2856
+ if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
2857
+ finalizeToolCallArgumentsDone(entry.block, event.arguments);
2858
+ entry.block[kStreamingArgumentsDone] = true;
2859
+ }
2860
+ } else if (event.type === "response.custom_tool_call_input.delta") {
2861
+ const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
2862
+ if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
2863
+ accumulateCustomToolCallInputDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
2864
+ }
2865
+ } else if (event.type === "response.custom_tool_call_input.done") {
2866
+ const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
2867
+ if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
2868
+ finalizeCustomToolCallInputDone(entry.block, event.input);
2869
+ entry.block[kStreamingArgumentsDone] = true;
2870
+ }
2871
+ } else if (event.type === "response.output_item.done") {
2872
+ const item = structuredCloneJSON(event.item);
2873
+ options?.onOutputItemDone?.(item);
2874
+ const entry =
2875
+ item.type === "function_call" || item.type === "custom_tool_call"
2876
+ ? lookupOpenItem({ output_index: event.output_index, item_id: item.id ?? item.call_id })
2877
+ : lookupOpenItem({ output_index: event.output_index, item_id: item.id });
2878
+ if (item.type === "reasoning") {
2879
+ // Prefer the routed entry; the bare itemId find misroutes when ids are
2880
+ // absent (`undefined === undefined` matches the FIRST thinking block) and
2881
+ // misses entirely when the done-event id drifts from the added-event id.
2882
+ const reasoningBlock =
2883
+ entry?.block.type === "thinking"
2884
+ ? entry.block
2885
+ : (output.content.find(b => b.type === "thinking" && (b as ThinkingContent).itemId === item.id) as
2886
+ | ThinkingContent
2887
+ | undefined);
2888
+ if (reasoningBlock) {
2889
+ reasoningBlock.thinking = finalizeReasoningThinking(item, reasoningBlock.thinking);
2890
+ reasoningBlock.thinkingSignature = JSON.stringify(item);
2891
+ stream.push({
2892
+ type: "thinking_end",
2893
+ contentIndex: contentIndexOf(reasoningBlock),
2894
+ content: reasoningBlock.thinking,
2895
+ partial: output,
2896
+ });
2897
+ }
2898
+ closeOpenItem(event.output_index, item.id, entry);
2899
+ } else if (item.type === "message") {
2900
+ const block = entry?.block.type === "text" ? entry.block : undefined;
2901
+ const text = finalizeMessageText(item, block?.text ?? "");
2902
+ const textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
2903
+ let contentIndex: number;
2904
+ if (block) {
2905
+ block.text = text;
2906
+ block.textSignature = textSignature;
2907
+ contentIndex = contentIndexOf(block);
2908
+ } else {
2909
+ // `output_item.added` never arrived (lossy proxy) — synthesize the
2910
+ // block so the final message still carries the authoritative text.
2911
+ const synthesized: TextContent = { type: "text", text, textSignature };
2912
+ output.content.push(synthesized);
2913
+ contentIndex = output.content.length - 1;
2914
+ }
2915
+ stream.push({ type: "text_end", contentIndex, content: text, partial: output });
2916
+ closeOpenItem(event.output_index, item.id, entry);
2917
+ } else if (item.type === "function_call") {
2918
+ const block = entry?.block.type === "toolCall" ? entry.block : undefined;
2919
+ const args = block?.[kStreamingArgumentsDone]
2920
+ ? block.arguments
2921
+ : item.arguments
2922
+ ? parseStreamingJson(item.arguments)
2923
+ : block?.[kStreamingPartialJson]
2924
+ ? parseStreamingJson(block[kStreamingPartialJson])
2925
+ : parseStreamingJson("{}");
2926
+ const toolCall: ToolCall = {
2927
+ type: "toolCall",
2928
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2929
+ name: item.name,
2930
+ arguments: args,
2931
+ };
2932
+ let contentIndex: number;
2933
+ if (block) {
2934
+ // Persist the authoritative final args on the stored block. The
2935
+ // throttled delta parser may have skipped the last partial parse,
2936
+ // leaving block.arguments stale (often `{}`); the emitted toolCall
2937
+ // and the persisted block must agree.
2938
+ block.arguments = args;
2939
+ clearStreamingPartialJson(block);
2940
+ contentIndex = contentIndexOf(block);
2941
+ } else {
2942
+ // `output_item.added` never arrived (lossy proxy) — synthesize the
2943
+ // block so the final message carries the call the consumer was told
2944
+ // completed (the agent loop executes tools from message.content).
2945
+ output.content.push(toolCall);
2946
+ contentIndex = output.content.length - 1;
2947
+ }
2948
+ closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
2949
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2950
+ } else if (item.type === "computer_call") {
2951
+ const block = entry?.block.type === "toolCall" ? entry.block : undefined;
2952
+ const toolCall: ToolCall = {
2953
+ type: "toolCall",
2954
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2955
+ name: "computer",
2956
+ arguments: {},
2957
+ providerMetadata: computerCallMetadata(item),
2958
+ };
2959
+ let contentIndex: number;
2960
+ if (block) {
2961
+ block.id = toolCall.id;
2962
+ block.providerMetadata = toolCall.providerMetadata;
2963
+ clearStreamingPartialJson(block);
2964
+ contentIndex = contentIndexOf(block);
2965
+ } else {
2966
+ output.content.push(toolCall);
2967
+ contentIndex = output.content.length - 1;
2968
+ }
2969
+ closeOpenItem(event.output_index, item.id, entry, item.call_id);
2970
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2971
+ } else if (item.type === "custom_tool_call") {
2972
+ const block = entry?.block.type === "toolCall" ? entry.block : undefined;
2973
+ const rawInput = block?.[kStreamingPartialJson] ? block[kStreamingPartialJson] : (item.input ?? "");
2974
+ const toolCall: ToolCall = {
2975
+ type: "toolCall",
2976
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2977
+ name: item.name,
2978
+ arguments: { input: rawInput },
2979
+ customWireName: item.name,
2980
+ };
2981
+ let contentIndex: number;
2982
+ if (block) {
2983
+ // Persist the final input on the stored block and drop the transient
2984
+ // accumulation buffer, mirroring the function_call branch above.
2985
+ block.arguments = { input: rawInput };
2986
+ clearStreamingPartialJson(block);
2987
+ contentIndex = contentIndexOf(block);
2988
+ } else {
2989
+ output.content.push(toolCall);
2990
+ contentIndex = output.content.length - 1;
2991
+ }
2992
+ closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
2993
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2994
+ } else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
2995
+ appendResponsesImageResult(output, stream, item.result);
2996
+ }
2997
+ } else if (terminalEvent) {
2998
+ const response = terminalEvent.response;
2999
+ const shouldPromoteIncompleteToolUse =
3000
+ response?.status === "incomplete" &&
3001
+ response.incomplete_details?.reason === "max_output_tokens" &&
3002
+ hasExecutableIncompleteResponsesToolCalls(output);
3003
+ finalizePendingResponsesToolCalls(output);
3004
+ if (response?.id) {
3005
+ output.responseId = response.id;
3006
+ }
3007
+ populateResponsesUsageFromResponse(output, response?.usage);
3008
+ calculateCost(model, output.usage);
3009
+ applyOpenRouterReportedCost(model, output.usage, response?.usage);
3010
+ applyOpenAIResponsesServiceTierCost(
3011
+ model,
3012
+ output.usage,
3013
+ (response as { service_tier?: unknown } | undefined)?.service_tier,
3014
+ options?.requestServiceTier,
3015
+ );
3016
+ output.stopReason = mapOpenAIResponsesStopReason(response?.status);
3017
+ if (response?.status === "failed" || response?.status === "cancelled") {
3018
+ const error = response?.error ?? (response as any)?.status_details?.error;
3019
+ const details = response?.incomplete_details;
3020
+ const statusDetailsReason = (response as any)?.status_details?.reason;
3021
+ const message = error
3022
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
3023
+ : details?.reason
3024
+ ? `incomplete: ${details.reason}`
3025
+ : typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
3026
+ ? `status_details: ${statusDetailsReason}`
3027
+ : "Unknown error (no error details in response)";
3028
+ throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
3029
+ }
3030
+ if (response?.status === "incomplete" && response.incomplete_details?.reason === "content_filter") {
3031
+ // A content-filtered turn is a failure, not a token-cap truncation —
3032
+ // mapping it to "length" would route the agent loop into "shorten your
3033
+ // output" recovery against a filtered prompt.
3034
+ throw new AIError.ProviderResponseError("incomplete: content_filter", {
3035
+ provider: model.provider,
3036
+ kind: "content-blocked",
3037
+ });
3038
+ }
3039
+ promoteResponsesToolUseStopReason(
3040
+ output,
3041
+ (response as { end_turn?: boolean } | undefined)?.end_turn,
3042
+ shouldPromoteIncompleteToolUse,
3043
+ );
3044
+ options?.onCompleted?.();
3045
+ // `response.completed`/`response.incomplete`/`response.done` is the last event of a
3046
+ // Responses stream. Stop pulling instead of waiting for the server to
3047
+ // close the connection: misbehaving providers keep the socket open
3048
+ // after the terminal event, which would park this loop until the idle
3049
+ // watchdog converts an already-successful turn into a timeout error.
3050
+ // Breaking unwinds the iterator chain (the consumer's `.return()`
3051
+ // reaches the SDK stream), actively releasing the connection.
3052
+ break;
3053
+ } else if (event.type === "error") {
3054
+ const err = (event as any).error ?? event;
3055
+ const code = err.code ?? "unknown";
3056
+ const message = err.message ?? "no message";
3057
+ throw new AIError.ProviderResponseError(`Error Code ${code}: ${message}`, {
3058
+ provider: model.provider,
3059
+ kind: "output",
3060
+ });
3061
+ } else if (event.type === "response.failed") {
3062
+ populateResponsesUsageFromResponse(output, event.response?.usage);
3063
+ const error = event.response?.error ?? (event.response as any)?.status_details?.error;
3064
+ const details = event.response?.incomplete_details;
3065
+ const message = error
3066
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
3067
+ : details?.reason
3068
+ ? `incomplete: ${details.reason}`
3069
+ : "Unknown error (no error details in response)";
3070
+ throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
3071
+ }
3072
+ }
3073
+ }
3074
+
3075
+ export function mapOpenAIResponsesStopReason(status: ResponseStatus | undefined): StopReason {
3076
+ if (!status) return "stop";
3077
+ switch (status) {
3078
+ case "completed":
3079
+ return "stop";
3080
+ case "incomplete":
3081
+ return "length";
3082
+ case "failed":
3083
+ case "cancelled":
3084
+ return "error";
3085
+ case "in_progress":
3086
+ case "queued":
3087
+ return "stop";
3088
+ default: {
3089
+ // Compile-time exhaustiveness; at runtime a brand-new status from the
3090
+ // server must degrade gracefully instead of failing a fully-streamed
3091
+ // response.
3092
+ const exhaustive: never = status;
3093
+ logger.warn("Unhandled OpenAI Responses stop reason", { status: exhaustive });
3094
+ return "stop";
3095
+ }
3096
+ }
3097
+ }
3098
+
3099
+ export function hasExecutableIncompleteResponsesToolCalls(output: AssistantMessage): boolean {
3100
+ let hasToolCall = false;
3101
+ for (const block of output.content) {
3102
+ if (block.type !== "toolCall") continue;
3103
+ hasToolCall = true;
3104
+ const pending = block as ToolCall & {
3105
+ [kStreamingPartialJson]?: string;
3106
+ [kStreamingArgumentsDone]?: boolean;
3107
+ };
3108
+ if (pending.providerMetadata?.type === "computer") {
3109
+ if (pending.providerMetadata.actions.length === 0) return false;
3110
+ continue;
3111
+ }
3112
+ const rawArguments = pending[kStreamingPartialJson];
3113
+ // `output_item.done` is not positive completion proof: our Responses
3114
+ // compatibility encoder force-closes still-open calls before forwarding an
3115
+ // upstream `length` stop. Only an explicit arguments/input-done event sets
3116
+ // this marker; an open ordinary call can instead prove completion with its
3117
+ // retained strict-complete JSON.
3118
+ if (pending[kStreamingArgumentsDone]) continue;
3119
+ if (pending.customWireName !== undefined || rawArguments === undefined) return false;
3120
+ if (classifyJsonPrefix(rawArguments) !== "complete") return false;
3121
+ }
3122
+ return hasToolCall;
3123
+ }
3124
+
3125
+ /**
3126
+ * Finalize any streamed toolCall block whose `output_item.done` never arrived
3127
+ * (lossy proxy, or a terminal event that raced the per-item done): parse the
3128
+ * accumulated `partialJson` into authoritative arguments and strip the transient
3129
+ * streaming fields so they never persist. Shared by the chat-Responses decoder
3130
+ * and the Codex decoder. Closed blocks already cleared these fields, so walking
3131
+ * the full content list leaves them untouched.
3132
+ */
3133
+ export function finalizePendingResponsesToolCalls(output: AssistantMessage): void {
3134
+ for (const block of output.content) {
3135
+ if (block.type !== "toolCall") continue;
3136
+ const pending = block as ToolCall & {
3137
+ [kStreamingPartialJson]?: string;
3138
+ [kStreamingLastParseLen]?: number;
3139
+ [kStreamingArgumentsDone]?: boolean;
3140
+ };
3141
+ if (pending[kStreamingPartialJson] && !pending[kStreamingArgumentsDone]) {
3142
+ pending.arguments =
3143
+ pending.customWireName !== undefined
3144
+ ? { input: pending[kStreamingPartialJson] }
3145
+ : parseStreamingJson(pending[kStreamingPartialJson]);
3146
+ }
3147
+ clearStreamingPartialJson(pending);
3148
+ }
3149
+ }
3150
+
3151
+ /**
3152
+ * Apply the Responses terminal stop-reason invariants shared by the chat-Responses
3153
+ * and Codex decoders: a turn that produced tool calls becomes `toolUse`, and a
3154
+ * Codex-lineage `end_turn: false` marker pauses the turn so the agent loop
3155
+ * re-samples instead of ending. Callers set `output.stopReason` from the wire
3156
+ * status first via {@link mapOpenAIResponsesStopReason}.
3157
+ */
3158
+ export function promoteResponsesToolUseStopReason(
3159
+ output: AssistantMessage,
3160
+ endTurn: boolean | undefined,
3161
+ promoteIncompleteToolUse = false,
3162
+ ): void {
3163
+ if (
3164
+ output.content.some(block => block.type === "toolCall") &&
3165
+ (output.stopReason === "stop" || (promoteIncompleteToolUse && output.stopReason === "length"))
3166
+ ) {
3167
+ output.stopReason = "toolUse";
3168
+ }
3169
+ if (endTurn === false && output.stopReason === "stop") {
3170
+ output.stopDetails = { type: "pause_turn" };
3171
+ }
3172
+ }
3173
+
3174
+ /** Initial empty `AssistantMessage` that streaming providers accumulate into. */
3175
+ export function createInitialResponsesAssistantMessage(api: Api, provider: string, modelId: string): AssistantMessage {
3176
+ return {
3177
+ role: "assistant",
3178
+ content: [],
3179
+ api,
3180
+ provider,
3181
+ model: modelId,
3182
+ usage: {
3183
+ input: 0,
3184
+ output: 0,
3185
+ cacheRead: 0,
3186
+ cacheWrite: 0,
3187
+ totalTokens: 0,
3188
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
3189
+ },
3190
+ stopReason: "stop",
3191
+ timestamp: Date.now(),
3192
+ };
3193
+ }
3194
+
3195
+ /** Extension fields we add on top of `ResponseCreateParamsStreaming` across the Responses-family providers. */
3196
+ export type ResponsesSamplingParamsExtras = {
3197
+ top_p?: number;
3198
+ top_k?: number;
3199
+ min_p?: number;
3200
+ presence_penalty?: number;
3201
+ repetition_penalty?: number;
3202
+ };
3203
+
3204
+ type CommonResponsesParams = ResponseCreateParamsStreaming & ResponsesSamplingParamsExtras;
3205
+
3206
+ type CommonSamplingOptions = Pick<
3207
+ StreamOptions,
3208
+ "temperature" | "topP" | "topK" | "minP" | "presencePenalty" | "repetitionPenalty" | "maxTokens"
3209
+ > & { serviceTier?: ServiceTier };
3210
+
3211
+ /**
3212
+ * Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
3213
+ * temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
3214
+ *
3215
+ * `max_output_tokens` is suppressed when {@link Model.omitMaxOutputTokens} is `true`, so
3216
+ * proxies (notably Ollama) that forward to upstream APIs with an unknown output-token cap
3217
+ * can let the upstream apply its own default instead of 400-ing on `maxTokens` values that
3218
+ * reflect the model's context window rather than the upstream output limit.
3219
+ */
3220
+ export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
3221
+ params: P,
3222
+ options: CommonSamplingOptions | undefined,
3223
+ model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
3224
+ compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
3225
+ },
3226
+ ): void {
3227
+ if (options?.maxTokens && !model.omitMaxOutputTokens) {
3228
+ params.max_output_tokens = Math.min(
3229
+ options.maxTokens,
3230
+ model.maxTokens ?? Number.POSITIVE_INFINITY,
3231
+ resolveOpenAIResponsesOutputClamp(model) ?? OPENAI_MAX_OUTPUT_TOKENS,
3232
+ );
3233
+ }
3234
+ // OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
3235
+ // sampling params with a 400 on every serving host (#5606).
3236
+ if (model.compat.supportsSamplingParams) {
3237
+ if (options?.temperature !== undefined) params.temperature = options.temperature;
3238
+ if (options?.topP !== undefined) params.top_p = options.topP;
3239
+ if (options?.topK !== undefined) params.top_k = options.topK;
3240
+ if (options?.minP !== undefined) params.min_p = options.minP;
3241
+ if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
3242
+ if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
3243
+ }
3244
+ applyOpenAIServiceTier(params, options?.serviceTier, model);
3245
+ }
3246
+
3247
+ type ReasoningOptions = {
3248
+ reasoning?: string;
3249
+ reasoningSummary?: "auto" | "detailed" | "concise" | null;
3250
+ disableReasoning?: boolean;
3251
+ toolChoice?: unknown;
3252
+ };
3253
+
3254
+ export interface ApplyResponsesCompatPolicyOptions {
3255
+ reasoningSummary?: "auto" | "detailed" | "concise" | null;
3256
+ mapEffort?: (effort: string) => string;
3257
+ }
3258
+
3259
+ export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreaming>(
3260
+ params: P,
3261
+ policy: OpenAICompatPolicy,
3262
+ options: ApplyResponsesCompatPolicyOptions | undefined,
3263
+ ): void {
3264
+ const reasoning = policy.reasoning;
3265
+ if (!reasoning.modelSupported) return;
3266
+ if (reasoning.includeEncryptedReasoning) {
3267
+ const include = params.include ?? [];
3268
+ if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
3269
+ params.include = include;
3270
+ }
3271
+
3272
+ if (reasoning.disabled) {
3273
+ if (reasoning.disableMode === "openrouter-enabled-false") {
3274
+ params.reasoning = { enabled: false } as P["reasoning"];
3275
+ return;
3276
+ }
3277
+ if (
3278
+ reasoning.disableMode === "lowest-effort" &&
3279
+ reasoning.wireEffort !== undefined &&
3280
+ !reasoning.omitReasoningEffort
3281
+ ) {
3282
+ type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
3283
+ params.reasoning = { effort: reasoning.wireEffort as ReasoningParam["effort"] } as P["reasoning"] &
3284
+ ReasoningParam;
3285
+ return;
3286
+ }
3287
+ return;
3288
+ }
3289
+
3290
+ if (reasoning.requestedEffort !== undefined || options?.reasoningSummary !== undefined) {
3291
+ if (reasoning.omitReasoningEffort) {
3292
+ if (options?.reasoningSummary !== undefined && options.reasoningSummary !== null) {
3293
+ type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
3294
+ params.reasoning = { summary: options.reasoningSummary || "auto" } as P["reasoning"] & ReasoningParam;
3295
+ }
3296
+ return;
3297
+ }
3298
+
3299
+ const requested = reasoning.requestedEffort ?? "medium";
3300
+ const wireEffort = reasoning.wireEffort ?? options?.mapEffort?.(requested) ?? requested;
3301
+ type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
3302
+ const reasoningParams: ReasoningParam = {
3303
+ effort: wireEffort as ReasoningParam["effort"],
3304
+ };
3305
+ if (options?.reasoningSummary !== null) {
3306
+ reasoningParams.summary = options?.reasoningSummary || "auto";
3307
+ }
3308
+ params.reasoning = reasoningParams as P["reasoning"];
3309
+ return;
3310
+ }
3311
+ }
3312
+
3313
+ /**
3314
+ * Apply reasoning-related Responses parameters. Default behavior comes from
3315
+ * catalog compat; include/omit arguments are explicit adapter-wrapper overrides.
3316
+ */
3317
+ export function applyResponsesReasoningParams<P extends ResponseCreateParamsStreaming>(
3318
+ params: P,
3319
+ model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">,
3320
+ options: ReasoningOptions | undefined,
3321
+ mapEffort?: (effort: string) => string,
3322
+ includeEncryptedReasoning?: boolean,
3323
+ omitReasoningEffort?: boolean,
3324
+ ): void {
3325
+ return applyResponsesCompatPolicy(
3326
+ params,
3327
+ resolveOpenAICompatPolicy(model, {
3328
+ endpoint: "responses",
3329
+ reasoning: options?.reasoning,
3330
+ disableReasoning: options?.disableReasoning,
3331
+ toolChoice: options?.toolChoice,
3332
+ includeEncryptedReasoning,
3333
+ omitReasoningEffort,
3334
+ }),
3335
+ { reasoningSummary: options?.reasoningSummary, mapEffort },
3336
+ );
3337
+ }
3338
+
3339
+ /** Populate `output.usage` from a Responses-API `response.usage` payload. Does not invoke `calculateCost`. */
3340
+ export function populateResponsesUsageFromResponse(
3341
+ output: AssistantMessage,
3342
+ usage:
3343
+ | {
3344
+ input_tokens?: number | null;
3345
+ output_tokens?: number | null;
3346
+ total_tokens?: number | null;
3347
+ prompt_cache_hit_tokens?: number | null;
3348
+ prompt_cache_miss_tokens?: number | null;
3349
+ input_tokens_details?: {
3350
+ cached_tokens?: number | null;
3351
+ cache_write_tokens?: number | null;
3352
+ orchestration_input_tokens?: number | null;
3353
+ orchestration_input_cached_tokens?: number | null;
3354
+ } | null;
3355
+ output_tokens_details?: {
3356
+ reasoning_tokens?: number | null;
3357
+ orchestration_output_tokens?: number | null;
3358
+ } | null;
3359
+ }
3360
+ | null
3361
+ | undefined,
3362
+ ): void {
3363
+ if (!usage) return;
3364
+ const details = usage.input_tokens_details;
3365
+ const outputDetails = usage.output_tokens_details;
3366
+ const reportedInputTokens = usage.input_tokens ?? 0;
3367
+ const reportedOutputTokens = usage.output_tokens ?? 0;
3368
+ const reportedCachedTokens = details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0;
3369
+ const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
3370
+ const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
3371
+ const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
3372
+ const reportedTotalTokens = typeof usage.total_tokens === "number" ? usage.total_tokens : undefined;
3373
+ const reportedPrimaryTokens = reportedInputTokens + reportedOutputTokens;
3374
+ const reportedWithSeparateOrchestration =
3375
+ reportedPrimaryTokens + orchestrationInputTokens + orchestrationOutputTokens;
3376
+ const primaryIncludesOrchestration =
3377
+ reportedTotalTokens !== undefined &&
3378
+ orchestrationInputTokens + orchestrationOutputTokens > 0 &&
3379
+ Math.abs(reportedTotalTokens - reportedPrimaryTokens) <=
3380
+ Math.abs(reportedTotalTokens - reportedWithSeparateOrchestration);
3381
+ const orchestrationInputCached = Math.min(orchestrationInputTokens, orchestrationInputCachedTokens);
3382
+ const orchestrationInput = Math.max(0, orchestrationInputTokens - orchestrationInputCached);
3383
+ const accounting = calculateOpenAIUsageAccounting({
3384
+ promptTokens: Math.max(0, reportedInputTokens - (primaryIncludesOrchestration ? orchestrationInputTokens : 0)),
3385
+ outputTokens: Math.max(0, reportedOutputTokens - (primaryIncludesOrchestration ? orchestrationOutputTokens : 0)),
3386
+ cachedTokens: Math.max(0, reportedCachedTokens - (primaryIncludesOrchestration ? orchestrationInputCached : 0)),
3387
+ reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
3388
+ cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
3389
+ cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
3390
+ hasDeepSeekCacheHitAndMiss:
3391
+ usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
3392
+ });
3393
+ const orchestrationTotal = orchestrationInput + orchestrationInputCached + orchestrationOutputTokens;
3394
+ if (orchestrationTotal > 0) {
3395
+ accounting.orchestration = {
3396
+ ...(orchestrationInput > 0 ? { input: orchestrationInput } : {}),
3397
+ ...(orchestrationInputCached > 0 ? { cacheRead: orchestrationInputCached } : {}),
3398
+ ...(orchestrationOutputTokens > 0 ? { output: orchestrationOutputTokens } : {}),
3399
+ };
3400
+ accounting.totalTokens = reportedTotalTokens ?? accounting.totalTokens + orchestrationTotal;
3401
+ }
3402
+
3403
+ // Wholesale replacement must not drop provider-annotated extras (Copilot
3404
+ // premium-request accounting): the failed/cancelled paths throw right after
3405
+ // this call with no later chance to re-apply.
3406
+ const premiumRequests = output.usage.premiumRequests;
3407
+ output.usage = {
3408
+ ...accounting,
3409
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
3410
+ };
3411
+ if (premiumRequests !== undefined) {
3412
+ output.usage.premiumRequests = premiumRequests;
3413
+ }
3414
+ }
3415
+
3416
+ /**
3417
+ * Structural equality for the chain prefix/option check, equivalent to the
3418
+ * default {@link Bun.deepEquals} (own enumerable keys, `absent ≡ own-undefined`)
3419
+ * except for two deliberate exclusions:
3420
+ * - **symbol-keyed properties are ignored** — `for…in` walks enumerable
3421
+ * *string* keys only (never symbols); these are plain wire items whose
3422
+ * prototype contributes no enumerable keys, so iteration is effectively
3423
+ * own-string-keyed. That is how the transient streaming symbols
3424
+ * (`block-symbols.ts`) stamped onto live request items are excluded (the
3425
+ * deep-cloned baseline never carries them). Do NOT add an
3426
+ * `Object.getOwnPropertySymbols` pass, or those symbols resurface and break
3427
+ * chaining.
3428
+ * - keys listed in `omitKeys` are skipped (the option compare omits `input`
3429
+ * and the per-turn `client_metadata`).
3430
+ * A defined value differing across sides IS a difference; a key undefined or
3431
+ * absent on both stays equal. Nested values use full {@link Bun.deepEquals}.
3432
+ */
3433
+ function deepEqualsWithout(a: unknown, b: unknown, omitKeys?: Record<string, boolean>): boolean {
3434
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return Bun.deepEquals(a, b);
3435
+ const ao = a as Record<string, unknown>;
3436
+ const bo = b as Record<string, unknown>;
3437
+ for (const key in ao) {
3438
+ if (omitKeys?.[key]) continue;
3439
+ const av = ao[key];
3440
+ const bv = bo[key];
3441
+ if (av !== bv && !Bun.deepEquals(av, bv)) return false;
3442
+ }
3443
+ for (const key in bo) {
3444
+ if (omitKeys?.[key]) continue;
3445
+ if (bo[key] !== undefined && !(key in ao)) return false;
3446
+ }
3447
+ return true;
3448
+ }
3449
+
3450
+ const TOP_LEVEL_EXCLUDE_MAP = {
3451
+ input: true,
3452
+ client_metadata: true,
3453
+ };
3454
+
3455
+ /**
3456
+ * Output-only lifecycle metadata excluded from per-item prefix identity:
3457
+ * replay sanitization strips `status` from message/function_call/custom
3458
+ * tool items (they reject output lifecycle fields), so raw response items
3459
+ * must not be distinguished from their sanitized replay form.
3460
+ */
3461
+ const ITEM_LIFECYCLE_EXCLUDE_MAP = {
3462
+ status: true,
3463
+ };
3464
+
3465
+ /**
3466
+ * Strict-prefix delta for stateful `previous_response_id` chaining (used by the
3467
+ * platform Responses provider and the Codex provider on both transports):
3468
+ * returns the input items the current request appends beyond the previous
3469
+ * request's input plus the previous response's output items, or null when the
3470
+ * request options differ or history mutated (the chain must break). Per-turn
3471
+ * `client_metadata` (e.g. rotating turn ids) is excluded from the option
3472
+ * comparison; codex-rs excludes it from the same check.
3473
+ */
3474
+ export function buildResponsesDeltaInput<TItem extends ResponseInputItem | InputItem>(
3475
+ previous: { input?: TItem[] } | undefined,
3476
+ previousResponseItems: readonly TItem[] | undefined,
3477
+ current: { input?: TItem[] },
3478
+ ): TItem[] | null {
3479
+ if (!previous) return null;
3480
+ if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
3481
+ if (!deepEqualsWithout(previous, current, TOP_LEVEL_EXCLUDE_MAP)) {
3482
+ return null;
3483
+ }
3484
+
3485
+ const baselineLen = (previous.input?.length ?? 0) + (previousResponseItems?.length ?? 0);
3486
+ if (current.input.length <= baselineLen) return null;
3487
+
3488
+ let index = 0;
3489
+ for (const series of [previous.input, previousResponseItems]) {
3490
+ if (!series) continue;
3491
+ for (const item of series) {
3492
+ if (deepEqualsWithout(item, current.input[index], ITEM_LIFECYCLE_EXCLUDE_MAP)) {
3493
+ index++;
3494
+ } else {
3495
+ return null;
3496
+ }
3497
+ }
3498
+ }
3499
+ return current.input.slice(index) as TItem[];
3500
+ }