jeopi-ai 16.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +4347 -0
  2. package/README.md +1193 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +73 -0
  5. package/dist/types/auth-broker/discover.d.ts +35 -0
  6. package/dist/types/auth-broker/index.d.ts +7 -0
  7. package/dist/types/auth-broker/refresher.d.ts +25 -0
  8. package/dist/types/auth-broker/remote-store.d.ts +102 -0
  9. package/dist/types/auth-broker/server.d.ts +43 -0
  10. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  11. package/dist/types/auth-broker/types.d.ts +107 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +411 -0
  13. package/dist/types/auth-gateway/http.d.ts +39 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +36 -0
  16. package/dist/types/auth-gateway/types.d.ts +123 -0
  17. package/dist/types/auth-retry.d.ts +124 -0
  18. package/dist/types/auth-storage.d.ts +1026 -0
  19. package/dist/types/dialect/anthropic.d.ts +15 -0
  20. package/dist/types/dialect/catalog.d.ts +3 -0
  21. package/dist/types/dialect/coercion.d.ts +23 -0
  22. package/dist/types/dialect/deepseek.d.ts +14 -0
  23. package/dist/types/dialect/demotion.d.ts +23 -0
  24. package/dist/types/dialect/examples.d.ts +2 -0
  25. package/dist/types/dialect/factory.d.ts +3 -0
  26. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  27. package/dist/types/dialect/gemini.d.ts +17 -0
  28. package/dist/types/dialect/gemma.d.ts +15 -0
  29. package/dist/types/dialect/glm.d.ts +9 -0
  30. package/dist/types/dialect/harmony.d.ts +8 -0
  31. package/dist/types/dialect/hermes.d.ts +9 -0
  32. package/dist/types/dialect/history.d.ts +3 -0
  33. package/dist/types/dialect/index.d.ts +11 -0
  34. package/dist/types/dialect/inventory.d.ts +12 -0
  35. package/dist/types/dialect/kimi.d.ts +14 -0
  36. package/dist/types/dialect/minimax.d.ts +3 -0
  37. package/dist/types/dialect/owned-stream.d.ts +4 -0
  38. package/dist/types/dialect/qwen3.d.ts +9 -0
  39. package/dist/types/dialect/rendering.d.ts +45 -0
  40. package/dist/types/dialect/thinking.d.ts +6 -0
  41. package/dist/types/dialect/types.d.ts +69 -0
  42. package/dist/types/dialect/xml.d.ts +9 -0
  43. package/dist/types/error/abort.d.ts +14 -0
  44. package/dist/types/error/auth-classify.d.ts +16 -0
  45. package/dist/types/error/auth.d.ts +27 -0
  46. package/dist/types/error/aws.d.ts +23 -0
  47. package/dist/types/error/classes.d.ts +102 -0
  48. package/dist/types/error/finalize.d.ts +39 -0
  49. package/dist/types/error/flags.d.ts +79 -0
  50. package/dist/types/error/format.d.ts +20 -0
  51. package/dist/types/error/gateway.d.ts +20 -0
  52. package/dist/types/error/index.d.ts +13 -0
  53. package/dist/types/error/oauth.d.ts +43 -0
  54. package/dist/types/error/provider.d.ts +42 -0
  55. package/dist/types/error/rate-limit.d.ts +59 -0
  56. package/dist/types/error/retryable.d.ts +27 -0
  57. package/dist/types/error/validation.d.ts +32 -0
  58. package/dist/types/index.d.ts +49 -0
  59. package/dist/types/provider-details.d.ts +24 -0
  60. package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
  61. package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
  62. package/dist/types/providers/__tests__/openai-codex-error.test.d.ts +1 -0
  63. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  64. package/dist/types/providers/anthropic-client.d.ts +94 -0
  65. package/dist/types/providers/anthropic-messages-server-schema.d.ts +497 -0
  66. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  67. package/dist/types/providers/anthropic-wire.d.ts +318 -0
  68. package/dist/types/providers/anthropic.d.ts +248 -0
  69. package/dist/types/providers/aws-credentials.d.ts +53 -0
  70. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  71. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  72. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  73. package/dist/types/providers/cursor.d.ts +91 -0
  74. package/dist/types/providers/devin.d.ts +12 -0
  75. package/dist/types/providers/error-message.d.ts +27 -0
  76. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  77. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  78. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  79. package/dist/types/providers/google-auth.d.ts +32 -0
  80. package/dist/types/providers/google-gemini-cli.d.ts +118 -0
  81. package/dist/types/providers/google-interactions.d.ts +65 -0
  82. package/dist/types/providers/google-shared.d.ts +203 -0
  83. package/dist/types/providers/google-types.d.ts +155 -0
  84. package/dist/types/providers/google-vertex.d.ts +7 -0
  85. package/dist/types/providers/google.d.ts +4 -0
  86. package/dist/types/providers/grammar.d.ts +1 -0
  87. package/dist/types/providers/kimi.d.ts +27 -0
  88. package/dist/types/providers/mock.d.ts +178 -0
  89. package/dist/types/providers/ollama.d.ts +7 -0
  90. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  91. package/dist/types/providers/openai-chat-server-schema.d.ts +695 -0
  92. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  93. package/dist/types/providers/openai-chat-wire.d.ts +644 -0
  94. package/dist/types/providers/openai-codex/request-transformer.d.ts +54 -0
  95. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  96. package/dist/types/providers/openai-codex-responses.d.ts +108 -0
  97. package/dist/types/providers/openai-completions.d.ts +45 -0
  98. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  99. package/dist/types/providers/openai-responses-server-schema.d.ts +349 -0
  100. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  101. package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
  102. package/dist/types/providers/openai-responses.d.ts +126 -0
  103. package/dist/types/providers/openai-shared.d.ts +506 -0
  104. package/dist/types/providers/pi-native-client.d.ts +13 -0
  105. package/dist/types/providers/pi-native-server.d.ts +69 -0
  106. package/dist/types/providers/register-builtins.d.ts +32 -0
  107. package/dist/types/providers/synthetic.d.ts +26 -0
  108. package/dist/types/providers/transform-messages.d.ts +11 -0
  109. package/dist/types/providers/vision-guard.d.ts +20 -0
  110. package/dist/types/registry/aimlapi.d.ts +4 -0
  111. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  112. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  113. package/dist/types/registry/anthropic.d.ts +10 -0
  114. package/dist/types/registry/api-key-login.d.ts +42 -0
  115. package/dist/types/registry/api-key-validation.d.ts +43 -0
  116. package/dist/types/registry/azure.d.ts +4 -0
  117. package/dist/types/registry/cerebras.d.ts +7 -0
  118. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  119. package/dist/types/registry/coreweave.d.ts +7 -0
  120. package/dist/types/registry/cursor.d.ts +7 -0
  121. package/dist/types/registry/deepseek.d.ts +8 -0
  122. package/dist/types/registry/derived.d.ts +5 -0
  123. package/dist/types/registry/devin.d.ts +8 -0
  124. package/dist/types/registry/firepass.d.ts +16 -0
  125. package/dist/types/registry/fireworks.d.ts +7 -0
  126. package/dist/types/registry/github-copilot.d.ts +7 -0
  127. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  128. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  129. package/dist/types/registry/google-antigravity.d.ts +9 -0
  130. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  131. package/dist/types/registry/google-vertex.d.ts +5 -0
  132. package/dist/types/registry/google.d.ts +4 -0
  133. package/dist/types/registry/groq.d.ts +4 -0
  134. package/dist/types/registry/huggingface.d.ts +7 -0
  135. package/dist/types/registry/index.d.ts +4 -0
  136. package/dist/types/registry/kagi.d.ts +14 -0
  137. package/dist/types/registry/kilo.d.ts +7 -0
  138. package/dist/types/registry/kimi-code.d.ts +7 -0
  139. package/dist/types/registry/litellm.d.ts +13 -0
  140. package/dist/types/registry/llama-cpp.d.ts +8 -0
  141. package/dist/types/registry/lm-studio.d.ts +8 -0
  142. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  143. package/dist/types/registry/minimax-code.d.ts +6 -0
  144. package/dist/types/registry/minimax.d.ts +4 -0
  145. package/dist/types/registry/mistral.d.ts +4 -0
  146. package/dist/types/registry/moonshot.d.ts +7 -0
  147. package/dist/types/registry/nanogpt.d.ts +7 -0
  148. package/dist/types/registry/nvidia.d.ts +7 -0
  149. package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
  150. package/dist/types/registry/oauth/anthropic.d.ts +23 -0
  151. package/dist/types/registry/oauth/callback-server.d.ts +72 -0
  152. package/dist/types/registry/oauth/cursor.d.ts +15 -0
  153. package/dist/types/registry/oauth/devin.d.ts +5 -0
  154. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  155. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  156. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  157. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  158. package/dist/types/registry/oauth/google-gemini-cli.d.ts +10 -0
  159. package/dist/types/registry/oauth/google-oauth-shared.d.ts +22 -0
  160. package/dist/types/registry/oauth/index.d.ts +64 -0
  161. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  162. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  163. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  164. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  165. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  166. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  167. package/dist/types/registry/oauth/types.d.ts +56 -0
  168. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  169. package/dist/types/registry/oauth/xai-oauth.d.ts +52 -0
  170. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  171. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  172. package/dist/types/registry/ollama.d.ts +12 -0
  173. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  174. package/dist/types/registry/openai-codex.d.ts +9 -0
  175. package/dist/types/registry/openai.d.ts +4 -0
  176. package/dist/types/registry/opencode-go.d.ts +6 -0
  177. package/dist/types/registry/opencode-zen.d.ts +6 -0
  178. package/dist/types/registry/openrouter.d.ts +13 -0
  179. package/dist/types/registry/parallel.d.ts +14 -0
  180. package/dist/types/registry/perplexity.d.ts +7 -0
  181. package/dist/types/registry/qianfan.d.ts +7 -0
  182. package/dist/types/registry/qwen-portal.d.ts +7 -0
  183. package/dist/types/registry/registry.d.ts +303 -0
  184. package/dist/types/registry/sakana.d.ts +7 -0
  185. package/dist/types/registry/synthetic.d.ts +6 -0
  186. package/dist/types/registry/tavily.d.ts +14 -0
  187. package/dist/types/registry/together.d.ts +6 -0
  188. package/dist/types/registry/types.d.ts +51 -0
  189. package/dist/types/registry/umans.d.ts +7 -0
  190. package/dist/types/registry/venice.d.ts +13 -0
  191. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  192. package/dist/types/registry/vllm.d.ts +7 -0
  193. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  194. package/dist/types/registry/xai-oauth.d.ts +7 -0
  195. package/dist/types/registry/xai.d.ts +4 -0
  196. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  197. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  198. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  199. package/dist/types/registry/xiaomi.d.ts +6 -0
  200. package/dist/types/registry/zai.d.ts +7 -0
  201. package/dist/types/registry/zenmux.d.ts +7 -0
  202. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  203. package/dist/types/stream.d.ts +44 -0
  204. package/dist/types/types.d.ts +715 -0
  205. package/dist/types/usage/claude.d.ts +4 -0
  206. package/dist/types/usage/gemini.d.ts +2 -0
  207. package/dist/types/usage/github-copilot.d.ts +7 -0
  208. package/dist/types/usage/google-antigravity.d.ts +15 -0
  209. package/dist/types/usage/kimi.d.ts +2 -0
  210. package/dist/types/usage/minimax-code.d.ts +2 -0
  211. package/dist/types/usage/ollama.d.ts +5 -0
  212. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  213. package/dist/types/usage/openai-codex-reset.d.ts +79 -0
  214. package/dist/types/usage/openai-codex.d.ts +3 -0
  215. package/dist/types/usage/opencode-go.d.ts +2 -0
  216. package/dist/types/usage/shared.d.ts +1 -0
  217. package/dist/types/usage/zai.d.ts +2 -0
  218. package/dist/types/usage.d.ts +346 -0
  219. package/dist/types/utils/abort.d.ts +25 -0
  220. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  221. package/dist/types/utils/block-symbols.d.ts +20 -0
  222. package/dist/types/utils/deterministic-id.d.ts +16 -0
  223. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  224. package/dist/types/utils/event-stream.d.ts +30 -0
  225. package/dist/types/utils/foundry.d.ts +1 -0
  226. package/dist/types/utils/google-validation.d.ts +2 -0
  227. package/dist/types/utils/harmony-leak.d.ts +118 -0
  228. package/dist/types/utils/http-inspector.d.ts +30 -0
  229. package/dist/types/utils/idle-iterator.d.ts +137 -0
  230. package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
  231. package/dist/types/utils/openai-http.d.ts +54 -0
  232. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  233. package/dist/types/utils/parse-bind.d.ts +23 -0
  234. package/dist/types/utils/provider-response.d.ts +3 -0
  235. package/dist/types/utils/proxy.d.ts +29 -0
  236. package/dist/types/utils/request-debug.d.ts +29 -0
  237. package/dist/types/utils/retry-after.d.ts +4 -0
  238. package/dist/types/utils/retry.d.ts +14 -0
  239. package/dist/types/utils/schema/adapt.d.ts +24 -0
  240. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  241. package/dist/types/utils/schema/dereference.d.ts +11 -0
  242. package/dist/types/utils/schema/draft.d.ts +10 -0
  243. package/dist/types/utils/schema/equality.d.ts +4 -0
  244. package/dist/types/utils/schema/fields.d.ts +54 -0
  245. package/dist/types/utils/schema/index.d.ts +15 -0
  246. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  247. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  248. package/dist/types/utils/schema/normalize.d.ts +124 -0
  249. package/dist/types/utils/schema/spill.d.ts +8 -0
  250. package/dist/types/utils/schema/stamps.d.ts +17 -0
  251. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  252. package/dist/types/utils/schema/types.d.ts +4 -0
  253. package/dist/types/utils/schema/typescript.d.ts +18 -0
  254. package/dist/types/utils/schema/wire.d.ts +92 -0
  255. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  256. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  257. package/dist/types/utils/sse-debug.d.ts +5 -0
  258. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  259. package/dist/types/utils/thinking-loop.d.ts +102 -0
  260. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  261. package/dist/types/utils/tool-choice.d.ts +50 -0
  262. package/dist/types/utils/validation.d.ts +42 -0
  263. package/dist/types/utils.d.ts +24 -0
  264. package/package.json +139 -0
  265. package/src/api-registry.ts +109 -0
  266. package/src/auth-broker/client.ts +359 -0
  267. package/src/auth-broker/discover.ts +222 -0
  268. package/src/auth-broker/index.ts +7 -0
  269. package/src/auth-broker/refresher.ts +117 -0
  270. package/src/auth-broker/remote-store.ts +657 -0
  271. package/src/auth-broker/server.ts +646 -0
  272. package/src/auth-broker/snapshot-cache.ts +191 -0
  273. package/src/auth-broker/types.ts +130 -0
  274. package/src/auth-broker/wire-schemas.ts +249 -0
  275. package/src/auth-gateway/http.ts +194 -0
  276. package/src/auth-gateway/index.ts +3 -0
  277. package/src/auth-gateway/server.ts +802 -0
  278. package/src/auth-gateway/types.ts +151 -0
  279. package/src/auth-retry.ts +250 -0
  280. package/src/auth-storage.ts +5576 -0
  281. package/src/dialect/anthropic.md +31 -0
  282. package/src/dialect/anthropic.ts +608 -0
  283. package/src/dialect/catalog.ts +29 -0
  284. package/src/dialect/coercion.ts +136 -0
  285. package/src/dialect/deepseek.md +24 -0
  286. package/src/dialect/deepseek.ts +609 -0
  287. package/src/dialect/demotion.ts +36 -0
  288. package/src/dialect/examples.ts +33 -0
  289. package/src/dialect/factory.ts +34 -0
  290. package/src/dialect/fenced-thinking.ts +184 -0
  291. package/src/dialect/gemini.md +44 -0
  292. package/src/dialect/gemini.ts +597 -0
  293. package/src/dialect/gemma.md +33 -0
  294. package/src/dialect/gemma.ts +387 -0
  295. package/src/dialect/glm.md +32 -0
  296. package/src/dialect/glm.ts +456 -0
  297. package/src/dialect/harmony.md +31 -0
  298. package/src/dialect/harmony.ts +346 -0
  299. package/src/dialect/hermes.md +25 -0
  300. package/src/dialect/hermes.ts +206 -0
  301. package/src/dialect/history.ts +81 -0
  302. package/src/dialect/index.ts +15 -0
  303. package/src/dialect/inventory.ts +73 -0
  304. package/src/dialect/kimi.md +24 -0
  305. package/src/dialect/kimi.ts +340 -0
  306. package/src/dialect/minimax.md +31 -0
  307. package/src/dialect/minimax.ts +95 -0
  308. package/src/dialect/owned-stream.ts +470 -0
  309. package/src/dialect/prompt-template.md +12 -0
  310. package/src/dialect/qwen3.md +28 -0
  311. package/src/dialect/qwen3.ts +240 -0
  312. package/src/dialect/rendering.ts +249 -0
  313. package/src/dialect/thinking.ts +122 -0
  314. package/src/dialect/types.ts +57 -0
  315. package/src/dialect/xml.md +22 -0
  316. package/src/dialect/xml.ts +90 -0
  317. package/src/error/abort.ts +18 -0
  318. package/src/error/auth-classify.ts +30 -0
  319. package/src/error/auth.ts +48 -0
  320. package/src/error/aws.ts +31 -0
  321. package/src/error/classes.ts +186 -0
  322. package/src/error/finalize.ts +69 -0
  323. package/src/error/flags.ts +506 -0
  324. package/src/error/format.ts +45 -0
  325. package/src/error/gateway.ts +96 -0
  326. package/src/error/index.ts +13 -0
  327. package/src/error/oauth.ts +58 -0
  328. package/src/error/provider.ts +62 -0
  329. package/src/error/rate-limit.ts +161 -0
  330. package/src/error/retryable.ts +70 -0
  331. package/src/error/validation.ts +44 -0
  332. package/src/index.ts +49 -0
  333. package/src/provider-details.ts +90 -0
  334. package/src/providers/__tests__/google-auth.test.ts +144 -0
  335. package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
  336. package/src/providers/__tests__/openai-codex-error.test.ts +84 -0
  337. package/src/providers/amazon-bedrock.ts +1042 -0
  338. package/src/providers/anthropic-client.ts +295 -0
  339. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  340. package/src/providers/anthropic-messages-server.ts +756 -0
  341. package/src/providers/anthropic-wire.ts +318 -0
  342. package/src/providers/anthropic.ts +4078 -0
  343. package/src/providers/aws-credentials.ts +586 -0
  344. package/src/providers/aws-eventstream.ts +181 -0
  345. package/src/providers/aws-sigv4.ts +218 -0
  346. package/src/providers/azure-openai-responses.ts +382 -0
  347. package/src/providers/cursor/proto/agent.proto +3526 -0
  348. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  349. package/src/providers/cursor/proto/buf.yaml +17 -0
  350. package/src/providers/cursor.ts +2695 -0
  351. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  352. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  353. package/src/providers/devin/proto/buf.yaml +17 -0
  354. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  355. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  356. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  357. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  358. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  359. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  360. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  361. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  362. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  363. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  364. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  365. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  366. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  367. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  368. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  369. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  370. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  371. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  372. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  373. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  374. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  375. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  376. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  377. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  378. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  379. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  380. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  381. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  382. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  383. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  384. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  385. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  386. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  387. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  388. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  389. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  390. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  391. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  392. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  393. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  394. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  395. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  396. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  397. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  398. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  399. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  400. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  401. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  402. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  403. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  404. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  405. package/src/providers/devin.ts +577 -0
  406. package/src/providers/error-message.ts +21 -0
  407. package/src/providers/github-copilot-headers.ts +141 -0
  408. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  409. package/src/providers/gitlab-duo-workflow.ts +3058 -0
  410. package/src/providers/gitlab-duo.ts +395 -0
  411. package/src/providers/google-auth.ts +350 -0
  412. package/src/providers/google-gemini-cli.ts +1362 -0
  413. package/src/providers/google-interactions.ts +753 -0
  414. package/src/providers/google-shared.ts +1103 -0
  415. package/src/providers/google-types.ts +180 -0
  416. package/src/providers/google-vertex.ts +183 -0
  417. package/src/providers/google.ts +87 -0
  418. package/src/providers/grammar.ts +70 -0
  419. package/src/providers/kimi.ts +52 -0
  420. package/src/providers/mock.ts +507 -0
  421. package/src/providers/ollama.ts +773 -0
  422. package/src/providers/openai-anthropic-shim.ts +152 -0
  423. package/src/providers/openai-chat-server-schema.ts +242 -0
  424. package/src/providers/openai-chat-server.ts +715 -0
  425. package/src/providers/openai-chat-wire.ts +847 -0
  426. package/src/providers/openai-codex/request-transformer.ts +295 -0
  427. package/src/providers/openai-codex/response-handler.ts +102 -0
  428. package/src/providers/openai-codex-responses.ts +3468 -0
  429. package/src/providers/openai-completions.ts +2173 -0
  430. package/src/providers/openai-reasoning-fallback.ts +269 -0
  431. package/src/providers/openai-responses-reasoning-suppression.md +1 -0
  432. package/src/providers/openai-responses-server-schema.ts +282 -0
  433. package/src/providers/openai-responses-server.ts +1280 -0
  434. package/src/providers/openai-responses-wire.ts +6391 -0
  435. package/src/providers/openai-responses.ts +1022 -0
  436. package/src/providers/openai-shared.ts +2648 -0
  437. package/src/providers/pi-native-client.ts +266 -0
  438. package/src/providers/pi-native-server.ts +242 -0
  439. package/src/providers/register-builtins.ts +475 -0
  440. package/src/providers/synthetic.ts +50 -0
  441. package/src/providers/transform-messages.ts +787 -0
  442. package/src/providers/vision-guard.ts +54 -0
  443. package/src/registry/aimlapi.ts +6 -0
  444. package/src/registry/alibaba-coding-plan.ts +95 -0
  445. package/src/registry/amazon-bedrock.ts +22 -0
  446. package/src/registry/anthropic.ts +26 -0
  447. package/src/registry/api-key-login.ts +112 -0
  448. package/src/registry/api-key-validation.ts +161 -0
  449. package/src/registry/azure.ts +6 -0
  450. package/src/registry/cerebras.ts +23 -0
  451. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  452. package/src/registry/coreweave.ts +40 -0
  453. package/src/registry/cursor.ts +20 -0
  454. package/src/registry/deepseek.ts +46 -0
  455. package/src/registry/derived.ts +9 -0
  456. package/src/registry/devin.ts +15 -0
  457. package/src/registry/firepass.ts +32 -0
  458. package/src/registry/fireworks.ts +28 -0
  459. package/src/registry/github-copilot.ts +22 -0
  460. package/src/registry/gitlab-duo-workflow.ts +20 -0
  461. package/src/registry/gitlab-duo.ts +19 -0
  462. package/src/registry/google-antigravity.ts +22 -0
  463. package/src/registry/google-gemini-cli.ts +22 -0
  464. package/src/registry/google-vertex.ts +38 -0
  465. package/src/registry/google.ts +6 -0
  466. package/src/registry/groq.ts +6 -0
  467. package/src/registry/huggingface.ts +29 -0
  468. package/src/registry/index.ts +4 -0
  469. package/src/registry/kagi.ts +46 -0
  470. package/src/registry/kilo.ts +114 -0
  471. package/src/registry/kimi-code.ts +17 -0
  472. package/src/registry/litellm.ts +45 -0
  473. package/src/registry/llama-cpp.ts +35 -0
  474. package/src/registry/lm-studio.ts +31 -0
  475. package/src/registry/minimax-code-cn.ts +12 -0
  476. package/src/registry/minimax-code.ts +12 -0
  477. package/src/registry/minimax.ts +6 -0
  478. package/src/registry/mistral.ts +6 -0
  479. package/src/registry/moonshot.ts +22 -0
  480. package/src/registry/nanogpt.ts +22 -0
  481. package/src/registry/nvidia.ts +61 -0
  482. package/src/registry/oauth/__tests__/xai-oauth.test.ts +104 -0
  483. package/src/registry/oauth/anthropic.ts +311 -0
  484. package/src/registry/oauth/callback-server.ts +315 -0
  485. package/src/registry/oauth/cursor.ts +171 -0
  486. package/src/registry/oauth/devin.ts +124 -0
  487. package/src/registry/oauth/github-copilot.ts +369 -0
  488. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  489. package/src/registry/oauth/gitlab-duo.ts +222 -0
  490. package/src/registry/oauth/google-antigravity.ts +209 -0
  491. package/src/registry/oauth/google-gemini-cli.ts +273 -0
  492. package/src/registry/oauth/google-oauth-shared.ts +125 -0
  493. package/src/registry/oauth/index.ts +269 -0
  494. package/src/registry/oauth/kimi.ts +289 -0
  495. package/src/registry/oauth/minimax-code.ts +53 -0
  496. package/src/registry/oauth/oauth.html +311 -0
  497. package/src/registry/oauth/openai-codex.ts +364 -0
  498. package/src/registry/oauth/opencode.ts +50 -0
  499. package/src/registry/oauth/perplexity.ts +228 -0
  500. package/src/registry/oauth/pkce.ts +18 -0
  501. package/src/registry/oauth/types.ts +65 -0
  502. package/src/registry/oauth/wafer.ts +24 -0
  503. package/src/registry/oauth/xai-oauth.ts +394 -0
  504. package/src/registry/oauth/xiaomi.ts +211 -0
  505. package/src/registry/ollama-cloud.ts +36 -0
  506. package/src/registry/ollama.ts +43 -0
  507. package/src/registry/openai-codex-device.ts +18 -0
  508. package/src/registry/openai-codex.ts +19 -0
  509. package/src/registry/openai.ts +6 -0
  510. package/src/registry/opencode-go.ts +12 -0
  511. package/src/registry/opencode-zen.ts +12 -0
  512. package/src/registry/openrouter.ts +28 -0
  513. package/src/registry/parallel.ts +45 -0
  514. package/src/registry/perplexity.ts +13 -0
  515. package/src/registry/qianfan.ts +27 -0
  516. package/src/registry/qwen-portal.ts +50 -0
  517. package/src/registry/registry.ts +161 -0
  518. package/src/registry/sakana.ts +22 -0
  519. package/src/registry/synthetic.ts +21 -0
  520. package/src/registry/tavily.ts +45 -0
  521. package/src/registry/together.ts +22 -0
  522. package/src/registry/types.ts +56 -0
  523. package/src/registry/umans.ts +23 -0
  524. package/src/registry/venice.ts +33 -0
  525. package/src/registry/vercel-ai-gateway.ts +38 -0
  526. package/src/registry/vllm.ts +34 -0
  527. package/src/registry/wafer-serverless.ts +12 -0
  528. package/src/registry/xai-oauth.ts +17 -0
  529. package/src/registry/xai.ts +6 -0
  530. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  531. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  532. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  533. package/src/registry/xiaomi.ts +12 -0
  534. package/src/registry/zai.ts +27 -0
  535. package/src/registry/zenmux.ts +22 -0
  536. package/src/registry/zhipu-coding-plan.ts +27 -0
  537. package/src/stream.ts +1778 -0
  538. package/src/types.ts +856 -0
  539. package/src/usage/claude.ts +485 -0
  540. package/src/usage/gemini.ts +258 -0
  541. package/src/usage/github-copilot.ts +424 -0
  542. package/src/usage/google-antigravity.ts +497 -0
  543. package/src/usage/kimi.ts +271 -0
  544. package/src/usage/minimax-code.ts +30 -0
  545. package/src/usage/ollama.ts +41 -0
  546. package/src/usage/openai-codex-base-url.ts +35 -0
  547. package/src/usage/openai-codex-reset.ts +174 -0
  548. package/src/usage/openai-codex.ts +535 -0
  549. package/src/usage/opencode-go.ts +89 -0
  550. package/src/usage/shared.ts +10 -0
  551. package/src/usage/zai.ts +321 -0
  552. package/src/usage.ts +333 -0
  553. package/src/utils/abort.ts +67 -0
  554. package/src/utils/anthropic-auth.ts +93 -0
  555. package/src/utils/block-symbols.ts +32 -0
  556. package/src/utils/deterministic-id.ts +20 -0
  557. package/src/utils/empty-completion-retry.ts +159 -0
  558. package/src/utils/event-stream.ts +171 -0
  559. package/src/utils/foundry.ts +8 -0
  560. package/src/utils/google-validation.ts +25 -0
  561. package/src/utils/harmony-leak.ts +456 -0
  562. package/src/utils/http-inspector.ts +168 -0
  563. package/src/utils/idle-iterator.ts +473 -0
  564. package/src/utils/leaked-thinking-stream.ts +294 -0
  565. package/src/utils/openai-http.ts +122 -0
  566. package/src/utils/openrouter-headers.ts +12 -0
  567. package/src/utils/parse-bind.ts +56 -0
  568. package/src/utils/provider-response.ts +30 -0
  569. package/src/utils/proxy.ts +240 -0
  570. package/src/utils/request-debug.ts +351 -0
  571. package/src/utils/retry-after.ts +110 -0
  572. package/src/utils/retry.ts +59 -0
  573. package/src/utils/schema/CONSTRAINTS.md +166 -0
  574. package/src/utils/schema/adapt.ts +36 -0
  575. package/src/utils/schema/compatibility.ts +435 -0
  576. package/src/utils/schema/dereference.ts +98 -0
  577. package/src/utils/schema/draft.ts +341 -0
  578. package/src/utils/schema/equality.ts +97 -0
  579. package/src/utils/schema/fields.ts +207 -0
  580. package/src/utils/schema/index.ts +15 -0
  581. package/src/utils/schema/json-schema-validator.ts +595 -0
  582. package/src/utils/schema/meta-validator.ts +167 -0
  583. package/src/utils/schema/normalize.ts +1901 -0
  584. package/src/utils/schema/spill.ts +43 -0
  585. package/src/utils/schema/stamps.ts +109 -0
  586. package/src/utils/schema/strict-tool-validation.ts +117 -0
  587. package/src/utils/schema/types.ts +10 -0
  588. package/src/utils/schema/typescript.ts +198 -0
  589. package/src/utils/schema/wire.ts +789 -0
  590. package/src/utils/schema/zod-decontaminate.ts +331 -0
  591. package/src/utils/sdk-stream-timeout.ts +43 -0
  592. package/src/utils/sse-debug.ts +18 -0
  593. package/src/utils/stream-markup-healing.ts +247 -0
  594. package/src/utils/thinking-loop.ts +552 -0
  595. package/src/utils/tool-call-loop-guard.ts +107 -0
  596. package/src/utils/tool-choice.ts +99 -0
  597. package/src/utils/validation.ts +1507 -0
  598. package/src/utils.ts +171 -0
@@ -0,0 +1,2648 @@
1
+ import type { Effort } from "jeopi-catalog/effort";
2
+ import { toFirepassWireModelId, toFireworksWireModelId } from "jeopi-catalog/fireworks-model-id";
3
+ import { isGlm52ReasoningEffortModelId } from "jeopi-catalog/identity";
4
+ import { getSupportedEfforts } from "jeopi-catalog/model-thinking";
5
+ import { calculateCost } from "jeopi-catalog/models";
6
+ import type {
7
+ OpenAICompat,
8
+ OpenAIReasoningDisableMode,
9
+ OpenAIStreamMarkupHealingPattern,
10
+ OpenRouterRouting,
11
+ ResolvedOpenAICompat,
12
+ ResolvedOpenAIResponsesCompat,
13
+ ResolvedOpenAISharedCompat,
14
+ VercelGatewayRouting,
15
+ } from "jeopi-catalog/types";
16
+ import {
17
+ COREWEAVE_PROJECT_HEADER,
18
+ coreWeaveProjectHeaders,
19
+ hasCoreWeaveProjectHeader,
20
+ removeBlankCoreWeaveProjectHeaders,
21
+ } from "jeopi-catalog/wire/coreweave";
22
+ import { parseGitHubCopilotApiKey } from "jeopi-catalog/wire/github-copilot";
23
+ import {
24
+ $env,
25
+ extractHttpStatusFromError,
26
+ logger,
27
+ parseStreamingJson,
28
+ parseStreamingJsonThrottled,
29
+ structuredCloneJSON,
30
+ } from "jeopi-utils";
31
+ import * as AIError from "../error";
32
+ import {
33
+ type Api,
34
+ type AssistantMessage,
35
+ type CacheRetention,
36
+ type Context,
37
+ type ImageContent,
38
+ type Message,
39
+ type MessageAttribution,
40
+ type Model,
41
+ OPENAI_MAX_OUTPUT_TOKENS,
42
+ type Provider,
43
+ type ServiceTier,
44
+ type StopReason,
45
+ type StreamOptions,
46
+ shouldSendServiceTier,
47
+ type TextContent,
48
+ type TextSignatureV1,
49
+ type ThinkingContent,
50
+ type Tool,
51
+ type ToolCall,
52
+ type ToolResultMessage,
53
+ } from "../types";
54
+ import {
55
+ getOpenAIResponsesHistoryItems,
56
+ getOpenAIResponsesHistoryPayload,
57
+ normalizeResponsesToolCallId,
58
+ normalizeSystemPrompts,
59
+ resolveCacheRetention,
60
+ sanitizeOpenAIResponsesHistoryItemsForReplay,
61
+ } from "../utils";
62
+ import {
63
+ clearStreamingPartialJson,
64
+ kStreamingArgumentsDone,
65
+ kStreamingLastParseLen,
66
+ kStreamingPartialJson,
67
+ } from "../utils/block-symbols";
68
+ import type { AssistantMessageEventStream } from "../utils/event-stream";
69
+ import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
70
+ import { getOpenRouterHeaders } from "../utils/openrouter-headers";
71
+ import { isForcedToolChoice } from "../utils/tool-choice";
72
+ import {
73
+ buildCopilotDynamicHeaders,
74
+ hasCopilotVisionInput,
75
+ resolveGitHubCopilotBaseUrl,
76
+ } from "./github-copilot-headers";
77
+ import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
78
+ import type { InputItem } from "./openai-codex/request-transformer";
79
+ import responsesReasoningSuppressionPrompt from "./openai-responses-reasoning-suppression.md" with { type: "text" };
80
+ import type {
81
+ ResponseContentPartAddedEvent,
82
+ ResponseCreateParamsStreaming,
83
+ ResponseCustomToolCall,
84
+ ResponseFunctionToolCall,
85
+ ResponseInput,
86
+ ResponseInputContent,
87
+ ResponseInputImage,
88
+ ResponseInputItem,
89
+ ResponseInputText,
90
+ ResponseOutputItem,
91
+ ResponseOutputMessage,
92
+ ResponseReasoningItem,
93
+ ResponseStatus,
94
+ ResponseStreamEvent,
95
+ } from "./openai-responses-wire";
96
+ import { transformMessages } from "./transform-messages";
97
+ import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
98
+
99
+ export interface OpenAIModelIdentity {
100
+ provider: string;
101
+ id: string;
102
+ baseUrl?: string;
103
+ }
104
+
105
+ export interface OpenAIStrictToolsScope {
106
+ provider: string;
107
+ baseUrl: string | undefined;
108
+ modelId: string;
109
+ }
110
+
111
+ export interface OpenAIStrictToolsState {
112
+ strictTools: {
113
+ disabledModelScopes: Set<string>;
114
+ };
115
+ }
116
+
117
+ export interface OpenAIRequestSetupModel extends OpenAIModelIdentity {
118
+ headers?: Record<string, string>;
119
+ premiumMultiplier?: number;
120
+ compat?: Pick<ResolvedOpenAISharedCompat, "promptCacheSessionHeader">;
121
+ }
122
+
123
+ export interface OpenAIResponsesCacheOptions {
124
+ cacheRetention?: CacheRetention;
125
+ sessionId?: string;
126
+ promptCacheKey?: string;
127
+ }
128
+
129
+ export interface OpenAIRequestSetupOptions {
130
+ apiKey?: string;
131
+ extraHeaders?: Record<string, string>;
132
+ initiatorOverride?: MessageAttribution;
133
+ messages: Message[];
134
+ defaultBaseUrl?: string;
135
+ prependHeaders?: () => Record<string, string>;
136
+ alibabaCodingPlanAuth?: boolean;
137
+ azureChatCompletions?: {
138
+ apiVersion: string;
139
+ deploymentName: string;
140
+ };
141
+ openAISessionId?: string;
142
+ promptCacheSessionId?: string;
143
+ }
144
+
145
+ export interface OpenAIRequestSetup {
146
+ copilotPremiumRequests: number | undefined;
147
+ baseUrl: string | undefined;
148
+ headers: Record<string, string>;
149
+ query: Record<string, string> | undefined;
150
+ requestHeaders: Record<string, string>;
151
+ }
152
+
153
+ function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
154
+ const value = baseUrl?.trim();
155
+ if (!value) return undefined;
156
+ const normalized = value.replace(/\/+$/, "");
157
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
158
+ }
159
+
160
+ function resolveSakanaRequestBaseUrl(): string | undefined {
161
+ return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
162
+ }
163
+
164
+ function applyCoreWeaveProjectHeader(headers: Record<string, string>): void {
165
+ removeBlankCoreWeaveProjectHeaders(headers);
166
+ if (hasCoreWeaveProjectHeader(headers)) {
167
+ return;
168
+ }
169
+ const projectHeaders = coreWeaveProjectHeaders($env);
170
+ if (projectHeaders) {
171
+ headers[COREWEAVE_PROJECT_HEADER] = projectHeaders[COREWEAVE_PROJECT_HEADER];
172
+ }
173
+ }
174
+
175
+ export function resolveOpenAIRequestSetup(
176
+ model: OpenAIRequestSetupModel,
177
+ options: OpenAIRequestSetupOptions,
178
+ ): OpenAIRequestSetup {
179
+ let apiKey = options.apiKey;
180
+ if (!apiKey) {
181
+ if (!$env.OPENAI_API_KEY) {
182
+ throw new AIError.MissingApiKeyError(
183
+ undefined,
184
+ "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.",
185
+ );
186
+ }
187
+ apiKey = $env.OPENAI_API_KEY;
188
+ }
189
+ const rawApiKey = apiKey;
190
+ let headers = { ...(model.headers ?? {}) };
191
+ if (model.provider === "openrouter") {
192
+ Object.assign(headers, getOpenRouterHeaders());
193
+ }
194
+ Object.assign(headers, options.extraHeaders);
195
+ if (model.provider === "coreweave") {
196
+ applyCoreWeaveProjectHeader(headers);
197
+ }
198
+ if (options.prependHeaders) {
199
+ headers = { ...options.prependHeaders(), ...headers };
200
+ }
201
+
202
+ let copilotPremiumRequests: number | undefined;
203
+ let baseUrl = model.baseUrl;
204
+ if (model.provider === "moonshot") {
205
+ // Bundled `moonshot` catalog models hardcode the international endpoint
206
+ // (`api.moonshot.ai`). MOONSHOT_BASE_URL lets users redirect the provider
207
+ // at the China platform (`api.moonshot.cn`), which only accepts China keys
208
+ // and rejects the international host. (#2883)
209
+ const moonshotBaseUrl = $env.MOONSHOT_BASE_URL?.trim();
210
+ if (moonshotBaseUrl) {
211
+ baseUrl = moonshotBaseUrl;
212
+ }
213
+ }
214
+ if (model.provider === "sakana") {
215
+ const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
216
+ if (sakanaBaseUrl) {
217
+ baseUrl = sakanaBaseUrl;
218
+ }
219
+ }
220
+ if (model.provider === "github-copilot") {
221
+ apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
222
+ const copilot = buildCopilotDynamicHeaders({
223
+ messages: options.messages,
224
+ hasImages: hasCopilotVisionInput(options.messages),
225
+ premiumMultiplier: model.premiumMultiplier,
226
+ headers,
227
+ initiatorOverride: options.initiatorOverride,
228
+ });
229
+ Object.assign(headers, copilot.headers);
230
+ copilotPremiumRequests = copilot.premiumRequests;
231
+ baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
232
+ }
233
+
234
+ if (options.alibabaCodingPlanAuth && model.provider === "alibaba-coding-plan") {
235
+ try {
236
+ const parsed = JSON.parse(rawApiKey);
237
+ if (typeof parsed?.token === "string") {
238
+ apiKey = parsed.token;
239
+ }
240
+ if (typeof parsed?.enterpriseUrl === "string") {
241
+ baseUrl = parsed.enterpriseUrl;
242
+ }
243
+ } catch {
244
+ // Not JSON — use raw apiKey and catalog baseUrl.
245
+ }
246
+ }
247
+
248
+ let query: Record<string, string> | undefined;
249
+ if (options.azureChatCompletions && baseUrl?.includes(".openai.azure.com")) {
250
+ if (!baseUrl.includes("/deployments/")) {
251
+ baseUrl = `${baseUrl}/deployments/${options.azureChatCompletions.deploymentName}`;
252
+ }
253
+ query = { "api-version": options.azureChatCompletions.apiVersion };
254
+ }
255
+
256
+ if (options.openAISessionId && model.provider === "openai") {
257
+ headers.session_id ??= options.openAISessionId;
258
+ headers["x-client-request-id"] ??= options.openAISessionId;
259
+ }
260
+ if (options.promptCacheSessionId && model.compat?.promptCacheSessionHeader) {
261
+ headers[model.compat.promptCacheSessionHeader] ??= options.promptCacheSessionId;
262
+ }
263
+
264
+ if (options.defaultBaseUrl !== undefined) {
265
+ baseUrl = baseUrl ?? ($env.OPENAI_BASE_URL?.trim() || options.defaultBaseUrl);
266
+ }
267
+ const requestHeaders = { ...headers };
268
+ headers.Authorization ??= `Bearer ${apiKey}`;
269
+ return { copilotPremiumRequests, baseUrl, headers, query, requestHeaders };
270
+ }
271
+
272
+ export function applyOpenAIServiceTier(
273
+ params: { service_tier?: ServiceTier | null | undefined },
274
+ serviceTier: ServiceTier | null | undefined,
275
+ provider: Provider | undefined,
276
+ ): void {
277
+ if (!shouldSendServiceTier(serviceTier, provider)) return;
278
+ if (serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority") {
279
+ params.service_tier = serviceTier;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Standard OpenAI Responses service-tier cost multipliers. The non-Codex
285
+ * Responses path bills the tier it was served (or requested): Flex processing is
286
+ * half price; Priority is a 2x premium. Codex bills the same tiers with its own
287
+ * table (Priority is 2.5x on gpt-5.5) and applies that separately.
288
+ */
289
+ function getOpenAIResponsesServiceTierCostMultiplier(tier: string | null | undefined): number {
290
+ switch (tier) {
291
+ case "flex":
292
+ return 0.5;
293
+ case "priority":
294
+ return 2;
295
+ default:
296
+ return 1;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Adjust resolved cost by the service tier OpenAI actually billed — parity with
302
+ * Codex (`applyCodexServiceTierPricing`), but with the standard (non-Codex)
303
+ * multipliers. The served tier comes from the response echo, falling back to the
304
+ * resolved request tier. Scoped to `provider: "openai"` (the only standard
305
+ * Responses biller) so an echoed `service_tier` from an Azure/OpenRouter/Copilot
306
+ * proxy can never skew those costs.
307
+ */
308
+ export function applyOpenAIResponsesServiceTierCost(
309
+ model: Pick<Model, "provider">,
310
+ usage: AssistantMessage["usage"],
311
+ responseServiceTier: unknown,
312
+ requestServiceTier: ServiceTier | null | undefined,
313
+ ): void {
314
+ if (model.provider !== "openai") return;
315
+ // The response echo is authoritative when present (OpenAI may downgrade a
316
+ // requested priority/flex turn to default under load); only fall back to the
317
+ // requested tier when the response omits the echo entirely.
318
+ const served = typeof responseServiceTier === "string" ? responseServiceTier : (requestServiceTier ?? undefined);
319
+ const multiplier = getOpenAIResponsesServiceTierCostMultiplier(served);
320
+ if (multiplier === 1) return;
321
+ usage.cost.input *= multiplier;
322
+ usage.cost.output *= multiplier;
323
+ usage.cost.cacheRead *= multiplier;
324
+ usage.cost.cacheWrite *= multiplier;
325
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
326
+ }
327
+
328
+ export interface OpenAIUsageAccountingInput {
329
+ promptTokens: number;
330
+ outputTokens: number;
331
+ cachedTokens: number;
332
+ reasoningTokens: number;
333
+ cacheWriteOpenRouter: number | undefined;
334
+ cacheWriteDeepSeek: number | undefined;
335
+ hasDeepSeekCacheHitAndMiss: boolean;
336
+ }
337
+
338
+ export interface OpenAIUsageAccounting {
339
+ input: number;
340
+ output: number;
341
+ cacheRead: number;
342
+ cacheWrite: number;
343
+ totalTokens: number;
344
+ reasoningTokens?: number;
345
+ }
346
+
347
+ export function calculateOpenAIUsageAccounting(accounting: OpenAIUsageAccountingInput): OpenAIUsageAccounting {
348
+ const cacheWriteTokens = accounting.cacheWriteOpenRouter ?? accounting.cacheWriteDeepSeek ?? 0;
349
+ const isDeepSeekUsage =
350
+ accounting.hasDeepSeekCacheHitAndMiss &&
351
+ accounting.cacheWriteOpenRouter === undefined &&
352
+ (accounting.cacheWriteDeepSeek ?? 0) > 0;
353
+ const input = isDeepSeekUsage
354
+ ? Math.max(0, accounting.promptTokens - accounting.cachedTokens)
355
+ : Math.max(0, accounting.promptTokens - accounting.cachedTokens - cacheWriteTokens);
356
+ const cacheWrite = isDeepSeekUsage ? 0 : cacheWriteTokens;
357
+ return {
358
+ input,
359
+ output: accounting.outputTokens,
360
+ cacheRead: accounting.cachedTokens,
361
+ cacheWrite,
362
+ totalTokens: input + accounting.outputTokens + accounting.cachedTokens + cacheWrite,
363
+ ...(accounting.reasoningTokens > 0 ? { reasoningTokens: accounting.reasoningTokens } : {}),
364
+ };
365
+ }
366
+
367
+ export function normalizeOpenAIResponsesPromptCacheKey(sessionId: string | undefined): string | undefined {
368
+ return normalizeOpenAIStableId(sessionId, 64, "pc_");
369
+ }
370
+
371
+ export function normalizeOpenRouterResponsesSessionId(sessionId: string | undefined): string | undefined {
372
+ return normalizeOpenAIStableId(sessionId, 256, "session_");
373
+ }
374
+
375
+ export function getOpenAIResponsesPromptCacheKey(options: OpenAIResponsesCacheOptions | undefined): string | undefined {
376
+ if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
377
+ return normalizeOpenAIResponsesPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
378
+ }
379
+
380
+ export function getOpenAIResponsesRoutingSessionId(
381
+ options: Pick<OpenAIResponsesCacheOptions, "cacheRetention" | "sessionId"> | undefined,
382
+ ): string | undefined {
383
+ if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
384
+ return normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
385
+ }
386
+
387
+ export function getOpenRouterResponsesSessionId(
388
+ options: Pick<OpenAIResponsesCacheOptions, "cacheRetention" | "sessionId"> | undefined,
389
+ ): string | undefined {
390
+ if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
391
+ return normalizeOpenRouterResponsesSessionId(options?.sessionId);
392
+ }
393
+
394
+ export function parseAzureDeploymentNameMap(value: string | undefined): Map<string, string> {
395
+ const map = new Map<string, string>();
396
+ if (!value) return map;
397
+ for (const entry of value.split(",")) {
398
+ const trimmed = entry.trim();
399
+ if (!trimmed) continue;
400
+ const [modelId, deploymentName] = trimmed.split("=", 2);
401
+ if (!modelId || !deploymentName) continue;
402
+ map.set(modelId.trim(), deploymentName.trim());
403
+ }
404
+ return map;
405
+ }
406
+
407
+ export function createOpenAIStrictToolsState(): OpenAIStrictToolsState {
408
+ return {
409
+ strictTools: {
410
+ disabledModelScopes: new Set<string>(),
411
+ },
412
+ };
413
+ }
414
+
415
+ export function clearOpenAIStrictToolsState(state: OpenAIStrictToolsState): void {
416
+ state.strictTools.disabledModelScopes.clear();
417
+ }
418
+
419
+ export function getOpenAIStrictToolsScope(
420
+ model: OpenAIModelIdentity,
421
+ resolvedBaseUrl: string | undefined,
422
+ ): OpenAIStrictToolsScope {
423
+ return {
424
+ provider: model.provider,
425
+ baseUrl: resolvedBaseUrl ?? model.baseUrl,
426
+ modelId: model.id,
427
+ };
428
+ }
429
+
430
+ export function isStrictToolsDisabledForScope(
431
+ state: OpenAIStrictToolsState | undefined,
432
+ scope: OpenAIStrictToolsScope | undefined,
433
+ ): boolean {
434
+ if (!scope) return false;
435
+ return (
436
+ state?.strictTools.disabledModelScopes.has(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`) ?? false
437
+ );
438
+ }
439
+
440
+ export function disableStrictToolsForScope(
441
+ state: OpenAIStrictToolsState | undefined,
442
+ scope: OpenAIStrictToolsScope | undefined,
443
+ ): void {
444
+ if (!scope) return;
445
+ state?.strictTools.disabledModelScopes.add(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`);
446
+ }
447
+
448
+ export function isOpenRouterAnthropicModel(model: OpenAIModelIdentity): boolean {
449
+ return model.provider === "openrouter" && model.id.toLowerCase().startsWith("anthropic/");
450
+ }
451
+
452
+ /**
453
+ * Append an OpenRouter routing-variant suffix (e.g. `:nitro`, `:floor`, `:online`, `:exacto`)
454
+ * to a model id when no explicit variant is already present. A variant is considered
455
+ * "already present" when `modelId` contains a colon after the last `/` separator —
456
+ * which covers both user-typed selectors (`anthropic/claude-haiku:nitro`) and catalog
457
+ * entries that bake the variant in (`deepseek/deepseek-v3.1-terminus:exacto`).
458
+ */
459
+ export function applyOpenRouterRoutingVariant(modelId: string, variant: string | undefined): string {
460
+ if (!variant) return modelId;
461
+ const lastSlash = modelId.lastIndexOf("/");
462
+ const lastColon = modelId.lastIndexOf(":");
463
+ if (lastColon > lastSlash) return modelId;
464
+ return `${modelId}:${variant}`;
465
+ }
466
+
467
+ export function applyWireModelIdTransform(
468
+ baseId: string,
469
+ mode: ResolvedOpenAISharedCompat["wireModelIdMode"],
470
+ openrouterVariant?: string,
471
+ ): string {
472
+ switch (mode) {
473
+ case "firepass":
474
+ return toFirepassWireModelId(baseId);
475
+ case "fireworks":
476
+ return toFireworksWireModelId(baseId);
477
+ case "openrouter":
478
+ return applyOpenRouterRoutingVariant(baseId, openrouterVariant);
479
+ default:
480
+ return baseId;
481
+ }
482
+ }
483
+
484
+ export interface OpenAIOutputTokenParam {
485
+ field: "max_tokens" | "max_completion_tokens" | "max_output_tokens";
486
+ value: number;
487
+ }
488
+
489
+ export interface ResolveOpenAIOutputTokenInput {
490
+ /** Wire field the endpoint expects for the output cap. */
491
+ field: OpenAIOutputTokenParam["field"];
492
+ /** Caller-supplied output cap (model-defaulted by `stream.ts`, or null/undefined on direct provider calls). */
493
+ maxTokens: number | null | undefined;
494
+ /** Whether the caller explicitly set `maxTokens` (routing omission only applies when false). */
495
+ maxTokensExplicit: boolean;
496
+ /** Model output cap (`model.maxTokens`). */
497
+ modelMaxTokens: number | null | undefined;
498
+ /** Drop the field entirely — proxies with unknown upstream caps (Ollama via `model.omitMaxOutputTokens`). */
499
+ omitMaxOutputTokens: boolean;
500
+ /** The model sits behind OpenRouter (catalog default caps are omitted so each upstream self-caps). */
501
+ isOpenRouterHost: boolean;
502
+ /** Endpoint always needs a cap (Kimi-family TPM math); supplies the model default when the caller did not. */
503
+ alwaysSendMaxTokens: boolean;
504
+ /** Hard provider clamp; defaults to {@link OPENAI_MAX_OUTPUT_TOKENS}. */
505
+ providerOutputClamp?: number;
506
+ }
507
+
508
+ /**
509
+ * Resolve the single output-token wire parameter shared by Chat Completions
510
+ * (`max_tokens`/`max_completion_tokens`) and the Responses family
511
+ * (`max_output_tokens`). Centralizes the provider exceptions that previously
512
+ * lived inline in both `buildParams`:
513
+ * - `alwaysSendMaxTokens`: Kimi-family endpoints derive TPM limits from the
514
+ * cap and require one on every call, so default from the model cap (or
515
+ * {@link OPENAI_MAX_OUTPUT_TOKENS}) when the caller omitted it.
516
+ * - OpenRouter routing omission: OpenRouter fans out to upstreams whose output
517
+ * caps differ from the catalog value, so a catalog default above the routed
518
+ * upstream's cap makes OpenRouter skip that upstream. Omit catalog defaults
519
+ * (explicit caller caps still win) so `provider.order`/`only` is honored.
520
+ * - model/provider clamp: never exceed `model.maxTokens` or the provider clamp
521
+ * (`OPENAI_MAX_OUTPUT_TOKENS`, raised for GLM-5.2 reasoning by the caller).
522
+ * - `omitMaxOutputTokens`: proxies (Ollama) with unknown upstream caps drop it.
523
+ */
524
+ export function resolveOpenAIOutputTokenParam(
525
+ input: ResolveOpenAIOutputTokenInput,
526
+ ): OpenAIOutputTokenParam | undefined {
527
+ if (input.omitMaxOutputTokens) return undefined;
528
+ const requested =
529
+ input.maxTokens ?? (input.alwaysSendMaxTokens ? (input.modelMaxTokens ?? OPENAI_MAX_OUTPUT_TOKENS) : undefined);
530
+ if (requested === undefined) return undefined;
531
+ if (input.isOpenRouterHost && !input.alwaysSendMaxTokens && !input.maxTokensExplicit) return undefined;
532
+ const value = Math.min(
533
+ requested,
534
+ input.modelMaxTokens ?? Number.POSITIVE_INFINITY,
535
+ input.providerOutputClamp ?? OPENAI_MAX_OUTPUT_TOKENS,
536
+ );
537
+ if (!(value > 0)) return undefined;
538
+ return { field: input.field, value };
539
+ }
540
+
541
+ export interface OpenAIGatewayRoutingParams {
542
+ provider?: OpenRouterRouting;
543
+ providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
544
+ }
545
+
546
+ export interface OpenAIGatewayRoutingCompat {
547
+ isOpenRouterHost: boolean;
548
+ openRouterRouting?: OpenRouterRouting;
549
+ isVercelGatewayHost?: boolean;
550
+ vercelGatewayRouting?: VercelGatewayRouting;
551
+ }
552
+
553
+ /**
554
+ * Apply gateway routing preferences to the request body. OpenRouter routes via
555
+ * the top-level `provider` field; the Vercel AI Gateway routes via
556
+ * `providerOptions.gateway`. Both Chat Completions and Responses call this; the
557
+ * Vercel branch is inert for Responses, whose resolved compat never sets
558
+ * `isVercelGatewayHost`.
559
+ */
560
+ export function applyOpenAIGatewayRouting(
561
+ params: OpenAIGatewayRoutingParams,
562
+ compat: OpenAIGatewayRoutingCompat,
563
+ ): void {
564
+ if (compat.isOpenRouterHost && compat.openRouterRouting) {
565
+ params.provider = compat.openRouterRouting;
566
+ }
567
+ if (compat.isVercelGatewayHost && compat.vercelGatewayRouting) {
568
+ const routing = compat.vercelGatewayRouting;
569
+ if (routing.only || routing.order) {
570
+ const gatewayOptions: { only?: string[]; order?: string[] } = {};
571
+ if (routing.only) gatewayOptions.only = routing.only;
572
+ if (routing.order) gatewayOptions.order = routing.order;
573
+ params.providerOptions = { gateway: gatewayOptions };
574
+ }
575
+ }
576
+ }
577
+
578
+ export interface OpenAIExtraBodyOptions {
579
+ /**
580
+ * Fireworks rejects DeepSeek-style `thinking` toggles alongside OpenAI-style
581
+ * `reasoning_effort`; drop `thinking` when the effort field carries the level.
582
+ */
583
+ dropThinkingWhenReasoningEffort?: boolean;
584
+ }
585
+
586
+ /**
587
+ * Merge a compat/options `extraBody` blob into the request params. When
588
+ * `dropThinkingWhenReasoningEffort` is set and `reasoning_effort` is present,
589
+ * delete the conflicting `thinking` toggle (Fireworks rejects both together).
590
+ */
591
+ export function applyOpenAIExtraBody<P extends object>(
592
+ params: P,
593
+ extraBody: Record<string, unknown> | undefined,
594
+ options?: OpenAIExtraBodyOptions,
595
+ ): void {
596
+ if (!extraBody) return;
597
+ Object.assign(params, extraBody);
598
+ if (options?.dropThinkingWhenReasoningEffort) {
599
+ const shaped = params as { reasoning_effort?: unknown; thinking?: unknown };
600
+ if (shaped.reasoning_effort !== undefined) {
601
+ delete shaped.thinking;
602
+ }
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Chat Completions streaming request body shaped by the OpenAI-family providers.
608
+ * Extends the vendored SDK params with the compat dialect fields pi-ai emits
609
+ * (binary `thinking`, Qwen `enable_thinking`/`chat_template_kwargs`, nested
610
+ * `reasoning`, gateway `provider`/`providerOptions`, sampling extras). Lives in
611
+ * the shared module beside the request-shaping helpers that mutate it.
612
+ */
613
+ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming, "reasoning_effort" | "service_tier"> & {
614
+ top_k?: number;
615
+ min_p?: number;
616
+ repetition_penalty?: number;
617
+ thinking?: { type: "enabled" | "disabled"; keep?: "all" };
618
+ enable_thinking?: boolean;
619
+ preserve_thinking?: boolean;
620
+ chat_template_kwargs?: { enable_thinking?: boolean; preserve_thinking?: boolean };
621
+ reasoning?: { effort?: string } | { enabled: false };
622
+ reasoning_effort?: string | null;
623
+ service_tier?: ServiceTier;
624
+ tool_stream?: boolean;
625
+ provider?: OpenAICompat["openRouterRouting"];
626
+ providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
627
+ };
628
+
629
+ /** Reasoning-relevant slice of caller options the Chat Completions dialect dispatch reads. */
630
+ export interface ChatCompletionsReasoningOptions {
631
+ reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
632
+ disableReasoning?: boolean;
633
+ }
634
+
635
+ export type OpenAICompatEndpoint = "chat-completions" | "responses";
636
+
637
+ export type OpenAIReasoningDisableReason = "caller" | "forced-tool-choice" | "tool-choice" | "not-requested";
638
+
639
+ export type OpenAICompatPolicyCompat = ResolvedOpenAISharedCompat &
640
+ Partial<ResolvedOpenAICompat> &
641
+ Partial<ResolvedOpenAIResponsesCompat>;
642
+
643
+ export interface ResolveOpenAICompatPolicyOptions {
644
+ endpoint: OpenAICompatEndpoint;
645
+ compat?: OpenAICompatPolicyCompat;
646
+ reasoning?: string;
647
+ disableReasoning?: boolean;
648
+ toolChoice?: unknown;
649
+ strictResponsesPairing?: boolean;
650
+ includeEncryptedReasoning?: boolean;
651
+ filterReasoningHistory?: boolean;
652
+ omitReasoningEffort?: boolean;
653
+ }
654
+
655
+ export interface OpenAICompatPolicy {
656
+ endpoint: OpenAICompatEndpoint;
657
+ compat: OpenAICompatPolicyCompat;
658
+ reasoning: {
659
+ modelSupported: boolean;
660
+ supportsParams: boolean;
661
+ requestedEffort?: string;
662
+ wireEffort?: string;
663
+ enabled: boolean;
664
+ disabled: boolean;
665
+ disableReason?: OpenAIReasoningDisableReason;
666
+ dialect: ResolvedOpenAISharedCompat["thinkingFormat"];
667
+ disableMode: OpenAIReasoningDisableMode;
668
+ omitReasoningEffort: boolean;
669
+ includeEncryptedReasoning: boolean;
670
+ filterReasoningHistory: boolean;
671
+ requiresReasoningContentForToolCalls: boolean;
672
+ requiresReasoningContentForAllAssistantTurns: boolean;
673
+ allowsSyntheticReasoningContentForToolCalls: boolean;
674
+ reasoningContentField?: OpenAICompat["reasoningContentField"];
675
+ requiresThinkingAsText: boolean;
676
+ };
677
+ tools: {
678
+ strictResponsesPairing: boolean;
679
+ toolCallIdKind: "default" | "openai-40" | "mistral-9-alnum";
680
+ };
681
+ messages: {
682
+ systemRole: "system" | "developer";
683
+ supportsDeveloperRole: boolean;
684
+ supportsMultipleSystemMessages: boolean;
685
+ };
686
+ stream: {
687
+ stripSpecialTokens: "deepseek" | false;
688
+ markupHealingPattern?: OpenAIStreamMarkupHealingPattern;
689
+ reasoningDeltasMayBeCumulative: boolean;
690
+ emptyLengthFinishIsContextError: boolean;
691
+ };
692
+ }
693
+
694
+ function mapOpenAIReasoningEffort(
695
+ model: Pick<Model, "thinking">,
696
+ compat: OpenAICompatPolicyCompat,
697
+ effort: string,
698
+ ): string {
699
+ const level = effort as Effort;
700
+ return compat.reasoningEffortMap?.[level] ?? model.thinking?.effortMap?.[level] ?? effort;
701
+ }
702
+
703
+ function isImplicitDisableWhenNotRequested(disableMode: OpenAIReasoningDisableMode): boolean {
704
+ return (
705
+ disableMode === "zai-thinking-disabled" ||
706
+ disableMode === "qwen-enable-thinking-false" ||
707
+ disableMode === "qwen-template-false"
708
+ );
709
+ }
710
+
711
+ export function resolveOpenAICompatPolicy<TApi extends Api>(
712
+ model: Model<TApi>,
713
+ options: ResolveOpenAICompatPolicyOptions,
714
+ ): OpenAICompatPolicy {
715
+ const baseCompat = (options.compat ?? model.compat) as OpenAICompatPolicyCompat;
716
+ const requestedEffort = options.reasoning;
717
+ const modelSupported = Boolean(model.reasoning);
718
+ const forcedToolChoiceSuppressesReasoning =
719
+ baseCompat.disableReasoningOnForcedToolChoice &&
720
+ baseCompat.supportsForcedToolChoice &&
721
+ isForcedToolChoice(options.toolChoice);
722
+ const anyToolChoiceSuppressesReasoning =
723
+ !forcedToolChoiceSuppressesReasoning &&
724
+ baseCompat.disableReasoningOnToolChoice &&
725
+ options.toolChoice !== undefined;
726
+ const requestedAndAllowed = requestedEffort !== undefined && !options.disableReasoning && modelSupported;
727
+ const conflictDisableReason: OpenAIReasoningDisableReason | undefined = forcedToolChoiceSuppressesReasoning
728
+ ? "forced-tool-choice"
729
+ : anyToolChoiceSuppressesReasoning
730
+ ? "tool-choice"
731
+ : undefined;
732
+ const disableReason: OpenAIReasoningDisableReason | undefined = options.disableReasoning
733
+ ? "caller"
734
+ : conflictDisableReason;
735
+ const enabledBeforeThinkingVariant = requestedAndAllowed && disableReason === undefined;
736
+ const baseWireEffort =
737
+ enabledBeforeThinkingVariant && requestedEffort !== undefined
738
+ ? mapOpenAIReasoningEffort(model, baseCompat, requestedEffort)
739
+ : undefined;
740
+ const disabledByNoneEffort =
741
+ enabledBeforeThinkingVariant &&
742
+ baseCompat.reasoningDisableMode === "zai-thinking-disabled" &&
743
+ baseWireEffort === "none";
744
+ const enabled = enabledBeforeThinkingVariant && !disabledByNoneEffort;
745
+ const compat =
746
+ enabled && baseCompat.whenThinking ? (baseCompat.whenThinking as OpenAICompatPolicyCompat) : baseCompat;
747
+ const omitReasoningEffort =
748
+ options.omitReasoningEffort ?? (compat.omitReasoningEffort || !compat.supportsReasoningEffort);
749
+ const disableMode = compat.reasoningDisableMode;
750
+ let wireEffort =
751
+ enabled && requestedEffort !== undefined ? mapOpenAIReasoningEffort(model, compat, requestedEffort) : undefined;
752
+ const disabledWithoutRequest =
753
+ modelSupported &&
754
+ requestedEffort === undefined &&
755
+ !options.disableReasoning &&
756
+ isImplicitDisableWhenNotRequested(disableMode);
757
+ const disabled =
758
+ (modelSupported && disableReason === "caller") ||
759
+ conflictDisableReason !== undefined ||
760
+ (modelSupported && disabledWithoutRequest) ||
761
+ disabledByNoneEffort;
762
+ if (
763
+ disabled &&
764
+ disableReason === "caller" &&
765
+ requestedEffort === undefined &&
766
+ disableMode === "lowest-effort" &&
767
+ compat.supportsReasoningEffort &&
768
+ !omitReasoningEffort
769
+ ) {
770
+ const minEffort = getSupportedEfforts(model)[0];
771
+ if (minEffort === undefined) {
772
+ throw new AIError.ConfigurationError(`Model ${model.provider}/${model.id} has no supported reasoning efforts`);
773
+ }
774
+ wireEffort = mapOpenAIReasoningEffort(model, compat, minEffort);
775
+ }
776
+
777
+ return {
778
+ endpoint: options.endpoint,
779
+ compat,
780
+ reasoning: {
781
+ modelSupported,
782
+ supportsParams: compat.supportsReasoningParams,
783
+ requestedEffort,
784
+ wireEffort,
785
+ enabled,
786
+ disabled,
787
+ disableReason: disableReason ?? (disabledWithoutRequest || disabledByNoneEffort ? "not-requested" : undefined),
788
+ dialect: compat.thinkingFormat,
789
+ requiresReasoningContentForToolCalls: compat.requiresReasoningContentForToolCalls,
790
+ requiresReasoningContentForAllAssistantTurns: compat.requiresReasoningContentForAllAssistantTurns,
791
+ allowsSyntheticReasoningContentForToolCalls: compat.allowsSyntheticReasoningContentForToolCalls,
792
+ reasoningContentField: compat.reasoningContentField,
793
+ requiresThinkingAsText: compat.requiresThinkingAsText,
794
+ disableMode,
795
+ omitReasoningEffort,
796
+ includeEncryptedReasoning: options.includeEncryptedReasoning ?? compat.includeEncryptedReasoning,
797
+ filterReasoningHistory: options.filterReasoningHistory ?? compat.filterReasoningHistory,
798
+ },
799
+ tools: {
800
+ strictResponsesPairing: options.strictResponsesPairing ?? compat.strictResponsesPairing ?? false,
801
+ toolCallIdKind: compat.requiresMistralToolIds
802
+ ? "mistral-9-alnum"
803
+ : compat.usesOpenAIToolCallIdLimit
804
+ ? "openai-40"
805
+ : "default",
806
+ },
807
+ messages: {
808
+ systemRole: modelSupported && compat.supportsDeveloperRole ? "developer" : "system",
809
+ supportsDeveloperRole: compat.supportsDeveloperRole,
810
+ supportsMultipleSystemMessages: compat.supportsMultipleSystemMessages ?? true,
811
+ },
812
+ stream: {
813
+ stripSpecialTokens: compat.stripDeepseekSpecialTokens ? "deepseek" : false,
814
+ markupHealingPattern: compat.streamMarkupHealingPattern,
815
+ reasoningDeltasMayBeCumulative: compat.reasoningDeltasMayBeCumulative,
816
+ emptyLengthFinishIsContextError: compat.emptyLengthFinishIsContextError,
817
+ },
818
+ };
819
+ }
820
+
821
+ function encodeChatCompletionsDisabledReasoning(
822
+ params: OpenAICompletionsParams,
823
+ disableMode: OpenAIReasoningDisableMode,
824
+ ): void {
825
+ delete params.reasoning_effort;
826
+ switch (disableMode) {
827
+ case "zai-thinking-disabled":
828
+ params.thinking = { type: "disabled" };
829
+ break;
830
+ case "qwen-enable-thinking-false":
831
+ params.enable_thinking = false;
832
+ break;
833
+ case "qwen-template-false":
834
+ params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: false };
835
+ break;
836
+ case "openrouter-enabled-false":
837
+ (params as typeof params & { reasoning?: { effort?: string } | { enabled: false } }).reasoning = {
838
+ enabled: false,
839
+ };
840
+ break;
841
+ default:
842
+ delete params.reasoning;
843
+ break;
844
+ }
845
+ }
846
+
847
+ export function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void {
848
+ // `preserve_thinking` is a chat-template HISTORY knob, not a per-turn
849
+ // thinking switch — it controls whether OLDER assistant turns render
850
+ // with `<think>...</think>` on Qwen3.6+. Emit it BEFORE the reasoning
851
+ // state branches and EVERY early-return below, because the wire shape
852
+ // must carry the kwarg in three cases the auto-detected
853
+ // `qwenPreserveThinking` flag covers but `reasoning.enabled` does not:
854
+ //
855
+ // 1. Discovered local Qwen models. `discoverOpenAICompatibleModels`
856
+ // stamps `reasoning: false` on every spec built from a generic
857
+ // `/v1/models` endpoint (the upstream doesn't advertise the
858
+ // capability), so `model.reasoning === false` → `reasoning.enabled
859
+ // === false`, the body wouldn't otherwise see the kwarg, and the
860
+ // encoder's `replayReasoningContent` branch would keep shipping
861
+ // `reasoning_content` only for the template to strip `<think>` from
862
+ // older turns anyway. Exactly the #3528 / #3541 symptom on every
863
+ // discovered Qwen build.
864
+ // 2. Caller-disabled reasoning. The slot's KV cache still holds prior
865
+ // `<think>...</think>` tokens from earlier thinking turns; the
866
+ // template must keep rendering them or cache invalidates at the
867
+ // first historic `<think>`.
868
+ // 3. Forced-tool-choice / DeepSeek-style auto-disable. Same reasoning
869
+ // as (2) — historic thinking blocks have to survive history replay
870
+ // even when the current turn cannot think.
871
+ //
872
+ // Non-Qwen templates ignore the parameter (jinja `is defined` check
873
+ // silently no-ops), so emitting it unconditionally for the Qwen-family
874
+ // + local-cache compat flag is safe.
875
+ if (policy.compat.qwenPreserveThinking) {
876
+ // Mirror the dialect split that gates `enable_thinking`. The
877
+ // `qwen` dialect rides the top-level field (the only place
878
+ // llama.cpp's `--jinja` hook AND Alibaba Cloud Model Studio's
879
+ // compatible-mode look) while the `qwen-chat-template` dialect
880
+ // (NVIDIA NIM, vLLM/SGLang's chat-template-kwargs path) MUST
881
+ // ride only the kwargs copy — NIM's request schema is
882
+ // `additionalProperties: false` and rejects every unknown
883
+ // top-level field, the very reason `enable_thinking` is
884
+ // route-split this way (#2299, see `catalog/src/compat/openai.ts`
885
+ // thinkingFormat comment).
886
+ if (policy.compat.thinkingFormat === "qwen") {
887
+ params.preserve_thinking = true;
888
+ }
889
+ params.chat_template_kwargs = { ...params.chat_template_kwargs, preserve_thinking: true };
890
+ }
891
+
892
+ const reasoning = policy.reasoning;
893
+ if ((!reasoning.modelSupported && !reasoning.disabled) || !reasoning.supportsParams) return;
894
+ if (reasoning.enabled) {
895
+ switch (reasoning.disableMode) {
896
+ case "zai-thinking-disabled":
897
+ if (reasoning.wireEffort === "none") {
898
+ encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
899
+ return;
900
+ }
901
+ params.thinking = { type: "enabled" };
902
+ if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
903
+ if (policy.compat.supportsReasoningEffort && reasoning.wireEffort !== undefined) {
904
+ params.reasoning_effort = reasoning.wireEffort as Effort;
905
+ }
906
+ break;
907
+ case "qwen-enable-thinking-false":
908
+ params.enable_thinking = true;
909
+ break;
910
+ case "qwen-template-false":
911
+ // Spread so the `preserve_thinking` kwarg hoisted above
912
+ // survives the merge — a bare `{ enable_thinking: true }`
913
+ // would clobber it.
914
+ params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: true };
915
+ break;
916
+ case "openrouter-enabled-false":
917
+ if (reasoning.wireEffort !== undefined) {
918
+ (params as typeof params & { reasoning?: { effort?: string } }).reasoning = {
919
+ effort: reasoning.wireEffort,
920
+ };
921
+ }
922
+ break;
923
+ default:
924
+ if (!reasoning.omitReasoningEffort && reasoning.wireEffort !== undefined) {
925
+ params.reasoning_effort = reasoning.wireEffort as Effort;
926
+ }
927
+ break;
928
+ }
929
+ return;
930
+ }
931
+ if (!reasoning.disabled) return;
932
+ if (
933
+ reasoning.disableReason === "caller" &&
934
+ reasoning.requestedEffort === undefined &&
935
+ reasoning.disableMode === "lowest-effort" &&
936
+ reasoning.wireEffort !== undefined
937
+ ) {
938
+ params.reasoning_effort = reasoning.wireEffort as Effort;
939
+ return;
940
+ }
941
+ encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
942
+ }
943
+
944
+ export function applyChatCompletionsReasoningParams(
945
+ params: OpenAICompletionsParams,
946
+ model: Model<"openai-completions">,
947
+ compat: ResolvedOpenAICompat,
948
+ options: (ChatCompletionsReasoningOptions & { toolChoice?: unknown }) | undefined,
949
+ ): void {
950
+ applyChatCompletionsCompatPolicy(
951
+ params,
952
+ resolveOpenAICompatPolicy(model, {
953
+ endpoint: "chat-completions",
954
+ compat,
955
+ reasoning: options?.reasoning,
956
+ disableReasoning: options?.disableReasoning,
957
+ toolChoice: options?.toolChoice,
958
+ }),
959
+ );
960
+ }
961
+
962
+ export function disableChatCompletionsReasoningForDialect(
963
+ params: OpenAICompletionsParams,
964
+ compat: ResolvedOpenAICompat,
965
+ ): void {
966
+ encodeChatCompletionsDisabledReasoning(params, compat.reasoningDisableMode);
967
+ }
968
+
969
+ /**
970
+ * Z.AI/GLM-5.2 reasoning-effort dialect predicate. GLM-5.2 models served on a
971
+ * Z.AI-format host (thinkingFormat "zai") accept `reasoning_effort`, stream tool
972
+ * calls via `tool_stream`, and clamp output to the model cap. Moonshot Kimi and
973
+ * Xiaomi MiMo also resolve to thinkingFormat "zai" with supportsReasoningEffort
974
+ * true but are NOT GLM-5.2, so the model-id check is load-bearing — never swap it
975
+ * for `compat.supportsReasoningEffort`.
976
+ */
977
+ function isZaiReasoningEffortDialect(model: Model<"openai-completions">, compat: ResolvedOpenAICompat): boolean {
978
+ return compat.thinkingFormat === "zai" && isGlm52ReasoningEffortModelId(model.id);
979
+ }
980
+
981
+ /**
982
+ * Output-token clamp for the Z.AI/GLM-5.2 reasoning dialect: these hosts accept
983
+ * the full model window on reasoning turns, so clamp to the model cap. Returns
984
+ * `undefined` for every other model, leaving {@link resolveOpenAIOutputTokenParam}
985
+ * on its default `OPENAI_MAX_OUTPUT_TOKENS` clamp.
986
+ */
987
+ export function resolveZaiReasoningOutputClamp(
988
+ model: Model<"openai-completions">,
989
+ compat: ResolvedOpenAICompat,
990
+ ): number | undefined {
991
+ return isZaiReasoningEffortDialect(model, compat) ? (model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS) : undefined;
992
+ }
993
+
994
+ /**
995
+ * Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present
996
+ * (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so).
997
+ */
998
+ export function applyChatCompletionsToolStream(
999
+ params: OpenAICompletionsParams,
1000
+ model: Model<"openai-completions">,
1001
+ compat: ResolvedOpenAICompat,
1002
+ ): void {
1003
+ if (
1004
+ isZaiReasoningEffortDialect(model, compat) &&
1005
+ compat.supportsReasoningEffort &&
1006
+ Array.isArray(params.tools) &&
1007
+ params.tools.length > 0
1008
+ ) {
1009
+ params.tool_stream = true;
1010
+ }
1011
+ }
1012
+
1013
+ export function isCompiledGrammarTooLargeStrictError(
1014
+ error: unknown,
1015
+ capturedErrorResponse: CapturedHttpErrorResponse | undefined,
1016
+ ): boolean {
1017
+ const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
1018
+ if (status !== 400) return false;
1019
+ const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
1020
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
1021
+ .join("\n");
1022
+ return (
1023
+ /invalid_request_error/i.test(messageParts) &&
1024
+ /compiled grammar/i.test(messageParts) &&
1025
+ /too large/i.test(messageParts)
1026
+ );
1027
+ }
1028
+
1029
+ export function shouldRetryWithoutStrictTools(
1030
+ error: unknown,
1031
+ capturedErrorResponse: CapturedHttpErrorResponse | undefined,
1032
+ strictToolsApplied: boolean,
1033
+ tools: Tool[] | undefined,
1034
+ ): boolean {
1035
+ if (!tools || tools.length === 0 || !strictToolsApplied) return false;
1036
+ const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
1037
+ if (status !== 400 && status !== 422) return false;
1038
+ const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
1039
+ .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
1040
+ .join("\n");
1041
+ return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function/i.test(
1042
+ messageParts,
1043
+ );
1044
+ }
1045
+
1046
+ function normalizeOpenAIStableId(value: string | undefined, maxLength: number, hashPrefix: string): string | undefined {
1047
+ if (!value || value.length === 0) return undefined;
1048
+ const wellFormed = value.toWellFormed();
1049
+ if (wellFormed.length <= maxLength) return wellFormed;
1050
+ return `${hashPrefix}${Bun.hash(wellFormed).toString(36)}`;
1051
+ }
1052
+
1053
+ export const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string> = new Set([
1054
+ "response.created",
1055
+ "response.output_item.added",
1056
+ "response.reasoning_summary_part.added",
1057
+ "response.reasoning_summary_text.delta",
1058
+ "response.reasoning_summary_part.done",
1059
+ "response.reasoning_text.delta",
1060
+ "response.content_part.added",
1061
+ "response.output_text.delta",
1062
+ "response.refusal.delta",
1063
+ "response.function_call_arguments.delta",
1064
+ "response.function_call_arguments.done",
1065
+ "response.custom_tool_call_input.delta",
1066
+ "response.custom_tool_call_input.done",
1067
+ "response.output_item.done",
1068
+ "response.completed",
1069
+ "response.incomplete",
1070
+ "response.failed",
1071
+ "error",
1072
+ ]);
1073
+
1074
+ export function isOpenAIResponsesProgressEvent(event: unknown): boolean {
1075
+ if (!event || typeof event !== "object") return false;
1076
+ const type = (event as { type?: unknown }).type;
1077
+ return typeof type === "string" && OPENAI_RESPONSES_PROGRESS_EVENT_TYPES.has(type);
1078
+ }
1079
+
1080
+ export function encodeTextSignatureV1(id: string, phase?: TextSignatureV1["phase"]): string {
1081
+ const payload: TextSignatureV1 = { v: 1, id };
1082
+ if (phase) payload.phase = phase;
1083
+ return JSON.stringify(payload);
1084
+ }
1085
+
1086
+ export function parseTextSignature(
1087
+ signature: string | undefined,
1088
+ ): { id: string; phase?: TextSignatureV1["phase"] } | undefined {
1089
+ if (!signature) return undefined;
1090
+ if (signature.startsWith("{")) {
1091
+ try {
1092
+ const parsed = JSON.parse(signature) as Partial<TextSignatureV1>;
1093
+ if (parsed.v === 1 && typeof parsed.id === "string") {
1094
+ if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
1095
+ return { id: parsed.id, phase: parsed.phase };
1096
+ }
1097
+ return { id: parsed.id };
1098
+ }
1099
+ } catch {
1100
+ // Fall through to legacy plain-string handling.
1101
+ }
1102
+ }
1103
+ return { id: signature };
1104
+ }
1105
+
1106
+ export function encodeResponsesToolCallId(callId: string, itemId: string | null | undefined): string {
1107
+ const stableItemId = itemId && itemId.length > 0 ? itemId : `fc_${Bun.hash(callId).toString(36)}`;
1108
+ return `${callId}|${stableItemId}`;
1109
+ }
1110
+
1111
+ export function normalizeResponsesToolCallIdForTransform(
1112
+ id: string,
1113
+ model?: Model<Api>,
1114
+ source?: AssistantMessage,
1115
+ ): string {
1116
+ if (!id.includes("|")) return id;
1117
+ const isForeignToolCall =
1118
+ source != null && model != null && (source.provider !== model.provider || source.api !== model.api);
1119
+ if (isForeignToolCall) {
1120
+ const [callId, itemId] = id.split("|");
1121
+ const normalizeIdPart = (part: string): string => {
1122
+ const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
1123
+ const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
1124
+ return truncated.replace(/_+$/, "");
1125
+ };
1126
+ const normalizedCallId = normalizeIdPart(callId);
1127
+ let normalizedItemId = `fc_${Bun.hash(itemId).toString(36)}`;
1128
+ if (normalizedItemId.length > 64) normalizedItemId = normalizedItemId.slice(0, 64);
1129
+ return `${normalizedCallId}|${normalizedItemId}`;
1130
+ }
1131
+ const normalized = normalizeResponsesToolCallId(id);
1132
+ return `${normalized.callId}|${normalized.itemId}`;
1133
+ }
1134
+
1135
+ export function collectKnownCallIds(messages: ResponseInput): Set<string> {
1136
+ const knownCallIds = new Set<string>();
1137
+ for (const item of messages) {
1138
+ if (item.type === "function_call" && typeof item.call_id === "string") {
1139
+ knownCallIds.add(item.call_id);
1140
+ } else if (
1141
+ (item as { type?: string }).type === "custom_tool_call" &&
1142
+ typeof (item as { call_id?: string }).call_id === "string"
1143
+ ) {
1144
+ knownCallIds.add((item as { call_id: string }).call_id);
1145
+ }
1146
+ }
1147
+ return knownCallIds;
1148
+ }
1149
+
1150
+ /** Scan replay items for call_ids that were originally custom tool calls. */
1151
+ export function collectCustomCallIds(messages: ResponseInput): Set<string> {
1152
+ const customCallIds = new Set<string>();
1153
+ for (const item of messages) {
1154
+ if (
1155
+ (item as { type?: string }).type === "custom_tool_call" &&
1156
+ typeof (item as { call_id?: string }).call_id === "string"
1157
+ ) {
1158
+ customCallIds.add((item as { call_id: string }).call_id);
1159
+ }
1160
+ }
1161
+ return customCallIds;
1162
+ }
1163
+
1164
+ /**
1165
+ * Convert orphan `function_call_output` / `custom_tool_call_output` items —
1166
+ * those whose `call_id` has no matching preceding `function_call` /
1167
+ * `custom_tool_call` in the same input — into assistant text notes.
1168
+ *
1169
+ * The Responses API rejects unpaired outputs with
1170
+ * `400 No tool call found for function call output with call_id …`. Orphans
1171
+ * sneak in through two paths today:
1172
+ *
1173
+ * - A previous turn's `providerPayload` snapshot replaces the input array via
1174
+ * the `dt: false` splice (see {@link convertConversationMessages}), wiping
1175
+ * the matching `function_call` while leaving the matching
1176
+ * `function_call_output` queued in a later `toolResult`.
1177
+ * - A locally-rejected tool call (argument-validation failure, hook reject,
1178
+ * aborted turn before the call streamed) produces a tool result without a
1179
+ * `function_call` ever landing in any persisted provider payload.
1180
+ *
1181
+ * Dropping the result loses information the model needs to recover; sending
1182
+ * it as-is 400s the request. Folding it into an assistant `message` preserves
1183
+ * the payload (call_id + truncated output) while staying within the Responses
1184
+ * input grammar. Matches the behavior of {@link transformRequestBody} in the
1185
+ * codex provider — issue #1351 / regression of #472.
1186
+ */
1187
+ export function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput {
1188
+ const knownCallIds = new Set<string>();
1189
+ for (const item of input) {
1190
+ const t = (item as { type?: string }).type;
1191
+ const callId = (item as { call_id?: unknown }).call_id;
1192
+ if (typeof callId !== "string") continue;
1193
+ if (t === "function_call" || t === "custom_tool_call") knownCallIds.add(callId);
1194
+ }
1195
+ let hasOrphan = false;
1196
+ for (const item of input) {
1197
+ const t = (item as { type?: string }).type;
1198
+ if (t !== "function_call_output" && t !== "custom_tool_call_output") continue;
1199
+ const callId = (item as { call_id?: unknown }).call_id;
1200
+ if (typeof callId === "string" && !knownCallIds.has(callId)) {
1201
+ hasOrphan = true;
1202
+ break;
1203
+ }
1204
+ }
1205
+ if (!hasOrphan) return input;
1206
+ return input.map(item => {
1207
+ const t = (item as { type?: string }).type;
1208
+ if (t !== "function_call_output" && t !== "custom_tool_call_output") return item;
1209
+ const record = item as { call_id?: unknown; output?: unknown; name?: unknown };
1210
+ const callId = record.call_id;
1211
+ if (typeof callId !== "string" || knownCallIds.has(callId)) return item;
1212
+ const toolName = typeof record.name === "string" && record.name.length > 0 ? record.name : "tool";
1213
+ const rawOutput = record.output;
1214
+ let text: string;
1215
+ if (typeof rawOutput === "string") text = rawOutput;
1216
+ else if (rawOutput == null) text = "";
1217
+ else {
1218
+ try {
1219
+ text = JSON.stringify(rawOutput);
1220
+ } catch {
1221
+ text = String(rawOutput);
1222
+ }
1223
+ }
1224
+ const ORPHAN_OUTPUT_LIMIT = 16_000;
1225
+ if (text.length > ORPHAN_OUTPUT_LIMIT) text = `${text.slice(0, ORPHAN_OUTPUT_LIMIT)}\n...[truncated]`;
1226
+ return {
1227
+ type: "message",
1228
+ role: "assistant",
1229
+ content: `[Orphan ${toolName} result; call_id=${callId}]: ${text}`,
1230
+ } as ResponseInput[number];
1231
+ });
1232
+ }
1233
+
1234
+ /** Placeholder output for a tool call whose result is absent from the input. */
1235
+ const ORPHAN_TOOL_CALL_PLACEHOLDER =
1236
+ "[No tool output recorded: the tool call was interrupted before it produced a result.]";
1237
+
1238
+ /**
1239
+ * Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
1240
+ * for every `function_call` / `custom_tool_call` whose `call_id` has no matching
1241
+ * output later in the same input. The Responses API rejects an unpaired call
1242
+ * with `400 No tool output found for function call …`.
1243
+ *
1244
+ * Orphan calls surface when the user branches/navigates the session tree to a
1245
+ * node that ends on a tool call (the tool-result child is excluded from the
1246
+ * reconstructed history) or when a turn is aborted/crashes after the call
1247
+ * streamed but before its result persisted. Dropping the call would erase the
1248
+ * assistant's action; a placeholder output keeps the call visible so the model
1249
+ * can recover (e.g. re-issue the call). Symmetric to
1250
+ * {@link repairOrphanResponsesToolOutputs}.
1251
+ */
1252
+ export function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput {
1253
+ const outputCallIds = new Set<string>();
1254
+ for (const item of input) {
1255
+ const t = (item as { type?: string }).type;
1256
+ if (t !== "function_call_output" && t !== "custom_tool_call_output") continue;
1257
+ const callId = (item as { call_id?: unknown }).call_id;
1258
+ if (typeof callId === "string") outputCallIds.add(callId);
1259
+ }
1260
+ let hasOrphan = false;
1261
+ for (const item of input) {
1262
+ const t = (item as { type?: string }).type;
1263
+ if (t !== "function_call" && t !== "custom_tool_call") continue;
1264
+ const callId = (item as { call_id?: unknown }).call_id;
1265
+ if (typeof callId === "string" && !outputCallIds.has(callId)) {
1266
+ hasOrphan = true;
1267
+ break;
1268
+ }
1269
+ }
1270
+ if (!hasOrphan) return input;
1271
+ const repaired: ResponseInput = [];
1272
+ for (const item of input) {
1273
+ repaired.push(item);
1274
+ const t = (item as { type?: string }).type;
1275
+ if (t !== "function_call" && t !== "custom_tool_call") continue;
1276
+ const callId = (item as { call_id?: unknown }).call_id;
1277
+ if (typeof callId !== "string" || outputCallIds.has(callId)) continue;
1278
+ repaired.push({
1279
+ type: t === "custom_tool_call" ? "custom_tool_call_output" : "function_call_output",
1280
+ call_id: callId,
1281
+ output: ORPHAN_TOOL_CALL_PLACEHOLDER,
1282
+ } as ResponseInput[number]);
1283
+ }
1284
+ return repaired;
1285
+ }
1286
+
1287
+ /**
1288
+ * Some Responses backends (notably GitHub Copilot) reject the OpenAI image
1289
+ * `detail: "original"` value with a 400. When the model does not advertise
1290
+ * support for it, degrade `"original"` to `"auto"` so the request still goes
1291
+ * through with the closest valid fidelity instead of failing outright. See #2822.
1292
+ */
1293
+ function clampResponsesImageDetail(
1294
+ detail: ImageContent["detail"],
1295
+ supportsImageDetailOriginal: boolean,
1296
+ ): ResponseInputImage["detail"] {
1297
+ const resolved = detail ?? "auto";
1298
+ return resolved === "original" && !supportsImageDetailOriginal ? "auto" : resolved;
1299
+ }
1300
+
1301
+ export function convertResponsesInputContent(
1302
+ content: string | Array<TextContent | ImageContent>,
1303
+ supportsImages: boolean,
1304
+ supportsImageDetailOriginal: boolean,
1305
+ ): ResponseInputContent[] | undefined {
1306
+ if (typeof content === "string") {
1307
+ if (content.trim().length === 0) return undefined;
1308
+ return [{ type: "input_text", text: content.toWellFormed() } satisfies ResponseInputText];
1309
+ }
1310
+
1311
+ const { textBlocks, imageBlocks, omittedImages } = partitionVisionContent(content, supportsImages);
1312
+ const normalizedContent: ResponseInputContent[] = [];
1313
+ for (const item of textBlocks) {
1314
+ const text = item.text.toWellFormed();
1315
+ if (text.trim().length === 0) continue;
1316
+ normalizedContent.push({
1317
+ type: "input_text",
1318
+ text,
1319
+ } satisfies ResponseInputText);
1320
+ }
1321
+ for (const item of imageBlocks) {
1322
+ normalizedContent.push({
1323
+ type: "input_image",
1324
+ detail: clampResponsesImageDetail(item.detail, supportsImageDetailOriginal),
1325
+ image_url: `data:${item.mimeType};base64,${item.data}`,
1326
+ } satisfies ResponseInputImage);
1327
+ }
1328
+ if (omittedImages) {
1329
+ normalizedContent.push({
1330
+ type: "input_text",
1331
+ text: NON_VISION_IMAGE_PLACEHOLDER,
1332
+ } satisfies ResponseInputText);
1333
+ }
1334
+ return normalizedContent.length > 0 ? normalizedContent : undefined;
1335
+ }
1336
+
1337
+ export interface BuildResponsesInputOptions<TApi extends Api> {
1338
+ model: Model<TApi>;
1339
+ context: Context;
1340
+ strictResponsesPairing: boolean;
1341
+ supportsImageDetailOriginal: boolean;
1342
+ systemRole?: "system" | "developer";
1343
+ nativeHistory?: {
1344
+ replay: boolean;
1345
+ filterReasoning: boolean;
1346
+ };
1347
+ includeThinkingSignatures?: boolean;
1348
+ developerStringContent?: boolean;
1349
+ repairOrphanOutputs?: boolean;
1350
+ /** Preserve assistant message item IDs from text signatures during fallback replay. */
1351
+ preserveAssistantMessageIds?: boolean;
1352
+ }
1353
+
1354
+ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput {
1355
+ const messages: ResponseInput = [];
1356
+ const systemPrompts = options.systemRole ? normalizeSystemPrompts(options.context.systemPrompt) : [];
1357
+ for (const systemPrompt of systemPrompts) {
1358
+ messages.push({ role: options.systemRole as "system" | "developer", content: systemPrompt });
1359
+ }
1360
+
1361
+ let knownCallIds = new Set<string>();
1362
+ const customCallIds = new Set<string>();
1363
+ const transformedMessages = transformMessages(
1364
+ options.context.messages,
1365
+ options.model,
1366
+ normalizeResponsesToolCallIdForTransform,
1367
+ );
1368
+ const filterReasoning = <T extends { type?: string }>(items: T[]): T[] =>
1369
+ options.nativeHistory?.filterReasoning ? items.filter(item => item?.type !== "reasoning") : items;
1370
+ const includeThinkingSignatures = options.includeThinkingSignatures ?? options.nativeHistory?.replay ?? true;
1371
+
1372
+ let msgIndex = 0;
1373
+ for (const msg of transformedMessages) {
1374
+ if (msg.role === "user" || msg.role === "developer") {
1375
+ const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
1376
+ const historyItems = options.nativeHistory
1377
+ ? getOpenAIResponsesHistoryItems(providerPayload, options.model.provider)
1378
+ : undefined;
1379
+ const shouldReplayPayloadItems =
1380
+ options.nativeHistory?.replay ||
1381
+ (historyItems?.some(item => {
1382
+ if (!item || typeof item !== "object") return false;
1383
+ const candidate = item as { type?: unknown };
1384
+ return candidate.type === "compaction" || candidate.type === "compaction_summary";
1385
+ }) ??
1386
+ false);
1387
+ if (historyItems && shouldReplayPayloadItems) {
1388
+ messages.push(...sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems)));
1389
+ knownCallIds = collectKnownCallIds(messages);
1390
+ for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
1391
+ msgIndex++;
1392
+ continue;
1393
+ }
1394
+ const content = convertResponsesInputContent(
1395
+ msg.content,
1396
+ options.model.input.includes("image"),
1397
+ options.supportsImageDetailOriginal,
1398
+ );
1399
+ if (!content) continue;
1400
+ messages.push({
1401
+ role: "user",
1402
+ content:
1403
+ options.developerStringContent && msg.role === "developer" && typeof msg.content === "string"
1404
+ ? msg.content.toWellFormed()
1405
+ : content,
1406
+ });
1407
+ } else if (msg.role === "assistant") {
1408
+ const assistantMsg = msg as AssistantMessage;
1409
+ const providerPayload =
1410
+ options.nativeHistory?.replay &&
1411
+ assistantMsg.api === options.model.api &&
1412
+ assistantMsg.model === options.model.id
1413
+ ? getOpenAIResponsesHistoryPayload(
1414
+ assistantMsg.providerPayload,
1415
+ options.model.provider,
1416
+ assistantMsg.provider,
1417
+ )
1418
+ : undefined;
1419
+ const historyItems = providerPayload?.items;
1420
+ if (historyItems) {
1421
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems));
1422
+ if (providerPayload?.dt) {
1423
+ messages.push(...sanitizedHistoryItems);
1424
+ } else {
1425
+ messages.splice(0, messages.length, ...sanitizedHistoryItems);
1426
+ }
1427
+ knownCallIds = collectKnownCallIds(messages);
1428
+ for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
1429
+ msgIndex++;
1430
+ continue;
1431
+ }
1432
+
1433
+ const outputItems = convertResponsesAssistantMessage(
1434
+ assistantMsg,
1435
+ options.model,
1436
+ msgIndex,
1437
+ knownCallIds,
1438
+ includeThinkingSignatures,
1439
+ customCallIds,
1440
+ options.preserveAssistantMessageIds,
1441
+ );
1442
+ if (outputItems.length === 0) continue;
1443
+ messages.push(...outputItems);
1444
+ } else if (msg.role === "toolResult") {
1445
+ appendResponsesToolResultMessages(
1446
+ messages,
1447
+ msg,
1448
+ options.model,
1449
+ options.strictResponsesPairing,
1450
+ options.supportsImageDetailOriginal,
1451
+ knownCallIds,
1452
+ customCallIds,
1453
+ );
1454
+ }
1455
+ msgIndex++;
1456
+ }
1457
+
1458
+ const withRepairedOutputs = options.repairOrphanOutputs ? repairOrphanResponsesToolOutputs(messages) : messages;
1459
+ return repairOrphanResponsesToolCalls(withRepairedOutputs);
1460
+ }
1461
+
1462
+ type ResponsesReplayAssistantMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
1463
+
1464
+ function parseResponseReasoningReplayItem(signature: string | undefined): ResponseReasoningItem | undefined {
1465
+ if (!signature) return undefined;
1466
+ try {
1467
+ const parsed = JSON.parse(signature) as unknown;
1468
+ if (!parsed || typeof parsed !== "object") return undefined;
1469
+ if (!("type" in parsed) || parsed.type !== "reasoning") return undefined;
1470
+ if (!("id" in parsed) || typeof parsed.id !== "string") return undefined;
1471
+ return parsed as ResponseReasoningItem;
1472
+ } catch {
1473
+ return undefined;
1474
+ }
1475
+ }
1476
+
1477
+ export function convertResponsesAssistantMessage<TApi extends Api>(
1478
+ assistantMsg: AssistantMessage,
1479
+ model: Model<TApi>,
1480
+ msgIndex: number,
1481
+ knownCallIds: Set<string>,
1482
+ includeThinkingSignatures = true,
1483
+ customCallIds?: Set<string>,
1484
+ preserveMessageIds = false,
1485
+ ): ResponseInput {
1486
+ const outputItems: ResponseInput = [];
1487
+ let unsignedTextBlocks = 0;
1488
+ const hasReplayableReasoningItem =
1489
+ includeThinkingSignatures &&
1490
+ assistantMsg.stopReason !== "error" &&
1491
+ assistantMsg.content.some(
1492
+ block => block.type === "thinking" && parseResponseReasoningReplayItem(block.thinkingSignature) !== undefined,
1493
+ );
1494
+ const isDifferentModel =
1495
+ assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
1496
+
1497
+ for (const block of assistantMsg.content) {
1498
+ if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
1499
+ if (!includeThinkingSignatures) {
1500
+ continue;
1501
+ }
1502
+ const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
1503
+ if (reasoningItem) outputItems.push(reasoningItem);
1504
+ continue;
1505
+ }
1506
+
1507
+ if (block.type === "text") {
1508
+ const parsedSignature = parseTextSignature(block.textSignature);
1509
+ let msgId = parsedSignature?.id;
1510
+ if (!msgId) {
1511
+ if (hasReplayableReasoningItem) {
1512
+ // Distinct ids per unsigned block: several text blocks in one message
1513
+ // (cross-provider replay downgrades thinking → text) must not share an id.
1514
+ msgId = unsignedTextBlocks === 0 ? `msg_${msgIndex}` : `msg_${msgIndex}_${unsignedTextBlocks}`;
1515
+ unsignedTextBlocks += 1;
1516
+ }
1517
+ } else if (!preserveMessageIds && !hasReplayableReasoningItem) {
1518
+ // Without the matching reasoning item the server rejects replayed
1519
+ // item ids (#4173) — drop them regardless of shape, including
1520
+ // legacy plain-string signatures that would otherwise fall into
1521
+ // the >64-char hash branch and fabricate a bogus msg_ id.
1522
+ msgId = undefined;
1523
+ } else if (msgId.length > 64) {
1524
+ msgId = `msg_${Bun.hash(msgId).toString(36)}`;
1525
+ }
1526
+ const messageItem: ResponsesReplayAssistantMessage = {
1527
+ type: "message",
1528
+ role: "assistant",
1529
+ content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
1530
+ status: "completed",
1531
+ ...(msgId ? { id: msgId } : {}),
1532
+ ...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}),
1533
+ };
1534
+ outputItems.push(messageItem as ResponseInput[number]);
1535
+ continue;
1536
+ }
1537
+
1538
+ if (block.type !== "toolCall") {
1539
+ continue;
1540
+ }
1541
+
1542
+ const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
1543
+ let itemId: string | undefined = normalized.itemId;
1544
+ if (
1545
+ !hasReplayableReasoningItem &&
1546
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
1547
+ ) {
1548
+ itemId = undefined;
1549
+ } else if (
1550
+ isDifferentModel &&
1551
+ (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
1552
+ ) {
1553
+ itemId = undefined;
1554
+ }
1555
+ knownCallIds.add(normalized.callId);
1556
+ if (block.customWireName) {
1557
+ const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
1558
+ customCallIds?.add(normalized.callId);
1559
+ outputItems.push({
1560
+ type: "custom_tool_call",
1561
+ ...(itemId ? { id: itemId } : {}),
1562
+ call_id: normalized.callId,
1563
+ name: block.customWireName,
1564
+ input: rawInput,
1565
+ } as ResponseInput[number]);
1566
+ continue;
1567
+ }
1568
+ outputItems.push({
1569
+ type: "function_call",
1570
+ ...(itemId ? { id: itemId } : {}),
1571
+ call_id: normalized.callId,
1572
+ name: block.name,
1573
+ arguments: JSON.stringify(block.arguments),
1574
+ });
1575
+ }
1576
+
1577
+ return outputItems;
1578
+ }
1579
+
1580
+ export function appendResponsesToolResultMessages<TApi extends Api>(
1581
+ messages: ResponseInput,
1582
+ toolResult: ToolResultMessage,
1583
+ model: Model<TApi>,
1584
+ strictResponsesPairing: boolean,
1585
+ supportsImageDetailOriginal: boolean,
1586
+ knownCallIds: ReadonlySet<string>,
1587
+ customCallIds?: ReadonlySet<string>,
1588
+ ): void {
1589
+ const supportsImages = model.input.includes("image");
1590
+ const textResult = toolResult.content
1591
+ .filter((block): block is TextContent => block.type === "text")
1592
+ .map(block => block.text)
1593
+ .join("\n");
1594
+ const hasImages = toolResult.content.some((block): block is ImageContent => block.type === "image");
1595
+ const omittedImages = hasImages && !supportsImages;
1596
+ const normalized = normalizeResponsesToolCallId(toolResult.toolCallId);
1597
+ const output = (
1598
+ omittedImages
1599
+ ? joinTextWithImagePlaceholder(textResult, true)
1600
+ : textResult.length > 0
1601
+ ? textResult
1602
+ : "(see attached image)"
1603
+ ).toWellFormed();
1604
+ if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
1605
+ // Strict backends (Azure, Copilot) reject unpaired outputs outright, but
1606
+ // silently dropping the result loses information the model needs. Fold it
1607
+ // into an assistant note instead (same shape as repairOrphanResponsesToolOutputs).
1608
+ const limit = 16_000;
1609
+ const noteText = output.length > limit ? `${output.slice(0, limit)}\n...[truncated]` : output;
1610
+ messages.push({
1611
+ type: "message",
1612
+ role: "assistant",
1613
+ content: `[Orphan ${toolResult.toolName || "tool"} result; call_id=${normalized.callId}]: ${noteText}`,
1614
+ } as ResponseInput[number]);
1615
+ return;
1616
+ }
1617
+ if (customCallIds?.has(normalized.callId)) {
1618
+ messages.push({
1619
+ type: "custom_tool_call_output",
1620
+ call_id: normalized.callId,
1621
+ output,
1622
+ } as ResponseInput[number]);
1623
+ } else {
1624
+ messages.push({
1625
+ type: "function_call_output",
1626
+ call_id: normalized.callId,
1627
+ output,
1628
+ });
1629
+ }
1630
+
1631
+ if (!hasImages || !supportsImages) {
1632
+ return;
1633
+ }
1634
+
1635
+ const contentParts: ResponseInputContent[] = [
1636
+ { type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText,
1637
+ ];
1638
+ for (const block of toolResult.content) {
1639
+ if (block.type === "image") {
1640
+ contentParts.push({
1641
+ type: "input_image",
1642
+ detail: clampResponsesImageDetail(block.detail, supportsImageDetailOriginal),
1643
+ image_url: `data:${block.mimeType};base64,${block.data}`,
1644
+ } satisfies ResponseInputImage);
1645
+ }
1646
+ }
1647
+ messages.push({ role: "user", content: contentParts });
1648
+ }
1649
+
1650
+ /**
1651
+ * Per-block accumulation helpers shared by the two Responses decode loops —
1652
+ * {@link processResponsesStream} (generic Responses) and the Codex stream
1653
+ * handler in `openai-codex-responses.ts`. Each endpoint keeps its own
1654
+ * item-routing, terminal handling, and transport bookkeeping; these own only
1655
+ * the leaf mutations on an already-resolved open block, so the
1656
+ * append/parse/finalize logic lives in exactly one place. The caller passes the
1657
+ * `contentIndex` its router resolved (generic uses `output.content.indexOf`;
1658
+ * Codex uses the open item's recorded index) so the emitted stream events match
1659
+ * each decoder's existing behavior byte-for-byte.
1660
+ */
1661
+ type ResponsesToolCallBlock = ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number };
1662
+
1663
+ export function appendReasoningSummaryPart(
1664
+ item: ResponseReasoningItem,
1665
+ part: ResponseReasoningItem["summary"][number],
1666
+ ): void {
1667
+ item.summary = item.summary || [];
1668
+ item.summary.push(part);
1669
+ }
1670
+
1671
+ export function appendReasoningSummaryTextDelta(
1672
+ item: ResponseReasoningItem,
1673
+ block: ThinkingContent,
1674
+ delta: string,
1675
+ stream: AssistantMessageEventStream,
1676
+ output: AssistantMessage,
1677
+ contentIndex: number,
1678
+ ): void {
1679
+ item.summary = item.summary || [];
1680
+ const lastPart = item.summary[item.summary.length - 1];
1681
+ if (!lastPart) return;
1682
+ block.thinking += delta;
1683
+ lastPart.text += delta;
1684
+ stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
1685
+ }
1686
+
1687
+ export function appendReasoningSummaryPartDone(
1688
+ item: ResponseReasoningItem,
1689
+ block: ThinkingContent,
1690
+ stream: AssistantMessageEventStream,
1691
+ output: AssistantMessage,
1692
+ contentIndex: number,
1693
+ ): void {
1694
+ item.summary = item.summary || [];
1695
+ const lastPart = item.summary[item.summary.length - 1];
1696
+ if (!lastPart) return;
1697
+ block.thinking += "\n\n";
1698
+ lastPart.text += "\n\n";
1699
+ stream.push({ type: "thinking_delta", contentIndex, delta: "\n\n", partial: output });
1700
+ }
1701
+
1702
+ export function appendMessageContentPart(
1703
+ item: ResponseOutputMessage,
1704
+ part: ResponseContentPartAddedEvent["part"] | undefined,
1705
+ ): void {
1706
+ item.content = item.content || [];
1707
+ if (part && (part.type === "output_text" || part.type === "refusal")) {
1708
+ item.content.push(part);
1709
+ }
1710
+ }
1711
+
1712
+ export function appendMessageTextDelta(
1713
+ item: ResponseOutputMessage,
1714
+ block: TextContent,
1715
+ delta: string,
1716
+ stream: AssistantMessageEventStream,
1717
+ output: AssistantMessage,
1718
+ contentIndex: number,
1719
+ partType: "output_text" | "refusal",
1720
+ ): void {
1721
+ item.content = item.content || [];
1722
+ let lastPart = item.content[item.content.length - 1];
1723
+ if (lastPart?.type !== partType) {
1724
+ // `content_part.added` never arrived (lossy proxy) — synthesize the part
1725
+ // so live text still streams instead of freezing until output_item.done.
1726
+ lastPart =
1727
+ partType === "output_text"
1728
+ ? { type: "output_text", text: "", annotations: [] }
1729
+ : { type: "refusal", refusal: "" };
1730
+ item.content.push(lastPart);
1731
+ }
1732
+ block.text += delta;
1733
+ if (lastPart.type === "output_text") {
1734
+ lastPart.text += delta;
1735
+ } else {
1736
+ lastPart.refusal += delta;
1737
+ }
1738
+ stream.push({ type: "text_delta", contentIndex, delta, partial: output });
1739
+ }
1740
+
1741
+ export function accumulateToolCallArgumentsDelta(
1742
+ block: ResponsesToolCallBlock,
1743
+ delta: string,
1744
+ stream: AssistantMessageEventStream,
1745
+ output: AssistantMessage,
1746
+ contentIndex: number,
1747
+ ): void {
1748
+ block[kStreamingPartialJson] += delta;
1749
+ const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
1750
+ if (throttled) {
1751
+ block.arguments = throttled.value;
1752
+ block[kStreamingLastParseLen] = throttled.parsedLen;
1753
+ }
1754
+ stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
1755
+ }
1756
+
1757
+ /**
1758
+ * Finalize streamed function-call arguments from the authoritative `.done`
1759
+ * payload. The caller owns the `argumentsDone` flag (generic Responses sets it;
1760
+ * Codex's block shape has no such field), so this only rewrites `arguments` and
1761
+ * drops the transient accumulation fields.
1762
+ */
1763
+ export function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void {
1764
+ block[kStreamingPartialJson] = args;
1765
+ block.arguments = parseStreamingJson(block[kStreamingPartialJson]);
1766
+ clearStreamingPartialJson(block);
1767
+ }
1768
+
1769
+ export function accumulateCustomToolCallInputDelta(
1770
+ block: ResponsesToolCallBlock,
1771
+ delta: string,
1772
+ stream: AssistantMessageEventStream,
1773
+ output: AssistantMessage,
1774
+ contentIndex: number,
1775
+ ): void {
1776
+ block[kStreamingPartialJson] += delta;
1777
+ block.arguments = { input: block[kStreamingPartialJson] };
1778
+ stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
1779
+ }
1780
+
1781
+ export function finalizeCustomToolCallInputDone(block: ResponsesToolCallBlock, input: string): void {
1782
+ block[kStreamingPartialJson] = input;
1783
+ block.arguments = { input };
1784
+ }
1785
+
1786
+ export interface ProcessResponsesStreamOptions {
1787
+ onFirstToken?: () => void;
1788
+ onOutputItemDone?: (item: ResponseOutputItem) => void;
1789
+ /**
1790
+ * Called when a terminal `response.completed` or `response.incomplete` event
1791
+ * is successfully processed. Only invoked on the successful-completion path;
1792
+ * thrown failure (`response.failed`) and cancellation paths never call this.
1793
+ * Used by callers to detect premature stream closure (i.e. the stream ended
1794
+ * without a recognized terminal event).
1795
+ */
1796
+ onCompleted?: () => void;
1797
+ /**
1798
+ * Caller-requested service tier, used to bill the served tier when the
1799
+ * response omits the `service_tier` echo. Only applied for `provider: "openai"`.
1800
+ */
1801
+ requestServiceTier?: ServiceTier;
1802
+ }
1803
+
1804
+ export async function processResponsesStream<TApi extends Api>(
1805
+ openaiStream: AsyncIterable<ResponseStreamEvent>,
1806
+ output: AssistantMessage,
1807
+ stream: AssistantMessageEventStream,
1808
+ model: Model<TApi>,
1809
+ options?: ProcessResponsesStreamOptions,
1810
+ ): Promise<void> {
1811
+ type StreamingToolCallBlock = ToolCall & {
1812
+ [kStreamingPartialJson]: string;
1813
+ [kStreamingLastParseLen]?: number;
1814
+ [kStreamingArgumentsDone]?: boolean;
1815
+ };
1816
+ interface StreamingItem {
1817
+ item: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
1818
+ block: ThinkingContent | TextContent | StreamingToolCallBlock;
1819
+ }
1820
+
1821
+ // Multiple items (parallel function_calls in particular) can be open at the same
1822
+ // time. OpenAI's spec routes every per-item event by `output_index`/`item_id`;
1823
+ // see https://github.com/can1357/oh-my-pi/issues/1880 — llama.cpp emits parallel
1824
+ // function_call deltas interleaved, and a singleton `current` reference would
1825
+ // fold them into the wrong block and drop arguments on every call but the last.
1826
+ //
1827
+ // OpenAI-compatible hosts can compound this by omitting `item.id` and
1828
+ // `output_index` on `output_item.added` while routing later argument deltas to
1829
+ // either the bare `call_id` or a synthesized `fc_<call_id>` item id. Register
1830
+ // both keys so each delta reaches its own block instead of falling back to the
1831
+ // most recently added parallel call.
1832
+ const openItemsByOutputIndex = new Map<number, StreamingItem>();
1833
+ const openItemsByItemId = new Map<string, StreamingItem>();
1834
+ const openItemsByPrefixedCallId = new Map<string, StreamingItem>();
1835
+ let lastOpenItem: StreamingItem | null = null;
1836
+ const openItemsInOrder: StreamingItem[] = [];
1837
+
1838
+ const prefixedFunctionCallItemKey = (callId: string | undefined): string | undefined =>
1839
+ callId ? `fc_${callId}` : undefined;
1840
+
1841
+ const registerOpenItem = (
1842
+ outputIndex: number | undefined,
1843
+ itemId: string | undefined,
1844
+ entry: StreamingItem,
1845
+ alternateItemKey?: string,
1846
+ prefixedAlternateItemKey?: string,
1847
+ ): void => {
1848
+ if (typeof outputIndex === "number") openItemsByOutputIndex.set(outputIndex, entry);
1849
+ if (itemId) openItemsByItemId.set(itemId, entry);
1850
+ if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.set(alternateItemKey, entry);
1851
+ if (
1852
+ prefixedAlternateItemKey &&
1853
+ prefixedAlternateItemKey !== itemId &&
1854
+ prefixedAlternateItemKey !== alternateItemKey
1855
+ ) {
1856
+ openItemsByPrefixedCallId.set(prefixedAlternateItemKey, entry);
1857
+ }
1858
+ openItemsInOrder.push(entry);
1859
+ lastOpenItem = entry;
1860
+ };
1861
+ const lookupOpenItem = (event: { output_index?: number; item_id?: string }): StreamingItem | undefined => {
1862
+ const hasKey = typeof event.output_index === "number" || event.item_id !== undefined;
1863
+ if (typeof event.output_index === "number") {
1864
+ const found = openItemsByOutputIndex.get(event.output_index);
1865
+ if (found) return found;
1866
+ }
1867
+ if (event.item_id) {
1868
+ const found = openItemsByItemId.get(event.item_id);
1869
+ if (found) return found;
1870
+ }
1871
+ // Keyed events whose item already closed are stale; drop them instead of
1872
+ // routing to a sibling. Only fully identifierless mock/proxy events use the
1873
+ // legacy singleton fallback.
1874
+ return hasKey ? undefined : (lastOpenItem ?? undefined);
1875
+ };
1876
+ const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
1877
+ typeof event.output_index === "number" || event.item_id !== undefined;
1878
+ const lookupOpenToolCallAlias = (
1879
+ event: { output_index?: number; item_id?: string },
1880
+ type: "function_call" | "custom_tool_call",
1881
+ ): StreamingItem | undefined => {
1882
+ if (typeof event.output_index === "number") {
1883
+ const byOutputIndex = openItemsByOutputIndex.get(event.output_index);
1884
+ if (byOutputIndex) return byOutputIndex;
1885
+ // A lossy host (llama.cpp/Ollama, issue #2015) can omit `output_index` on
1886
+ // `output_item.added` while still stamping the spec-required field on the
1887
+ // delta. The index was never registered, so fall through to the prefixed
1888
+ // alias / exact item-id maps instead of dropping to `lastOpenItem`.
1889
+ }
1890
+ if (event.item_id) {
1891
+ // Prefixed call-id aliases share the same wire namespace as real call ids.
1892
+ // Argument/input events can use the prefixed form, while final
1893
+ // output_item.done events below use exact call ids; keep aliases in a
1894
+ // separate map so a real `call_id: "fc_x"` cannot overwrite the alias
1895
+ // for `call_id: "x"`.
1896
+ const alias = openItemsByPrefixedCallId.get(event.item_id);
1897
+ if (alias?.item.type === type) return alias;
1898
+ const exact = openItemsByItemId.get(event.item_id);
1899
+ if (exact) return exact;
1900
+ }
1901
+ return lookupOpenItem(event);
1902
+ };
1903
+ const lookupOpenFunctionCallItem = (event: {
1904
+ output_index?: number;
1905
+ item_id?: string;
1906
+ }): StreamingItem | undefined => {
1907
+ if (hasOpenItemKey(event)) return lookupOpenToolCallAlias(event, "function_call");
1908
+ for (const candidate of openItemsInOrder) {
1909
+ if (
1910
+ candidate.item.type === "function_call" &&
1911
+ candidate.block.type === "toolCall" &&
1912
+ !candidate.block[kStreamingArgumentsDone]
1913
+ ) {
1914
+ return candidate;
1915
+ }
1916
+ }
1917
+ return lastOpenItem?.item.type === "function_call" ? lastOpenItem : undefined;
1918
+ };
1919
+ const closeOpenItem = (
1920
+ outputIndex: number | undefined,
1921
+ itemId: string | undefined,
1922
+ entry: StreamingItem | undefined,
1923
+ alternateItemKey?: string,
1924
+ prefixedAlternateItemKey?: string,
1925
+ ): void => {
1926
+ if (typeof outputIndex === "number") openItemsByOutputIndex.delete(outputIndex);
1927
+ if (itemId) openItemsByItemId.delete(itemId);
1928
+ if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.delete(alternateItemKey);
1929
+ if (
1930
+ prefixedAlternateItemKey &&
1931
+ prefixedAlternateItemKey !== itemId &&
1932
+ prefixedAlternateItemKey !== alternateItemKey &&
1933
+ openItemsByPrefixedCallId.get(prefixedAlternateItemKey) === entry
1934
+ ) {
1935
+ openItemsByPrefixedCallId.delete(prefixedAlternateItemKey);
1936
+ }
1937
+ if (entry) {
1938
+ const index = openItemsInOrder.indexOf(entry);
1939
+ if (index >= 0) openItemsInOrder.splice(index, 1);
1940
+ }
1941
+ if (entry && lastOpenItem === entry) lastOpenItem = null;
1942
+ };
1943
+ const contentIndexOf = (block: ThinkingContent | TextContent | StreamingToolCallBlock): number =>
1944
+ output.content.indexOf(block);
1945
+
1946
+ let sawFirstToken = false;
1947
+
1948
+ for await (const event of openaiStream) {
1949
+ if (event.type === "response.created") {
1950
+ output.responseId = event.response.id;
1951
+ } else if (event.type === "response.output_item.added") {
1952
+ if (!sawFirstToken) {
1953
+ sawFirstToken = true;
1954
+ options?.onFirstToken?.();
1955
+ }
1956
+ const item = event.item;
1957
+ if (item.type === "reasoning") {
1958
+ const block: ThinkingContent = { type: "thinking", thinking: "", itemId: item.id };
1959
+ output.content.push(block);
1960
+ registerOpenItem(event.output_index, item.id, { item, block });
1961
+ stream.push({ type: "thinking_start", contentIndex: contentIndexOf(block), partial: output });
1962
+ } else if (item.type === "message") {
1963
+ const block: TextContent = {
1964
+ type: "text",
1965
+ text: "",
1966
+ textSignature: encodeTextSignatureV1(item.id, item.phase ?? undefined),
1967
+ };
1968
+ output.content.push(block);
1969
+ registerOpenItem(event.output_index, item.id, { item, block });
1970
+ stream.push({ type: "text_start", contentIndex: contentIndexOf(block), partial: output });
1971
+ } else if (item.type === "function_call") {
1972
+ const block: StreamingToolCallBlock = {
1973
+ type: "toolCall",
1974
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1975
+ name: item.name,
1976
+ arguments: {},
1977
+ [kStreamingPartialJson]: item.arguments || "",
1978
+ };
1979
+ output.content.push(block);
1980
+ registerOpenItem(
1981
+ event.output_index,
1982
+ item.id,
1983
+ { item, block },
1984
+ item.call_id,
1985
+ prefixedFunctionCallItemKey(item.call_id),
1986
+ );
1987
+ stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
1988
+ } else if (item.type === "custom_tool_call") {
1989
+ const block: StreamingToolCallBlock = {
1990
+ type: "toolCall",
1991
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1992
+ // Preserve the raw wire name (e.g. `apply_patch`). The agent-loop
1993
+ // dispatcher matches it against both `Tool.name` and
1994
+ // `Tool.customWireName`, so this stays wire-accurate through
1995
+ // history replay while still routing to the right handler.
1996
+ name: item.name,
1997
+ arguments: { input: item.input ?? "" },
1998
+ customWireName: item.name,
1999
+ // Custom tools stream a raw string, but we reuse `partialJson` as the
2000
+ // accumulation buffer so later code that inspects the field still works.
2001
+ [kStreamingPartialJson]: item.input ?? "",
2002
+ };
2003
+ output.content.push(block);
2004
+ registerOpenItem(
2005
+ event.output_index,
2006
+ item.id,
2007
+ { item, block },
2008
+ item.call_id,
2009
+ prefixedFunctionCallItemKey(item.call_id),
2010
+ );
2011
+ stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
2012
+ }
2013
+ } else if (event.type === "response.reasoning_summary_part.added") {
2014
+ const entry = lookupOpenItem(event);
2015
+ if (entry?.item.type === "reasoning") appendReasoningSummaryPart(entry.item, event.part);
2016
+ } else if (event.type === "response.reasoning_summary_text.delta") {
2017
+ const entry = lookupOpenItem(event);
2018
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
2019
+ appendReasoningSummaryTextDelta(
2020
+ entry.item,
2021
+ entry.block,
2022
+ event.delta,
2023
+ stream,
2024
+ output,
2025
+ contentIndexOf(entry.block),
2026
+ );
2027
+ }
2028
+ } else if (event.type === "response.reasoning_summary_part.done") {
2029
+ const entry = lookupOpenItem(event);
2030
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
2031
+ appendReasoningSummaryPartDone(entry.item, entry.block, stream, output, contentIndexOf(entry.block));
2032
+ }
2033
+ } else if (event.type === "response.reasoning_text.delta") {
2034
+ // Raw reasoning text delta from local providers that stream thinking
2035
+ // directly rather than via the OpenAI summary tracking protocol.
2036
+ const entry = lookupOpenItem(event);
2037
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
2038
+ entry.block.thinking += event.delta;
2039
+ stream.push({
2040
+ type: "thinking_delta",
2041
+ contentIndex: contentIndexOf(entry.block),
2042
+ delta: event.delta,
2043
+ partial: output,
2044
+ });
2045
+ }
2046
+ } else if (event.type === "response.content_part.added") {
2047
+ const entry = lookupOpenItem(event);
2048
+ if (entry?.item.type === "message") appendMessageContentPart(entry.item, event.part);
2049
+ } else if (event.type === "response.output_text.delta") {
2050
+ const entry = lookupOpenItem(event);
2051
+ if (entry?.item.type === "message" && entry.block.type === "text") {
2052
+ appendMessageTextDelta(
2053
+ entry.item,
2054
+ entry.block,
2055
+ event.delta,
2056
+ stream,
2057
+ output,
2058
+ contentIndexOf(entry.block),
2059
+ "output_text",
2060
+ );
2061
+ }
2062
+ } else if (event.type === "response.refusal.delta") {
2063
+ const entry = lookupOpenItem(event);
2064
+ if (entry?.item.type === "message" && entry.block.type === "text") {
2065
+ appendMessageTextDelta(
2066
+ entry.item,
2067
+ entry.block,
2068
+ event.delta,
2069
+ stream,
2070
+ output,
2071
+ contentIndexOf(entry.block),
2072
+ "refusal",
2073
+ );
2074
+ }
2075
+ } else if (event.type === "response.function_call_arguments.delta") {
2076
+ const entry = lookupOpenFunctionCallItem(event);
2077
+ if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
2078
+ accumulateToolCallArgumentsDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
2079
+ }
2080
+ } else if (event.type === "response.function_call_arguments.done") {
2081
+ const entry = lookupOpenFunctionCallItem(event);
2082
+ if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
2083
+ finalizeToolCallArgumentsDone(entry.block, event.arguments);
2084
+ entry.block[kStreamingArgumentsDone] = true;
2085
+ }
2086
+ } else if (event.type === "response.custom_tool_call_input.delta") {
2087
+ const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
2088
+ if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
2089
+ accumulateCustomToolCallInputDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
2090
+ }
2091
+ } else if (event.type === "response.custom_tool_call_input.done") {
2092
+ const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
2093
+ if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
2094
+ finalizeCustomToolCallInputDone(entry.block, event.input);
2095
+ }
2096
+ } else if (event.type === "response.output_item.done") {
2097
+ const item = structuredCloneJSON(event.item);
2098
+ options?.onOutputItemDone?.(item);
2099
+ const entry =
2100
+ item.type === "function_call" || item.type === "custom_tool_call"
2101
+ ? lookupOpenItem({ output_index: event.output_index, item_id: item.id ?? item.call_id })
2102
+ : lookupOpenItem({ output_index: event.output_index, item_id: item.id });
2103
+ if (item.type === "reasoning") {
2104
+ const thinking =
2105
+ item.summary?.length > 0
2106
+ ? item.summary.map(part => part.text).join("\n\n")
2107
+ : item.content?.[0]?.type === "reasoning_text"
2108
+ ? (item.content[0].text ?? "")
2109
+ : "";
2110
+ // Prefer the routed entry; the bare itemId find misroutes when ids are
2111
+ // absent (`undefined === undefined` matches the FIRST thinking block) and
2112
+ // misses entirely when the done-event id drifts from the added-event id.
2113
+ const reasoningBlock =
2114
+ entry?.block.type === "thinking"
2115
+ ? entry.block
2116
+ : (output.content.find(b => b.type === "thinking" && (b as ThinkingContent).itemId === item.id) as
2117
+ | ThinkingContent
2118
+ | undefined);
2119
+ if (reasoningBlock) {
2120
+ reasoningBlock.thinking = thinking;
2121
+ reasoningBlock.thinkingSignature = JSON.stringify(item);
2122
+ stream.push({
2123
+ type: "thinking_end",
2124
+ contentIndex: contentIndexOf(reasoningBlock),
2125
+ content: thinking,
2126
+ partial: output,
2127
+ });
2128
+ }
2129
+ closeOpenItem(event.output_index, item.id, entry);
2130
+ } else if (item.type === "message") {
2131
+ const block = entry?.block.type === "text" ? entry.block : undefined;
2132
+ const text = item.content
2133
+ .map(part => (part.type === "output_text" ? (part.text ?? "") : (part.refusal ?? "")))
2134
+ .join("");
2135
+ const textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
2136
+ let contentIndex: number;
2137
+ if (block) {
2138
+ block.text = text;
2139
+ block.textSignature = textSignature;
2140
+ contentIndex = contentIndexOf(block);
2141
+ } else {
2142
+ // `output_item.added` never arrived (lossy proxy) — synthesize the
2143
+ // block so the final message still carries the authoritative text.
2144
+ const synthesized: TextContent = { type: "text", text, textSignature };
2145
+ output.content.push(synthesized);
2146
+ contentIndex = output.content.length - 1;
2147
+ }
2148
+ stream.push({ type: "text_end", contentIndex, content: text, partial: output });
2149
+ closeOpenItem(event.output_index, item.id, entry);
2150
+ } else if (item.type === "function_call") {
2151
+ const block = entry?.block.type === "toolCall" ? entry.block : undefined;
2152
+ const args = block?.[kStreamingArgumentsDone]
2153
+ ? block.arguments
2154
+ : block?.[kStreamingPartialJson]
2155
+ ? parseStreamingJson(block[kStreamingPartialJson])
2156
+ : parseStreamingJson(item.arguments || "{}");
2157
+ const toolCall: ToolCall = {
2158
+ type: "toolCall",
2159
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2160
+ name: item.name,
2161
+ arguments: args,
2162
+ };
2163
+ let contentIndex: number;
2164
+ if (block) {
2165
+ // Persist the authoritative final args on the stored block. The
2166
+ // throttled delta parser may have skipped the last partial parse,
2167
+ // leaving block.arguments stale (often `{}`); the emitted toolCall
2168
+ // and the persisted block must agree.
2169
+ block.arguments = args;
2170
+ clearStreamingPartialJson(block);
2171
+ contentIndex = contentIndexOf(block);
2172
+ } else {
2173
+ // `output_item.added` never arrived (lossy proxy) — synthesize the
2174
+ // block so the final message carries the call the consumer was told
2175
+ // completed (the agent loop executes tools from message.content).
2176
+ output.content.push(toolCall);
2177
+ contentIndex = output.content.length - 1;
2178
+ }
2179
+ closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
2180
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2181
+ } else if (item.type === "custom_tool_call") {
2182
+ const block = entry?.block.type === "toolCall" ? entry.block : undefined;
2183
+ const rawInput = block?.[kStreamingPartialJson] ? block[kStreamingPartialJson] : (item.input ?? "");
2184
+ const toolCall: ToolCall = {
2185
+ type: "toolCall",
2186
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2187
+ name: item.name,
2188
+ arguments: { input: rawInput },
2189
+ customWireName: item.name,
2190
+ };
2191
+ let contentIndex: number;
2192
+ if (block) {
2193
+ // Persist the final input on the stored block and drop the transient
2194
+ // accumulation buffer, mirroring the function_call branch above.
2195
+ block.arguments = { input: rawInput };
2196
+ clearStreamingPartialJson(block);
2197
+ contentIndex = contentIndexOf(block);
2198
+ } else {
2199
+ output.content.push(toolCall);
2200
+ contentIndex = output.content.length - 1;
2201
+ }
2202
+ closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
2203
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2204
+ }
2205
+ } else if (event.type === "response.completed" || event.type === "response.incomplete") {
2206
+ const response = event.response;
2207
+ finalizePendingResponsesToolCalls(output);
2208
+ if (response?.id) {
2209
+ output.responseId = response.id;
2210
+ }
2211
+ populateResponsesUsageFromResponse(output, response?.usage);
2212
+ calculateCost(model, output.usage);
2213
+ applyOpenAIResponsesServiceTierCost(
2214
+ model,
2215
+ output.usage,
2216
+ (response as { service_tier?: unknown } | undefined)?.service_tier,
2217
+ options?.requestServiceTier,
2218
+ );
2219
+ output.stopReason = mapOpenAIResponsesStopReason(response?.status);
2220
+ if (response?.status === "failed" || response?.status === "cancelled") {
2221
+ const error = response?.error ?? (response as any)?.status_details?.error;
2222
+ const details = response?.incomplete_details;
2223
+ const statusDetailsReason = (response as any)?.status_details?.reason;
2224
+ const message = error
2225
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
2226
+ : details?.reason
2227
+ ? `incomplete: ${details.reason}`
2228
+ : typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
2229
+ ? `status_details: ${statusDetailsReason}`
2230
+ : "Unknown error (no error details in response)";
2231
+ throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
2232
+ }
2233
+ if (response?.status === "incomplete" && response.incomplete_details?.reason === "content_filter") {
2234
+ // A content-filtered turn is a failure, not a token-cap truncation —
2235
+ // mapping it to "length" would route the agent loop into "shorten your
2236
+ // output" recovery against a filtered prompt.
2237
+ throw new AIError.ProviderResponseError("incomplete: content_filter", {
2238
+ provider: model.provider,
2239
+ kind: "content-blocked",
2240
+ });
2241
+ }
2242
+ promoteResponsesToolUseStopReason(output, (response as { end_turn?: boolean } | undefined)?.end_turn);
2243
+ options?.onCompleted?.();
2244
+ // `response.completed`/`response.incomplete` is the last event of a
2245
+ // Responses stream. Stop pulling instead of waiting for the server to
2246
+ // close the connection: misbehaving providers keep the socket open
2247
+ // after the terminal event, which would park this loop until the idle
2248
+ // watchdog converts an already-successful turn into a timeout error.
2249
+ // Breaking unwinds the iterator chain (the consumer's `.return()`
2250
+ // reaches the SDK stream), actively releasing the connection.
2251
+ break;
2252
+ } else if (event.type === "error") {
2253
+ const err = (event as any).error ?? event;
2254
+ const code = err.code ?? "unknown";
2255
+ const message = err.message ?? "no message";
2256
+ throw new AIError.ProviderResponseError(`Error Code ${code}: ${message}`, {
2257
+ provider: model.provider,
2258
+ kind: "output",
2259
+ });
2260
+ } else if (event.type === "response.failed") {
2261
+ populateResponsesUsageFromResponse(output, event.response?.usage);
2262
+ const error = event.response?.error ?? (event.response as any)?.status_details?.error;
2263
+ const details = event.response?.incomplete_details;
2264
+ const message = error
2265
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
2266
+ : details?.reason
2267
+ ? `incomplete: ${details.reason}`
2268
+ : "Unknown error (no error details in response)";
2269
+ throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
2270
+ }
2271
+ }
2272
+ }
2273
+
2274
+ export function mapOpenAIResponsesStopReason(status: ResponseStatus | undefined): StopReason {
2275
+ if (!status) return "stop";
2276
+ switch (status) {
2277
+ case "completed":
2278
+ return "stop";
2279
+ case "incomplete":
2280
+ return "length";
2281
+ case "failed":
2282
+ case "cancelled":
2283
+ return "error";
2284
+ case "in_progress":
2285
+ case "queued":
2286
+ return "stop";
2287
+ default: {
2288
+ // Compile-time exhaustiveness; at runtime a brand-new status from the
2289
+ // server must degrade gracefully instead of failing a fully-streamed
2290
+ // response.
2291
+ const exhaustive: never = status;
2292
+ logger.warn("Unhandled OpenAI Responses stop reason", { status: exhaustive });
2293
+ return "stop";
2294
+ }
2295
+ }
2296
+ }
2297
+
2298
+ /**
2299
+ * Finalize any streamed toolCall block whose `output_item.done` never arrived
2300
+ * (lossy proxy, or a terminal event that raced the per-item done): parse the
2301
+ * accumulated `partialJson` into authoritative arguments and strip the transient
2302
+ * streaming fields so they never persist. Shared by the chat-Responses decoder
2303
+ * and the Codex decoder. Closed blocks already cleared these fields, so walking
2304
+ * the full content list leaves them untouched.
2305
+ */
2306
+ export function finalizePendingResponsesToolCalls(output: AssistantMessage): void {
2307
+ for (const block of output.content) {
2308
+ if (block.type !== "toolCall") continue;
2309
+ const pending = block as ToolCall & {
2310
+ [kStreamingPartialJson]?: string;
2311
+ [kStreamingLastParseLen]?: number;
2312
+ [kStreamingArgumentsDone]?: boolean;
2313
+ };
2314
+ if (pending[kStreamingPartialJson] && !pending[kStreamingArgumentsDone]) {
2315
+ pending.arguments =
2316
+ pending.customWireName !== undefined
2317
+ ? { input: pending[kStreamingPartialJson] }
2318
+ : parseStreamingJson(pending[kStreamingPartialJson]);
2319
+ }
2320
+ clearStreamingPartialJson(pending);
2321
+ }
2322
+ }
2323
+
2324
+ /**
2325
+ * Apply the Responses terminal stop-reason invariants shared by the chat-Responses
2326
+ * and Codex decoders: a turn that produced tool calls becomes `toolUse`, and a
2327
+ * Codex-lineage `end_turn: false` marker pauses the turn so the agent loop
2328
+ * re-samples instead of ending. Callers set `output.stopReason` from the wire
2329
+ * status first via {@link mapOpenAIResponsesStopReason}.
2330
+ */
2331
+ export function promoteResponsesToolUseStopReason(output: AssistantMessage, endTurn: boolean | undefined): void {
2332
+ if (output.content.some(block => block.type === "toolCall") && output.stopReason === "stop") {
2333
+ output.stopReason = "toolUse";
2334
+ }
2335
+ if (endTurn === false && output.stopReason === "stop") {
2336
+ output.stopDetails = { type: "pause_turn" };
2337
+ }
2338
+ }
2339
+
2340
+ /** Initial empty `AssistantMessage` that streaming providers accumulate into. */
2341
+ export function createInitialResponsesAssistantMessage(api: Api, provider: string, modelId: string): AssistantMessage {
2342
+ return {
2343
+ role: "assistant",
2344
+ content: [],
2345
+ api,
2346
+ provider,
2347
+ model: modelId,
2348
+ usage: {
2349
+ input: 0,
2350
+ output: 0,
2351
+ cacheRead: 0,
2352
+ cacheWrite: 0,
2353
+ totalTokens: 0,
2354
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
2355
+ },
2356
+ stopReason: "stop",
2357
+ timestamp: Date.now(),
2358
+ };
2359
+ }
2360
+
2361
+ /** Extension fields we add on top of `ResponseCreateParamsStreaming` across the Responses-family providers. */
2362
+ export type ResponsesSamplingParamsExtras = {
2363
+ top_p?: number;
2364
+ top_k?: number;
2365
+ min_p?: number;
2366
+ presence_penalty?: number;
2367
+ repetition_penalty?: number;
2368
+ };
2369
+
2370
+ type CommonResponsesParams = ResponseCreateParamsStreaming & ResponsesSamplingParamsExtras;
2371
+
2372
+ type CommonSamplingOptions = Pick<
2373
+ StreamOptions,
2374
+ "temperature" | "topP" | "topK" | "minP" | "presencePenalty" | "repetitionPenalty" | "maxTokens"
2375
+ > & { serviceTier?: ServiceTier };
2376
+
2377
+ /**
2378
+ * Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
2379
+ * temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
2380
+ *
2381
+ * `max_output_tokens` is suppressed when {@link Model.omitMaxOutputTokens} is `true`, so
2382
+ * proxies (notably Ollama) that forward to upstream APIs with an unknown output-token cap
2383
+ * can let the upstream apply its own default instead of 400-ing on `maxTokens` values that
2384
+ * reflect the model's context window rather than the upstream output limit.
2385
+ */
2386
+ export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
2387
+ params: P,
2388
+ options: CommonSamplingOptions | undefined,
2389
+ model: Pick<Model, "provider" | "omitMaxOutputTokens" | "maxTokens">,
2390
+ ): void {
2391
+ if (options?.maxTokens && !model.omitMaxOutputTokens) {
2392
+ params.max_output_tokens = Math.min(
2393
+ options.maxTokens,
2394
+ model.maxTokens ?? Number.POSITIVE_INFINITY,
2395
+ OPENAI_MAX_OUTPUT_TOKENS,
2396
+ );
2397
+ }
2398
+ if (options?.temperature !== undefined) params.temperature = options.temperature;
2399
+ if (options?.topP !== undefined) params.top_p = options.topP;
2400
+ if (options?.topK !== undefined) params.top_k = options.topK;
2401
+ if (options?.minP !== undefined) params.min_p = options.minP;
2402
+ if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
2403
+ if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
2404
+ applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
2405
+ }
2406
+
2407
+ const RESPONSES_REASONING_SUPPRESSION_PROMPT = responsesReasoningSuppressionPrompt.trim();
2408
+
2409
+ type ReasoningOptions = {
2410
+ reasoning?: string;
2411
+ reasoningSummary?: "auto" | "detailed" | "concise" | null;
2412
+ disableReasoning?: boolean;
2413
+ toolChoice?: unknown;
2414
+ };
2415
+
2416
+ export interface ApplyResponsesCompatPolicyOptions {
2417
+ reasoningSummary?: "auto" | "detailed" | "concise" | null;
2418
+ mapEffort?: (effort: string) => string;
2419
+ }
2420
+
2421
+ export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreaming>(
2422
+ params: P,
2423
+ messages: ResponseInput,
2424
+ policy: OpenAICompatPolicy,
2425
+ options: ApplyResponsesCompatPolicyOptions | undefined,
2426
+ ): number {
2427
+ const reasoning = policy.reasoning;
2428
+ if (!reasoning.modelSupported) return 0;
2429
+ if (reasoning.includeEncryptedReasoning) {
2430
+ const include = params.include ?? [];
2431
+ if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
2432
+ params.include = include;
2433
+ }
2434
+
2435
+ if (reasoning.disabled) {
2436
+ if (reasoning.disableMode === "openrouter-enabled-false") {
2437
+ params.reasoning = { enabled: false } as P["reasoning"];
2438
+ return 0;
2439
+ }
2440
+ if (
2441
+ reasoning.disableMode === "lowest-effort" &&
2442
+ reasoning.wireEffort !== undefined &&
2443
+ !reasoning.omitReasoningEffort
2444
+ ) {
2445
+ type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
2446
+ params.reasoning = { effort: reasoning.wireEffort as ReasoningParam["effort"] } as P["reasoning"] &
2447
+ ReasoningParam;
2448
+ return 0;
2449
+ }
2450
+ if (policy.compat.requiresReasoningSuppressionPrompt && reasoning.requestedEffort === undefined) {
2451
+ messages.push({
2452
+ role: "developer",
2453
+ content: [{ type: "input_text", text: RESPONSES_REASONING_SUPPRESSION_PROMPT }],
2454
+ });
2455
+ return 1;
2456
+ }
2457
+ return 0;
2458
+ }
2459
+
2460
+ if (reasoning.requestedEffort !== undefined || options?.reasoningSummary !== undefined) {
2461
+ if (reasoning.omitReasoningEffort) {
2462
+ if (options?.reasoningSummary !== undefined && options.reasoningSummary !== null) {
2463
+ type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
2464
+ params.reasoning = { summary: options.reasoningSummary || "auto" } as P["reasoning"] & ReasoningParam;
2465
+ }
2466
+ return 0;
2467
+ }
2468
+
2469
+ const requested = reasoning.requestedEffort ?? "medium";
2470
+ const wireEffort = reasoning.wireEffort ?? options?.mapEffort?.(requested) ?? requested;
2471
+ type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
2472
+ const reasoningParams: ReasoningParam = {
2473
+ effort: wireEffort as ReasoningParam["effort"],
2474
+ };
2475
+ if (options?.reasoningSummary !== null) {
2476
+ reasoningParams.summary = options?.reasoningSummary || "auto";
2477
+ }
2478
+ params.reasoning = reasoningParams as P["reasoning"];
2479
+ return 0;
2480
+ }
2481
+
2482
+ if (policy.compat.requiresReasoningSuppressionPrompt) {
2483
+ messages.push({
2484
+ role: "developer",
2485
+ content: [{ type: "input_text", text: RESPONSES_REASONING_SUPPRESSION_PROMPT }],
2486
+ });
2487
+ return 1;
2488
+ }
2489
+ return 0;
2490
+ }
2491
+
2492
+ /**
2493
+ * Apply reasoning-related Responses parameters. Default behavior comes from
2494
+ * catalog compat; include/omit arguments are explicit adapter-wrapper overrides.
2495
+ */
2496
+ export function applyResponsesReasoningParams<P extends ResponseCreateParamsStreaming>(
2497
+ params: P,
2498
+ model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">,
2499
+ options: ReasoningOptions | undefined,
2500
+ messages: ResponseInput,
2501
+ mapEffort?: (effort: string) => string,
2502
+ includeEncryptedReasoning?: boolean,
2503
+ omitReasoningEffort?: boolean,
2504
+ ): number {
2505
+ return applyResponsesCompatPolicy(
2506
+ params,
2507
+ messages,
2508
+ resolveOpenAICompatPolicy(model, {
2509
+ endpoint: "responses",
2510
+ reasoning: options?.reasoning,
2511
+ disableReasoning: options?.disableReasoning,
2512
+ toolChoice: options?.toolChoice,
2513
+ includeEncryptedReasoning,
2514
+ omitReasoningEffort,
2515
+ }),
2516
+ { reasoningSummary: options?.reasoningSummary, mapEffort },
2517
+ );
2518
+ }
2519
+
2520
+ /** Populate `output.usage` from a Responses-API `response.usage` payload. Does not invoke `calculateCost`. */
2521
+ export function populateResponsesUsageFromResponse(
2522
+ output: AssistantMessage,
2523
+ usage:
2524
+ | {
2525
+ input_tokens?: number | null;
2526
+ output_tokens?: number | null;
2527
+ total_tokens?: number | null;
2528
+ prompt_cache_hit_tokens?: number | null;
2529
+ prompt_cache_miss_tokens?: number | null;
2530
+ input_tokens_details?: {
2531
+ cached_tokens?: number | null;
2532
+ cache_write_tokens?: number | null;
2533
+ orchestration_input_tokens?: number | null;
2534
+ orchestration_input_cached_tokens?: number | null;
2535
+ } | null;
2536
+ output_tokens_details?: {
2537
+ reasoning_tokens?: number | null;
2538
+ orchestration_output_tokens?: number | null;
2539
+ } | null;
2540
+ }
2541
+ | null
2542
+ | undefined,
2543
+ ): void {
2544
+ if (!usage) return;
2545
+ const details = usage.input_tokens_details;
2546
+ const outputDetails = usage.output_tokens_details;
2547
+ const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
2548
+ const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
2549
+ const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
2550
+ const accounting = calculateOpenAIUsageAccounting({
2551
+ promptTokens: (usage.input_tokens ?? 0) + orchestrationInputTokens,
2552
+ outputTokens: (usage.output_tokens ?? 0) + orchestrationOutputTokens,
2553
+ cachedTokens: (details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0) + orchestrationInputCachedTokens,
2554
+ reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
2555
+ cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
2556
+ cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
2557
+ hasDeepSeekCacheHitAndMiss:
2558
+ usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
2559
+ });
2560
+
2561
+ // Wholesale replacement must not drop provider-annotated extras (Copilot
2562
+ // premium-request accounting): the failed/cancelled paths throw right after
2563
+ // this call with no later chance to re-apply.
2564
+ const premiumRequests = output.usage.premiumRequests;
2565
+ output.usage = {
2566
+ ...accounting,
2567
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
2568
+ };
2569
+ if (premiumRequests !== undefined) {
2570
+ output.usage.premiumRequests = premiumRequests;
2571
+ }
2572
+ }
2573
+
2574
+ /**
2575
+ * Structural equality for the chain prefix/option check, equivalent to the
2576
+ * default {@link Bun.deepEquals} (own enumerable keys, `absent ≡ own-undefined`)
2577
+ * except for two deliberate exclusions:
2578
+ * - **symbol-keyed properties are ignored** — `for…in` walks enumerable
2579
+ * *string* keys only (never symbols); these are plain wire items whose
2580
+ * prototype contributes no enumerable keys, so iteration is effectively
2581
+ * own-string-keyed. That is how the transient streaming symbols
2582
+ * (`block-symbols.ts`) stamped onto live request items are excluded (the
2583
+ * deep-cloned baseline never carries them). Do NOT add an
2584
+ * `Object.getOwnPropertySymbols` pass, or those symbols resurface and break
2585
+ * chaining.
2586
+ * - keys listed in `omitKeys` are skipped (the option compare omits `input`
2587
+ * and the per-turn `client_metadata`).
2588
+ * A defined value differing across sides IS a difference; a key undefined or
2589
+ * absent on both stays equal. Nested values use full {@link Bun.deepEquals}.
2590
+ */
2591
+ function deepEqualsWithout(a: unknown, b: unknown, omitKeys?: Record<string, boolean>): boolean {
2592
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return Bun.deepEquals(a, b);
2593
+ const ao = a as Record<string, unknown>;
2594
+ const bo = b as Record<string, unknown>;
2595
+ for (const key in ao) {
2596
+ if (omitKeys?.[key]) continue;
2597
+ const av = ao[key];
2598
+ const bv = bo[key];
2599
+ if (av !== bv && !Bun.deepEquals(av, bv)) return false;
2600
+ }
2601
+ for (const key in bo) {
2602
+ if (omitKeys?.[key]) continue;
2603
+ if (bo[key] !== undefined && !(key in ao)) return false;
2604
+ }
2605
+ return true;
2606
+ }
2607
+
2608
+ const TOP_LEVEL_EXCLUDE_MAP = {
2609
+ input: true,
2610
+ client_metadata: true,
2611
+ };
2612
+
2613
+ /**
2614
+ * Strict-prefix delta for stateful `previous_response_id` chaining (used by the
2615
+ * platform Responses provider and the Codex provider on both transports):
2616
+ * returns the input items the current request appends beyond the previous
2617
+ * request's input plus the previous response's output items, or null when the
2618
+ * request options differ or history mutated (the chain must break). Per-turn
2619
+ * `client_metadata` (e.g. rotating turn ids) is excluded from the option
2620
+ * comparison; codex-rs excludes it from the same check.
2621
+ */
2622
+ export function buildResponsesDeltaInput<TItem extends ResponseInputItem | InputItem>(
2623
+ previous: { input?: TItem[] } | undefined,
2624
+ previousResponseItems: readonly TItem[] | undefined,
2625
+ current: { input?: TItem[] },
2626
+ ): TItem[] | null {
2627
+ if (!previous) return null;
2628
+ if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
2629
+ if (!deepEqualsWithout(previous, current, TOP_LEVEL_EXCLUDE_MAP)) {
2630
+ return null;
2631
+ }
2632
+
2633
+ const baselineLen = (previous.input?.length ?? 0) + (previousResponseItems?.length ?? 0);
2634
+ if (current.input.length <= baselineLen) return null;
2635
+
2636
+ let index = 0;
2637
+ for (const series of [previous.input, previousResponseItems]) {
2638
+ if (!series) continue;
2639
+ for (const item of series) {
2640
+ if (deepEqualsWithout(item, current.input[index])) {
2641
+ index++;
2642
+ } else {
2643
+ return null;
2644
+ }
2645
+ }
2646
+ }
2647
+ return current.input.slice(index) as TItem[];
2648
+ }