@linxiraos/pi-ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (639) hide show
  1. package/CHANGELOG.md +5066 -0
  2. package/README.md +1195 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
  5. package/dist/types/auth-broker/client.d.ts +93 -0
  6. package/dist/types/auth-broker/discover.d.ts +39 -0
  7. package/dist/types/auth-broker/index.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +136 -0
  10. package/dist/types/auth-broker/server.d.ts +48 -0
  11. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  12. package/dist/types/auth-broker/types.d.ts +152 -0
  13. package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
  14. package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
  15. package/dist/types/auth-gateway/http.d.ts +56 -0
  16. package/dist/types/auth-gateway/index.d.ts +3 -0
  17. package/dist/types/auth-gateway/server.d.ts +36 -0
  18. package/dist/types/auth-gateway/types.d.ts +127 -0
  19. package/dist/types/auth-retry.d.ts +150 -0
  20. package/dist/types/auth-storage.d.ts +1258 -0
  21. package/dist/types/dialect/anthropic.d.ts +15 -0
  22. package/dist/types/dialect/catalog.d.ts +3 -0
  23. package/dist/types/dialect/coercion.d.ts +23 -0
  24. package/dist/types/dialect/deepseek.d.ts +14 -0
  25. package/dist/types/dialect/demotion.d.ts +30 -0
  26. package/dist/types/dialect/examples.d.ts +18 -0
  27. package/dist/types/dialect/factory.d.ts +3 -0
  28. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  29. package/dist/types/dialect/gemini.d.ts +17 -0
  30. package/dist/types/dialect/gemma.d.ts +15 -0
  31. package/dist/types/dialect/glm.d.ts +9 -0
  32. package/dist/types/dialect/harmony.d.ts +8 -0
  33. package/dist/types/dialect/hermes.d.ts +9 -0
  34. package/dist/types/dialect/history.d.ts +3 -0
  35. package/dist/types/dialect/index.d.ts +11 -0
  36. package/dist/types/dialect/inventory.d.ts +9 -0
  37. package/dist/types/dialect/kimi.d.ts +14 -0
  38. package/dist/types/dialect/minimax.d.ts +3 -0
  39. package/dist/types/dialect/owned-stream.d.ts +4 -0
  40. package/dist/types/dialect/qwen3.d.ts +9 -0
  41. package/dist/types/dialect/rendering.d.ts +54 -0
  42. package/dist/types/dialect/thinking.d.ts +6 -0
  43. package/dist/types/dialect/types.d.ts +68 -0
  44. package/dist/types/dialect/xml.d.ts +9 -0
  45. package/dist/types/error/abort.d.ts +14 -0
  46. package/dist/types/error/auth-classify.d.ts +20 -0
  47. package/dist/types/error/auth.d.ts +27 -0
  48. package/dist/types/error/aws.d.ts +27 -0
  49. package/dist/types/error/classes.d.ts +106 -0
  50. package/dist/types/error/finalize.d.ts +39 -0
  51. package/dist/types/error/flags.d.ts +94 -0
  52. package/dist/types/error/format.d.ts +20 -0
  53. package/dist/types/error/gateway.d.ts +20 -0
  54. package/dist/types/error/index.d.ts +14 -0
  55. package/dist/types/error/oauth.d.ts +43 -0
  56. package/dist/types/error/provider.d.ts +42 -0
  57. package/dist/types/error/rate-limit.d.ts +80 -0
  58. package/dist/types/error/retryable.d.ts +27 -0
  59. package/dist/types/error/validation.d.ts +32 -0
  60. package/dist/types/index.d.ts +51 -0
  61. package/dist/types/provider-details.d.ts +24 -0
  62. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  63. package/dist/types/providers/anthropic-client.d.ts +106 -0
  64. package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
  65. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  66. package/dist/types/providers/anthropic-wire.d.ts +345 -0
  67. package/dist/types/providers/anthropic.d.ts +261 -0
  68. package/dist/types/providers/aws-credentials.d.ts +48 -0
  69. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  70. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  71. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  72. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  73. package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
  74. package/dist/types/providers/cowork-fetch.d.ts +3 -0
  75. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  76. package/dist/types/providers/cursor-pi-args.d.ts +105 -0
  77. package/dist/types/providers/cursor.d.ts +221 -0
  78. package/dist/types/providers/devin.d.ts +12 -0
  79. package/dist/types/providers/error-message.d.ts +25 -0
  80. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  81. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  82. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  83. package/dist/types/providers/google-auth.d.ts +24 -0
  84. package/dist/types/providers/google-gemini-cli.d.ts +120 -0
  85. package/dist/types/providers/google-shared.d.ts +203 -0
  86. package/dist/types/providers/google-types.d.ts +155 -0
  87. package/dist/types/providers/google-vertex.d.ts +7 -0
  88. package/dist/types/providers/google.d.ts +4 -0
  89. package/dist/types/providers/grammar.d.ts +1 -0
  90. package/dist/types/providers/kimi.d.ts +27 -0
  91. package/dist/types/providers/mock.d.ts +179 -0
  92. package/dist/types/providers/ollama.d.ts +8 -0
  93. package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
  94. package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
  95. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  96. package/dist/types/providers/openai-chat-wire.d.ts +669 -0
  97. package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
  98. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  99. package/dist/types/providers/openai-codex-responses.d.ts +238 -0
  100. package/dist/types/providers/openai-completions.d.ts +48 -0
  101. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  102. package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
  103. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  104. package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
  105. package/dist/types/providers/openai-responses.d.ts +131 -0
  106. package/dist/types/providers/openai-shared.d.ts +624 -0
  107. package/dist/types/providers/pi-native-client.d.ts +13 -0
  108. package/dist/types/providers/pi-native-server.d.ts +69 -0
  109. package/dist/types/providers/register-builtins.d.ts +37 -0
  110. package/dist/types/providers/synthetic.d.ts +26 -0
  111. package/dist/types/providers/transform-messages.d.ts +32 -0
  112. package/dist/types/providers/vision-guard.d.ts +20 -0
  113. package/dist/types/registry/aiand.d.ts +7 -0
  114. package/dist/types/registry/aimlapi.d.ts +4 -0
  115. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  116. package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
  117. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  118. package/dist/types/registry/anthropic.d.ts +10 -0
  119. package/dist/types/registry/api-key-login.d.ts +42 -0
  120. package/dist/types/registry/api-key-validation.d.ts +43 -0
  121. package/dist/types/registry/aws.d.ts +13 -0
  122. package/dist/types/registry/azure.d.ts +4 -0
  123. package/dist/types/registry/baseten.d.ts +7 -0
  124. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  125. package/dist/types/registry/cerebras.d.ts +7 -0
  126. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  127. package/dist/types/registry/coreweave.d.ts +7 -0
  128. package/dist/types/registry/cursor.d.ts +7 -0
  129. package/dist/types/registry/deepseek.d.ts +8 -0
  130. package/dist/types/registry/derived.d.ts +5 -0
  131. package/dist/types/registry/devin.d.ts +8 -0
  132. package/dist/types/registry/exa.d.ts +8 -0
  133. package/dist/types/registry/firepass.d.ts +16 -0
  134. package/dist/types/registry/fireworks.d.ts +7 -0
  135. package/dist/types/registry/github-copilot.d.ts +7 -0
  136. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  137. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  138. package/dist/types/registry/gmi-cloud.d.ts +7 -0
  139. package/dist/types/registry/google-antigravity.d.ts +9 -0
  140. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  141. package/dist/types/registry/google-vertex.d.ts +5 -0
  142. package/dist/types/registry/google.d.ts +4 -0
  143. package/dist/types/registry/groq.d.ts +4 -0
  144. package/dist/types/registry/huggingface.d.ts +7 -0
  145. package/dist/types/registry/index.d.ts +4 -0
  146. package/dist/types/registry/kagi.d.ts +14 -0
  147. package/dist/types/registry/kilo.d.ts +7 -0
  148. package/dist/types/registry/kimi-code.d.ts +7 -0
  149. package/dist/types/registry/litellm.d.ts +13 -0
  150. package/dist/types/registry/llama-cpp.d.ts +8 -0
  151. package/dist/types/registry/lm-studio.d.ts +8 -0
  152. package/dist/types/registry/meta.d.ts +7 -0
  153. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  154. package/dist/types/registry/minimax-code.d.ts +6 -0
  155. package/dist/types/registry/minimax.d.ts +4 -0
  156. package/dist/types/registry/mistral.d.ts +4 -0
  157. package/dist/types/registry/moonshot.d.ts +7 -0
  158. package/dist/types/registry/nanogpt.d.ts +7 -0
  159. package/dist/types/registry/novita.d.ts +6 -0
  160. package/dist/types/registry/nvidia.d.ts +7 -0
  161. package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
  162. package/dist/types/registry/oauth/anthropic.d.ts +24 -0
  163. package/dist/types/registry/oauth/callback-server.d.ts +74 -0
  164. package/dist/types/registry/oauth/cursor.d.ts +16 -0
  165. package/dist/types/registry/oauth/device-code.d.ts +25 -0
  166. package/dist/types/registry/oauth/devin.d.ts +5 -0
  167. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  168. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  169. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  170. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  171. package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
  172. package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
  173. package/dist/types/registry/oauth/index.d.ts +45 -0
  174. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  175. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  176. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  177. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  178. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  179. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  180. package/dist/types/registry/oauth/types.d.ts +87 -0
  181. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  182. package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
  183. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  184. package/dist/types/registry/oauth/zai.d.ts +25 -0
  185. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  186. package/dist/types/registry/ollama.d.ts +12 -0
  187. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  188. package/dist/types/registry/openai-codex.d.ts +9 -0
  189. package/dist/types/registry/openai.d.ts +4 -0
  190. package/dist/types/registry/opencode-go.d.ts +6 -0
  191. package/dist/types/registry/opencode-zen.d.ts +6 -0
  192. package/dist/types/registry/openrouter.d.ts +13 -0
  193. package/dist/types/registry/parallel.d.ts +14 -0
  194. package/dist/types/registry/perplexity.d.ts +7 -0
  195. package/dist/types/registry/qianfan.d.ts +7 -0
  196. package/dist/types/registry/qwen-portal.d.ts +7 -0
  197. package/dist/types/registry/registry.d.ts +367 -0
  198. package/dist/types/registry/sakana.d.ts +7 -0
  199. package/dist/types/registry/siliconflow-cn.d.ts +7 -0
  200. package/dist/types/registry/siliconflow.d.ts +7 -0
  201. package/dist/types/registry/synthetic.d.ts +6 -0
  202. package/dist/types/registry/tavily.d.ts +14 -0
  203. package/dist/types/registry/together.d.ts +6 -0
  204. package/dist/types/registry/types.d.ts +75 -0
  205. package/dist/types/registry/umans.d.ts +7 -0
  206. package/dist/types/registry/venice.d.ts +13 -0
  207. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  208. package/dist/types/registry/vllm.d.ts +7 -0
  209. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  210. package/dist/types/registry/xai-oauth.d.ts +7 -0
  211. package/dist/types/registry/xai.d.ts +7 -0
  212. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  213. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  214. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  215. package/dist/types/registry/xiaomi.d.ts +6 -0
  216. package/dist/types/registry/zai.d.ts +15 -0
  217. package/dist/types/registry/zenmux.d.ts +7 -0
  218. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  219. package/dist/types/stream.d.ts +46 -0
  220. package/dist/types/types.d.ts +1064 -0
  221. package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
  222. package/dist/types/usage/claude.d.ts +4 -0
  223. package/dist/types/usage/cursor.d.ts +4 -0
  224. package/dist/types/usage/gemini.d.ts +2 -0
  225. package/dist/types/usage/github-copilot.d.ts +7 -0
  226. package/dist/types/usage/google-antigravity.d.ts +15 -0
  227. package/dist/types/usage/kimi.d.ts +2 -0
  228. package/dist/types/usage/minimax-code.d.ts +3 -0
  229. package/dist/types/usage/ollama.d.ts +5 -0
  230. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  231. package/dist/types/usage/openai-codex-reset.d.ts +88 -0
  232. package/dist/types/usage/openai-codex.d.ts +10 -0
  233. package/dist/types/usage/opencode-go.d.ts +2 -0
  234. package/dist/types/usage/shared.d.ts +1 -0
  235. package/dist/types/usage/synthetic.d.ts +2 -0
  236. package/dist/types/usage/umans.d.ts +2 -0
  237. package/dist/types/usage/xai-oauth.d.ts +12 -0
  238. package/dist/types/usage/zai.d.ts +3 -0
  239. package/dist/types/usage.d.ts +527 -0
  240. package/dist/types/utils/abort.d.ts +25 -0
  241. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  242. package/dist/types/utils/aws-profile.d.ts +17 -0
  243. package/dist/types/utils/block-symbols.d.ts +62 -0
  244. package/dist/types/utils/deterministic-id.d.ts +16 -0
  245. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  246. package/dist/types/utils/event-stream.d.ts +39 -0
  247. package/dist/types/utils/foundry.d.ts +1 -0
  248. package/dist/types/utils/google-validation.d.ts +2 -0
  249. package/dist/types/utils/harmony-leak.d.ts +135 -0
  250. package/dist/types/utils/http-inspector.d.ts +49 -0
  251. package/dist/types/utils/idle-iterator.d.ts +149 -0
  252. package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
  253. package/dist/types/utils/openai-http.d.ts +48 -0
  254. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  255. package/dist/types/utils/parse-bind.d.ts +23 -0
  256. package/dist/types/utils/provider-response.d.ts +3 -0
  257. package/dist/types/utils/proxy.d.ts +39 -0
  258. package/dist/types/utils/request-debug.d.ts +29 -0
  259. package/dist/types/utils/retry-after.d.ts +4 -0
  260. package/dist/types/utils/retry.d.ts +14 -0
  261. package/dist/types/utils/schema/adapt.d.ts +24 -0
  262. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  263. package/dist/types/utils/schema/dereference.d.ts +11 -0
  264. package/dist/types/utils/schema/draft.d.ts +10 -0
  265. package/dist/types/utils/schema/equality.d.ts +4 -0
  266. package/dist/types/utils/schema/fields.d.ts +54 -0
  267. package/dist/types/utils/schema/index.d.ts +14 -0
  268. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  269. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  270. package/dist/types/utils/schema/normalize.d.ts +153 -0
  271. package/dist/types/utils/schema/spill.d.ts +8 -0
  272. package/dist/types/utils/schema/stamps.d.ts +17 -0
  273. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  274. package/dist/types/utils/schema/types.d.ts +4 -0
  275. package/dist/types/utils/schema/typescript.d.ts +24 -0
  276. package/dist/types/utils/schema/wire.d.ts +52 -0
  277. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  278. package/dist/types/utils/sse-debug.d.ts +5 -0
  279. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  280. package/dist/types/utils/thinking-loop.d.ts +102 -0
  281. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  282. package/dist/types/utils/tool-choice.d.ts +52 -0
  283. package/dist/types/utils/validation.d.ts +28 -0
  284. package/dist/types/utils.d.ts +57 -0
  285. package/package.json +138 -0
  286. package/src/api-registry.ts +109 -0
  287. package/src/auth/sqlite-credential-store.ts +2066 -0
  288. package/src/auth-broker/client.ts +471 -0
  289. package/src/auth-broker/discover.ts +310 -0
  290. package/src/auth-broker/index.ts +7 -0
  291. package/src/auth-broker/refresher.ts +117 -0
  292. package/src/auth-broker/remote-store.ts +1332 -0
  293. package/src/auth-broker/server.ts +898 -0
  294. package/src/auth-broker/snapshot-cache.ts +200 -0
  295. package/src/auth-broker/types.ts +193 -0
  296. package/src/auth-broker/wire-schema-resource.ts +487 -0
  297. package/src/auth-broker/wire-schemas.ts +43 -0
  298. package/src/auth-gateway/http.ts +227 -0
  299. package/src/auth-gateway/index.ts +3 -0
  300. package/src/auth-gateway/server.ts +836 -0
  301. package/src/auth-gateway/types.ts +153 -0
  302. package/src/auth-retry.ts +401 -0
  303. package/src/auth-storage.ts +6540 -0
  304. package/src/dialect/anthropic.md +31 -0
  305. package/src/dialect/anthropic.ts +608 -0
  306. package/src/dialect/catalog.ts +29 -0
  307. package/src/dialect/coercion.ts +136 -0
  308. package/src/dialect/deepseek.md +24 -0
  309. package/src/dialect/deepseek.ts +609 -0
  310. package/src/dialect/demotion.ts +40 -0
  311. package/src/dialect/examples.ts +71 -0
  312. package/src/dialect/factory.ts +34 -0
  313. package/src/dialect/fenced-thinking.ts +184 -0
  314. package/src/dialect/gemini.md +44 -0
  315. package/src/dialect/gemini.ts +583 -0
  316. package/src/dialect/gemma.md +33 -0
  317. package/src/dialect/gemma.ts +387 -0
  318. package/src/dialect/glm.md +32 -0
  319. package/src/dialect/glm.ts +579 -0
  320. package/src/dialect/harmony.md +31 -0
  321. package/src/dialect/harmony.ts +345 -0
  322. package/src/dialect/hermes.md +25 -0
  323. package/src/dialect/hermes.ts +206 -0
  324. package/src/dialect/history.ts +81 -0
  325. package/src/dialect/index.ts +15 -0
  326. package/src/dialect/inventory.ts +30 -0
  327. package/src/dialect/kimi.md +24 -0
  328. package/src/dialect/kimi.ts +340 -0
  329. package/src/dialect/minimax.md +31 -0
  330. package/src/dialect/minimax.ts +95 -0
  331. package/src/dialect/owned-stream.ts +481 -0
  332. package/src/dialect/prompt-template.md +12 -0
  333. package/src/dialect/qwen3.md +28 -0
  334. package/src/dialect/qwen3.ts +240 -0
  335. package/src/dialect/rendering.ts +304 -0
  336. package/src/dialect/thinking.ts +292 -0
  337. package/src/dialect/types.ts +56 -0
  338. package/src/dialect/xml.md +22 -0
  339. package/src/dialect/xml.ts +90 -0
  340. package/src/error/abort.ts +18 -0
  341. package/src/error/auth-classify.ts +47 -0
  342. package/src/error/auth.ts +48 -0
  343. package/src/error/aws.ts +35 -0
  344. package/src/error/classes.ts +281 -0
  345. package/src/error/finalize.ts +69 -0
  346. package/src/error/flags.ts +602 -0
  347. package/src/error/format.ts +45 -0
  348. package/src/error/gateway.ts +96 -0
  349. package/src/error/index.ts +14 -0
  350. package/src/error/oauth.ts +58 -0
  351. package/src/error/provider.ts +63 -0
  352. package/src/error/rate-limit.ts +303 -0
  353. package/src/error/retryable.ts +70 -0
  354. package/src/error/validation.ts +44 -0
  355. package/src/index.ts +51 -0
  356. package/src/provider-details.ts +90 -0
  357. package/src/providers/amazon-bedrock.ts +1064 -0
  358. package/src/providers/anthropic-client.ts +317 -0
  359. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  360. package/src/providers/anthropic-messages-server.ts +818 -0
  361. package/src/providers/anthropic-wire.ts +359 -0
  362. package/src/providers/anthropic.ts +4539 -0
  363. package/src/providers/aws-credentials.ts +772 -0
  364. package/src/providers/aws-eventstream.ts +181 -0
  365. package/src/providers/aws-sigv4.ts +218 -0
  366. package/src/providers/azure-openai-responses.ts +438 -0
  367. package/src/providers/bedrock-mantle.ts +110 -0
  368. package/src/providers/claude-code-fingerprint.ts +20 -0
  369. package/src/providers/cowork-fetch.ts +201 -0
  370. package/src/providers/cursor/exec-modern.ts +496 -0
  371. package/src/providers/cursor/proto/agent.proto +4533 -0
  372. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  373. package/src/providers/cursor/proto/buf.yaml +17 -0
  374. package/src/providers/cursor-pi-args.ts +165 -0
  375. package/src/providers/cursor.ts +4689 -0
  376. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  377. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  378. package/src/providers/devin/proto/buf.yaml +17 -0
  379. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  380. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  381. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  382. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  383. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  384. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  385. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  386. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  387. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  388. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  389. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  390. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  391. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  392. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  393. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  394. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  395. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  396. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  397. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  398. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  399. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  400. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  401. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  402. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  403. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  404. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  405. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  406. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  407. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  408. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  409. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  410. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  411. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  412. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  413. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  414. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  415. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  416. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  417. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  418. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  419. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  420. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  421. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  422. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  423. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  424. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  425. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  426. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  427. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  428. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  429. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  430. package/src/providers/devin.ts +679 -0
  431. package/src/providers/error-message.ts +23 -0
  432. package/src/providers/github-copilot-headers.ts +141 -0
  433. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  434. package/src/providers/gitlab-duo-workflow.ts +3135 -0
  435. package/src/providers/gitlab-duo.ts +399 -0
  436. package/src/providers/google-auth.ts +330 -0
  437. package/src/providers/google-gemini-cli.ts +1370 -0
  438. package/src/providers/google-shared.ts +1122 -0
  439. package/src/providers/google-types.ts +180 -0
  440. package/src/providers/google-vertex.ts +135 -0
  441. package/src/providers/google.ts +47 -0
  442. package/src/providers/grammar.ts +70 -0
  443. package/src/providers/kimi.ts +51 -0
  444. package/src/providers/mock.ts +514 -0
  445. package/src/providers/ollama.ts +776 -0
  446. package/src/providers/openai-anthropic-shim.ts +166 -0
  447. package/src/providers/openai-chat-server-schema.ts +243 -0
  448. package/src/providers/openai-chat-server.ts +752 -0
  449. package/src/providers/openai-chat-wire.ts +859 -0
  450. package/src/providers/openai-codex/request-transformer.ts +491 -0
  451. package/src/providers/openai-codex/response-handler.ts +102 -0
  452. package/src/providers/openai-codex-responses.ts +4716 -0
  453. package/src/providers/openai-completions.ts +2389 -0
  454. package/src/providers/openai-reasoning-fallback.ts +269 -0
  455. package/src/providers/openai-responses-server-schema.ts +397 -0
  456. package/src/providers/openai-responses-server.ts +1466 -0
  457. package/src/providers/openai-responses-wire.ts +6416 -0
  458. package/src/providers/openai-responses.ts +1393 -0
  459. package/src/providers/openai-shared.ts +3500 -0
  460. package/src/providers/pi-native-client.ts +275 -0
  461. package/src/providers/pi-native-server.ts +245 -0
  462. package/src/providers/register-builtins.ts +503 -0
  463. package/src/providers/synthetic.ts +50 -0
  464. package/src/providers/transform-messages.ts +1083 -0
  465. package/src/providers/vision-guard.ts +54 -0
  466. package/src/registry/aiand.ts +22 -0
  467. package/src/registry/aimlapi.ts +6 -0
  468. package/src/registry/alibaba-coding-plan.ts +104 -0
  469. package/src/registry/alibaba-token-plan.ts +125 -0
  470. package/src/registry/amazon-bedrock.ts +22 -0
  471. package/src/registry/anthropic.ts +26 -0
  472. package/src/registry/api-key-login.ts +115 -0
  473. package/src/registry/api-key-validation.ts +145 -0
  474. package/src/registry/aws.ts +57 -0
  475. package/src/registry/azure.ts +6 -0
  476. package/src/registry/baseten.ts +22 -0
  477. package/src/registry/bedrock-mantle.ts +34 -0
  478. package/src/registry/cerebras.ts +23 -0
  479. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  480. package/src/registry/coreweave.ts +40 -0
  481. package/src/registry/cursor.ts +20 -0
  482. package/src/registry/deepseek.ts +46 -0
  483. package/src/registry/derived.ts +9 -0
  484. package/src/registry/devin.ts +15 -0
  485. package/src/registry/exa.ts +19 -0
  486. package/src/registry/firepass.ts +32 -0
  487. package/src/registry/fireworks.ts +28 -0
  488. package/src/registry/github-copilot.ts +22 -0
  489. package/src/registry/gitlab-duo-workflow.ts +20 -0
  490. package/src/registry/gitlab-duo.ts +19 -0
  491. package/src/registry/gmi-cloud.ts +22 -0
  492. package/src/registry/google-antigravity.ts +22 -0
  493. package/src/registry/google-gemini-cli.ts +22 -0
  494. package/src/registry/google-vertex.ts +38 -0
  495. package/src/registry/google.ts +6 -0
  496. package/src/registry/groq.ts +6 -0
  497. package/src/registry/huggingface.ts +29 -0
  498. package/src/registry/index.ts +4 -0
  499. package/src/registry/kagi.ts +46 -0
  500. package/src/registry/kilo.ts +114 -0
  501. package/src/registry/kimi-code.ts +17 -0
  502. package/src/registry/litellm.ts +45 -0
  503. package/src/registry/llama-cpp.ts +35 -0
  504. package/src/registry/lm-studio.ts +31 -0
  505. package/src/registry/meta.ts +22 -0
  506. package/src/registry/minimax-code-cn.ts +12 -0
  507. package/src/registry/minimax-code.ts +12 -0
  508. package/src/registry/minimax.ts +6 -0
  509. package/src/registry/mistral.ts +6 -0
  510. package/src/registry/moonshot.ts +28 -0
  511. package/src/registry/nanogpt.ts +22 -0
  512. package/src/registry/novita.ts +25 -0
  513. package/src/registry/nvidia.ts +61 -0
  514. package/src/registry/oauth/anthropic-constants.ts +12 -0
  515. package/src/registry/oauth/anthropic.ts +346 -0
  516. package/src/registry/oauth/callback-server.ts +438 -0
  517. package/src/registry/oauth/cursor.ts +187 -0
  518. package/src/registry/oauth/device-code.ts +92 -0
  519. package/src/registry/oauth/devin.ts +124 -0
  520. package/src/registry/oauth/github-copilot.ts +369 -0
  521. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  522. package/src/registry/oauth/gitlab-duo.ts +222 -0
  523. package/src/registry/oauth/google-antigravity.ts +225 -0
  524. package/src/registry/oauth/google-gemini-cli.ts +297 -0
  525. package/src/registry/oauth/google-oauth-shared.ts +211 -0
  526. package/src/registry/oauth/index.ts +187 -0
  527. package/src/registry/oauth/kimi.ts +297 -0
  528. package/src/registry/oauth/minimax-code.ts +53 -0
  529. package/src/registry/oauth/oauth.html +317 -0
  530. package/src/registry/oauth/openai-codex.ts +384 -0
  531. package/src/registry/oauth/opencode.ts +50 -0
  532. package/src/registry/oauth/perplexity.ts +228 -0
  533. package/src/registry/oauth/pkce.ts +18 -0
  534. package/src/registry/oauth/types.ts +96 -0
  535. package/src/registry/oauth/wafer.ts +24 -0
  536. package/src/registry/oauth/xai-oauth.ts +559 -0
  537. package/src/registry/oauth/xiaomi.ts +211 -0
  538. package/src/registry/oauth/zai.ts +285 -0
  539. package/src/registry/ollama-cloud.ts +36 -0
  540. package/src/registry/ollama.ts +43 -0
  541. package/src/registry/openai-codex-device.ts +18 -0
  542. package/src/registry/openai-codex.ts +19 -0
  543. package/src/registry/openai.ts +6 -0
  544. package/src/registry/opencode-go.ts +12 -0
  545. package/src/registry/opencode-zen.ts +12 -0
  546. package/src/registry/openrouter.ts +28 -0
  547. package/src/registry/parallel.ts +45 -0
  548. package/src/registry/perplexity.ts +13 -0
  549. package/src/registry/qianfan.ts +27 -0
  550. package/src/registry/qwen-portal.ts +50 -0
  551. package/src/registry/registry.ts +182 -0
  552. package/src/registry/sakana.ts +22 -0
  553. package/src/registry/siliconflow-cn.ts +22 -0
  554. package/src/registry/siliconflow.ts +22 -0
  555. package/src/registry/synthetic.ts +21 -0
  556. package/src/registry/tavily.ts +45 -0
  557. package/src/registry/together.ts +22 -0
  558. package/src/registry/types.ts +86 -0
  559. package/src/registry/umans.ts +23 -0
  560. package/src/registry/venice.ts +33 -0
  561. package/src/registry/vercel-ai-gateway.ts +38 -0
  562. package/src/registry/vllm.ts +34 -0
  563. package/src/registry/wafer-serverless.ts +12 -0
  564. package/src/registry/xai-oauth.ts +17 -0
  565. package/src/registry/xai.ts +22 -0
  566. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  567. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  568. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  569. package/src/registry/xiaomi.ts +12 -0
  570. package/src/registry/zai.ts +41 -0
  571. package/src/registry/zenmux.ts +22 -0
  572. package/src/registry/zhipu-coding-plan.ts +27 -0
  573. package/src/stream.ts +1944 -0
  574. package/src/types.ts +1243 -0
  575. package/src/usage/alibaba-token-plan.ts +230 -0
  576. package/src/usage/claude.ts +830 -0
  577. package/src/usage/cursor.ts +335 -0
  578. package/src/usage/gemini.ts +258 -0
  579. package/src/usage/github-copilot.ts +424 -0
  580. package/src/usage/google-antigravity.ts +497 -0
  581. package/src/usage/kimi.ts +277 -0
  582. package/src/usage/minimax-code.ts +291 -0
  583. package/src/usage/ollama.ts +41 -0
  584. package/src/usage/openai-codex-base-url.ts +35 -0
  585. package/src/usage/openai-codex-reset.ts +205 -0
  586. package/src/usage/openai-codex.ts +627 -0
  587. package/src/usage/opencode-go.ts +89 -0
  588. package/src/usage/shared.ts +10 -0
  589. package/src/usage/synthetic.ts +180 -0
  590. package/src/usage/umans.ts +192 -0
  591. package/src/usage/xai-oauth.ts +414 -0
  592. package/src/usage/zai.ts +370 -0
  593. package/src/usage.ts +411 -0
  594. package/src/utils/abort.ts +67 -0
  595. package/src/utils/anthropic-auth.ts +93 -0
  596. package/src/utils/aws-profile.ts +88 -0
  597. package/src/utils/block-symbols.ts +78 -0
  598. package/src/utils/deterministic-id.ts +20 -0
  599. package/src/utils/empty-completion-retry.ts +161 -0
  600. package/src/utils/event-stream.ts +202 -0
  601. package/src/utils/foundry.ts +8 -0
  602. package/src/utils/google-validation.ts +25 -0
  603. package/src/utils/harmony-leak.ts +500 -0
  604. package/src/utils/http-inspector.ts +196 -0
  605. package/src/utils/idle-iterator.ts +531 -0
  606. package/src/utils/leaked-thinking-stream.ts +483 -0
  607. package/src/utils/openai-http.ts +119 -0
  608. package/src/utils/openrouter-headers.ts +12 -0
  609. package/src/utils/parse-bind.ts +56 -0
  610. package/src/utils/provider-response.ts +30 -0
  611. package/src/utils/proxy.ts +314 -0
  612. package/src/utils/request-debug.ts +351 -0
  613. package/src/utils/retry-after.ts +121 -0
  614. package/src/utils/retry.ts +77 -0
  615. package/src/utils/schema/CONSTRAINTS.md +168 -0
  616. package/src/utils/schema/adapt.ts +36 -0
  617. package/src/utils/schema/compatibility.ts +435 -0
  618. package/src/utils/schema/dereference.ts +98 -0
  619. package/src/utils/schema/draft.ts +341 -0
  620. package/src/utils/schema/equality.ts +97 -0
  621. package/src/utils/schema/fields.ts +210 -0
  622. package/src/utils/schema/index.ts +14 -0
  623. package/src/utils/schema/json-schema-validator.ts +595 -0
  624. package/src/utils/schema/meta-validator.ts +167 -0
  625. package/src/utils/schema/normalize.ts +2314 -0
  626. package/src/utils/schema/spill.ts +43 -0
  627. package/src/utils/schema/stamps.ts +109 -0
  628. package/src/utils/schema/strict-tool-validation.ts +117 -0
  629. package/src/utils/schema/types.ts +10 -0
  630. package/src/utils/schema/typescript.ts +212 -0
  631. package/src/utils/schema/wire.ts +662 -0
  632. package/src/utils/sdk-stream-timeout.ts +43 -0
  633. package/src/utils/sse-debug.ts +18 -0
  634. package/src/utils/stream-markup-healing.ts +247 -0
  635. package/src/utils/thinking-loop.ts +552 -0
  636. package/src/utils/tool-call-loop-guard.ts +107 -0
  637. package/src/utils/tool-choice.ts +101 -0
  638. package/src/utils/validation.ts +1932 -0
  639. package/src/utils.ts +492 -0
@@ -0,0 +1,4716 @@
1
+ import * as os from "node:os";
2
+ import { scheduler } from "node:timers/promises";
3
+ import { calculateCost } from "@linxiraos/pi-catalog/models";
4
+ import {
5
+ CODEX_BASE_URL,
6
+ CODEX_CLIENT_VERSION,
7
+ getCodexAccountId,
8
+ OPENAI_HEADER_VALUES,
9
+ OPENAI_HEADERS,
10
+ } from "@linxiraos/pi-catalog/wire/codex";
11
+ import { type } from "@linxiraos/pi-omptype";
12
+ import {
13
+ $env,
14
+ $flag,
15
+ asRecord,
16
+ fetchWithRetry,
17
+ getInstallId,
18
+ logger,
19
+ parseStreamingJson,
20
+ readSseJson,
21
+ structuredCloneJSON,
22
+ } from "@linxiraos/pi-utils";
23
+ import packageJson from "../../package.json" with { type: "json" };
24
+ import * as AIError from "../error";
25
+ import { getEnvApiKey, isOfficialCodexApiUrl } from "../stream";
26
+ import type {
27
+ Api,
28
+ AssistantMessage,
29
+ CodexCompactionContext,
30
+ CodexCompactionRequestContext,
31
+ Context,
32
+ FetchImpl,
33
+ Model,
34
+ ProviderSessionState,
35
+ RawSseEvent,
36
+ ServiceTier,
37
+ StreamFunction,
38
+ StreamOptions,
39
+ TextContent,
40
+ ThinkingContent,
41
+ Tool,
42
+ ToolCall,
43
+ ToolChoice,
44
+ ToolResultMessage,
45
+ Usage,
46
+ } from "../types";
47
+ import {
48
+ createOpenAIResponsesHistoryPayload,
49
+ getOpenAIResponsesHistoryItems,
50
+ getOpenAIResponsesHistoryPayload,
51
+ normalizeSystemPrompts,
52
+ sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
53
+ sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
54
+ stripOpenAIResponsesComputerLinkedReasoningIdsForReplay,
55
+ } from "../utils";
56
+ import { clearStreamingPartialJson, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols";
57
+ import { AssistantMessageEventStream } from "../utils/event-stream";
58
+ import { escapeHarmonyControlTokens, isHarmonyDialectModel } from "../utils/harmony-leak";
59
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
60
+ import {
61
+ armPreResponseTimeout,
62
+ getOpenAIStreamFirstEventTimeoutMs,
63
+ getOpenAIStreamIdleTimeoutMs,
64
+ iterateWithIdleTimeout,
65
+ } from "../utils/idle-iterator";
66
+ import { getProxyForUrl } from "../utils/proxy";
67
+ import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
68
+ import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
69
+ import { notifyRawSseEvent } from "../utils/sse-debug";
70
+ import { compactGrammarDefinition } from "./grammar";
71
+ import {
72
+ type CodexLiteShapedBody,
73
+ type CodexReasoningContext,
74
+ type CodexRequestOptions,
75
+ type InputItem,
76
+ type RequestBody,
77
+ resolveCodexResponsesLite,
78
+ transformRequestBody,
79
+ } from "./openai-codex/request-transformer";
80
+ import { CodexApiError } from "./openai-codex/response-handler";
81
+ import type {
82
+ ResponseComputerToolCall,
83
+ ResponseCustomToolCall,
84
+ ResponseFunctionToolCall,
85
+ ResponseInput,
86
+ ResponseInputContent,
87
+ ResponseOutputItem,
88
+ ResponseOutputMessage,
89
+ ResponseReasoningItem,
90
+ ResponseStatus,
91
+ } from "./openai-responses-wire";
92
+ import {
93
+ accumulateCustomToolCallInputDelta,
94
+ accumulateToolCallArgumentsDelta,
95
+ appendMessageContentPart,
96
+ appendMessageTextDelta,
97
+ appendReasoningSummaryPart,
98
+ appendReasoningSummaryPartDone,
99
+ appendReasoningSummaryTextDelta,
100
+ appendResponsesImageResult,
101
+ appendResponsesToolResultMessages,
102
+ applyOpenAIServiceTier,
103
+ applyReasoningSummaryDone,
104
+ buildResponsesDeltaInput,
105
+ computerCallMetadata,
106
+ convertResponsesAssistantMessage,
107
+ convertResponsesInputContent,
108
+ createSequentialCutoffSummaryState,
109
+ encodeResponsesToolCallId,
110
+ encodeTextSignatureV1,
111
+ escapeReplayedControlTokens,
112
+ finalizeCustomToolCallInputDone,
113
+ finalizeMessageText,
114
+ finalizePendingResponsesToolCalls,
115
+ finalizeReasoningThinking,
116
+ finalizeToolCallArgumentsDone,
117
+ getOpenAIPromptCacheKey,
118
+ hasExecutableIncompleteResponsesToolCalls,
119
+ isOpenAIResponsesProgressEvent,
120
+ mapOpenAIResponsesStopReason,
121
+ normalizeOpenAIPromptCacheKey,
122
+ populateResponsesUsageFromResponse,
123
+ promoteResponsesToolUseStopReason,
124
+ type SequentialCutoffSummaryState,
125
+ } from "./openai-shared";
126
+ import { redactSensitiveInObject, transformMessages } from "./transform-messages";
127
+
128
+ export interface OpenAICodexResponsesOptions extends StreamOptions {
129
+ reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
130
+ reasoningSummary?: "auto" | "concise" | "detailed" | null;
131
+ /** Explicit `reasoning.context` replay scope. Omitted by default so Codex applies its native request policy. */
132
+ reasoningContext?: CodexReasoningContext;
133
+ textVerbosity?: "low" | "medium" | "high";
134
+ codexMode?: boolean;
135
+ toolChoice?: ToolChoice;
136
+ preferWebsockets?: boolean;
137
+ serviceTier?: ServiceTier;
138
+ /**
139
+ * Responses Lite transport override; defaults to the model's catalog
140
+ * `useResponsesLite` flag (codex-rs `use_responses_lite`). Sends
141
+ * `x-openai-internal-codex-responses-lite: true` on HTTP requests and on the
142
+ * WebSocket upgrade (the marker is connection-scoped there, so lite and
143
+ * non-lite turns never share a pooled socket), moves instructions/tools
144
+ * into input items, strips image detail, and disables parallel tool
145
+ * calling — mirroring codex-rs.
146
+ */
147
+ responsesLite?: boolean;
148
+ /**
149
+ * Additional fields embedded in the canonical
150
+ * `client_metadata["x-codex-turn-metadata"]` JSON blob. Reserved identity
151
+ * keys are ignored; extras are never emitted as top-level metadata fields.
152
+ */
153
+ clientMetadata?: Record<string, string>;
154
+ /**
155
+ * Turn id of the initiating (parent) Codex turn for nested requests such as
156
+ * subagent spawns (codex-rs `parent_turn_id`, #35835). Emitted both as the
157
+ * flat `client_metadata.parent_turn_id` key and inside the
158
+ * `x-codex-turn-metadata` JSON blob; blank values are ignored. The key is
159
+ * reserved: `clientMetadata` extras cannot supply it.
160
+ */
161
+ parentTurnId?: string;
162
+ /**
163
+ * Invoked when the server streams a `response.metadata` event carrying
164
+ * ChatGPT moderation metadata (`metadata.openai_chatgpt_moderation_metadata`)
165
+ * for first-party presentation parity. Diagnostic observer: failures are
166
+ * swallowed and must not alter the stream.
167
+ */
168
+ onModerationMetadata?: (metadata: unknown) => void;
169
+ }
170
+
171
+ /** Raw V2 compaction body accepted by the Codex transport selector. */
172
+ export interface OpenAICodexCompactionBody extends CodexLiteShapedBody {
173
+ model: string;
174
+ [key: string]: unknown;
175
+ }
176
+
177
+ /** Transport controls for a provider-native Codex V2 compaction stream. */
178
+ export interface OpenAICodexCompactionStreamOptions extends OpenAICodexResponsesOptions {
179
+ apiKey: string;
180
+ }
181
+
182
+ /** Inputs for synthesizing Codex request identity outside the normal stream path. */
183
+ export interface OpenAICodexCompatibilityMetadataOptions {
184
+ sessionId?: string;
185
+ providerSessionState?: Map<string, ProviderSessionState>;
186
+ requestKind: OpenAICodexRequestKind;
187
+ compaction?: CodexCompactionRequestContext;
188
+ startNewTurn?: boolean;
189
+ turnStartedAtUnixMs?: number;
190
+ clientMetadata?: Readonly<Record<string, string>>;
191
+ /** Parent Codex turn id for nested requests; see {@link OpenAICodexResponsesOptions.parentTurnId}. */
192
+ parentTurnId?: string;
193
+ /** Add the direct installation header required by `/responses/compact`. */
194
+ includeInstallationHeader?: boolean;
195
+ }
196
+
197
+ /** Canonical Codex body metadata and compatibility headers for one request. */
198
+ export interface OpenAICodexCompatibilityMetadata {
199
+ clientMetadata: Record<string, string>;
200
+ headers: Record<string, string>;
201
+ }
202
+
203
+ /** Live Codex session state to preserve after a successful history rewrite. */
204
+ export interface OpenAICodexCompactionResetOptions {
205
+ providerSessionState?: Map<string, ProviderSessionState>;
206
+ sessionId?: string;
207
+ compaction: CodexCompactionContext;
208
+ }
209
+
210
+ /** Add the selected wire implementation to one logical compaction context. */
211
+ export function createOpenAICodexCompactionRequestContext(options: {
212
+ context: CodexCompactionContext | undefined;
213
+ implementation: "responses" | "responses_compaction_v2" | "responses_compact";
214
+ }): CodexCompactionRequestContext | undefined {
215
+ const context = options.context;
216
+ if (!context) return undefined;
217
+ return {
218
+ operationId: context.operationId,
219
+ trigger: context.trigger,
220
+ reason: context.reason,
221
+ implementation: options.implementation,
222
+ phase: context.phase,
223
+ strategy: context.strategy,
224
+ };
225
+ }
226
+
227
+ const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
228
+ const CODEX_MAX_RETRIES = 5;
229
+ const CODEX_RETRY_DELAY_MS = 500;
230
+
231
+ function resolveCodexSseMaxAttempts(value: number | undefined): number {
232
+ if (value === undefined) return CODEX_MAX_RETRIES + 1;
233
+ if (!Number.isFinite(value)) return 1;
234
+ return Math.max(1, Math.trunc(value));
235
+ }
236
+ const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
237
+ const CODEX_WEBSOCKET_PING_INTERVAL_MS = Number($env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS || 10_000);
238
+ const CODEX_WEBSOCKET_PONG_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS || 60_000);
239
+ const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = Number($env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY || 4096);
240
+ /**
241
+ * Maximum quiet period (no inbound frames AND no observed pong) we'll trust a
242
+ * reused WebSocket for before forcing a fresh handshake. Codex backends and
243
+ * intermediaries occasionally evict idle sockets server-side without sending a
244
+ * FIN, leaving the local `readyState` as OPEN while the next `send()` becomes a
245
+ * write into a half-open buffer. Reusing such a socket parks the next request
246
+ * at `#nextMessage` until the first-event/idle timeout fires (issue #1450). The
247
+ * heartbeat below also catches dead sockets, but only after `pongTimeoutMs`
248
+ * (default 60s) and only while a request is active — this gate closes the door
249
+ * earlier and even when the gap between requests is purely client-side (tool
250
+ * execution, user typing, etc.). Set `PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS=0`
251
+ * to disable.
252
+ */
253
+ const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = Number($env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS || 30_000);
254
+ /**
255
+ * Steady-state liveness ceiling for the Codex WebSocket transport. Distinct from
256
+ * the OMP-wide stream watchdog removed in #1392: a WebSocket can stay TCP-open
257
+ * indefinitely without exchanging frames (server crash after upgrade, half-open
258
+ * network path), so we still need a transport-internal cap to detect those
259
+ * states and trigger the WS→SSE fallback. Only applies AFTER the first event
260
+ * has arrived — slow first-token paths wait as long as the caller permits.
261
+ */
262
+ const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS || 300_000);
263
+ /**
264
+ * Maximum wait for the first WebSocket event before falling back to SSE.
265
+ * Unlike a stream watchdog, this triggers a transport switch (not a request
266
+ * failure) — the outer retry loop catches the timeout error and re-runs on
267
+ * SSE. Generous default so legitimately slow first-token providers still get
268
+ * a chance on the WS transport before falling through.
269
+ */
270
+ const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS || 300_000);
271
+ const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
272
+ const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
273
+ const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
274
+ const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
275
+ const CODEX_RETRYABLE_EVENT_MESSAGE =
276
+ /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
277
+ const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
278
+
279
+ /**
280
+ * Host integration boundary for just-in-time `x-oai-attestation` header
281
+ * values (codex-rs `AttestationProvider`). Resolves to the full header value
282
+ * — an `{"v":1,"s":0,"t":"v1.…"}` envelope — or `undefined` when no
283
+ * attestation should be sent.
284
+ */
285
+ export type CodexAttestationProvider = () => Promise<string | undefined>;
286
+
287
+ let codexAttestationProvider: CodexAttestationProvider | undefined;
288
+
289
+ /**
290
+ * Install the process-wide attestation hook consulted for upstream Codex
291
+ * requests (codex-rs stores its provider on `ModelClient` construction). The
292
+ * hook is only consulted for ChatGPT-OAuth credentials and runs just-in-time
293
+ * per request; WebSocket handshakes resolve once per connection because the
294
+ * header is connection-scoped there.
295
+ */
296
+ export function setCodexAttestationProvider(provider: CodexAttestationProvider | undefined): void {
297
+ codexAttestationProvider = provider;
298
+ }
299
+
300
+ /**
301
+ * Resolve the `x-oai-attestation` header value for one upstream request.
302
+ * Gated on ChatGPT-OAuth credentials (a Codex JWT carries `chatgpt_account_id`;
303
+ * codex-rs gates on `auth.is_chatgpt_auth()`). A throwing hook degrades to no
304
+ * header rather than failing the request.
305
+ */
306
+ export async function getCodexAttestationHeader(accountId: string | undefined): Promise<string | undefined> {
307
+ if (!accountId || !codexAttestationProvider) return undefined;
308
+ try {
309
+ return await codexAttestationProvider();
310
+ } catch {
311
+ return undefined;
312
+ }
313
+ }
314
+ const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
315
+ const X_MODELS_ETAG_HEADER = "x-models-etag";
316
+ /** WebSocket frames cannot carry per-request HTTP headers; codex-rs mirrors the lite marker into `client_metadata` under this key. */
317
+ const CODEX_WS_RESPONSES_LITE_CLIENT_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
318
+ /** `response.metadata` payload key carrying ChatGPT moderation metadata. */
319
+ const CODEX_MODERATION_METADATA_KEY = "openai_chatgpt_moderation_metadata";
320
+ /** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
321
+ const CODEX_WEBSOCKET_FATAL_PATTERNS = ["websocket error:", "websocket closed before open", "connection timeout"];
322
+ /** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
323
+ const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
324
+ const CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES = new Set(["response.done", "response.incomplete"]);
325
+ // Provider/model failure mode: Codex can keep a response alive by streaming
326
+ // whitespace-only function-call argument deltas forever. Those frames count as
327
+ // transport activity, so idle timers never fire; cap the run before raw debug
328
+ // buffers and partial JSON grow without semantic progress.
329
+ const CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT = 256;
330
+ const CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT = 16 * 1024;
331
+ const CODEX_WHITESPACE_LOOP_RETRY_LIMIT = 2;
332
+ const CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS = 250;
333
+
334
+ function isCodexStreamProgressEvent(event: unknown): boolean {
335
+ if (isOpenAIResponsesProgressEvent(event)) return true;
336
+ if (!event || typeof event !== "object") return false;
337
+ const type = (event as { type?: unknown }).type;
338
+ return typeof type === "string" && CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES.has(type);
339
+ }
340
+
341
+ function extractCodexFrameResponseId(frame: Record<string, unknown>): string | undefined {
342
+ const response = (frame as { response?: { id?: unknown } }).response;
343
+ const id = response?.id;
344
+ return typeof id === "string" && id.length > 0 ? id : undefined;
345
+ }
346
+
347
+ function extractCodexFrameSequenceNumber(frame: Record<string, unknown>): number | undefined {
348
+ const raw = (frame as { sequence_number?: unknown }).sequence_number;
349
+ return typeof raw === "number" && Number.isFinite(raw) ? Math.trunc(raw) : undefined;
350
+ }
351
+
352
+ type CodexWebSocketTimeoutDetails = {
353
+ lastEventAt: number;
354
+ lastEventType?: string;
355
+ lastProgressAt: number;
356
+ lastProgressEventType?: string;
357
+ };
358
+
359
+ function createCodexWebSocketTimeoutMessage(reason: string, details: CodexWebSocketTimeoutDetails): string {
360
+ const now = Date.now();
361
+ const lastEvent = details.lastEventType
362
+ ? `${details.lastEventType} ${Math.max(0, now - details.lastEventAt)}ms ago`
363
+ : "none";
364
+ const lastProgress = details.lastProgressEventType
365
+ ? `${details.lastProgressEventType} ${Math.max(0, now - details.lastProgressAt)}ms ago`
366
+ : "none";
367
+ return `${reason} (last event: ${lastEvent}; last progress: ${lastProgress})`;
368
+ }
369
+
370
+ type CodexTransport = "sse" | "websocket";
371
+ type CodexEventItem =
372
+ | ResponseReasoningItem
373
+ | ResponseOutputMessage
374
+ | ResponseFunctionToolCall
375
+ | ResponseCustomToolCall
376
+ | ResponseComputerToolCall
377
+ | ResponseOutputItem.ImageGenerationCall;
378
+ type CodexOutputBlock =
379
+ | ThinkingContent
380
+ | TextContent
381
+ | (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number });
382
+
383
+ interface CodexResponseUsage {
384
+ input_tokens?: number;
385
+ output_tokens?: number;
386
+ total_tokens?: number;
387
+ prompt_cache_hit_tokens?: number;
388
+ input_tokens_details?: {
389
+ cached_tokens?: number;
390
+ cache_write_tokens?: number;
391
+ orchestration_input_tokens?: number;
392
+ orchestration_input_cached_tokens?: number;
393
+ };
394
+ output_tokens_details?: {
395
+ reasoning_tokens?: number;
396
+ orchestration_output_tokens?: number;
397
+ };
398
+ }
399
+
400
+ /** Shape of the Codex request sent on the latest provider turn. */
401
+ export interface OpenAICodexTurnRequestDiagnostics {
402
+ transport: "sse" | "websocket";
403
+ previousResponseIdPresent: boolean;
404
+ inputItemCount: number;
405
+ inputItemTypes: string[];
406
+ firstInputItemType?: string;
407
+ inputJsonBytes: number;
408
+ promptCacheKey?: string;
409
+ toolsHash?: string;
410
+ optionsHash: string;
411
+ canAppendBeforeRequest: boolean;
412
+ }
413
+
414
+ /** Raw provider usage plus the normalized buckets OMP displays for the latest Codex turn. */
415
+ export interface OpenAICodexTurnUsageDiagnostics {
416
+ rawInputTokens: number;
417
+ rawCachedTokens: number;
418
+ rawUncachedTokens: number;
419
+ rawOutputTokens: number;
420
+ rawTotalTokens?: number;
421
+ rawOrchestrationInputTokens?: number;
422
+ rawOrchestrationCachedTokens?: number;
423
+ rawOrchestrationOutputTokens?: number;
424
+ displayedInputTokens: number;
425
+ displayedOutputTokens: number;
426
+ displayedCacheReadTokens: number;
427
+ displayedCacheWriteTokens: number;
428
+ displayedTotalTokens: number;
429
+ displayedOrchestrationInputTokens: number;
430
+ displayedOrchestrationCacheReadTokens: number;
431
+ displayedOrchestrationOutputTokens: number;
432
+ }
433
+
434
+ /** Latest Codex turn request/usage diagnostics exposed to debug UIs and tests. */
435
+ export interface OpenAICodexTurnDiagnostics {
436
+ request: OpenAICodexTurnRequestDiagnostics;
437
+ usage?: OpenAICodexTurnUsageDiagnostics;
438
+ }
439
+
440
+ /**
441
+ * Per-session request-shape counters and latest turn diagnostics. Despite the
442
+ * name, these cover both transports.
443
+ */
444
+ export interface OpenAICodexWebSocketDebugStats {
445
+ fullContextRequests: number;
446
+ deltaRequests: number;
447
+ lastInputItems: number;
448
+ lastDeltaInputItems?: number;
449
+ lastPreviousResponseId?: string;
450
+ lastTurn?: OpenAICodexTurnDiagnostics;
451
+ }
452
+
453
+ /**
454
+ * Per-session transport state shared by BOTH transports: websocket turn
455
+ * chaining (`previous_response_id` baseline), turn-state/models-etag headers,
456
+ * websocket connection pooling, and debug stats. The name is historical — SSE-only
457
+ * sessions use it too.
458
+ */
459
+ type CodexWebSocketSessionState = {
460
+ disableWebsocket: boolean;
461
+ lastRequest?: RequestBody;
462
+ lastResponseId?: string;
463
+ lastResponseItems?: InputItem[];
464
+ canAppend: boolean;
465
+ turnState?: string;
466
+ modelsEtag?: string;
467
+ connection?: CodexWebSocketConnection;
468
+ lastTransport?: CodexTransport;
469
+ fallbackCount: number;
470
+ lastFallbackAt?: number;
471
+ prewarmed: boolean;
472
+ stats: OpenAICodexWebSocketDebugStats;
473
+ };
474
+
475
+ interface CodexProviderSessionState extends ProviderSessionState {
476
+ webSocketSessions: Map<string, CodexWebSocketSessionState>;
477
+ webSocketPublicToPrivate: Map<string, string>;
478
+ metadataSessions: Map<string, CodexMetadataSessionState>;
479
+ }
480
+
481
+ /** Request classification encoded in Codex turn metadata. */
482
+ export type OpenAICodexRequestKind = "turn" | "prewarm" | "compaction";
483
+
484
+ interface CodexMetadataSessionState {
485
+ sessionId: string;
486
+ threadId: string;
487
+ windowId: string;
488
+ turnId?: string;
489
+ turnStartedAtUnixMs?: number;
490
+ compactionOperationId?: string;
491
+ reuseTurnForNextRequest?: boolean;
492
+ }
493
+
494
+ interface CodexCompatibilityIdentity {
495
+ installationId: string;
496
+ sessionId: string;
497
+ threadId: string;
498
+ windowId: string;
499
+ turnMetadataJson?: string;
500
+ }
501
+
502
+ interface CodexRequestMetadata extends CodexCompatibilityIdentity {
503
+ turnId: string;
504
+ turnMetadataJson: string;
505
+ clientMetadata: Record<string, string>;
506
+ }
507
+
508
+ const CODEX_RESERVED_METADATA_KEYS: Record<string, true> = {
509
+ installation_id: true,
510
+ [OPENAI_HEADERS.INSTALLATION_ID]: true,
511
+ session_id: true,
512
+ thread_id: true,
513
+ turn_id: true,
514
+ window_id: true,
515
+ [OPENAI_HEADERS.WINDOW_ID]: true,
516
+ [OPENAI_HEADERS.TURN_METADATA]: true,
517
+ [OPENAI_HEADERS.PARENT_THREAD_ID]: true,
518
+ [OPENAI_HEADERS.SUBAGENT]: true,
519
+ request_kind: true,
520
+ compaction: true,
521
+ // codex-rs reserves `code_mode_tool_names` for its Responses Lite Code Mode
522
+ // mapping (#35271); OMP never emits it, but callers must not smuggle it in
523
+ // as an extra either.
524
+ code_mode_tool_names: true,
525
+ turn_started_at_unix_ms: true,
526
+ forked_from_thread_id: true,
527
+ parent_thread_id: true,
528
+ parent_turn_id: true,
529
+ subagent_kind: true,
530
+ thread_source: true,
531
+ sandbox: true,
532
+ workspaces: true,
533
+ };
534
+
535
+ function createCodexMetadataSessionState(sessionId: string): CodexMetadataSessionState {
536
+ return {
537
+ sessionId,
538
+ threadId: crypto.randomUUID(),
539
+ windowId: crypto.randomUUID(),
540
+ };
541
+ }
542
+
543
+ function getOrCreateCodexMetadataSessionState(
544
+ sessionId: string,
545
+ providerState: CodexProviderSessionState | undefined,
546
+ ): CodexMetadataSessionState {
547
+ if (!providerState) return createCodexMetadataSessionState(sessionId);
548
+ const existing = providerState.metadataSessions.get(sessionId);
549
+ if (existing) return existing;
550
+ const created = createCodexMetadataSessionState(sessionId);
551
+ providerState.metadataSessions.set(sessionId, created);
552
+ return created;
553
+ }
554
+
555
+ function createCodexCompatibilityIdentity(session: CodexMetadataSessionState): CodexCompatibilityIdentity {
556
+ return {
557
+ installationId: getInstallId(),
558
+ sessionId: session.sessionId,
559
+ threadId: session.threadId,
560
+ windowId: session.windowId,
561
+ };
562
+ }
563
+
564
+ function resolveCodexStartNewTurn(
565
+ session: CodexMetadataSessionState,
566
+ requestKind: OpenAICodexRequestKind,
567
+ compaction: CodexCompactionRequestContext | undefined,
568
+ override: boolean | undefined,
569
+ ): boolean {
570
+ if (requestKind !== "compaction") {
571
+ if (requestKind === "turn") {
572
+ const reuseCompactionTurn = session.reuseTurnForNextRequest === true;
573
+ session.reuseTurnForNextRequest = false;
574
+ session.compactionOperationId = undefined;
575
+ if (reuseCompactionTurn) return false;
576
+ }
577
+ return override ?? requestKind === "turn";
578
+ }
579
+ if (!compaction) return override ?? false;
580
+ const startsNewOperation = session.compactionOperationId !== compaction.operationId;
581
+ if (startsNewOperation) session.reuseTurnForNextRequest = false;
582
+ session.compactionOperationId = compaction.operationId;
583
+ return override ?? (compaction.phase !== "mid_turn" && startsNewOperation);
584
+ }
585
+
586
+ function toAsciiJsonString(value: Record<string, unknown>): string {
587
+ return JSON.stringify(value).replace(
588
+ /[\x7f-\uffff]/g,
589
+ char => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
590
+ );
591
+ }
592
+
593
+ function createCodexRequestMetadata(
594
+ session: CodexMetadataSessionState,
595
+ requestKind: OpenAICodexRequestKind,
596
+ options: {
597
+ startNewTurn: boolean;
598
+ turnStartedAtUnixMs?: number;
599
+ clientMetadata?: Readonly<Record<string, string>>;
600
+ parentTurnId?: string;
601
+ compaction?: CodexCompactionRequestContext;
602
+ },
603
+ ): CodexRequestMetadata {
604
+ if (options.startNewTurn || !session.turnId) {
605
+ session.turnId = crypto.randomUUID();
606
+ session.turnStartedAtUnixMs = options.turnStartedAtUnixMs;
607
+ }
608
+ const identity = createCodexCompatibilityIdentity(session);
609
+ // codex-rs `set_parent_turn_id` ignores blank values; keep the original
610
+ // spelling when non-blank.
611
+ const parentTurnId = options.parentTurnId?.trim() ? options.parentTurnId : undefined;
612
+ const extra: Record<string, string> = {};
613
+ const callerMetadata = options.clientMetadata;
614
+ if (callerMetadata) {
615
+ for (const key in callerMetadata) {
616
+ if (!CODEX_RESERVED_METADATA_KEYS[key]) extra[key] = callerMetadata[key];
617
+ }
618
+ }
619
+ const turnMetadata: Record<string, unknown> = {
620
+ installation_id: identity.installationId,
621
+ session_id: identity.sessionId,
622
+ thread_id: identity.threadId,
623
+ turn_id: session.turnId,
624
+ window_id: identity.windowId,
625
+ request_kind: requestKind,
626
+ };
627
+ if (parentTurnId) turnMetadata.parent_turn_id = parentTurnId;
628
+ if (options.compaction) {
629
+ turnMetadata.compaction = {
630
+ trigger: options.compaction.trigger,
631
+ reason: options.compaction.reason,
632
+ implementation: options.compaction.implementation,
633
+ phase: options.compaction.phase,
634
+ strategy: options.compaction.strategy,
635
+ };
636
+ }
637
+ if (session.turnStartedAtUnixMs !== undefined) {
638
+ turnMetadata.turn_started_at_unix_ms = session.turnStartedAtUnixMs;
639
+ }
640
+ for (const key in extra) turnMetadata[key] = extra[key];
641
+ const turnMetadataJson = toAsciiJsonString(turnMetadata);
642
+ const clientMetadata: Record<string, string> = {
643
+ [OPENAI_HEADERS.INSTALLATION_ID]: identity.installationId,
644
+ session_id: identity.sessionId,
645
+ thread_id: identity.threadId,
646
+ [OPENAI_HEADERS.WINDOW_ID]: identity.windowId,
647
+ turn_id: session.turnId,
648
+ };
649
+ // Both projections, mirroring codex-rs `CodexResponsesMetadata::to_client_metadata`:
650
+ // the flat key above/below AND the field inside the turn-metadata JSON.
651
+ if (parentTurnId) clientMetadata.parent_turn_id = parentTurnId;
652
+ clientMetadata[OPENAI_HEADERS.TURN_METADATA] = turnMetadataJson;
653
+ return {
654
+ ...identity,
655
+ turnId: session.turnId,
656
+ turnMetadataJson,
657
+ clientMetadata,
658
+ };
659
+ }
660
+
661
+ function applyCodexCompatibilityHeaders(headers: Headers, metadata: CodexCompatibilityIdentity): void {
662
+ headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.sessionId);
663
+ headers.set(OPENAI_HEADERS.THREAD_ID, metadata.threadId);
664
+ headers.set(OPENAI_HEADERS.WINDOW_ID, metadata.windowId);
665
+ if (metadata.turnMetadataJson) {
666
+ headers.set(OPENAI_HEADERS.TURN_METADATA, metadata.turnMetadataJson);
667
+ } else {
668
+ headers.delete(OPENAI_HEADERS.TURN_METADATA);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Synthesize Codex request identity for raw provider routes such as remote
674
+ * compaction while reusing the live session's thread, window, and turn.
675
+ */
676
+ export function createOpenAICodexCompatibilityMetadata(
677
+ options: OpenAICodexCompatibilityMetadataOptions,
678
+ ): OpenAICodexCompatibilityMetadata {
679
+ const providerState = getCodexProviderSessionState(options.providerSessionState);
680
+ const sessionId = normalizeOpenAIPromptCacheKey(options.sessionId) ?? crypto.randomUUID();
681
+ const session = getOrCreateCodexMetadataSessionState(sessionId, providerState);
682
+ const startNewTurn = resolveCodexStartNewTurn(
683
+ session,
684
+ options.requestKind,
685
+ options.compaction,
686
+ options.startNewTurn,
687
+ );
688
+ const metadata = createCodexRequestMetadata(session, options.requestKind, {
689
+ startNewTurn,
690
+ turnStartedAtUnixMs: options.turnStartedAtUnixMs ?? (startNewTurn || !session.turnId ? Date.now() : undefined),
691
+ clientMetadata: options.clientMetadata,
692
+ parentTurnId: options.parentTurnId,
693
+ compaction: options.compaction,
694
+ });
695
+ const headers = new Headers();
696
+ applyCodexCompatibilityHeaders(headers, metadata);
697
+ if (options.includeInstallationHeader) {
698
+ headers.set(OPENAI_HEADERS.INSTALLATION_ID, metadata.installationId);
699
+ }
700
+ return {
701
+ clientMetadata: { ...metadata.clientMetadata },
702
+ headers: Object.fromEntries(headers.entries()),
703
+ };
704
+ }
705
+
706
+ /**
707
+ * Invalidate Codex history-dependent transport state after compaction while
708
+ * retaining the session identity and live connection.
709
+ */
710
+ export function resetOpenAICodexHistoryAfterCompaction(options: OpenAICodexCompactionResetOptions): void {
711
+ const providerState = options.providerSessionState?.get(CODEX_PROVIDER_SESSION_STATE_KEY);
712
+ if (!isCodexProviderSessionState(providerState)) return;
713
+ for (const websocketState of providerState.webSocketSessions.values()) {
714
+ resetCodexWebSocketAppendState(websocketState);
715
+ if (options.compaction.phase !== "mid_turn") websocketState.turnState = undefined;
716
+ }
717
+ const sessionId = normalizeOpenAIPromptCacheKey(options.sessionId);
718
+ if (!sessionId) return;
719
+ const metadataSession = providerState.metadataSessions.get(sessionId);
720
+ if (!metadataSession) return;
721
+ metadataSession.windowId = crypto.randomUUID();
722
+ metadataSession.compactionOperationId = undefined;
723
+ metadataSession.reuseTurnForNextRequest = options.compaction.phase !== "standalone_turn";
724
+ }
725
+
726
+ interface CodexRequestContext {
727
+ apiKey: string;
728
+ accountId?: string;
729
+ baseUrl: string;
730
+ url: string;
731
+ requestHeaders: Record<string, string>;
732
+ codexClientVersion: string;
733
+ transportSessionId?: string;
734
+ providerSessionState?: CodexProviderSessionState;
735
+ isolatedTransportState?: CodexProviderSessionState;
736
+ websocketState?: CodexWebSocketSessionState;
737
+ responsesLite: boolean;
738
+ requestMetadata?: CodexRequestMetadata;
739
+ transformedBody: RequestBody;
740
+ rawRequestDump: RawHttpRequestDump;
741
+ }
742
+
743
+ interface CodexRequestSetup {
744
+ requestSignal: AbortSignal;
745
+ wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
746
+ requestAbortController: AbortController;
747
+ firstEventTimeoutMs: number | undefined;
748
+ websocketIdleTimeoutMs: number | undefined;
749
+ websocketFirstEventTimeoutMs: number | undefined;
750
+ }
751
+
752
+ interface CodexOpenItem {
753
+ item: CodexEventItem;
754
+ block: CodexOutputBlock | null;
755
+ /** Index of {@link block} in `output.content`; `-1` when no block was created for this item. */
756
+ contentIndex: number;
757
+ itemId?: string;
758
+ outputIndex?: number;
759
+ }
760
+
761
+ class CodexStreamRuntime {
762
+ eventStream: AsyncGenerator<Record<string, unknown>>;
763
+ requestBodyForState: RequestBody;
764
+ transport: CodexTransport;
765
+ websocketState?: CodexWebSocketSessionState;
766
+ /**
767
+ * Items open on the wire keyed by `item.id`. `response.output_item.added`
768
+ * registers here; `output_item.done` removes. A keyed event whose `item_id`
769
+ * is not present is dropped rather than appended to a sibling.
770
+ */
771
+ openItems = new Map<string, CodexOpenItem>();
772
+ /**
773
+ * Items open on the wire keyed by `output_index` for streams whose function
774
+ * call items omit `id`; these still carry `output_index` on deltas/done.
775
+ */
776
+ openItemsByOutputIndex = new Map<number, CodexOpenItem>();
777
+ /**
778
+ * Most recently added open item for events that omit both `item_id` and
779
+ * `output_index`. Always tracks the latest `output_item.added`, including
780
+ * fully keyless items that never make it into the keyed maps; cleared when
781
+ * its item closes.
782
+ */
783
+ currentEntry: CodexOpenItem | null = null;
784
+ /** Convenience mirrors of {@link currentEntry} for legacy singleton handlers. */
785
+ currentItem: CodexEventItem | null = null;
786
+ currentBlock: CodexOutputBlock | null = null;
787
+ nativeOutputItems: Array<Record<string, unknown>> = [];
788
+ /** Sequential-cutoff summary sections/emitted text, global to the response (indices span reasoning items). */
789
+ cutoffSummaries: SequentialCutoffSummaryState = createSequentialCutoffSummaryState();
790
+ /** Summary deltas buffered while waiting to see whether atomic `.done` events arrive. */
791
+ pendingSummaryDeltas = new Map<CodexOpenItem, string[]>();
792
+ websocketStreamRetries = 0;
793
+ providerRetryAttempt = 0;
794
+ sawTerminalEvent = false;
795
+ canSafelyReplayWebsocketOverSse = true;
796
+ whitespaceToolCallArgumentsDelta?: CodexWhitespaceToolCallArgumentsDeltaState;
797
+ whitespaceLoopRetries = 0;
798
+
799
+ constructor(initial: {
800
+ eventStream: AsyncGenerator<Record<string, unknown>>;
801
+ requestBodyForState: RequestBody;
802
+ transport: CodexTransport;
803
+ websocketState?: CodexWebSocketSessionState;
804
+ }) {
805
+ this.eventStream = initial.eventStream;
806
+ this.requestBodyForState = initial.requestBodyForState;
807
+ this.transport = initial.transport;
808
+ this.websocketState = initial.websocketState;
809
+ }
810
+
811
+ /**
812
+ * Wipe per-attempt accumulator state before a recovery path replays the turn.
813
+ * Keeps {@link openItems} and the legacy singleton-current pointers in lockstep
814
+ * with {@link nativeOutputItems} so a stale delta from the failed attempt can't
815
+ * bind to a sibling on the retry.
816
+ */
817
+ resetAccumulators(): void {
818
+ this.openItems.clear();
819
+ this.openItemsByOutputIndex.clear();
820
+ this.currentEntry = null;
821
+ this.currentItem = null;
822
+ this.currentBlock = null;
823
+ this.nativeOutputItems.length = 0;
824
+ this.pendingSummaryDeltas.clear();
825
+ this.cutoffSummaries = createSequentialCutoffSummaryState();
826
+ }
827
+
828
+ /**
829
+ * Look up the open item a Codex stream event targets. `item_id` wins because it
830
+ * uniquely identifies a response item; `output_index` covers idless function
831
+ * call items. A keyed event whose target is already closed is dropped instead
832
+ * of being routed to a sibling. Only streams that omit both keys fall back to
833
+ * {@link currentEntry} — the most recently added item, including fully keyless
834
+ * ones that never reached the keyed maps.
835
+ */
836
+ openItemForEvent(rawEvent: Record<string, unknown>): CodexOpenItem | null {
837
+ const itemId = typeof rawEvent.item_id === "string" ? rawEvent.item_id : "";
838
+ if (itemId) return this.openItems.get(itemId) ?? null;
839
+ const outputIndex =
840
+ typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
841
+ ? Math.trunc(rawEvent.output_index)
842
+ : undefined;
843
+ if (outputIndex !== undefined) return this.openItemsByOutputIndex.get(outputIndex) ?? null;
844
+ return this.currentEntry;
845
+ }
846
+ queueSummaryDelta(entry: CodexOpenItem | null | undefined, delta: string): void {
847
+ if (entry?.block?.type !== "thinking" || delta.length === 0) return;
848
+ const pending = this.pendingSummaryDeltas.get(entry) ?? [];
849
+ pending.push(delta);
850
+ this.pendingSummaryDeltas.set(entry, pending);
851
+ }
852
+
853
+ takeSummaryDeltas(entry: CodexOpenItem | null | undefined): string[] {
854
+ if (!entry) return [];
855
+ const pending = this.pendingSummaryDeltas.get(entry) ?? [];
856
+ this.pendingSummaryDeltas.delete(entry);
857
+ return pending;
858
+ }
859
+
860
+ closeOpenItem(entry: CodexOpenItem | null | undefined): void {
861
+ if (!entry) return;
862
+ if (entry.itemId) this.openItems.delete(entry.itemId);
863
+ if (entry.outputIndex !== undefined) this.openItemsByOutputIndex.delete(entry.outputIndex);
864
+ if (this.currentEntry === entry) {
865
+ this.currentEntry = null;
866
+ this.currentItem = null;
867
+ this.currentBlock = null;
868
+ }
869
+ }
870
+
871
+ observeWhitespaceToolCallArgumentsDelta(
872
+ rawEvent: Record<string, unknown>,
873
+ delta: string,
874
+ ): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
875
+ if (!isJsonWhitespaceOnly(delta)) {
876
+ this.whitespaceToolCallArgumentsDelta = undefined;
877
+ return undefined;
878
+ }
879
+
880
+ const itemId =
881
+ typeof rawEvent.item_id === "string" && rawEvent.item_id.length > 0
882
+ ? rawEvent.item_id
883
+ : (this.currentItem?.id ?? "");
884
+ const outputIndex =
885
+ typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
886
+ ? Math.trunc(rawEvent.output_index)
887
+ : undefined;
888
+ const sequenceNumber =
889
+ typeof rawEvent.sequence_number === "number" && Number.isFinite(rawEvent.sequence_number)
890
+ ? Math.trunc(rawEvent.sequence_number)
891
+ : undefined;
892
+ let state = this.whitespaceToolCallArgumentsDelta;
893
+ if (!state || state.itemId !== itemId || state.outputIndex !== outputIndex) {
894
+ state = {
895
+ itemId,
896
+ outputIndex,
897
+ consecutiveEvents: 0,
898
+ consecutiveChars: 0,
899
+ firstSequenceNumber: sequenceNumber,
900
+ };
901
+ this.whitespaceToolCallArgumentsDelta = state;
902
+ }
903
+
904
+ state.consecutiveEvents += 1;
905
+ state.consecutiveChars += delta.length;
906
+ state.lastSequenceNumber = sequenceNumber;
907
+ if (
908
+ state.consecutiveEvents < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT &&
909
+ state.consecutiveChars < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT
910
+ ) {
911
+ return undefined;
912
+ }
913
+
914
+ const itemLabel = itemId ? ` for item ${itemId}` : "";
915
+ const sequenceLabel =
916
+ state.firstSequenceNumber === undefined || state.lastSequenceNumber === undefined
917
+ ? ""
918
+ : `, sequence ${state.firstSequenceNumber}..${state.lastSequenceNumber}`;
919
+ return {
920
+ message: `Interrupted OpenAI Codex response after ${state.consecutiveEvents} consecutive whitespace-only tool-call argument delta events (${state.consecutiveChars} chars${sequenceLabel})${itemLabel}.`,
921
+ };
922
+ }
923
+
924
+ handleToolCallArgumentsDelta(
925
+ rawEvent: Record<string, unknown>,
926
+ stream: AssistantMessageEventStream,
927
+ output: AssistantMessage,
928
+ ): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
929
+ const delta = (rawEvent as { delta?: string }).delta || "";
930
+ // Observe BEFORE the item/block guard: degenerate whitespace frames can keep
931
+ // arriving after the item closed (entry detached) and still count as
932
+ // progress for the idle watchdogs — dropping them unobserved would reopen
933
+ // the infinite-loop hole the breaker exists for.
934
+ const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
935
+ if (interruption) return interruption;
936
+ // Route to the entry the event keys to; a delta whose item already closed
937
+ // is dropped instead of leaking into a sibling tool call (#2619).
938
+ const entry = this.openItemForEvent(rawEvent);
939
+ if (!entry) return undefined;
940
+ if (entry.item.type !== "function_call" || entry.block?.type !== "toolCall") return undefined;
941
+ accumulateToolCallArgumentsDelta(entry.block, delta, stream, output, entry.contentIndex);
942
+ return undefined;
943
+ }
944
+
945
+ handleToolCallArgumentsDone(rawEvent: Record<string, unknown>): void {
946
+ const entry = this.openItemForEvent(rawEvent);
947
+ if (entry?.item.type !== "function_call" || entry.block?.type !== "toolCall") return;
948
+ const args = (rawEvent as { arguments?: string }).arguments;
949
+ if (typeof args === "string") finalizeToolCallArgumentsDone(entry.block, args);
950
+ }
951
+
952
+ handleCustomToolCallInputDelta(
953
+ rawEvent: Record<string, unknown>,
954
+ stream: AssistantMessageEventStream,
955
+ output: AssistantMessage,
956
+ ): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
957
+ const delta = (rawEvent as { delta?: string }).delta || "";
958
+ // Observe BEFORE the item/block guard — see handleToolCallArgumentsDelta.
959
+ const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
960
+ if (interruption) return interruption;
961
+ const entry = this.openItemForEvent(rawEvent);
962
+ if (!entry) return undefined;
963
+ if (entry.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return undefined;
964
+ accumulateCustomToolCallInputDelta(entry.block, delta, stream, output, entry.contentIndex);
965
+ return undefined;
966
+ }
967
+
968
+ handleCustomToolCallInputDone(rawEvent: Record<string, unknown>): void {
969
+ const entry = this.openItemForEvent(rawEvent);
970
+ if (entry?.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return;
971
+ const input = (rawEvent as { input?: string }).input;
972
+ if (typeof input === "string") finalizeCustomToolCallInputDone(entry.block, input);
973
+ }
974
+
975
+ handleResponseCreated(rawEvent: Record<string, unknown>): void {
976
+ const response = (rawEvent as { response?: { id?: string } }).response;
977
+ const state = this.websocketState;
978
+ if (state && this.transport === "websocket" && typeof response?.id === "string" && response.id.length > 0) {
979
+ state.lastResponseId = response.id;
980
+ }
981
+ }
982
+ }
983
+
984
+ interface CodexWhitespaceToolCallArgumentsDeltaState {
985
+ itemId: string;
986
+ outputIndex?: number;
987
+ consecutiveEvents: number;
988
+ consecutiveChars: number;
989
+ firstSequenceNumber?: number;
990
+ lastSequenceNumber?: number;
991
+ }
992
+
993
+ interface CodexWhitespaceToolCallArgumentsDeltaInterruption {
994
+ message: string;
995
+ }
996
+
997
+ interface CodexStreamFailureContext {
998
+ model: Model<"openai-codex-responses">;
999
+ output: AssistantMessage;
1000
+ options: OpenAICodexResponsesOptions | undefined;
1001
+ requestContext: CodexRequestContext;
1002
+ startTime: number;
1003
+ firstTokenTime?: number;
1004
+ }
1005
+
1006
+ interface CodexStreamCompletion {
1007
+ firstTokenTime?: number;
1008
+ }
1009
+
1010
+ function createCodexProviderSessionState(): CodexProviderSessionState {
1011
+ const state: CodexProviderSessionState = {
1012
+ webSocketSessions: new Map(),
1013
+ webSocketPublicToPrivate: new Map(),
1014
+ metadataSessions: new Map(),
1015
+ close: () => {
1016
+ for (const session of state.webSocketSessions.values()) {
1017
+ session.connection?.close("session_disposed");
1018
+ }
1019
+ state.webSocketSessions.clear();
1020
+ state.webSocketPublicToPrivate.clear();
1021
+ state.metadataSessions.clear();
1022
+ },
1023
+ };
1024
+ return state;
1025
+ }
1026
+
1027
+ function isCodexProviderSessionState(state: ProviderSessionState | undefined): state is CodexProviderSessionState {
1028
+ return (
1029
+ state !== undefined &&
1030
+ "webSocketSessions" in state &&
1031
+ state.webSocketSessions instanceof Map &&
1032
+ "webSocketPublicToPrivate" in state &&
1033
+ state.webSocketPublicToPrivate instanceof Map &&
1034
+ "metadataSessions" in state &&
1035
+ state.metadataSessions instanceof Map
1036
+ );
1037
+ }
1038
+
1039
+ function getCodexProviderSessionState(
1040
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
1041
+ ): CodexProviderSessionState | undefined {
1042
+ if (!providerSessionState) return undefined;
1043
+ const existing = providerSessionState.get(CODEX_PROVIDER_SESSION_STATE_KEY);
1044
+ if (isCodexProviderSessionState(existing)) return existing;
1045
+ const created = createCodexProviderSessionState();
1046
+ providerSessionState.set(CODEX_PROVIDER_SESSION_STATE_KEY, created);
1047
+ return created;
1048
+ }
1049
+
1050
+ function isCodexWebSocketRetryableStreamError(error: unknown): boolean {
1051
+ if (!(error instanceof CodexWebSocketTransportError)) return false;
1052
+ const message = error.message.toLowerCase();
1053
+ return (
1054
+ message.includes("websocket closed (") ||
1055
+ message.includes("websocket closed before response completion") ||
1056
+ message.includes("websocket connection is unavailable") ||
1057
+ message.includes("websocket send failed") ||
1058
+ message.includes("websocket ping failed") ||
1059
+ message.includes("websocket pong timeout") ||
1060
+ message.includes("websocket message queue exceeded") ||
1061
+ message.includes("websocket request already in progress") ||
1062
+ message.includes("idle timeout waiting for websocket") ||
1063
+ message.includes("timeout waiting for first websocket event") ||
1064
+ message.includes("syntaxerror") ||
1065
+ message.includes("json")
1066
+ );
1067
+ }
1068
+ function toCodexHeaderRecord(value: unknown): Record<string, string> | null {
1069
+ if (!value || typeof value !== "object") return null;
1070
+ const headers: Record<string, string> = {};
1071
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
1072
+ if (typeof entry === "string") {
1073
+ headers[key] = entry;
1074
+ } else if (Array.isArray(entry) && entry.every(item => typeof item === "string")) {
1075
+ headers[key] = entry.join(",");
1076
+ } else if (typeof entry === "number" || typeof entry === "boolean") {
1077
+ headers[key] = String(entry);
1078
+ }
1079
+ }
1080
+ return Object.keys(headers).length > 0 ? headers : null;
1081
+ }
1082
+
1083
+ function toCodexHeaders(value: unknown): Headers | undefined {
1084
+ if (!value) return undefined;
1085
+ if (value instanceof Headers) return value;
1086
+ if (Array.isArray(value)) {
1087
+ try {
1088
+ return new Headers(value as Array<[string, string]>);
1089
+ } catch {
1090
+ return undefined;
1091
+ }
1092
+ }
1093
+ const record = toCodexHeaderRecord(value);
1094
+ if (!record) return undefined;
1095
+ return new Headers(record);
1096
+ }
1097
+
1098
+ function updateCodexSessionMetadataFromHeaders(
1099
+ state: CodexWebSocketSessionState | undefined,
1100
+ headers: Headers | Record<string, string> | null | undefined,
1101
+ ): void {
1102
+ if (!state || !headers) return;
1103
+ const resolvedHeaders = headers instanceof Headers ? headers : new Headers(headers);
1104
+ const turnState = resolvedHeaders.get(X_CODEX_TURN_STATE_HEADER);
1105
+ if (turnState && turnState.length > 0) {
1106
+ state.turnState = turnState;
1107
+ }
1108
+ const modelsEtag = resolvedHeaders.get(X_MODELS_ETAG_HEADER);
1109
+ if (modelsEtag && modelsEtag.length > 0) {
1110
+ state.modelsEtag = modelsEtag;
1111
+ }
1112
+ }
1113
+
1114
+ function extractCodexWebSocketHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): Headers | undefined {
1115
+ const eventRecord = openEvent as Record<string, unknown> | undefined;
1116
+ const eventResponse = eventRecord?.response as Record<string, unknown> | undefined;
1117
+ const socketRecord = socket as unknown as Record<string, unknown>;
1118
+ const socketResponse = socketRecord.response as Record<string, unknown> | undefined;
1119
+ const socketHandshake = socketRecord.handshake as Record<string, unknown> | undefined;
1120
+ return (
1121
+ toCodexHeaders(eventRecord?.responseHeaders) ??
1122
+ toCodexHeaders(eventRecord?.headers) ??
1123
+ toCodexHeaders(eventResponse?.headers) ??
1124
+ toCodexHeaders(socketRecord.responseHeaders) ??
1125
+ toCodexHeaders(socketRecord.handshakeHeaders) ??
1126
+ toCodexHeaders(socketResponse?.headers) ??
1127
+ toCodexHeaders(socketHandshake?.headers)
1128
+ );
1129
+ }
1130
+
1131
+ // Synthesizes a `RawSseEvent` for a Codex WebSocket frame so the same debug
1132
+ // pipeline used for HTTP SSE (`onSseEvent` → `RawSseDebugBuffer.recordEvent`)
1133
+ // also captures WebSocket traffic. The `raw` array mirrors SSE wire format
1134
+ // (one line per field) so the existing TUI viewer renders it identically:
1135
+ // : ws ← <type>
1136
+ // event: <type>
1137
+ // data: <json>
1138
+ // Outbound (client → server) uses `: ws → <type>`. The viewer pretty-prints
1139
+ // `data:` JSON lines, so we keep the wire JSON single-line here and let the
1140
+ // renderer expand it.
1141
+ function notifyCodexWebSocketInbound(
1142
+ observer: ((event: RawSseEvent) => void) | undefined,
1143
+ parsed: Record<string, unknown>,
1144
+ text: string,
1145
+ ): void {
1146
+ const type = typeof parsed.type === "string" ? parsed.type : null;
1147
+ const raw: string[] = [`: ws ← ${type ?? "(untyped)"}`];
1148
+ if (type) raw.push(`event: ${type}`);
1149
+ raw.push(`data: ${text}`);
1150
+ notifyRawSseEvent(observer, { event: type, data: text, raw });
1151
+ }
1152
+
1153
+ function notifyCodexWebSocketOutbound(
1154
+ observer: ((event: RawSseEvent) => void) | undefined,
1155
+ request: Record<string, unknown>,
1156
+ payload: string,
1157
+ ): void {
1158
+ const type = typeof request.type === "string" ? request.type : null;
1159
+ const raw: string[] = [`: ws → ${type ?? "(untyped)"}`];
1160
+ if (type) raw.push(`event: ${type}`);
1161
+ raw.push(`data: ${payload}`);
1162
+ notifyRawSseEvent(observer, { event: type, data: payload, raw });
1163
+ }
1164
+
1165
+ function notifyCodexWebSocketMalformed(
1166
+ observer: ((event: RawSseEvent) => void) | undefined,
1167
+ data: unknown,
1168
+ error: unknown,
1169
+ ): void {
1170
+ const text = typeof data === "string" ? data : "";
1171
+ const reason = error instanceof Error ? error.message : String(error);
1172
+ const raw: string[] = [`: ws ← (parse-error: ${reason})`];
1173
+ if (text) raw.push(`data: ${text}`);
1174
+ notifyRawSseEvent(observer, { event: "parse_error", data: text, raw });
1175
+ }
1176
+
1177
+ /** @internal Exported for tests. */
1178
+ export function normalizeCodexToolChoice(
1179
+ choice: ToolChoice | undefined,
1180
+ tools: Tool[] = [],
1181
+ model?: Model<"openai-codex-responses">,
1182
+ ): string | Record<string, unknown> | undefined {
1183
+ if (!choice) return undefined;
1184
+ if (typeof choice === "string") return choice;
1185
+ const allowFreeform = model ? model.applyPatchToolType === "freeform" : false;
1186
+ const mapName = (name: string): Record<string, string> | undefined => {
1187
+ const directTool = tools.find(tool => tool.name === name);
1188
+ const customTool = allowFreeform
1189
+ ? tools.find(tool => tool.customFormat && (tool.name === name || tool.customWireName === name))
1190
+ : undefined;
1191
+ const offeredTool = customTool ?? directTool;
1192
+ if (!offeredTool) return undefined;
1193
+ if (offeredTool.native?.type === "computer" && model?.supportsComputerUse === true) {
1194
+ return { type: "computer" };
1195
+ }
1196
+ return customTool
1197
+ ? { type: "custom", name: customTool.customWireName ?? customTool.name }
1198
+ : { type: "function", name: offeredTool.name };
1199
+ };
1200
+ if (choice.type === "computer") {
1201
+ const computer = tools.find(tool => tool.native?.type === "computer");
1202
+ if (!computer) return undefined;
1203
+ return model?.supportsComputerUse === true ? { type: "computer" } : { type: "function", name: computer.name };
1204
+ }
1205
+ if (choice.type === "function") {
1206
+ if ("function" in choice && choice.function?.name) {
1207
+ return mapName(choice.function.name);
1208
+ }
1209
+ if ("name" in choice && choice.name) {
1210
+ return mapName(choice.name);
1211
+ }
1212
+ }
1213
+ if (choice.type === "tool" && choice.name) {
1214
+ return mapName(choice.name);
1215
+ }
1216
+ return undefined;
1217
+ }
1218
+ function unrollCodexComputerItems(items: ResponseInput, supportsImageDetailOriginal: boolean): ResponseInput {
1219
+ const replayItems = stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items);
1220
+ const unrolled: ResponseInput = [];
1221
+ for (const item of replayItems) {
1222
+ if (item.type === "computer_call") {
1223
+ const actions = item.actions ?? (item.action ? [item.action] : []);
1224
+ unrolled.push({
1225
+ type: "function_call",
1226
+ call_id: item.call_id,
1227
+ name: "computer",
1228
+ arguments: JSON.stringify({ actions }),
1229
+ status: item.status,
1230
+ });
1231
+ continue;
1232
+ }
1233
+ if (item.type === "computer_call_output") {
1234
+ const image =
1235
+ typeof item.output.image_url === "string" && item.output.image_url.length > 0
1236
+ ? ({
1237
+ type: "input_image",
1238
+ detail: supportsImageDetailOriginal ? "original" : "auto",
1239
+ image_url: item.output.image_url,
1240
+ } satisfies ResponseInputContent)
1241
+ : typeof item.output.file_id === "string" && item.output.file_id.length > 0
1242
+ ? ({
1243
+ type: "input_image",
1244
+ detail: supportsImageDetailOriginal ? "original" : "auto",
1245
+ file_id: item.output.file_id,
1246
+ } satisfies ResponseInputContent)
1247
+ : undefined;
1248
+ unrolled.push({
1249
+ type: "function_call_output",
1250
+ call_id: item.call_id,
1251
+ output: image ? "(see attached image)" : "",
1252
+ });
1253
+ if (image) {
1254
+ unrolled.push({
1255
+ role: "user",
1256
+ content: [{ type: "input_text", text: "Attached image from computer tool result:" }, image],
1257
+ });
1258
+ }
1259
+ continue;
1260
+ }
1261
+ unrolled.push(item);
1262
+ }
1263
+ return unrolled;
1264
+ }
1265
+
1266
+ function unrollCodexComputerAssistantMessage(message: AssistantMessage): AssistantMessage {
1267
+ let changed = false;
1268
+ const content = message.content.map(block => {
1269
+ if (block.type !== "toolCall" || block.providerMetadata?.type !== "computer") return block;
1270
+ changed = true;
1271
+ const call: ToolCall = {
1272
+ ...block,
1273
+ arguments: { actions: structuredCloneJSON(block.providerMetadata.actions) },
1274
+ };
1275
+ delete call.providerMetadata;
1276
+ return call;
1277
+ });
1278
+ return changed ? { ...message, content } : message;
1279
+ }
1280
+
1281
+ function unrollCodexComputerToolResult(message: ToolResultMessage): ToolResultMessage {
1282
+ if (message.providerMetadata?.type !== "computer") return message;
1283
+ const result: ToolResultMessage = { ...message };
1284
+ delete result.providerMetadata;
1285
+ return result;
1286
+ }
1287
+
1288
+ function getCodexServiceTierCostMultiplier(
1289
+ model: Pick<Model<"openai-codex-responses">, "id">,
1290
+ serviceTier: ServiceTier | "default" | undefined,
1291
+ ): number {
1292
+ switch (serviceTier) {
1293
+ case "flex":
1294
+ return 0.5;
1295
+ case "priority":
1296
+ return model.id === "gpt-5.5" ? 2.5 : 2;
1297
+ default:
1298
+ return 1;
1299
+ }
1300
+ }
1301
+
1302
+ function resolveCodexCostServiceTier(res: unknown, req?: unknown): ServiceTier | "default" | undefined {
1303
+ switch (res) {
1304
+ case "flex":
1305
+ return "flex";
1306
+ case "priority":
1307
+ return "priority";
1308
+ default:
1309
+ if (req === "flex" || req === "priority") {
1310
+ return req;
1311
+ }
1312
+ return "default";
1313
+ }
1314
+ }
1315
+
1316
+ function applyCodexServiceTierPricing(
1317
+ model: Pick<Model<"openai-codex-responses">, "id">,
1318
+ usage: AssistantMessage["usage"],
1319
+ resTier: unknown,
1320
+ reqTier: unknown,
1321
+ ): void {
1322
+ const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
1323
+ const multiplier = getCodexServiceTierCostMultiplier(model, resolvedTier);
1324
+ if (multiplier === 1) return;
1325
+ usage.cost.input *= multiplier;
1326
+ usage.cost.output *= multiplier;
1327
+ usage.cost.cacheRead *= multiplier;
1328
+ usage.cost.cacheWrite *= multiplier;
1329
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
1330
+ }
1331
+
1332
+ function resetOutputState(output: AssistantMessage): void {
1333
+ output.content.length = 0;
1334
+ output.usage = {
1335
+ input: 0,
1336
+ output: 0,
1337
+ cacheRead: 0,
1338
+ cacheWrite: 0,
1339
+ totalTokens: 0,
1340
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1341
+ };
1342
+ output.stopReason = "stop";
1343
+ output.stopDetails = undefined;
1344
+ }
1345
+
1346
+ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): CodexRequestSetup {
1347
+ const requestAbortController = new AbortController();
1348
+ const requestSignal = options?.signal
1349
+ ? AbortSignal.any([options.signal, requestAbortController.signal])
1350
+ : requestAbortController.signal;
1351
+ const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
1352
+ const websocketIdleTimeoutMs = options?.streamIdleTimeoutMs ?? CODEX_WEBSOCKET_IDLE_TIMEOUT_MS;
1353
+ const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
1354
+ const websocketFirstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS;
1355
+ const wrapCodexSseStream = (
1356
+ source: AsyncGenerator<Record<string, unknown>>,
1357
+ ): AsyncGenerator<Record<string, unknown>> =>
1358
+ iterateWithIdleTimeout(source, {
1359
+ idleTimeoutMs,
1360
+ firstItemTimeoutMs: firstEventTimeoutMs,
1361
+ firstItemErrorMessage: "OpenAI Codex SSE stream timed out while waiting for the first event",
1362
+ errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
1363
+ onIdle: () => requestAbortController.abort(),
1364
+ onFirstItemTimeout: () => requestAbortController.abort(),
1365
+ abortSignal: options?.signal,
1366
+ isProgressItem: isCodexStreamProgressEvent,
1367
+ });
1368
+ return {
1369
+ requestAbortController,
1370
+ requestSignal,
1371
+ wrapCodexSseStream,
1372
+ firstEventTimeoutMs,
1373
+ websocketIdleTimeoutMs,
1374
+ websocketFirstEventTimeoutMs,
1375
+ };
1376
+ }
1377
+
1378
+ function createCodexRequestContext(
1379
+ model: Model<"openai-codex-responses">,
1380
+ transformedBody: RequestBody,
1381
+ options: OpenAICodexResponsesOptions | undefined,
1382
+ contextOptions: {
1383
+ isolateCompactionTransport: boolean;
1384
+ startNewTurn?: boolean;
1385
+ turnStartedAtUnixMs?: number;
1386
+ },
1387
+ ): CodexRequestContext {
1388
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
1389
+ if (!apiKey) {
1390
+ throw new AIError.MissingApiKeyError(model.provider);
1391
+ }
1392
+
1393
+ const accountId = getCodexAccountId(apiKey);
1394
+ const baseUrl = model.baseUrl || CODEX_BASE_URL;
1395
+ const url = resolveCodexResponsesUrl(baseUrl);
1396
+
1397
+ const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
1398
+ const codexClientVersion = CODEX_CLIENT_VERSION;
1399
+ const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
1400
+ const rawRequestDump: RawHttpRequestDump = {
1401
+ provider: model.provider,
1402
+ api: model.api,
1403
+ model: model.id,
1404
+ method: "POST",
1405
+ url,
1406
+ body: transformedBody,
1407
+ };
1408
+
1409
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
1410
+ const isolatedTransportState =
1411
+ contextOptions.isolateCompactionTransport && options?.codexCompaction
1412
+ ? createCodexProviderSessionState()
1413
+ : undefined;
1414
+ const transportProviderSessionState = isolatedTransportState ?? providerSessionState;
1415
+ const responsesLite = resolveCodexResponsesLite(model, options?.responsesLite);
1416
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
1417
+ const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : undefined;
1418
+ if (sessionKey && publicSessionKey) {
1419
+ transportProviderSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
1420
+ }
1421
+ const sharedWebsocketState =
1422
+ sessionKey && providerSessionState
1423
+ ? isolatedTransportState
1424
+ ? providerSessionState.webSocketSessions.get(sessionKey)
1425
+ : getCodexWebSocketSessionState(sessionKey, providerSessionState)
1426
+ : undefined;
1427
+ const websocketState =
1428
+ sessionKey && isolatedTransportState
1429
+ ? getCodexWebSocketSessionState(sessionKey, isolatedTransportState)
1430
+ : sharedWebsocketState;
1431
+ if (isolatedTransportState && websocketState && sharedWebsocketState) {
1432
+ websocketState.disableWebsocket = sharedWebsocketState.disableWebsocket;
1433
+ websocketState.turnState = sharedWebsocketState.turnState;
1434
+ websocketState.modelsEtag = sharedWebsocketState.modelsEtag;
1435
+ }
1436
+ const metadataSessionId = transportSessionId ?? crypto.randomUUID();
1437
+ const metadataSession = getOrCreateCodexMetadataSessionState(metadataSessionId, providerSessionState);
1438
+ const compaction = options?.codexCompaction;
1439
+ const requestKind: OpenAICodexRequestKind = compaction ? "compaction" : "turn";
1440
+ const startNewTurn = resolveCodexStartNewTurn(metadataSession, requestKind, compaction, contextOptions.startNewTurn);
1441
+ if (websocketState && startNewTurn) {
1442
+ // Codex scopes turn-state to one turn. Mid-turn compaction preserves it;
1443
+ // a pre-turn or standalone compaction starts without it.
1444
+ websocketState.turnState = undefined;
1445
+ }
1446
+ const requestMetadata = createCodexRequestMetadata(metadataSession, requestKind, {
1447
+ startNewTurn,
1448
+ turnStartedAtUnixMs: compaction
1449
+ ? startNewTurn || !metadataSession.turnId
1450
+ ? Date.now()
1451
+ : undefined
1452
+ : contextOptions.turnStartedAtUnixMs,
1453
+ clientMetadata: transformedBody.client_metadata,
1454
+ parentTurnId: options?.parentTurnId,
1455
+ compaction,
1456
+ });
1457
+ transformedBody.client_metadata = requestMetadata.clientMetadata;
1458
+ return {
1459
+ apiKey,
1460
+ accountId,
1461
+ baseUrl,
1462
+ url,
1463
+ requestHeaders,
1464
+ transportSessionId,
1465
+ providerSessionState,
1466
+ isolatedTransportState,
1467
+ websocketState,
1468
+ responsesLite,
1469
+ requestMetadata,
1470
+ codexClientVersion,
1471
+ transformedBody,
1472
+ rawRequestDump,
1473
+ };
1474
+ }
1475
+
1476
+ async function buildCodexRequestContext(
1477
+ model: Model<"openai-codex-responses">,
1478
+ context: Context,
1479
+ options: OpenAICodexResponsesOptions | undefined,
1480
+ ): Promise<CodexRequestContext> {
1481
+ const promptCacheKey = getOpenAIPromptCacheKey(options);
1482
+ const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
1483
+ return createCodexRequestContext(model, transformedBody, options, {
1484
+ isolateCompactionTransport: true,
1485
+ startNewTurn: options?.codexCompaction ? undefined : !isCodexWithinTurnContinuation(context),
1486
+ turnStartedAtUnixMs: options?.codexCompaction ? undefined : getCodexTurnStartedAtUnixMs(context),
1487
+ });
1488
+ }
1489
+
1490
+ /** @internal Exported for tests. */
1491
+ export async function buildTransformedCodexRequestBody(
1492
+ model: Model<"openai-codex-responses">,
1493
+ context: Context,
1494
+ options: OpenAICodexResponsesOptions | undefined,
1495
+ promptCacheKey = getOpenAIPromptCacheKey(options),
1496
+ ): Promise<RequestBody> {
1497
+ const params: RequestBody = {
1498
+ model: model.requestModelId ?? model.id,
1499
+ input: convertMessages(model, context),
1500
+ stream: true,
1501
+ prompt_cache_key: promptCacheKey,
1502
+ };
1503
+
1504
+ // `maxTokens` is intentionally not forwarded: transformRequestBody strips
1505
+ // `max_output_tokens`/`max_completion_tokens` (the Codex backend rejects
1506
+ // caller-supplied output caps). Sampling controls (`temperature`, `top_p`,
1507
+ // `top_k`, `min_p`, `presence_penalty`, `repetition_penalty`,
1508
+ // `frequency_penalty`, `stop`) are likewise refused with
1509
+ // `{"detail":"Unsupported parameter: temperature"}` etc., so we drop
1510
+ // everything from `StreamOptions` rather than forwarding any of them.
1511
+ // (#3117 — codex-rs sends none of these either.)
1512
+ applyOpenAIServiceTier(params, options?.serviceTier, model);
1513
+ if (context.tools && context.tools.length > 0) {
1514
+ params.tools = convertOpenAICodexResponsesTools(context.tools, model);
1515
+ if (options?.toolChoice) {
1516
+ const toolChoice = normalizeCodexToolChoice(options.toolChoice, context.tools, model);
1517
+ if (toolChoice) {
1518
+ params.tool_choice = toolChoice;
1519
+ }
1520
+ }
1521
+ }
1522
+
1523
+ const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
1524
+ if (systemPrompts.length > 0) {
1525
+ params.instructions = systemPrompts[0];
1526
+ }
1527
+ const developerMessages = systemPrompts.slice(1);
1528
+ if (options?.clientMetadata && Object.keys(options.clientMetadata).length > 0) {
1529
+ params.client_metadata = { ...options.clientMetadata };
1530
+ }
1531
+ const codexOptions: CodexRequestOptions = {
1532
+ reasoningEffort: options?.reasoning,
1533
+ reasoningSummary: options?.reasoningSummary,
1534
+ reasoningContext: options?.reasoningContext,
1535
+ textVerbosity: options?.textVerbosity,
1536
+ include: options?.include,
1537
+ responsesLite: options?.responsesLite,
1538
+ };
1539
+
1540
+ return transformRequestBody(params, model, codexOptions, { developerMessages });
1541
+ }
1542
+
1543
+ async function openInitialCodexEventStream(
1544
+ model: Model<"openai-codex-responses">,
1545
+ options: OpenAICodexResponsesOptions | undefined,
1546
+ requestSetup: CodexRequestSetup,
1547
+ requestContext: CodexRequestContext,
1548
+ ): Promise<{
1549
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1550
+ requestBodyForState: RequestBody;
1551
+ transport: CodexTransport;
1552
+ }> {
1553
+ const { transformedBody, websocketState } = requestContext;
1554
+ if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
1555
+ const websocketRetryBudget = CODEX_WEBSOCKET_RETRY_BUDGET;
1556
+ let websocketRetries = 0;
1557
+ while (true) {
1558
+ try {
1559
+ return await openCodexWebSocketTransport(
1560
+ model,
1561
+ options,
1562
+ requestContext,
1563
+ requestSetup,
1564
+ websocketState,
1565
+ websocketRetries,
1566
+ options ? event => options.onSseEvent?.(event, model) : undefined,
1567
+ );
1568
+ } catch (error) {
1569
+ if (!(error instanceof CodexWebSocketTransportError)) throw error;
1570
+ const fatalWebSocketMessage = error.message.toLowerCase();
1571
+ const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern =>
1572
+ fatalWebSocketMessage.includes(pattern.toLowerCase()),
1573
+ );
1574
+ const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
1575
+ recordCodexWebSocketFailure(websocketState, activateFallback);
1576
+ CODEX_DEBUG &&
1577
+ logger.debug("[codex] codex websocket fallback", {
1578
+ error: error.message,
1579
+ retry: websocketRetries,
1580
+ retryBudget: websocketRetryBudget,
1581
+ activated: activateFallback,
1582
+ fatal: isFatal,
1583
+ });
1584
+ if (!activateFallback) {
1585
+ websocketRetries += 1;
1586
+ await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, websocketRetries), {
1587
+ signal: requestSetup.requestSignal,
1588
+ });
1589
+ continue;
1590
+ }
1591
+ break;
1592
+ }
1593
+ }
1594
+ }
1595
+ return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
1596
+ }
1597
+
1598
+ function toCodexRequestBody(body: OpenAICodexCompactionBody): RequestBody {
1599
+ const request: RequestBody = { model: body.model };
1600
+ for (const key in body) {
1601
+ if (key !== "model") request[key] = body[key];
1602
+ }
1603
+ return request;
1604
+ }
1605
+
1606
+ /**
1607
+ * Open a provider-native V2 compaction stream through Codex's WebSocket-first
1608
+ * transport, replaying WebSocket transport failures over SSE.
1609
+ */
1610
+ export async function openCodexCompactionEventStream(
1611
+ model: Model<"openai-codex-responses">,
1612
+ body: OpenAICodexCompactionBody,
1613
+ options: OpenAICodexCompactionStreamOptions,
1614
+ ): Promise<AsyncGenerator<Record<string, unknown>>> {
1615
+ const requestSetup = createRequestSetup(options);
1616
+ let requestContext: CodexRequestContext;
1617
+ let initial: {
1618
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1619
+ requestBodyForState: RequestBody;
1620
+ transport: CodexTransport;
1621
+ };
1622
+ try {
1623
+ requestContext = createCodexRequestContext(model, toCodexRequestBody(body), options, {
1624
+ isolateCompactionTransport: false,
1625
+ });
1626
+ initial = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
1627
+ } catch (error) {
1628
+ requestSetup.requestAbortController.abort();
1629
+ throw error;
1630
+ }
1631
+
1632
+ if (requestContext.websocketState) {
1633
+ requestContext.websocketState.lastTransport = initial.transport;
1634
+ // The compaction request may use the existing append baseline, but the
1635
+ // replacement history makes that baseline stale for the next normal turn.
1636
+ resetCodexWebSocketAppendState(requestContext.websocketState);
1637
+ }
1638
+ return streamCodexCompactionEvents(model, options, requestSetup, requestContext, initial);
1639
+ }
1640
+
1641
+ async function* streamCodexCompactionEvents(
1642
+ model: Model<"openai-codex-responses">,
1643
+ options: OpenAICodexCompactionStreamOptions,
1644
+ requestSetup: CodexRequestSetup,
1645
+ requestContext: CodexRequestContext,
1646
+ initial: {
1647
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1648
+ requestBodyForState: RequestBody;
1649
+ transport: CodexTransport;
1650
+ },
1651
+ ): AsyncGenerator<Record<string, unknown>> {
1652
+ let completed = false;
1653
+ const websocketState = requestContext.websocketState;
1654
+ const previousTurnState = websocketState?.turnState;
1655
+ const previousModelsEtag = websocketState?.modelsEtag;
1656
+ try {
1657
+ if (initial.transport === "websocket") {
1658
+ // Do not expose a WebSocket attempt until it finishes: an SSE replay
1659
+ // must replace, not extend, any partial compaction output.
1660
+ const bufferedEvents: Array<Record<string, unknown>> = [];
1661
+ try {
1662
+ for await (const event of initial.eventStream) bufferedEvents.push(event);
1663
+ } catch (error) {
1664
+ if (options.signal?.aborted || !(error instanceof CodexWebSocketTransportError)) {
1665
+ throw error;
1666
+ }
1667
+ const state = requestContext.websocketState;
1668
+ if (state) recordCodexWebSocketFailure(state, true);
1669
+ const fallback = await openCodexSseTransport(model, requestContext, requestSetup, options, state);
1670
+ if (state) state.lastTransport = fallback.transport;
1671
+ yield* drainCodexCompactionEvents(fallback.eventStream, requestContext.websocketState);
1672
+ completed = true;
1673
+ return;
1674
+ }
1675
+ // Apply metadata only once the WebSocket attempt succeeded: a discarded
1676
+ // attempt must not leak its `x-codex-turn-state` into the session.
1677
+ for (const event of bufferedEvents) {
1678
+ applyCodexCompactionResponseMetadata(requestContext.websocketState, event);
1679
+ yield event;
1680
+ }
1681
+ } else {
1682
+ yield* drainCodexCompactionEvents(initial.eventStream, requestContext.websocketState);
1683
+ }
1684
+ completed = true;
1685
+ } finally {
1686
+ if (!completed) {
1687
+ requestSetup.requestAbortController.abort();
1688
+ if (websocketState) {
1689
+ websocketState.turnState = previousTurnState;
1690
+ websocketState.modelsEtag = previousModelsEtag;
1691
+ }
1692
+ }
1693
+ }
1694
+ }
1695
+
1696
+ /**
1697
+ * Capture `x-codex-turn-state`/`x-models-etag` refreshes carried by a
1698
+ * `response.metadata` frame so a mid-turn compaction leaves the live session on
1699
+ * the latest turn state, matching the normal Codex stream processor.
1700
+ */
1701
+ function applyCodexCompactionResponseMetadata(
1702
+ state: CodexWebSocketSessionState | undefined,
1703
+ event: Record<string, unknown>,
1704
+ ): void {
1705
+ if (!state || event.type !== "response.metadata") return;
1706
+ updateCodexSessionMetadataFromHeaders(state, toCodexHeaders(event.headers));
1707
+ }
1708
+
1709
+ async function* drainCodexCompactionEvents(
1710
+ events: AsyncGenerator<Record<string, unknown>>,
1711
+ state: CodexWebSocketSessionState | undefined,
1712
+ ): AsyncGenerator<Record<string, unknown>> {
1713
+ for await (const event of events) {
1714
+ applyCodexCompactionResponseMetadata(state, event);
1715
+ yield event;
1716
+ }
1717
+ }
1718
+ async function openCodexWebSocketTransport(
1719
+ model: Model<"openai-codex-responses">,
1720
+ options: OpenAICodexResponsesOptions | undefined,
1721
+ requestContext: CodexRequestContext,
1722
+ requestSetup: CodexRequestSetup,
1723
+ websocketState: CodexWebSocketSessionState,
1724
+ retry: number,
1725
+ onSseEvent?: (event: RawSseEvent) => void,
1726
+ ): Promise<{
1727
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1728
+ requestBodyForState: RequestBody;
1729
+ transport: CodexTransport;
1730
+ }> {
1731
+ const canAppendBeforeRequest = websocketState.canAppend === true;
1732
+ const chainedBody = buildCodexChainedRequestBody(requestContext.transformedBody, websocketState);
1733
+ // WebSocket frames cannot carry per-request HTTP headers. Canonical Codex
1734
+ // request identity is already in `client_metadata`; connection-scoped
1735
+ // compatibility values that can change after the upgrade ride alongside it
1736
+ // on every `response.create`.
1737
+ const websocketClientMetadata = { ...(chainedBody.client_metadata ?? {}) };
1738
+ if (requestContext.responsesLite) {
1739
+ websocketClientMetadata[CODEX_WS_RESPONSES_LITE_CLIENT_METADATA_KEY] = "true";
1740
+ }
1741
+ if (websocketState.turnState) {
1742
+ websocketClientMetadata[X_CODEX_TURN_STATE_HEADER] = websocketState.turnState;
1743
+ }
1744
+ let websocketRequest = {
1745
+ type: "response.create",
1746
+ ...chainedBody,
1747
+ client_metadata: websocketClientMetadata,
1748
+ };
1749
+ const replacementWebsocketRequest = await options?.onPayload?.(websocketRequest, model);
1750
+ if (replacementWebsocketRequest !== undefined) {
1751
+ websocketRequest = replacementWebsocketRequest as typeof websocketRequest;
1752
+ }
1753
+ recordCodexTurnRequestDiagnostics(websocketState, websocketRequest, "websocket", canAppendBeforeRequest);
1754
+ const websocketHeaders = createCodexHeaders(
1755
+ requestContext.requestHeaders,
1756
+ requestContext.accountId,
1757
+ requestContext.apiKey,
1758
+ requestContext.codexClientVersion,
1759
+ requestContext.transportSessionId,
1760
+ "websocket",
1761
+ websocketState,
1762
+ requestContext.responsesLite,
1763
+ requestContext.requestMetadata,
1764
+ await getCodexAttestationHeader(requestContext.accountId),
1765
+ );
1766
+ const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
1767
+ // `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options`);
1768
+ // recorded state must reflect what was actually sent — the sequential-cutoff
1769
+ // summary decoder keys off it.
1770
+ if (websocketRequest.stream_options === undefined) {
1771
+ delete requestBodyForState.stream_options;
1772
+ } else {
1773
+ requestBodyForState.stream_options = websocketRequest.stream_options;
1774
+ }
1775
+ requestContext.rawRequestDump.body = websocketRequest;
1776
+ CODEX_DEBUG &&
1777
+ logger.debug("[codex] codex websocket request", {
1778
+ url: toWebSocketUrl(requestContext.url),
1779
+ model: requestContext.transformedBody.model,
1780
+ reasoningEffort: requestContext.transformedBody.reasoning?.effort ?? null,
1781
+ headers: redactHeaders(websocketHeaders),
1782
+ sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
1783
+ sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
1784
+ requestType: websocketRequest.type,
1785
+ retry,
1786
+ retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
1787
+ });
1788
+ const websocketConnection = await getOrCreateCodexWebSocketConnection(
1789
+ websocketState,
1790
+ toWebSocketUrl(requestContext.url),
1791
+ websocketHeaders,
1792
+ model.provider,
1793
+ requestSetup.requestSignal,
1794
+ );
1795
+ const eventStream = websocketConnection.streamRequest(
1796
+ websocketRequest,
1797
+ {
1798
+ idleTimeoutMs: requestSetup.websocketIdleTimeoutMs,
1799
+ firstEventTimeoutMs: requestSetup.websocketFirstEventTimeoutMs,
1800
+ },
1801
+ requestSetup.requestSignal,
1802
+ onSseEvent,
1803
+ );
1804
+ return {
1805
+ eventStream,
1806
+ requestBodyForState,
1807
+ transport: "websocket",
1808
+ };
1809
+ }
1810
+
1811
+ function getCodexTurnStartedAtUnixMs(context: Context): number {
1812
+ for (let i = context.messages.length - 1; i >= 0; i--) {
1813
+ const message = context.messages[i];
1814
+ if (message?.role === "user" && Number.isFinite(message.timestamp)) {
1815
+ return Math.trunc(message.timestamp);
1816
+ }
1817
+ }
1818
+ return Date.now();
1819
+ }
1820
+
1821
+ /**
1822
+ * True when the request continues the current turn (everything after the
1823
+ * last assistant message is tool results), false when a new user turn starts.
1824
+ * Mirrors codex-rs, which scopes `x-codex-turn-state` to a single turn and
1825
+ * clears it when the next one begins.
1826
+ */
1827
+ function isCodexWithinTurnContinuation(context: Context): boolean {
1828
+ for (let i = context.messages.length - 1; i >= 0; i--) {
1829
+ const role = context.messages[i]?.role;
1830
+ if (role === "toolResult") continue;
1831
+ return role === "assistant";
1832
+ }
1833
+ return false;
1834
+ }
1835
+
1836
+ async function openCodexSseTransport(
1837
+ model: Model<"openai-codex-responses">,
1838
+ requestContext: CodexRequestContext,
1839
+ requestSetup: CodexRequestSetup,
1840
+ options: OpenAICodexResponsesOptions | undefined,
1841
+ state: CodexWebSocketSessionState | undefined,
1842
+ body = requestContext.transformedBody,
1843
+ ): Promise<{
1844
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1845
+ requestBodyForState: RequestBody;
1846
+ transport: CodexTransport;
1847
+ }> {
1848
+ const open = async (wireBody: RequestBody) => {
1849
+ // Keep the 400 dump honest: record the body actually sent on the wire.
1850
+ requestContext.rawRequestDump.body = wireBody;
1851
+ return requestSetup.wrapCodexSseStream(
1852
+ await openCodexSseEventStream(
1853
+ requestContext.url,
1854
+ requestContext.requestHeaders,
1855
+ requestContext.accountId,
1856
+ requestContext.apiKey,
1857
+ requestContext.transportSessionId,
1858
+ wireBody,
1859
+ state,
1860
+ requestContext.responsesLite,
1861
+ requestContext.codexClientVersion,
1862
+ requestContext.requestMetadata,
1863
+ requestSetup.requestSignal,
1864
+ requestSetup.firstEventTimeoutMs,
1865
+ options?.codexSseMaxAttempts,
1866
+ event => options?.onSseEvent?.(event, model),
1867
+ options?.fetch,
1868
+ ),
1869
+ );
1870
+ };
1871
+ const canAppendBeforeRequest = state?.canAppend === true;
1872
+ let wireBody = body;
1873
+ const replacementWireBody = await options?.onPayload?.(wireBody, model);
1874
+ if (replacementWireBody !== undefined) {
1875
+ wireBody = replacementWireBody as RequestBody;
1876
+ }
1877
+ recordCodexTurnRequestDiagnostics(state, wireBody, "sse", canAppendBeforeRequest);
1878
+ return { eventStream: await open(wireBody), requestBodyForState: structuredCloneJSON(wireBody), transport: "sse" };
1879
+ }
1880
+
1881
+ function isJsonWhitespaceOnly(value: string): boolean {
1882
+ for (let index = 0; index < value.length; index += 1) {
1883
+ const code = value.charCodeAt(index);
1884
+ if (code !== 0x09 && code !== 0x0a && code !== 0x0d && code !== 0x20) {
1885
+ return false;
1886
+ }
1887
+ }
1888
+ return true;
1889
+ }
1890
+
1891
+ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null {
1892
+ if (item.type === "reasoning") {
1893
+ return { type: "thinking", thinking: "" };
1894
+ }
1895
+ if (item.type === "message") {
1896
+ const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
1897
+ return { type: "text", text: "", textSignature: encodeTextSignatureV1(item.id, phase) };
1898
+ }
1899
+ if (item.type === "function_call") {
1900
+ return {
1901
+ type: "toolCall",
1902
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1903
+ name: item.name,
1904
+ arguments: {},
1905
+ [kStreamingPartialJson]: item.arguments || "",
1906
+ };
1907
+ }
1908
+ if (item.type === "computer_call") {
1909
+ return {
1910
+ type: "toolCall",
1911
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1912
+ name: "computer",
1913
+ arguments: {},
1914
+ providerMetadata: computerCallMetadata(item),
1915
+ [kStreamingPartialJson]: "",
1916
+ };
1917
+ }
1918
+ if (item.type === "custom_tool_call") {
1919
+ // Wire name flows through unchanged; the agent-loop dispatcher also
1920
+ // matches `Tool.customWireName`. Reuse `partialJson` as the
1921
+ // accumulation buffer for the raw input string.
1922
+ return {
1923
+ type: "toolCall",
1924
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1925
+ name: item.name,
1926
+ arguments: { input: item.input ?? "" },
1927
+ customWireName: item.name,
1928
+ [kStreamingPartialJson]: item.input ?? "",
1929
+ };
1930
+ }
1931
+ return null;
1932
+ }
1933
+
1934
+ function getOutputBlockStartEventType(block: CodexOutputBlock): "thinking_start" | "text_start" | "toolcall_start" {
1935
+ if (block.type === "thinking") return "thinking_start";
1936
+ if (block.type === "text") return "text_start";
1937
+ return "toolcall_start";
1938
+ }
1939
+
1940
+ const CODEX_STALE_PREVIOUS_RESPONSE_CODES: Record<string, true> = {
1941
+ // OpenAI-standard code for an expired/missing `previous_response_id` chain.
1942
+ previous_response_not_found: true,
1943
+ // Proxy-specific: upstream response anchor expired. Same recovery class —
1944
+ // retry the turn with full context and no `previous_response_id`.
1945
+ codex_previous_response_stale: true,
1946
+ };
1947
+
1948
+ function isCodexStalePreviousResponseError(error: unknown): boolean {
1949
+ if (!(error instanceof Error)) return false;
1950
+ if (
1951
+ "code" in error &&
1952
+ typeof error.code === "string" &&
1953
+ Object.hasOwn(CODEX_STALE_PREVIOUS_RESPONSE_CODES, error.code)
1954
+ ) {
1955
+ return true;
1956
+ }
1957
+ // Message-based fallback for providers/proxies that report the condition
1958
+ // without a canonical code. Also covers "unsupported": the backend
1959
+ // intermittently rejects the parameter outright with
1960
+ // `{"detail":"Unsupported parameter: previous_response_id"}` (no
1961
+ // `error.code`); treat it like a stale chain so the turn replays with full
1962
+ // context instead of surfacing the 400.
1963
+ return (
1964
+ /previous[ _]?response/i.test(error.message) &&
1965
+ /not[ _]?found|invalid|expired|stale|unsupported/i.test(error.message)
1966
+ );
1967
+ }
1968
+
1969
+ async function handleCodexStreamFailure(context: CodexStreamFailureContext, error: unknown): Promise<AssistantMessage> {
1970
+ const { output } = context;
1971
+ if (context.requestContext.websocketState) {
1972
+ resetCodexWebSocketAppendState(context.requestContext.websocketState);
1973
+ context.requestContext.websocketState.turnState = undefined;
1974
+ context.requestContext.websocketState.modelsEtag = undefined;
1975
+ }
1976
+ const result = await AIError.finalize(error, {
1977
+ api: context.model.api,
1978
+ signal: context.options?.signal,
1979
+ rawRequestDump: context.requestContext.rawRequestDump,
1980
+ });
1981
+ output.stopReason = result.stopReason;
1982
+ output.errorStatus = result.status;
1983
+ output.errorId = result.id;
1984
+ output.errorMessage = result.message;
1985
+ output.duration = performance.now() - context.startTime;
1986
+ if (context.firstTokenTime) {
1987
+ output.ttft = context.firstTokenTime - context.startTime;
1988
+ }
1989
+ return output;
1990
+ }
1991
+
1992
+ /**
1993
+ * Owns one `streamOpenAICodexResponses` call: the request scaffolding
1994
+ * (model/output/stream/options/request context) plus the per-attempt
1995
+ * {@link CodexStreamRuntime}. Drives the event loop in {@link process}, applies
1996
+ * the transport-fallback / retry recovery ladder, and emits the final message
1997
+ * in {@link finalize}. The runtime object is mutated in place across retries
1998
+ * (event stream and accumulators are swapped/reset), never reassigned.
1999
+ */
2000
+ class CodexStreamProcessor {
2001
+ runtime: CodexStreamRuntime;
2002
+ model: Model<"openai-codex-responses">;
2003
+ output: AssistantMessage;
2004
+ stream: AssistantMessageEventStream;
2005
+ options: OpenAICodexResponsesOptions | undefined;
2006
+ requestSetup: CodexRequestSetup;
2007
+ requestContext: CodexRequestContext;
2008
+ startTime: number;
2009
+ firstTokenTime?: number;
2010
+
2011
+ constructor(init: {
2012
+ runtime: CodexStreamRuntime;
2013
+ model: Model<"openai-codex-responses">;
2014
+ output: AssistantMessage;
2015
+ stream: AssistantMessageEventStream;
2016
+ options: OpenAICodexResponsesOptions | undefined;
2017
+ requestSetup: CodexRequestSetup;
2018
+ requestContext: CodexRequestContext;
2019
+ startTime: number;
2020
+ }) {
2021
+ this.runtime = init.runtime;
2022
+ this.model = init.model;
2023
+ this.output = init.output;
2024
+ this.stream = init.stream;
2025
+ this.options = init.options;
2026
+ this.requestSetup = init.requestSetup;
2027
+ this.requestContext = init.requestContext;
2028
+ this.startTime = init.startTime;
2029
+ }
2030
+
2031
+ /**
2032
+ * Whether the request actually sent (post-`onPayload`) opted into
2033
+ * sequential-cutoff summary delivery. Atomic `.done` events are preferred;
2034
+ * incremental deltas remain buffered as a compatibility fallback when a
2035
+ * transport emits them instead.
2036
+ */
2037
+ get #sequentialCutoffSummaries(): boolean {
2038
+ return this.runtime.requestBodyForState.stream_options?.reasoning_summary_delivery === "sequential_cutoff";
2039
+ }
2040
+
2041
+ async process(): Promise<CodexStreamCompletion> {
2042
+ const { output, stream } = this;
2043
+ stream.push({ type: "start", partial: output });
2044
+
2045
+ while (true) {
2046
+ try {
2047
+ let firstTokenTime = this.firstTokenTime;
2048
+ for await (const rawEvent of this.runtime.eventStream) {
2049
+ firstTokenTime = this.#handleStreamEvent(rawEvent, firstTokenTime);
2050
+ if (this.runtime.sawTerminalEvent) break;
2051
+ }
2052
+ return { firstTokenTime };
2053
+ } catch (error) {
2054
+ const recovered = await this.#recoverStreamError(error);
2055
+ if (!recovered) {
2056
+ throw error;
2057
+ }
2058
+ stream.push({ type: "start", partial: output });
2059
+ }
2060
+ }
2061
+ }
2062
+
2063
+ #handleStreamEvent(rawEvent: Record<string, unknown>, firstTokenTime: number | undefined): number | undefined {
2064
+ const { output, stream } = this;
2065
+ const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
2066
+ if (!eventType) return firstTokenTime;
2067
+
2068
+ if (eventType === "response.output_item.added") {
2069
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
2070
+ if (!firstTokenTime) firstTokenTime = performance.now();
2071
+ const item = rawEvent.item as CodexEventItem;
2072
+ this.runtime.currentItem = item;
2073
+ this.runtime.currentBlock = createOutputBlockForItem(item);
2074
+ let contentIndex = -1;
2075
+ if (this.runtime.currentBlock) {
2076
+ output.content.push(this.runtime.currentBlock);
2077
+ contentIndex = output.content.length - 1;
2078
+ }
2079
+ // Track every open item by every stable key the wire gives us. `item.id`
2080
+ // is best; `output_index` preserves idless function/custom tool calls and
2081
+ // keeps their final args authoritative when only `output_item.done`
2082
+ // carries the full payload.
2083
+ const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : undefined;
2084
+ const outputIndex =
2085
+ typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
2086
+ ? Math.trunc(rawEvent.output_index)
2087
+ : undefined;
2088
+ const entry: CodexOpenItem = { item, block: this.runtime.currentBlock, contentIndex, itemId, outputIndex };
2089
+ this.runtime.currentEntry = entry;
2090
+ if (itemId) this.runtime.openItems.set(itemId, entry);
2091
+ if (outputIndex !== undefined) this.runtime.openItemsByOutputIndex.set(outputIndex, entry);
2092
+ if (!this.runtime.currentBlock) return firstTokenTime;
2093
+ stream.push({
2094
+ type: getOutputBlockStartEventType(this.runtime.currentBlock),
2095
+ contentIndex,
2096
+ partial: output,
2097
+ });
2098
+ return firstTokenTime;
2099
+ }
2100
+
2101
+ if (eventType === "response.reasoning_summary_part.added") {
2102
+ if (this.#sequentialCutoffSummaries) return firstTokenTime;
2103
+ const entry = this.runtime.openItemForEvent(rawEvent);
2104
+ if (entry?.item.type === "reasoning") {
2105
+ appendReasoningSummaryPart(
2106
+ entry.item,
2107
+ (rawEvent as { part: ResponseReasoningItem["summary"][number] }).part,
2108
+ );
2109
+ }
2110
+ return firstTokenTime;
2111
+ }
2112
+ if (eventType === "response.reasoning_summary_text.delta") {
2113
+ const entry = this.runtime.openItemForEvent(rawEvent);
2114
+ const delta = typeof rawEvent.delta === "string" ? rawEvent.delta : "";
2115
+ if (this.#sequentialCutoffSummaries) {
2116
+ // Some Codex transports still emit incremental summary deltas even
2117
+ // after opting into sequential-cutoff delivery. Buffer them until
2118
+ // output_item.done so atomic `.done` events can supersede them
2119
+ // without duplicate UI output.
2120
+ this.runtime.queueSummaryDelta(entry, delta);
2121
+ return firstTokenTime;
2122
+ }
2123
+ if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") {
2124
+ appendReasoningSummaryTextDelta(entry.item, entry.block, delta, stream, output, entry.contentIndex);
2125
+ }
2126
+ return firstTokenTime;
2127
+ }
2128
+
2129
+ if (eventType === "response.reasoning_summary_text.done") {
2130
+ // Outside the cutoff contract the text already streamed via `.delta`.
2131
+ if (!this.#sequentialCutoffSummaries) return firstTokenTime;
2132
+ const entry = this.runtime.openItemForEvent(rawEvent);
2133
+ if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") {
2134
+ this.runtime.takeSummaryDeltas(entry);
2135
+ if (!firstTokenTime) firstTokenTime = performance.now();
2136
+ const summaryIndex =
2137
+ typeof rawEvent.summary_index === "number" && Number.isFinite(rawEvent.summary_index)
2138
+ ? Math.trunc(rawEvent.summary_index)
2139
+ : 0;
2140
+ applyReasoningSummaryDone(
2141
+ this.runtime.cutoffSummaries,
2142
+ entry.block,
2143
+ typeof rawEvent.text === "string" ? rawEvent.text : "",
2144
+ summaryIndex,
2145
+ stream,
2146
+ output,
2147
+ entry.contentIndex,
2148
+ );
2149
+ }
2150
+ return firstTokenTime;
2151
+ }
2152
+
2153
+ if (eventType === "response.reasoning_text.delta") {
2154
+ const entry = this.runtime.openItemForEvent(rawEvent);
2155
+ const delta = typeof rawEvent.delta === "string" ? rawEvent.delta : "";
2156
+ if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") {
2157
+ entry.block.thinking += delta;
2158
+ stream.push({
2159
+ type: "thinking_delta",
2160
+ contentIndex: entry.contentIndex,
2161
+ delta,
2162
+ partial: output,
2163
+ });
2164
+ }
2165
+ return firstTokenTime;
2166
+ }
2167
+
2168
+ if (eventType === "response.reasoning_summary_part.done") {
2169
+ const entry = this.runtime.openItemForEvent(rawEvent);
2170
+ if (this.#sequentialCutoffSummaries) {
2171
+ if (entry && this.runtime.pendingSummaryDeltas.has(entry)) this.runtime.queueSummaryDelta(entry, "\n\n");
2172
+ return firstTokenTime;
2173
+ }
2174
+ if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") {
2175
+ appendReasoningSummaryPartDone(entry.item, entry.block, stream, output, entry.contentIndex);
2176
+ }
2177
+ return firstTokenTime;
2178
+ }
2179
+
2180
+ if (eventType === "response.content_part.added") {
2181
+ const entry = this.runtime.openItemForEvent(rawEvent);
2182
+ if (entry?.item.type === "message") {
2183
+ appendMessageContentPart(
2184
+ entry.item,
2185
+ (rawEvent as { part?: ResponseOutputMessage["content"][number] }).part,
2186
+ );
2187
+ }
2188
+ return firstTokenTime;
2189
+ }
2190
+
2191
+ if (eventType === "response.output_text.delta" || eventType === "response.refusal.delta") {
2192
+ const entry = this.runtime.openItemForEvent(rawEvent);
2193
+ if (entry?.item.type === "message" && entry.block?.type === "text") {
2194
+ appendMessageTextDelta(
2195
+ entry.item,
2196
+ entry.block,
2197
+ (rawEvent as { delta?: string }).delta || "",
2198
+ stream,
2199
+ output,
2200
+ entry.contentIndex,
2201
+ eventType === "response.refusal.delta" ? "refusal" : "output_text",
2202
+ );
2203
+ }
2204
+ return firstTokenTime;
2205
+ }
2206
+
2207
+ if (eventType === "response.function_call_arguments.delta") {
2208
+ const interruption = this.runtime.handleToolCallArgumentsDelta(rawEvent, stream, output);
2209
+ if (interruption) {
2210
+ this.runtime.websocketState?.connection?.close("degenerate-tool-call");
2211
+ throw new CodexWhitespaceToolCallLoopError(interruption.message);
2212
+ }
2213
+ return firstTokenTime;
2214
+ }
2215
+
2216
+ if (eventType === "response.function_call_arguments.done") {
2217
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
2218
+ this.runtime.handleToolCallArgumentsDone(rawEvent);
2219
+ return firstTokenTime;
2220
+ }
2221
+
2222
+ if (eventType === "response.custom_tool_call_input.delta") {
2223
+ const interruption = this.runtime.handleCustomToolCallInputDelta(rawEvent, stream, output);
2224
+ if (interruption) {
2225
+ this.runtime.websocketState?.connection?.close("degenerate-tool-call");
2226
+ throw new CodexWhitespaceToolCallLoopError(interruption.message);
2227
+ }
2228
+ return firstTokenTime;
2229
+ }
2230
+
2231
+ if (eventType === "response.custom_tool_call_input.done") {
2232
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
2233
+ this.runtime.handleCustomToolCallInputDone(rawEvent);
2234
+ return firstTokenTime;
2235
+ }
2236
+
2237
+ if (eventType === "response.output_item.done") {
2238
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
2239
+ this.#handleOutputItemDone(rawEvent);
2240
+ return firstTokenTime;
2241
+ }
2242
+
2243
+ if (eventType === "response.created") {
2244
+ this.runtime.handleResponseCreated(rawEvent);
2245
+ return firstTokenTime;
2246
+ }
2247
+
2248
+ if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
2249
+ this.#handleResponseCompleted(rawEvent);
2250
+ return firstTokenTime;
2251
+ }
2252
+
2253
+ if (eventType === "response.metadata") {
2254
+ // The WebSocket transport has no per-response HTTP headers; codex-rs
2255
+ // mirrors them into this event's `headers` and reads
2256
+ // `x-codex-turn-state` from there (ResponsesStreamEvent::turn_state).
2257
+ // Pick up the refresh so same-turn follow-ups echo the latest turn
2258
+ // state on either transport.
2259
+ updateCodexSessionMetadataFromHeaders(this.requestContext.websocketState, toCodexHeaders(rawEvent.headers));
2260
+ const moderation = asRecord(rawEvent.metadata)?.[CODEX_MODERATION_METADATA_KEY];
2261
+ if (moderation !== undefined) {
2262
+ try {
2263
+ this.options?.onModerationMetadata?.(moderation);
2264
+ } catch {
2265
+ // Diagnostic observer: failures must not disturb the stream.
2266
+ }
2267
+ }
2268
+ return firstTokenTime;
2269
+ }
2270
+
2271
+ if (eventType === "error" || eventType === "response.failed") {
2272
+ throw createCodexProviderStreamError(rawEvent);
2273
+ }
2274
+
2275
+ return firstTokenTime;
2276
+ }
2277
+
2278
+ #flushSummaryDeltas(entry: CodexOpenItem | null): void {
2279
+ if (entry?.block?.type !== "thinking") return;
2280
+ for (const delta of this.runtime.takeSummaryDeltas(entry)) {
2281
+ entry.block.thinking += delta;
2282
+ this.stream.push({
2283
+ type: "thinking_delta",
2284
+ contentIndex: entry.contentIndex,
2285
+ delta,
2286
+ partial: this.output,
2287
+ });
2288
+ }
2289
+ }
2290
+ #handleOutputItemDone(rawEvent: Record<string, unknown>): void {
2291
+ const { runtime, output, stream } = this;
2292
+ const rawItem = rawEvent.item;
2293
+ if (!rawItem || typeof rawItem !== "object") return;
2294
+ const item = structuredCloneJSON(rawItem) as CodexEventItem;
2295
+ if (item.type === "image_generation_call" && item.result) item.status = "completed";
2296
+ runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
2297
+
2298
+ // Match the finalization to the OPEN ITEM that started this block, not the
2299
+ // singleton current — interleaved items can finish out of order, so the
2300
+ // most-recently-added block may belong to a sibling (#2619). Some Codex
2301
+ // function/custom tool items omit `id`; in that case `output_index` still
2302
+ // routes `output_item.done` to the block that received `output_item.added`.
2303
+ const itemId = "id" in item && typeof item.id === "string" ? item.id : "";
2304
+ const entry = (itemId ? runtime.openItems.get(itemId) : null) ?? runtime.openItemForEvent(rawEvent);
2305
+ const block = entry?.block ?? null;
2306
+ const contentIndex = entry?.contentIndex ?? output.content.length - 1;
2307
+
2308
+ if (item.type === "image_generation_call" && item.result) {
2309
+ appendResponsesImageResult(output, stream, item.result);
2310
+ runtime.closeOpenItem(entry);
2311
+ return;
2312
+ }
2313
+
2314
+ if (item.type === "reasoning" && block?.type === "thinking") {
2315
+ this.#flushSummaryDeltas(entry);
2316
+ block.thinking = finalizeReasoningThinking(
2317
+ item,
2318
+ block.thinking,
2319
+ this.#sequentialCutoffSummaries ? this.runtime.cutoffSummaries : undefined,
2320
+ );
2321
+ block.thinkingSignature = JSON.stringify(item);
2322
+ stream.push({
2323
+ type: "thinking_end",
2324
+ contentIndex,
2325
+ content: block.thinking,
2326
+ partial: output,
2327
+ });
2328
+ runtime.closeOpenItem(entry);
2329
+ return;
2330
+ }
2331
+
2332
+ if (item.type === "message" && block?.type === "text") {
2333
+ block.text = finalizeMessageText(item, block.text);
2334
+ const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
2335
+ block.textSignature = encodeTextSignatureV1(item.id, phase);
2336
+ stream.push({
2337
+ type: "text_end",
2338
+ contentIndex,
2339
+ content: block.text,
2340
+ partial: output,
2341
+ });
2342
+ runtime.closeOpenItem(entry);
2343
+ return;
2344
+ }
2345
+
2346
+ if (item.type === "function_call") {
2347
+ const toolCall: ToolCall = {
2348
+ type: "toolCall",
2349
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2350
+ name: item.name,
2351
+ arguments: parseStreamingJson(item.arguments || "{}"),
2352
+ };
2353
+ if (block?.type === "toolCall") {
2354
+ // Persist the authoritative final args on the stored block; the throttled
2355
+ // delta parser may have left block.arguments stale (often `{}`).
2356
+ block.arguments = toolCall.arguments;
2357
+ clearStreamingPartialJson(block);
2358
+ }
2359
+ // Detach so a late/duplicate arguments.delta cannot append to the
2360
+ // finished block or trip the whitespace-loop guard against it.
2361
+ runtime.closeOpenItem(entry);
2362
+ runtime.canSafelyReplayWebsocketOverSse = false;
2363
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2364
+ return;
2365
+ }
2366
+
2367
+ if (item.type === "computer_call") {
2368
+ const toolCall: ToolCall = {
2369
+ type: "toolCall",
2370
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2371
+ name: "computer",
2372
+ arguments: {},
2373
+ providerMetadata: computerCallMetadata(item),
2374
+ };
2375
+ let resolvedContentIndex = contentIndex;
2376
+ if (block?.type === "toolCall") {
2377
+ block.id = toolCall.id;
2378
+ block.providerMetadata = toolCall.providerMetadata;
2379
+ clearStreamingPartialJson(block);
2380
+ } else {
2381
+ output.content.push(toolCall);
2382
+ resolvedContentIndex = output.content.length - 1;
2383
+ }
2384
+ runtime.closeOpenItem(entry);
2385
+ runtime.canSafelyReplayWebsocketOverSse = false;
2386
+ stream.push({ type: "toolcall_end", contentIndex: resolvedContentIndex, toolCall, partial: output });
2387
+ return;
2388
+ }
2389
+
2390
+ if (item.type === "custom_tool_call") {
2391
+ const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : undefined;
2392
+ const rawInput = partial && partial.length > 0 ? partial : (item.input ?? "");
2393
+ const toolCall: ToolCall = {
2394
+ type: "toolCall",
2395
+ id: encodeResponsesToolCallId(item.call_id, item.id),
2396
+ name: item.name,
2397
+ arguments: { input: rawInput },
2398
+ customWireName: item.name,
2399
+ };
2400
+ if (block?.type === "toolCall") {
2401
+ block.arguments = { input: rawInput };
2402
+ clearStreamingPartialJson(block);
2403
+ }
2404
+ runtime.closeOpenItem(entry);
2405
+ runtime.canSafelyReplayWebsocketOverSse = false;
2406
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2407
+ return;
2408
+ }
2409
+ }
2410
+
2411
+ #handleResponseCompleted(rawEvent: Record<string, unknown>): void {
2412
+ const { runtime, model, output } = this;
2413
+ runtime.sawTerminalEvent = true;
2414
+ const rawResponse = rawEvent.response;
2415
+ const response = rawResponse && typeof rawResponse === "object" ? rawResponse : undefined;
2416
+ const responseId = response && "id" in response && typeof response.id === "string" ? response.id : undefined;
2417
+ const usage = response && "usage" in response ? parseCodexResponseUsage(response.usage) : undefined;
2418
+ const serviceTier =
2419
+ response && "service_tier" in response ? parseCodexServiceTier(response.service_tier) : undefined;
2420
+ const status = response && "status" in response ? parseCodexResponseStatus(response.status) : undefined;
2421
+ const endTurn = response && "end_turn" in response ? response.end_turn : undefined;
2422
+
2423
+ populateResponsesUsageFromResponse(output, usage);
2424
+ recordCodexTurnUsageDiagnostics(runtime.websocketState, usage, output.usage);
2425
+ if (responseId) {
2426
+ output.responseId = responseId;
2427
+ }
2428
+
2429
+ const state = runtime.websocketState;
2430
+ if (state) {
2431
+ if (runtime.transport !== "websocket") {
2432
+ // SSE turns never chain (previous_response_id is websocket-only on this
2433
+ // endpoint); a completed SSE turn also invalidates any websocket append
2434
+ // baseline, which no longer matches the transcript.
2435
+ resetCodexWebSocketAppendState(state);
2436
+ } else {
2437
+ state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
2438
+ const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
2439
+ structuredCloneJSON(runtime.nativeOutputItems),
2440
+ );
2441
+ if (responseId && replayableResponseItems) {
2442
+ state.lastResponseId = responseId;
2443
+ state.lastResponseItems = replayableResponseItems;
2444
+ state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
2445
+ } else {
2446
+ // Without both a response id and replayable output, the append baseline cannot be trusted.
2447
+ state.canAppend = false;
2448
+ }
2449
+ }
2450
+ }
2451
+
2452
+ const incompleteDetails =
2453
+ response &&
2454
+ "incomplete_details" in response &&
2455
+ response.incomplete_details &&
2456
+ typeof response.incomplete_details === "object"
2457
+ ? response.incomplete_details
2458
+ : undefined;
2459
+ const shouldPromoteIncompleteToolUse =
2460
+ status === "incomplete" &&
2461
+ incompleteDetails !== undefined &&
2462
+ "reason" in incompleteDetails &&
2463
+ incompleteDetails.reason === "max_output_tokens" &&
2464
+ hasExecutableIncompleteResponsesToolCalls(output);
2465
+ finalizePendingResponsesToolCalls(output);
2466
+
2467
+ calculateCost(model, output.usage);
2468
+ applyCodexServiceTierPricing(model, output.usage, serviceTier, runtime.requestBodyForState.service_tier);
2469
+ output.stopReason = mapOpenAIResponsesStopReason(status);
2470
+ promoteResponsesToolUseStopReason(
2471
+ output,
2472
+ endTurn === true ? true : endTurn === false ? false : undefined,
2473
+ shouldPromoteIncompleteToolUse,
2474
+ );
2475
+ }
2476
+
2477
+ async #recoverStreamError(error: unknown): Promise<boolean> {
2478
+ if (await this.#tryRecoverWhitespaceToolCallLoop(error)) {
2479
+ return true;
2480
+ }
2481
+ if (await this.#tryReconnectWebSocketOnConnectionLimit(error)) {
2482
+ return true;
2483
+ }
2484
+ if (await this.#tryRecoverPreviousResponseNotFound(error)) {
2485
+ return true;
2486
+ }
2487
+ if (await this.#tryReplayWebsocketFailureOverSse(error)) {
2488
+ return true;
2489
+ }
2490
+ if (await this.#tryRetryProviderError(error)) {
2491
+ return true;
2492
+ }
2493
+ return false;
2494
+ }
2495
+
2496
+ /**
2497
+ * Recover from the degenerate whitespace-only tool-call argument loop
2498
+ * ({@link CodexWhitespaceToolCallLoopError}). The interrupted function call has
2499
+ * no usable arguments, so drop the partial turn and replay the request from
2500
+ * scratch — bounded by {@link CODEX_WHITESPACE_LOOP_RETRY_LIMIT}. Sampling
2501
+ * nondeterminism usually breaks the loop on a fresh attempt; once the budget is
2502
+ * exhausted the original error is surfaced (now without the junk tool call
2503
+ * polluting the message). Replay is refused once any visible content was already
2504
+ * delivered to the consumer — a finished tool call (`canSafelyReplayWebsocketOverSse`),
2505
+ * or any streamed text/commentary block still in `output.content` after the degenerate
2506
+ * tool call is dropped — because replaying re-emits already-streamed deltas.
2507
+ */
2508
+ async #tryRecoverWhitespaceToolCallLoop(error: unknown): Promise<boolean> {
2509
+ if (!(error instanceof CodexWhitespaceToolCallLoopError)) {
2510
+ return false;
2511
+ }
2512
+ // Drop the half-built degenerate tool call whether or not we retry, so it
2513
+ // never reaches the caller's message.
2514
+ this.#dropTrailingDegenerateToolCall();
2515
+ if (
2516
+ this.runtime.whitespaceLoopRetries >= CODEX_WHITESPACE_LOOP_RETRY_LIMIT ||
2517
+ !this.runtime.canSafelyReplayWebsocketOverSse ||
2518
+ this.output.content.some(block => block.type !== "thinking") ||
2519
+ this.options?.signal?.aborted
2520
+ ) {
2521
+ return false;
2522
+ }
2523
+
2524
+ this.runtime.whitespaceLoopRetries += 1;
2525
+ const websocketState = this.requestContext.websocketState;
2526
+ if (websocketState) {
2527
+ resetCodexWebSocketAppendState(websocketState);
2528
+ websocketState.turnState = undefined;
2529
+ websocketState.modelsEtag = undefined;
2530
+ }
2531
+
2532
+ CODEX_DEBUG &&
2533
+ logger.debug("[codex] retrying codex turn after whitespace-only tool-call argument loop", {
2534
+ retry: this.runtime.whitespaceLoopRetries,
2535
+ retryBudget: CODEX_WHITESPACE_LOOP_RETRY_LIMIT,
2536
+ transport: this.runtime.transport,
2537
+ });
2538
+
2539
+ this.runtime.resetAccumulators();
2540
+ this.runtime.sawTerminalEvent = false;
2541
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
2542
+ resetOutputState(this.output);
2543
+ this.firstTokenTime = undefined;
2544
+ await scheduler.wait(CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS * this.runtime.whitespaceLoopRetries, {
2545
+ signal: this.requestSetup.requestSignal,
2546
+ });
2547
+
2548
+ if (this.runtime.transport === "websocket" && websocketState) {
2549
+ await this.#reopenWebSocketStream(websocketState);
2550
+ return true;
2551
+ }
2552
+
2553
+ await this.#reopenSseStream(websocketState);
2554
+ return true;
2555
+ }
2556
+
2557
+ /**
2558
+ * Pop the half-built degenerate tool-call block (the one whose arguments were
2559
+ * nothing but whitespace) off the output accumulator so it never surfaces in the
2560
+ * caller's message. Any legitimate content produced before it is preserved.
2561
+ */
2562
+ #dropTrailingDegenerateToolCall(): void {
2563
+ const { runtime, output } = this;
2564
+ const block = runtime.currentBlock;
2565
+ if (block && block.type === "toolCall" && output.content[output.content.length - 1] === block) {
2566
+ output.content.pop();
2567
+ }
2568
+ runtime.closeOpenItem(runtime.currentEntry);
2569
+ }
2570
+
2571
+ /**
2572
+ * Handles `websocket_connection_limit_reached` errors by closing the stale connection
2573
+ * and opening a fresh websocket. If content has already been emitted to the caller,
2574
+ * falls back to SSE replay (same as other WS failures) since we cannot safely
2575
+ * continue a partial response on a new connection. If a tool call was already
2576
+ * delivered (`canSafelyReplayWebsocketOverSse` is false), the error surfaces
2577
+ * instead — replaying would re-emit the same tool calls.
2578
+ */
2579
+ async #tryReconnectWebSocketOnConnectionLimit(error: unknown): Promise<boolean> {
2580
+ if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") {
2581
+ return false;
2582
+ }
2583
+ const websocketState = this.requestContext.websocketState;
2584
+ if (!websocketState || this.runtime.transport !== "websocket" || this.options?.signal?.aborted) {
2585
+ return false;
2586
+ }
2587
+
2588
+ // Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
2589
+ websocketState.connection?.close("connection_limit");
2590
+ websocketState.connection = undefined;
2591
+ resetCodexWebSocketAppendState(websocketState);
2592
+
2593
+ if (this.output.content.length > 0 && !this.runtime.canSafelyReplayWebsocketOverSse) {
2594
+ // A toolcall_end already reached the consumer; a full replay would emit
2595
+ // the same tool calls a second time. Let the error surface instead.
2596
+ return false;
2597
+ }
2598
+
2599
+ CODEX_DEBUG &&
2600
+ logger.debug("[codex] codex websocket connection limit reached, reconnecting", {
2601
+ hadContent: this.output.content.length > 0,
2602
+ retry: this.runtime.websocketStreamRetries,
2603
+ });
2604
+
2605
+ if (this.output.content.length > 0) {
2606
+ // Content already emitted to the caller — cannot safely continue on a new WS.
2607
+ // Reset and replay the full request over SSE.
2608
+ this.runtime.resetAccumulators();
2609
+ resetOutputState(this.output);
2610
+ this.firstTokenTime = undefined;
2611
+ recordCodexWebSocketFailure(websocketState, true);
2612
+ await this.#reopenSseStream(websocketState);
2613
+ return true;
2614
+ }
2615
+
2616
+ // No content emitted yet — clear accumulator state from the failed attempt
2617
+ // (blockless native items can exist even with empty content) and reconnect
2618
+ // over websocket, bounded by the shared retry budget: an account-scoped
2619
+ // limit can reject every fresh connection, and an unbounded loop would
2620
+ // hammer the endpoint with zero backoff.
2621
+ this.runtime.resetAccumulators();
2622
+ this.firstTokenTime = undefined;
2623
+ if (this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET) {
2624
+ recordCodexWebSocketFailure(websocketState, true);
2625
+ await this.#reopenSseStream(websocketState);
2626
+ return true;
2627
+ }
2628
+ this.runtime.websocketStreamRetries += 1;
2629
+ await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), {
2630
+ signal: this.requestSetup.requestSignal,
2631
+ });
2632
+ await this.#reopenWebSocketStream(websocketState);
2633
+ return true;
2634
+ }
2635
+
2636
+ async #tryRecoverPreviousResponseNotFound(error: unknown): Promise<boolean> {
2637
+ const websocketState = this.requestContext.websocketState;
2638
+ if (
2639
+ !isCodexStalePreviousResponseError(error) ||
2640
+ !websocketState ||
2641
+ this.output.content.length > 0 ||
2642
+ this.options?.signal?.aborted ||
2643
+ this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
2644
+ ) {
2645
+ return false;
2646
+ }
2647
+ if (this.runtime.transport !== "websocket") {
2648
+ // SSE never sends previous_response_id; let other recovery handle it.
2649
+ return false;
2650
+ }
2651
+
2652
+ this.runtime.providerRetryAttempt += 1;
2653
+ resetCodexWebSocketAppendState(websocketState);
2654
+ websocketState.turnState = undefined;
2655
+ websocketState.modelsEtag = undefined;
2656
+ this.runtime.resetAccumulators();
2657
+ this.runtime.sawTerminalEvent = false;
2658
+ resetOutputState(this.output);
2659
+ this.firstTokenTime = undefined;
2660
+
2661
+ CODEX_DEBUG &&
2662
+ logger.debug("[codex] codex previous_response_id expired; retrying with full context", {
2663
+ retry: this.runtime.providerRetryAttempt,
2664
+ });
2665
+ await this.#reopenWebSocketStream(websocketState);
2666
+ return true;
2667
+ }
2668
+
2669
+ async #tryReplayWebsocketFailureOverSse(error: unknown): Promise<boolean> {
2670
+ const websocketState = this.requestContext.websocketState;
2671
+ const canReplay =
2672
+ this.runtime.transport === "websocket" &&
2673
+ websocketState &&
2674
+ isCodexWebSocketRetryableStreamError(error) &&
2675
+ this.runtime.canSafelyReplayWebsocketOverSse &&
2676
+ !this.runtime.sawTerminalEvent &&
2677
+ !this.options?.signal?.aborted;
2678
+ if (!canReplay) return false;
2679
+
2680
+ const state = websocketState;
2681
+ const streamError = error instanceof Error ? error : new Error(String(error));
2682
+ const replayingBufferedOutputOverSse = this.output.content.length > 0;
2683
+ const fatalWebSocketMessage = streamError.message.toLowerCase();
2684
+ const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern =>
2685
+ fatalWebSocketMessage.includes(pattern.toLowerCase()),
2686
+ );
2687
+ const activateFallback =
2688
+ replayingBufferedOutputOverSse ||
2689
+ isFatal ||
2690
+ this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET;
2691
+ recordCodexWebSocketFailure(state, activateFallback);
2692
+ CODEX_DEBUG &&
2693
+ logger.debug("[codex] codex websocket stream fallback", {
2694
+ error: streamError.message,
2695
+ retry: this.runtime.websocketStreamRetries,
2696
+ retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
2697
+ activated: activateFallback,
2698
+ fatal: isFatal,
2699
+ replayedBufferedOutput: replayingBufferedOutputOverSse,
2700
+ });
2701
+
2702
+ if (!activateFallback) {
2703
+ this.runtime.websocketStreamRetries += 1;
2704
+ // Full re-send on a fresh socket: clear accumulator state from the failed
2705
+ // attempt. Content is empty here, but blockless native items (e.g.
2706
+ // web_search_call) may already have accumulated.
2707
+ this.runtime.resetAccumulators();
2708
+ this.firstTokenTime = undefined;
2709
+ await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), {
2710
+ signal: this.requestSetup.requestSignal,
2711
+ });
2712
+ await this.#reopenWebSocketStream(state);
2713
+ return true;
2714
+ }
2715
+
2716
+ this.runtime.resetAccumulators();
2717
+ resetOutputState(this.output);
2718
+ this.firstTokenTime = undefined;
2719
+
2720
+ await this.#reopenSseStream(state);
2721
+ return true;
2722
+ }
2723
+
2724
+ async #tryRetryProviderError(error: unknown): Promise<boolean> {
2725
+ if (
2726
+ !(error instanceof CodexProviderStreamError && error.retryable) ||
2727
+ this.output.content.length > 0 ||
2728
+ this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
2729
+ this.options?.signal?.aborted
2730
+ ) {
2731
+ return false;
2732
+ }
2733
+
2734
+ this.runtime.providerRetryAttempt += 1;
2735
+ const websocketState = this.requestContext.websocketState;
2736
+ if (websocketState) {
2737
+ resetCodexWebSocketAppendState(websocketState);
2738
+ websocketState.turnState = undefined;
2739
+ websocketState.modelsEtag = undefined;
2740
+ }
2741
+
2742
+ CODEX_DEBUG &&
2743
+ logger.debug("[codex] retrying codex provider stream error", {
2744
+ error: error instanceof Error ? error.message : String(error),
2745
+ retry: this.runtime.providerRetryAttempt,
2746
+ retryBudget: CODEX_MAX_RETRIES,
2747
+ transport: this.runtime.transport,
2748
+ });
2749
+
2750
+ this.runtime.resetAccumulators();
2751
+ this.runtime.sawTerminalEvent = false;
2752
+ resetOutputState(this.output);
2753
+ this.firstTokenTime = undefined;
2754
+ await scheduler.wait(CODEX_RETRY_DELAY_MS * this.runtime.providerRetryAttempt, {
2755
+ signal: this.requestSetup.requestSignal,
2756
+ });
2757
+
2758
+ if (this.runtime.transport === "websocket" && websocketState) {
2759
+ await this.#reopenWebSocketStream(websocketState);
2760
+ return true;
2761
+ }
2762
+
2763
+ await this.#reopenSseStream(websocketState);
2764
+ return true;
2765
+ }
2766
+
2767
+ async #reopenWebSocketStream(state: CodexWebSocketSessionState): Promise<void> {
2768
+ try {
2769
+ const next = await openCodexWebSocketTransport(
2770
+ this.model,
2771
+ this.options,
2772
+ this.requestContext,
2773
+ this.requestSetup,
2774
+ state,
2775
+ this.runtime.websocketStreamRetries,
2776
+ this.options ? event => this.options?.onSseEvent?.(event, this.model) : undefined,
2777
+ );
2778
+ this.runtime.eventStream = next.eventStream;
2779
+ this.runtime.requestBodyForState = next.requestBodyForState;
2780
+ this.runtime.transport = next.transport;
2781
+ state.lastTransport = next.transport;
2782
+ } catch (error) {
2783
+ if (!(error instanceof CodexWebSocketTransportError)) throw error;
2784
+ // Reopen failed at the websocket layer (handshake refused, connect timeout, etc.).
2785
+ // Activate fallback so subsequent turns use SSE, and replay this turn over SSE
2786
+ // instead of surfacing a raw transport error to the caller.
2787
+ recordCodexWebSocketFailure(state, true);
2788
+ CODEX_DEBUG &&
2789
+ logger.debug("[codex] codex websocket reopen failed, falling back to SSE", {
2790
+ error: error.message,
2791
+ retry: this.runtime.websocketStreamRetries,
2792
+ });
2793
+ await this.#reopenSseStream(state);
2794
+ }
2795
+ }
2796
+
2797
+ async #reopenSseStream(state: CodexWebSocketSessionState | undefined): Promise<void> {
2798
+ const next = await openCodexSseTransport(this.model, this.requestContext, this.requestSetup, this.options, state);
2799
+ this.runtime.eventStream = next.eventStream;
2800
+ this.runtime.requestBodyForState = next.requestBodyForState;
2801
+ this.runtime.transport = next.transport;
2802
+ if (state) {
2803
+ state.lastTransport = next.transport;
2804
+ }
2805
+ }
2806
+
2807
+ finalize(completion: CodexStreamCompletion): AssistantMessage {
2808
+ const { output } = this;
2809
+ if (this.options?.signal?.aborted) {
2810
+ throw new AIError.AbortError();
2811
+ }
2812
+ if (!this.runtime.sawTerminalEvent) {
2813
+ if (this.requestContext.websocketState) {
2814
+ resetCodexWebSocketAppendState(this.requestContext.websocketState);
2815
+ this.requestContext.websocketState.turnState = undefined;
2816
+ this.requestContext.websocketState.modelsEtag = undefined;
2817
+ }
2818
+ CODEX_DEBUG &&
2819
+ logger.debug("[codex] codex stream ended unexpectedly", {
2820
+ transport: this.runtime.transport,
2821
+ terminalEventSeen: this.runtime.sawTerminalEvent,
2822
+ unexpectedStreamEnd: true,
2823
+ sentTurnStateHeader: Boolean(this.requestContext.websocketState?.turnState),
2824
+ sentModelsEtagHeader: Boolean(this.requestContext.websocketState?.modelsEtag),
2825
+ });
2826
+ throw new CodexProviderStreamError("Codex stream ended before terminal completion event", false);
2827
+ }
2828
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
2829
+ throw new CodexProviderStreamError("Codex response failed", false);
2830
+ }
2831
+
2832
+ output.providerPayload = createOpenAIResponsesHistoryPayload(this.model.provider, this.runtime.nativeOutputItems);
2833
+ output.duration = performance.now() - this.startTime;
2834
+ if (completion.firstTokenTime) {
2835
+ output.ttft = completion.firstTokenTime - this.startTime;
2836
+ }
2837
+ return output;
2838
+ }
2839
+ }
2840
+
2841
+ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
2842
+ model: Model<"openai-codex-responses">,
2843
+ context: Context,
2844
+ options?: OpenAICodexResponsesOptions,
2845
+ ): AssistantMessageEventStream => {
2846
+ const stream = new AssistantMessageEventStream();
2847
+
2848
+ (async () => {
2849
+ const startTime = performance.now();
2850
+ const output: AssistantMessage = {
2851
+ role: "assistant",
2852
+ content: [],
2853
+ api: "openai-codex-responses" as Api,
2854
+ provider: model.provider,
2855
+ model: model.id,
2856
+ usage: {
2857
+ input: 0,
2858
+ output: 0,
2859
+ cacheRead: 0,
2860
+ cacheWrite: 0,
2861
+ totalTokens: 0,
2862
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
2863
+ },
2864
+ stopReason: "stop",
2865
+ timestamp: Date.now(),
2866
+ };
2867
+ const requestSetup = createRequestSetup(options);
2868
+ let processingContext: CodexStreamProcessor | undefined;
2869
+ let requestContext: CodexRequestContext | undefined;
2870
+
2871
+ try {
2872
+ requestContext = await buildCodexRequestContext(model, context, options);
2873
+ const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
2874
+ const runtime = new CodexStreamRuntime({
2875
+ ...initialTransport,
2876
+ websocketState: requestContext.websocketState,
2877
+ });
2878
+ if (requestContext.websocketState) {
2879
+ requestContext.websocketState.lastTransport = initialTransport.transport;
2880
+ }
2881
+
2882
+ processingContext = new CodexStreamProcessor({
2883
+ runtime,
2884
+ model,
2885
+ output,
2886
+ stream,
2887
+ options,
2888
+ requestSetup,
2889
+ requestContext,
2890
+ startTime,
2891
+ });
2892
+
2893
+ const completion = await processingContext.process();
2894
+ processingContext.firstTokenTime = completion.firstTokenTime;
2895
+ const message = processingContext.finalize(completion);
2896
+ stream.push({ type: "done", reason: message.stopReason as "stop" | "length" | "toolUse", message });
2897
+ stream.end();
2898
+ } catch (error) {
2899
+ const failureContext =
2900
+ processingContext ??
2901
+ ({
2902
+ model,
2903
+ output,
2904
+ options,
2905
+ requestContext: requestContext ?? {
2906
+ apiKey: "",
2907
+ accountId: "",
2908
+ baseUrl: model.baseUrl || CODEX_BASE_URL,
2909
+ url: "",
2910
+ requestHeaders: {},
2911
+ codexClientVersion: CODEX_CLIENT_VERSION,
2912
+ responsesLite: options?.responsesLite === true,
2913
+ transformedBody: { model: model.id },
2914
+ rawRequestDump: {
2915
+ provider: model.provider,
2916
+ api: output.api,
2917
+ model: model.id,
2918
+ method: "POST",
2919
+ url: "",
2920
+ body: { model: model.id },
2921
+ },
2922
+ },
2923
+ startTime,
2924
+ } satisfies CodexStreamFailureContext);
2925
+ try {
2926
+ const failure = await handleCodexStreamFailure(failureContext, error);
2927
+ stream.push({ type: "error", reason: failure.stopReason as "error" | "aborted", error: failure });
2928
+ } catch (failureError) {
2929
+ // Last resort — the failure handler itself threw (exotic error object or
2930
+ // request-dump formatting). Never leave the stream un-ended.
2931
+ logger.error("Codex stream failure handler threw", {
2932
+ error: failureError instanceof Error ? failureError.message : String(failureError),
2933
+ });
2934
+ output.stopReason = "error";
2935
+ output.errorMessage ??= error instanceof Error ? error.message : String(error);
2936
+ stream.push({ type: "error", reason: "error", error: output });
2937
+ }
2938
+ stream.end();
2939
+ } finally {
2940
+ requestContext?.isolatedTransportState?.close();
2941
+ }
2942
+ })();
2943
+
2944
+ return stream;
2945
+ };
2946
+
2947
+ export async function prewarmOpenAICodexResponses(
2948
+ model: Model<"openai-codex-responses">,
2949
+ options?: Pick<
2950
+ OpenAICodexResponsesOptions,
2951
+ "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite"
2952
+ >,
2953
+ ): Promise<void> {
2954
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
2955
+ if (!apiKey) return;
2956
+ const accountId = getCodexAccountId(apiKey);
2957
+ const baseUrl = model.baseUrl || CODEX_BASE_URL;
2958
+ const url = resolveCodexResponsesUrl(baseUrl);
2959
+ const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
2960
+ const promptCacheKey = transportSessionId;
2961
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
2962
+ const responsesLite = resolveCodexResponsesLite(model, options?.responsesLite);
2963
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
2964
+ const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : undefined;
2965
+ if (publicSessionKey && sessionKey) {
2966
+ providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
2967
+ }
2968
+ if (!sessionKey || !providerSessionState) return;
2969
+ const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
2970
+ if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
2971
+ const metadataSession = getOrCreateCodexMetadataSessionState(
2972
+ transportSessionId ?? crypto.randomUUID(),
2973
+ providerSessionState,
2974
+ );
2975
+ const codexClientVersion = CODEX_CLIENT_VERSION;
2976
+ const requestIdentity = createCodexCompatibilityIdentity(metadataSession);
2977
+ const attestation = await getCodexAttestationHeader(accountId);
2978
+ const headers = logger.time(
2979
+ "prewarmCodex:createHeaders",
2980
+ createCodexHeaders,
2981
+ { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
2982
+ accountId,
2983
+ apiKey,
2984
+ codexClientVersion,
2985
+ promptCacheKey,
2986
+ "websocket",
2987
+ state,
2988
+ responsesLite,
2989
+ requestIdentity,
2990
+ attestation,
2991
+ );
2992
+ await logger.time(
2993
+ "prewarmCodex:establishWs",
2994
+ getOrCreateCodexWebSocketConnection,
2995
+ state,
2996
+ toWebSocketUrl(url),
2997
+ headers,
2998
+ model.provider,
2999
+ options?.signal,
3000
+ );
3001
+ state.prewarmed = true;
3002
+ }
3003
+
3004
+ function getCodexWebSocketSessionKey(
3005
+ normalizedSessionId: string | undefined,
3006
+ model: Model<"openai-codex-responses">,
3007
+ accountId: string | undefined,
3008
+ apiKey: string,
3009
+ baseUrl: string,
3010
+ responsesLite: boolean,
3011
+ ): string | undefined {
3012
+ if (!normalizedSessionId) return undefined;
3013
+ const credentialKey = accountId ? `account:${accountId}` : `token:${Bun.hash(apiKey).toString(36)}`;
3014
+ // Responses Lite is connection-scoped on the WebSocket upgrade, so lite and
3015
+ // non-lite turns must never share a pooled socket or append state.
3016
+ const liteSuffix = responsesLite ? ":lite" : "";
3017
+ return `${credentialKey}:${baseUrl}:${model.id}:${normalizedSessionId}${liteSuffix}`;
3018
+ }
3019
+
3020
+ function getCodexWebSocketSessionState(
3021
+ sessionKey: string,
3022
+ providerSessionState: CodexProviderSessionState,
3023
+ ): CodexWebSocketSessionState {
3024
+ const existing = providerSessionState.webSocketSessions.get(sessionKey);
3025
+ if (existing) return existing;
3026
+ const created: CodexWebSocketSessionState = {
3027
+ disableWebsocket: false,
3028
+ canAppend: false,
3029
+ fallbackCount: 0,
3030
+ prewarmed: false,
3031
+ stats: {
3032
+ fullContextRequests: 0,
3033
+ deltaRequests: 0,
3034
+ lastInputItems: 0,
3035
+ },
3036
+ };
3037
+ providerSessionState.webSocketSessions.set(sessionKey, created);
3038
+ return created;
3039
+ }
3040
+
3041
+ function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void {
3042
+ state.canAppend = false;
3043
+ state.lastRequest = undefined;
3044
+ state.lastResponseId = undefined;
3045
+ state.lastResponseItems = undefined;
3046
+ }
3047
+
3048
+ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activateFallback: boolean): void {
3049
+ resetCodexWebSocketAppendState(state);
3050
+ // Never tear down a CONNECTING socket: it belongs to a concurrent caller's
3051
+ // in-flight handshake (prewarm/request race); closing it would reject that
3052
+ // caller with a fatal "websocket closed before open" and disable websockets
3053
+ // for the whole session.
3054
+ if (state.connection && !state.connection.isConnecting()) {
3055
+ state.connection.close("fallback");
3056
+ state.connection = undefined;
3057
+ }
3058
+ state.lastFallbackAt = Date.now();
3059
+ if (activateFallback && !state.disableWebsocket) {
3060
+ state.disableWebsocket = true;
3061
+ state.fallbackCount += 1;
3062
+ }
3063
+ }
3064
+
3065
+ function getCodexWebSocketEnvValue(): boolean | undefined {
3066
+ const envVal = $env.PI_CODEX_WEBSOCKET;
3067
+ if (envVal !== undefined) {
3068
+ return $flag("PI_CODEX_WEBSOCKET");
3069
+ }
3070
+ return undefined;
3071
+ }
3072
+
3073
+ function shouldUseCodexWebSocket(
3074
+ model: Model<"openai-codex-responses">,
3075
+ state: CodexWebSocketSessionState | undefined,
3076
+ preferWebsockets?: boolean,
3077
+ ): boolean {
3078
+ // Explicitly disabled by the model or session state.
3079
+ if (model.preferWebsockets === false) return false;
3080
+ // Explicitly disabled by the session state.
3081
+ if (!state || state.disableWebsocket) return false;
3082
+ // Env val > Preference
3083
+ const envVal = getCodexWebSocketEnvValue();
3084
+ if (envVal !== undefined) return envVal;
3085
+ // Negative preference overrides model preference; otherwise use the model's preference.
3086
+ if (preferWebsockets === false) return false;
3087
+ return true;
3088
+ }
3089
+
3090
+ export interface OpenAICodexTransportDetails {
3091
+ websocketPreferred: boolean;
3092
+ lastTransport?: CodexTransport;
3093
+ websocketDisabled: boolean;
3094
+ websocketConnected: boolean;
3095
+ fallbackCount: number;
3096
+ canAppend: boolean;
3097
+ prewarmed: boolean;
3098
+ hasSessionState: boolean;
3099
+ hasTurnState: boolean;
3100
+ lastFallbackAt?: number;
3101
+ }
3102
+
3103
+ function getCodexWebSocketStateForPublicSession(
3104
+ model: Model<"openai-codex-responses">,
3105
+ options:
3106
+ | {
3107
+ sessionId?: string;
3108
+ baseUrl?: string;
3109
+ providerSessionState?: Map<string, ProviderSessionState>;
3110
+ }
3111
+ | undefined,
3112
+ ): CodexWebSocketSessionState | undefined {
3113
+ const baseUrl = options?.baseUrl || model.baseUrl || CODEX_BASE_URL;
3114
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
3115
+ const normalizedSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
3116
+ const publicSessionKey = normalizedSessionId ? `${baseUrl}:${model.id}:${normalizedSessionId}` : undefined;
3117
+ const privateSessionKey = publicSessionKey
3118
+ ? providerSessionState?.webSocketPublicToPrivate.get(publicSessionKey)
3119
+ : undefined;
3120
+ return privateSessionKey ? providerSessionState?.webSocketSessions.get(privateSessionKey) : undefined;
3121
+ }
3122
+
3123
+ export function getOpenAICodexWebSocketDebugStats(
3124
+ model: Model<"openai-codex-responses">,
3125
+ options?: {
3126
+ sessionId?: string;
3127
+ baseUrl?: string;
3128
+ providerSessionState?: Map<string, ProviderSessionState>;
3129
+ },
3130
+ ): OpenAICodexWebSocketDebugStats | undefined {
3131
+ const stats = getCodexWebSocketStateForPublicSession(model, options)?.stats;
3132
+ return stats ? { ...stats } : undefined;
3133
+ }
3134
+
3135
+ export function getOpenAICodexTransportDetails(
3136
+ model: Model<"openai-codex-responses">,
3137
+ options?: {
3138
+ sessionId?: string;
3139
+ baseUrl?: string;
3140
+ preferWebsockets?: boolean;
3141
+ providerSessionState?: Map<string, ProviderSessionState>;
3142
+ },
3143
+ ): OpenAICodexTransportDetails {
3144
+ const envVal = getCodexWebSocketEnvValue();
3145
+ const websocketPreferred =
3146
+ envVal !== undefined
3147
+ ? envVal
3148
+ : options?.preferWebsockets === false
3149
+ ? false
3150
+ : options?.preferWebsockets === true || model.preferWebsockets === true;
3151
+ const state = getCodexWebSocketStateForPublicSession(model, options);
3152
+
3153
+ return {
3154
+ websocketPreferred,
3155
+ lastTransport: state?.lastTransport,
3156
+ websocketDisabled: state?.disableWebsocket ?? false,
3157
+ websocketConnected: state?.connection?.isOpen() ?? false,
3158
+ fallbackCount: state?.fallbackCount ?? 0,
3159
+ canAppend: state?.canAppend ?? false,
3160
+ prewarmed: state?.prewarmed ?? false,
3161
+ hasSessionState: state !== undefined,
3162
+ hasTurnState: state?.turnState !== undefined,
3163
+ lastFallbackAt: state?.lastFallbackAt,
3164
+ };
3165
+ }
3166
+
3167
+ const codexDiagnosticsTextEncoder = new TextEncoder();
3168
+
3169
+ function jsonByteLength(value: unknown): number {
3170
+ const json = JSON.stringify(value);
3171
+ return codexDiagnosticsTextEncoder.encode(json === undefined ? "undefined" : json).byteLength;
3172
+ }
3173
+
3174
+ function hashJson(value: unknown): string {
3175
+ const json = JSON.stringify(value);
3176
+ return String(Bun.hash(json === undefined ? "undefined" : json));
3177
+ }
3178
+
3179
+ function parseCodexServiceTier(value: unknown): ServiceTier | undefined {
3180
+ switch (value) {
3181
+ case "auto":
3182
+ case "default":
3183
+ case "flex":
3184
+ case "scale":
3185
+ case "priority":
3186
+ return value;
3187
+ default:
3188
+ return undefined;
3189
+ }
3190
+ }
3191
+
3192
+ function parseCodexResponseStatus(value: unknown): ResponseStatus | undefined {
3193
+ switch (value) {
3194
+ case "completed":
3195
+ case "failed":
3196
+ case "in_progress":
3197
+ case "cancelled":
3198
+ case "queued":
3199
+ case "incomplete":
3200
+ return value;
3201
+ default:
3202
+ return undefined;
3203
+ }
3204
+ }
3205
+
3206
+ function parseCodexResponseUsage(value: unknown): CodexResponseUsage | undefined {
3207
+ if (!value || typeof value !== "object") return undefined;
3208
+ const usage: CodexResponseUsage = {};
3209
+ let hasUsage = false;
3210
+ if ("input_tokens" in value && typeof value.input_tokens === "number") {
3211
+ usage.input_tokens = value.input_tokens;
3212
+ hasUsage = true;
3213
+ }
3214
+ if ("output_tokens" in value && typeof value.output_tokens === "number") {
3215
+ usage.output_tokens = value.output_tokens;
3216
+ hasUsage = true;
3217
+ }
3218
+ if ("total_tokens" in value && typeof value.total_tokens === "number") {
3219
+ usage.total_tokens = value.total_tokens;
3220
+ hasUsage = true;
3221
+ }
3222
+ if ("prompt_cache_hit_tokens" in value && typeof value.prompt_cache_hit_tokens === "number") {
3223
+ usage.prompt_cache_hit_tokens = value.prompt_cache_hit_tokens;
3224
+ hasUsage = true;
3225
+ }
3226
+ if (
3227
+ "input_tokens_details" in value &&
3228
+ value.input_tokens_details &&
3229
+ typeof value.input_tokens_details === "object"
3230
+ ) {
3231
+ const details = value.input_tokens_details;
3232
+ const parsedDetails: NonNullable<CodexResponseUsage["input_tokens_details"]> = {};
3233
+ let hasDetails = false;
3234
+ if ("cached_tokens" in details && typeof details.cached_tokens === "number") {
3235
+ parsedDetails.cached_tokens = details.cached_tokens;
3236
+ hasDetails = true;
3237
+ }
3238
+ if ("cache_write_tokens" in details && typeof details.cache_write_tokens === "number") {
3239
+ parsedDetails.cache_write_tokens = details.cache_write_tokens;
3240
+ hasDetails = true;
3241
+ }
3242
+ if ("orchestration_input_tokens" in details && typeof details.orchestration_input_tokens === "number") {
3243
+ parsedDetails.orchestration_input_tokens = details.orchestration_input_tokens;
3244
+ hasDetails = true;
3245
+ }
3246
+ if (
3247
+ "orchestration_input_cached_tokens" in details &&
3248
+ typeof details.orchestration_input_cached_tokens === "number"
3249
+ ) {
3250
+ parsedDetails.orchestration_input_cached_tokens = details.orchestration_input_cached_tokens;
3251
+ hasDetails = true;
3252
+ }
3253
+ if (hasDetails) {
3254
+ usage.input_tokens_details = parsedDetails;
3255
+ hasUsage = true;
3256
+ }
3257
+ }
3258
+ if (
3259
+ "output_tokens_details" in value &&
3260
+ value.output_tokens_details &&
3261
+ typeof value.output_tokens_details === "object"
3262
+ ) {
3263
+ const details = value.output_tokens_details;
3264
+ const parsedDetails: NonNullable<CodexResponseUsage["output_tokens_details"]> = {};
3265
+ let hasDetails = false;
3266
+ if ("reasoning_tokens" in details && typeof details.reasoning_tokens === "number") {
3267
+ parsedDetails.reasoning_tokens = details.reasoning_tokens;
3268
+ hasDetails = true;
3269
+ }
3270
+ if ("orchestration_output_tokens" in details && typeof details.orchestration_output_tokens === "number") {
3271
+ parsedDetails.orchestration_output_tokens = details.orchestration_output_tokens;
3272
+ hasDetails = true;
3273
+ }
3274
+ if (hasDetails) {
3275
+ usage.output_tokens_details = parsedDetails;
3276
+ hasUsage = true;
3277
+ }
3278
+ }
3279
+ return hasUsage ? usage : undefined;
3280
+ }
3281
+
3282
+ function describeCodexInputItemType(item: unknown): string {
3283
+ if (item && typeof item === "object") {
3284
+ if ("type" in item && typeof item.type === "string") return item.type;
3285
+ if ("role" in item && typeof item.role === "string") return item.role;
3286
+ }
3287
+ return typeof item;
3288
+ }
3289
+
3290
+ function createCodexOptionsHash(request: Record<string, unknown>): string {
3291
+ const options: Record<string, unknown> = {};
3292
+ for (const key in request) {
3293
+ if (key === "input" || key === "previous_response_id" || key === "type" || key === "client_metadata") {
3294
+ continue;
3295
+ }
3296
+ options[key] = request[key];
3297
+ }
3298
+ return hashJson(options);
3299
+ }
3300
+
3301
+ function buildCodexTurnRequestDiagnostics(
3302
+ request: Record<string, unknown>,
3303
+ transport: CodexTransport,
3304
+ canAppendBeforeRequest: boolean,
3305
+ ): OpenAICodexTurnRequestDiagnostics {
3306
+ const input = request.input;
3307
+ const inputItems = Array.isArray(input) ? input : [];
3308
+ const inputItemTypes = inputItems.map(describeCodexInputItemType);
3309
+ const promptCacheKey = typeof request.prompt_cache_key === "string" ? request.prompt_cache_key : undefined;
3310
+ const toolsHash = request.tools === undefined ? undefined : hashJson(request.tools);
3311
+ return {
3312
+ transport,
3313
+ previousResponseIdPresent:
3314
+ typeof request.previous_response_id === "string" && request.previous_response_id.length > 0,
3315
+ inputItemCount: inputItems.length,
3316
+ inputItemTypes,
3317
+ ...(inputItemTypes[0] ? { firstInputItemType: inputItemTypes[0] } : {}),
3318
+ inputJsonBytes: jsonByteLength(inputItems),
3319
+ ...(promptCacheKey !== undefined ? { promptCacheKey } : {}),
3320
+ ...(toolsHash !== undefined ? { toolsHash } : {}),
3321
+ optionsHash: createCodexOptionsHash(request),
3322
+ canAppendBeforeRequest,
3323
+ };
3324
+ }
3325
+
3326
+ function recordCodexTurnRequestDiagnostics(
3327
+ state: CodexWebSocketSessionState | undefined,
3328
+ request: Record<string, unknown>,
3329
+ transport: CodexTransport,
3330
+ canAppendBeforeRequest: boolean,
3331
+ ): void {
3332
+ if (!state) return;
3333
+ const input = request.input;
3334
+ state.stats.lastInputItems = Array.isArray(input) ? input.length : 0;
3335
+ const previousResponseId =
3336
+ typeof request.previous_response_id === "string" ? request.previous_response_id : undefined;
3337
+ if (previousResponseId && previousResponseId.length > 0) {
3338
+ state.stats.deltaRequests += 1;
3339
+ state.stats.lastDeltaInputItems = state.stats.lastInputItems;
3340
+ state.stats.lastPreviousResponseId = previousResponseId;
3341
+ } else {
3342
+ state.stats.fullContextRequests += 1;
3343
+ state.stats.lastDeltaInputItems = undefined;
3344
+ state.stats.lastPreviousResponseId = undefined;
3345
+ }
3346
+ state.stats.lastTurn = {
3347
+ request: buildCodexTurnRequestDiagnostics(request, transport, canAppendBeforeRequest),
3348
+ };
3349
+ CODEX_DEBUG && logger.debug("[codex] codex turn request diagnostics", { diagnostics: state.stats.lastTurn.request });
3350
+ }
3351
+
3352
+ function recordCodexTurnUsageDiagnostics(
3353
+ state: CodexWebSocketSessionState | undefined,
3354
+ rawUsage: CodexResponseUsage | undefined,
3355
+ displayedUsage: Usage,
3356
+ ): void {
3357
+ if (!state?.stats.lastTurn || !rawUsage) return;
3358
+ const details = rawUsage.input_tokens_details;
3359
+ const outputDetails = rawUsage.output_tokens_details;
3360
+ const rawInputTokens = rawUsage.input_tokens ?? 0;
3361
+ const rawCachedTokens = details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
3362
+ const usageDiagnostics: OpenAICodexTurnUsageDiagnostics = {
3363
+ rawInputTokens,
3364
+ rawCachedTokens,
3365
+ rawUncachedTokens: Math.max(0, rawInputTokens - rawCachedTokens),
3366
+ rawOutputTokens: rawUsage.output_tokens ?? 0,
3367
+ ...(typeof rawUsage.total_tokens === "number" ? { rawTotalTokens: rawUsage.total_tokens } : {}),
3368
+ ...(typeof details?.orchestration_input_tokens === "number"
3369
+ ? { rawOrchestrationInputTokens: details.orchestration_input_tokens }
3370
+ : {}),
3371
+ ...(typeof details?.orchestration_input_cached_tokens === "number"
3372
+ ? { rawOrchestrationCachedTokens: details.orchestration_input_cached_tokens }
3373
+ : {}),
3374
+ ...(typeof outputDetails?.orchestration_output_tokens === "number"
3375
+ ? { rawOrchestrationOutputTokens: outputDetails.orchestration_output_tokens }
3376
+ : {}),
3377
+ displayedInputTokens: displayedUsage.input,
3378
+ displayedOutputTokens: displayedUsage.output,
3379
+ displayedCacheReadTokens: displayedUsage.cacheRead,
3380
+ displayedCacheWriteTokens: displayedUsage.cacheWrite,
3381
+ displayedTotalTokens: displayedUsage.totalTokens,
3382
+ displayedOrchestrationInputTokens: displayedUsage.orchestration?.input ?? 0,
3383
+ displayedOrchestrationCacheReadTokens: displayedUsage.orchestration?.cacheRead ?? 0,
3384
+ displayedOrchestrationOutputTokens: displayedUsage.orchestration?.output ?? 0,
3385
+ };
3386
+ state.stats.lastTurn = {
3387
+ ...state.stats.lastTurn,
3388
+ usage: usageDiagnostics,
3389
+ };
3390
+ CODEX_DEBUG && logger.debug("[codex] codex turn diagnostics", { diagnostics: state.stats.lastTurn });
3391
+ }
3392
+
3393
+ /**
3394
+ * Shape the next websocket turn's request body: when the session's append
3395
+ * baseline is intact (same options, strict history prefix), chain via
3396
+ * `previous_response_id` + delta-only `input`; otherwise break the chain and
3397
+ * replay the full transcript. SSE requests never chain — the HTTP endpoint's
3398
+ * request schema has no `previous_response_id` (codex-rs carries it only on
3399
+ * websocket `response.create` frames) and strict gateway validators 400 it
3400
+ * with `{"detail":"Unsupported parameter: previous_response_id"}`.
3401
+ */
3402
+ function buildCodexChainedRequestBody(
3403
+ requestBody: RequestBody,
3404
+ state: CodexWebSocketSessionState | undefined,
3405
+ ): RequestBody {
3406
+ const chainable = state?.canAppend === true;
3407
+ const appendInput = chainable
3408
+ ? buildResponsesDeltaInput(state.lastRequest, state.lastResponseItems, requestBody)
3409
+ : null;
3410
+ if (appendInput && appendInput.length > 0 && state?.lastResponseId) {
3411
+ return { ...requestBody, previous_response_id: state.lastResponseId, input: appendInput };
3412
+ }
3413
+ if (chainable && state) {
3414
+ // Chaining was eligible but the prefix/options check failed: history
3415
+ // mutated or options changed — break the chain.
3416
+ CODEX_DEBUG &&
3417
+ logger.debug("[codex] codex append reset", {
3418
+ hadTurnStateHeader: Boolean(state.turnState),
3419
+ hadModelsEtagHeader: Boolean(state.modelsEtag),
3420
+ });
3421
+ resetCodexWebSocketAppendState(state);
3422
+ state.turnState = undefined;
3423
+ state.modelsEtag = undefined;
3424
+ }
3425
+ return requestBody;
3426
+ }
3427
+
3428
+ function toWebSocketUrl(url: string): string {
3429
+ const parsed = new URL(url);
3430
+ if (parsed.protocol === "https:") {
3431
+ parsed.protocol = "wss:";
3432
+ } else if (parsed.protocol === "http:") {
3433
+ parsed.protocol = "ws:";
3434
+ }
3435
+ return parsed.toString();
3436
+ }
3437
+
3438
+ function headersToRecord(headers: Headers): Record<string, string> {
3439
+ const result: Record<string, string> = {};
3440
+ for (const [key, value] of headers.entries()) {
3441
+ result[key] = value;
3442
+ }
3443
+ return result;
3444
+ }
3445
+
3446
+ interface CodexWebSocketRequestTimeouts {
3447
+ idleTimeoutMs?: number;
3448
+ firstEventTimeoutMs?: number;
3449
+ }
3450
+
3451
+ interface CodexWebSocketConnectionOptions {
3452
+ onHandshakeHeaders?: (headers: Headers) => void;
3453
+ proxy?: string;
3454
+ }
3455
+
3456
+ class CodexWebSocketConnection {
3457
+ #url: string;
3458
+ #headers: Record<string, string>;
3459
+ #proxy?: string;
3460
+ #onHandshakeHeaders?: (headers: Headers) => void;
3461
+ #socket: Bun.WebSocket | null = null;
3462
+ #queue: Array<Record<string, unknown> | Error | null> = [];
3463
+ #waiters: Array<() => void> = [];
3464
+ #connectPromise?: Promise<void>;
3465
+ #activeRequest = false;
3466
+ #streamObserver?: (event: RawSseEvent) => void;
3467
+ #heartbeatInterval: NodeJS.Timeout | undefined;
3468
+ #removePongListener?: () => void;
3469
+ #handshakeHeaders?: Headers;
3470
+ #debugResponseLog?: RequestDebugResponseLog;
3471
+ /**
3472
+ * Wall-clock of the most recent inbound activity on this socket — any
3473
+ * decoded message, any pong, or the moment the handshake completed. Used
3474
+ * by {@link isHealthyForReuse} so we don't write a continuation frame into
3475
+ * a TCP-open-but-server-evicted socket whose `readyState` still says OPEN.
3476
+ */
3477
+ #lastInboundAt = 0;
3478
+ /** Wall-clock of the last heartbeat ping we issued; 0 if none yet. */
3479
+ #lastPingAt = 0;
3480
+ /**
3481
+ * Most recent `response.id` accepted on this socket, retained across
3482
+ * requests. Lets the next request drop a trailing/duplicate frame from the
3483
+ * previous (cleanly-completed) response that outlived the queue drain.
3484
+ */
3485
+ #lastSeenResponseId?: string;
3486
+
3487
+ constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
3488
+ this.#url = url;
3489
+ this.#headers = headers;
3490
+ this.#proxy = options.proxy;
3491
+ this.#onHandshakeHeaders = options.onHandshakeHeaders;
3492
+ }
3493
+
3494
+ isOpen(): boolean {
3495
+ return this.#socket?.readyState === WebSocket.OPEN;
3496
+ }
3497
+
3498
+ /** True while a handshake (possibly started by another caller) is still in flight. */
3499
+ isConnecting(): boolean {
3500
+ return this.#connectPromise !== undefined;
3501
+ }
3502
+
3503
+ /**
3504
+ * Stricter variant of {@link isOpen} for the connection-pool reuse gate.
3505
+ * Refuses sockets that have been silent past {@link CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS}.
3506
+ *
3507
+ * Bun's `WebSocket` does not always surface server-side eviction (no
3508
+ * `onclose`, no `onerror`), so a socket can sit in readyState OPEN long
3509
+ * after the upstream has dropped it. Reusing such a socket sends the next
3510
+ * `response.create` into a half-open write buffer and parks the reader
3511
+ * until the first-event / idle timeout fires (issue #1450). Forcing a
3512
+ * reconnect on any suspect socket trades a sub-second handshake for a
3513
+ * 60–300 s stall.
3514
+ */
3515
+ isHealthyForReuse(): boolean {
3516
+ if (!this.isOpen()) return false;
3517
+ const maxIdleMs = CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS;
3518
+ if (maxIdleMs <= 0) return true;
3519
+ // Initial connect sets #lastInboundAt; any later message or pong refreshes
3520
+ // it. A zero value means the field was never initialized, which itself is
3521
+ // a desync — treat as unhealthy.
3522
+ if (this.#lastInboundAt === 0) return false;
3523
+ return Date.now() - this.#lastInboundAt <= maxIdleMs;
3524
+ }
3525
+
3526
+ matchesAuth(headers: Record<string, string>): boolean {
3527
+ return this.#headers.authorization === headers.authorization;
3528
+ }
3529
+
3530
+ close(reason = "done"): void {
3531
+ if (
3532
+ this.#socket &&
3533
+ (this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)
3534
+ ) {
3535
+ this.#socket.close(1000, reason);
3536
+ }
3537
+ this.#socket = null;
3538
+ this.#stopHeartbeat();
3539
+ }
3540
+
3541
+ async connect(signal?: AbortSignal): Promise<void> {
3542
+ if (this.isOpen()) return;
3543
+ if (this.#connectPromise) {
3544
+ logger.time("codexWs:awaitSharedHandshake");
3545
+ await this.#connectPromise;
3546
+ return;
3547
+ }
3548
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
3549
+ this.#connectPromise = promise;
3550
+ const socket = new (WebSocket as unknown as new (url: string, opts: Bun.WebSocketOptions) => Bun.WebSocket)(
3551
+ this.#url,
3552
+ { headers: this.#headers, proxy: this.#proxy },
3553
+ );
3554
+ socket.binaryType = "nodebuffer";
3555
+ this.#socket = socket;
3556
+ let settled = false;
3557
+ let timeout: NodeJS.Timeout | undefined;
3558
+ const clearPending = () => {
3559
+ if (timeout !== undefined) {
3560
+ clearTimeout(timeout);
3561
+ timeout = undefined;
3562
+ }
3563
+ if (signal) signal.removeEventListener("abort", onAbort);
3564
+ };
3565
+ const onAbort = () => {
3566
+ socket.close(1000, "aborted");
3567
+ if (!settled) {
3568
+ settled = true;
3569
+ clearPending();
3570
+ reject(new CodexWebSocketTransportError(`request was aborted`));
3571
+ }
3572
+ };
3573
+ if (signal) {
3574
+ if (signal.aborted) {
3575
+ onAbort();
3576
+ } else {
3577
+ signal.addEventListener("abort", onAbort, { once: true });
3578
+ }
3579
+ }
3580
+ if (!settled) {
3581
+ timeout = setTimeout(() => {
3582
+ socket.close(1000, "connect-timeout");
3583
+ if (!settled) {
3584
+ settled = true;
3585
+ clearPending();
3586
+ reject(new CodexWebSocketTransportError(`connection timeout`));
3587
+ }
3588
+ }, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
3589
+ }
3590
+
3591
+ socket.onopen = event => {
3592
+ if (!settled) {
3593
+ settled = true;
3594
+ clearPending();
3595
+ this.#lastInboundAt = Date.now();
3596
+ this.#captureHandshakeHeaders(socket, event);
3597
+ this.#startHeartbeat(socket);
3598
+ resolve();
3599
+ }
3600
+ };
3601
+ socket.onerror = event => {
3602
+ const eventRecord = event as unknown as Record<string, unknown>;
3603
+ const detail =
3604
+ (typeof eventRecord.message === "string" && eventRecord.message) ||
3605
+ (eventRecord.error instanceof Error && eventRecord.error.message) ||
3606
+ String(event.type);
3607
+ const error = new CodexWebSocketTransportError(`websocket error: ${detail}`);
3608
+ if (!settled) {
3609
+ settled = true;
3610
+ clearPending();
3611
+ reject(error);
3612
+ return;
3613
+ }
3614
+ this.#push(error);
3615
+ };
3616
+ socket.onclose = event => {
3617
+ this.#socket = null;
3618
+ this.#stopHeartbeat();
3619
+ if (!settled) {
3620
+ settled = true;
3621
+ clearPending();
3622
+ reject(new CodexWebSocketTransportError(`websocket closed before open (${event.code})`));
3623
+ return;
3624
+ }
3625
+ this.#push(new CodexWebSocketTransportError(`websocket closed (${event.code})`));
3626
+ this.#push(null);
3627
+ };
3628
+ socket.onmessage = event => {
3629
+ // Stamp inbound activity before parsing so even malformed frames refresh
3630
+ // the liveness clock — what matters for reuse health is that the upstream
3631
+ // is still talking to us, not that every frame is well-formed.
3632
+ this.#lastInboundAt = Date.now();
3633
+ this.#writeDebugWebSocketFrame(event.data);
3634
+ try {
3635
+ const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
3636
+ if (!text) return;
3637
+ const parsed = JSON.parse(text) as Record<string, unknown>;
3638
+ if (parsed.type === "error" && typeof parsed.error === "object" && parsed.error) {
3639
+ const inner = parsed.error as Record<string, unknown>;
3640
+ if (typeof parsed.code !== "string" && typeof inner.code === "string") {
3641
+ parsed.code = inner.code;
3642
+ }
3643
+ if (typeof parsed.message !== "string" && typeof inner.message === "string") {
3644
+ parsed.message = inner.message;
3645
+ }
3646
+ }
3647
+ notifyCodexWebSocketInbound(this.#streamObserver, parsed, text);
3648
+ this.#push(parsed);
3649
+ } catch (error) {
3650
+ notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
3651
+ this.#push(new CodexWebSocketTransportError(`${String(error)}`));
3652
+ }
3653
+ };
3654
+
3655
+ logger.time("codexWs:awaitTcpHandshake");
3656
+ try {
3657
+ await promise;
3658
+ } finally {
3659
+ this.#connectPromise = undefined;
3660
+ }
3661
+ }
3662
+
3663
+ async *streamRequest(
3664
+ request: Record<string, unknown>,
3665
+ timeouts: CodexWebSocketRequestTimeouts,
3666
+ signal?: AbortSignal,
3667
+ onSseEvent?: (event: RawSseEvent) => void,
3668
+ ): AsyncGenerator<Record<string, unknown>> {
3669
+ if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
3670
+ throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
3671
+ }
3672
+ if (this.#activeRequest) {
3673
+ throw new CodexWebSocketTransportError(`websocket request already in progress`);
3674
+ }
3675
+ if (signal?.aborted) {
3676
+ throw new CodexWebSocketTransportError(`request was aborted`);
3677
+ }
3678
+ this.#activeRequest = true;
3679
+ this.#streamObserver = onSseEvent;
3680
+ // Drain any non-error frames left over from a prior request before sending.
3681
+ // `CodexStreamProcessor.process` breaks its `for-await` on the terminal event,
3682
+ // which interrupts our generator at `yield next` (the post-yield `break`
3683
+ // never runs). Any frame that landed between the consumer's break and the
3684
+ // generator's `finally` lingers in `#queue` and would otherwise become the
3685
+ // first frame of THIS request — a stale `response.completed` would end the
3686
+ // turn immediately with empty output, and a stale non-progress frame would
3687
+ // flip `sawFirstEvent` and silently downgrade the first-event timeout to
3688
+ // the longer idle timeout. Transport errors are preserved so we surface
3689
+ // the death signal instead of writing into a dead socket.
3690
+ this.#dropStaleFrames();
3691
+ const onAbort = () => {
3692
+ this.close("aborted");
3693
+ this.#push(new CodexWebSocketTransportError(`request was aborted`));
3694
+ };
3695
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
3696
+
3697
+ try {
3698
+ const debugSession = isRequestDebugEnabled()
3699
+ ? await createRequestDebugSession({
3700
+ protocol: "websocket",
3701
+ method: "POST",
3702
+ url: this.#url,
3703
+ headers: this.#headers,
3704
+ body: request,
3705
+ })
3706
+ : undefined;
3707
+ this.#debugResponseLog = debugSession
3708
+ ? await debugSession.openResponseLog("WebSocket 101 Switching Protocols", this.#handshakeHeaders)
3709
+ : undefined;
3710
+
3711
+ const requestPayload = JSON.stringify(request);
3712
+ notifyCodexWebSocketOutbound(onSseEvent, request, requestPayload);
3713
+ // Re-check liveness: the debug-session await above can outlive the socket.
3714
+ const socket = this.#socket;
3715
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
3716
+ throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
3717
+ }
3718
+ try {
3719
+ socket.send(requestPayload);
3720
+ } catch (error) {
3721
+ throw new CodexWebSocketTransportError(
3722
+ `websocket send failed: ${error instanceof Error ? error.message : String(error)}`,
3723
+ );
3724
+ }
3725
+ let sawFirstEvent = false;
3726
+ const { idleTimeoutMs, firstEventTimeoutMs } = timeouts;
3727
+ let lastProgressAt = Date.now();
3728
+ let lastProgressEventType: string | undefined;
3729
+ let lastEventAt = lastProgressAt;
3730
+ let lastEventType: string | undefined;
3731
+ // Cross-request frame guard: lock onto this response's id and reject
3732
+ // frames belonging to another response interleaved on the reused socket.
3733
+ let activeResponseId: string | undefined;
3734
+ let lastSequence: number | undefined;
3735
+ const priorResponseId = this.#lastSeenResponseId;
3736
+ while (true) {
3737
+ let timeoutMs: number | undefined;
3738
+ let timeoutReason: string;
3739
+ if (sawFirstEvent) {
3740
+ timeoutReason = createCodexWebSocketTimeoutMessage("idle timeout waiting for websocket", {
3741
+ lastEventAt,
3742
+ lastEventType,
3743
+ lastProgressAt,
3744
+ lastProgressEventType,
3745
+ });
3746
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
3747
+ timeoutMs = idleTimeoutMs - (Date.now() - lastProgressAt);
3748
+ if (timeoutMs <= 0) {
3749
+ CODEX_DEBUG &&
3750
+ logger.debug("[codex] codex websocket idle timeout", {
3751
+ lastEventType,
3752
+ lastProgressEventType,
3753
+ msSinceLastEvent: Date.now() - lastEventAt,
3754
+ msSinceLastProgress: Date.now() - lastProgressAt,
3755
+ });
3756
+ throw new CodexWebSocketTransportError(`${timeoutReason}`);
3757
+ }
3758
+ }
3759
+ } else {
3760
+ timeoutReason = createCodexWebSocketTimeoutMessage("timeout waiting for first websocket event", {
3761
+ lastEventAt,
3762
+ lastEventType,
3763
+ lastProgressAt,
3764
+ lastProgressEventType,
3765
+ });
3766
+ if (firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0) {
3767
+ timeoutMs = firstEventTimeoutMs;
3768
+ }
3769
+ }
3770
+ const next = await this.#nextMessage(timeoutMs, timeoutReason);
3771
+ if (next instanceof Error) {
3772
+ throw next;
3773
+ }
3774
+ if (next === null) {
3775
+ throw new CodexWebSocketTransportError(`websocket closed before response completion`);
3776
+ }
3777
+ const eventType = typeof next.type === "string" ? next.type : "";
3778
+ // Cross-request frame guard. The socket is reused across turns. Upstream
3779
+ // codex-rs leans on the protocol guarantee that nothing follows a
3780
+ // response's terminal event, but our queue can still surface a trailing
3781
+ // or duplicate frame from a cleanly-completed prior response after
3782
+ // #dropStaleFrames() drained the queue at send time. Attaching such a
3783
+ // frame to THIS turn misattributes an earlier turn's output (a stale
3784
+ // `response.completed` ends the turn early; a stale item makes the model
3785
+ // see an unrelated call). Only lifecycle events (created/completed/
3786
+ // failed/incomplete) carry a `response.id` — exactly the harmful ones —
3787
+ // so key the guard on it and let idless frames (deltas, the rate-limit/
3788
+ // metadata preamble, created-less streams) pass through, matching
3789
+ // upstream rather than gating on `response.created`.
3790
+ const frameResponseId = extractCodexFrameResponseId(next);
3791
+ const frameSequence = extractCodexFrameSequenceNumber(next);
3792
+ if (frameResponseId !== undefined) {
3793
+ if (activeResponseId === undefined) {
3794
+ if (priorResponseId !== undefined && frameResponseId === priorResponseId) {
3795
+ // Trailing/duplicate frame of the previous response that
3796
+ // outlived the drain. Drop without locking or advancing the
3797
+ // first-event clocks so our own response can still start.
3798
+ continue;
3799
+ }
3800
+ activeResponseId = frameResponseId;
3801
+ } else if (frameResponseId !== activeResponseId) {
3802
+ // A different response is interleaving on the socket; the idless
3803
+ // deltas that follow are indistinguishable, so fail closed
3804
+ // (retryable) instead of risking misattribution.
3805
+ this.close("stale-frame");
3806
+ throw new CodexWebSocketTransportError(
3807
+ `websocket frame for response ${frameResponseId} interleaved into active response ${activeResponseId}`,
3808
+ );
3809
+ }
3810
+ this.#lastSeenResponseId = frameResponseId;
3811
+ }
3812
+ if (frameSequence !== undefined) {
3813
+ if (activeResponseId !== undefined && lastSequence !== undefined && frameSequence < lastSequence) {
3814
+ this.close("stale-frame");
3815
+ throw new CodexWebSocketTransportError(
3816
+ `websocket sequence_number ${frameSequence} regressed below ${lastSequence} within response ${activeResponseId}`,
3817
+ );
3818
+ }
3819
+ lastSequence = frameSequence;
3820
+ }
3821
+ sawFirstEvent = true;
3822
+ lastEventAt = Date.now();
3823
+ lastEventType = eventType || undefined;
3824
+ if (isCodexStreamProgressEvent(next)) {
3825
+ lastProgressAt = lastEventAt;
3826
+ lastProgressEventType = lastEventType;
3827
+ }
3828
+ yield next;
3829
+ if (
3830
+ eventType === "response.completed" ||
3831
+ eventType === "response.done" ||
3832
+ eventType === "response.incomplete" ||
3833
+ eventType === "response.failed" ||
3834
+ eventType === "error"
3835
+ ) {
3836
+ break;
3837
+ }
3838
+ }
3839
+ } finally {
3840
+ this.#activeRequest = false;
3841
+ this.#streamObserver = undefined;
3842
+ if (signal) {
3843
+ signal.removeEventListener("abort", onAbort);
3844
+ }
3845
+ const debugResponseLog = this.#debugResponseLog;
3846
+ this.#debugResponseLog = undefined;
3847
+ await debugResponseLog?.close();
3848
+ }
3849
+ }
3850
+
3851
+ #captureHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): void {
3852
+ const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
3853
+ if (!headers) return;
3854
+ this.#handshakeHeaders = headers;
3855
+ this.#onHandshakeHeaders?.(headers);
3856
+ }
3857
+
3858
+ #writeDebugWebSocketFrame(data: unknown): void {
3859
+ const log = this.#debugResponseLog;
3860
+ if (!log) return;
3861
+ if (typeof data === "string") {
3862
+ log.write(data);
3863
+ return;
3864
+ }
3865
+ if (data instanceof Uint8Array) {
3866
+ log.write(data);
3867
+ return;
3868
+ }
3869
+ if (data instanceof ArrayBuffer) {
3870
+ log.write(new Uint8Array(data));
3871
+ return;
3872
+ }
3873
+ log.write(String(data));
3874
+ }
3875
+
3876
+ #startHeartbeat(socket: Bun.WebSocket): void {
3877
+ this.#stopHeartbeat();
3878
+ const intervalMs = CODEX_WEBSOCKET_PING_INTERVAL_MS;
3879
+ if (intervalMs <= 0) return;
3880
+
3881
+ this.#lastPingAt = 0;
3882
+ const socketEventTarget = socket as EventTarget;
3883
+ const onPong = () => {
3884
+ // Pongs are inbound activity — refresh the reuse-health clock so a quiet
3885
+ // but ping-responsive socket stays trustworthy across requests.
3886
+ this.#lastInboundAt = Date.now();
3887
+ };
3888
+ if (
3889
+ typeof socketEventTarget.addEventListener === "function" &&
3890
+ typeof socketEventTarget.removeEventListener === "function"
3891
+ ) {
3892
+ socketEventTarget.addEventListener("pong", onPong);
3893
+ this.#removePongListener = () => socketEventTarget.removeEventListener("pong", onPong);
3894
+ }
3895
+
3896
+ this.#heartbeatInterval = setInterval(() => {
3897
+ if (this.#socket !== socket || socket.readyState !== WebSocket.OPEN) {
3898
+ this.#stopHeartbeat();
3899
+ return;
3900
+ }
3901
+ // Fail-closed on missing pongs even when no pong has ever been observed.
3902
+ // The previous `#observedPong &&` guard disabled the timeout entirely on
3903
+ // runtimes where Bun does not surface a `pong` event for our outgoing
3904
+ // pings (issue #1450) — letting truly dead sockets sail through the
3905
+ // pool until the per-request first-event / idle timeout (60–300 s)
3906
+ // finally fired. Instead, trigger on inbound silence: if we sent a
3907
+ // ping at least `pongTimeoutMs` ago and have received no traffic of
3908
+ // any kind (data frame or pong) since, the socket is unhealthy.
3909
+ const pongTimeoutMs = CODEX_WEBSOCKET_PONG_TIMEOUT_MS;
3910
+ if (
3911
+ pongTimeoutMs > 0 &&
3912
+ this.#lastPingAt > 0 &&
3913
+ this.#lastPingAt > this.#lastInboundAt &&
3914
+ Date.now() - this.#lastPingAt > pongTimeoutMs
3915
+ ) {
3916
+ this.#failQueue(new CodexWebSocketTransportError(`websocket pong timeout`), "pong-timeout");
3917
+ return;
3918
+ }
3919
+ if (typeof socket.ping !== "function") {
3920
+ this.#stopHeartbeat();
3921
+ return;
3922
+ }
3923
+ try {
3924
+ socket.ping();
3925
+ this.#lastPingAt = Date.now();
3926
+ } catch (error) {
3927
+ this.#failQueue(
3928
+ new CodexWebSocketTransportError(
3929
+ `websocket ping failed: ${error instanceof Error ? error.message : String(error)}`,
3930
+ ),
3931
+ "ping-failed",
3932
+ );
3933
+ }
3934
+ }, intervalMs);
3935
+ this.#heartbeatInterval.unref();
3936
+ }
3937
+
3938
+ #stopHeartbeat(): void {
3939
+ if (this.#heartbeatInterval) {
3940
+ clearInterval(this.#heartbeatInterval);
3941
+ this.#heartbeatInterval = undefined;
3942
+ }
3943
+ if (this.#removePongListener) {
3944
+ this.#removePongListener();
3945
+ this.#removePongListener = undefined;
3946
+ }
3947
+ this.#lastPingAt = 0;
3948
+ }
3949
+
3950
+ #failQueue(error: Error, closeReason: string): void {
3951
+ CODEX_DEBUG && logger.debug("[codex] codex websocket transport failure", { error: error.message, closeReason });
3952
+ this.#queue.length = 0;
3953
+ this.#queue.push(error);
3954
+ this.close(closeReason);
3955
+ this.#wakeWaiters();
3956
+ }
3957
+
3958
+ /**
3959
+ * Discard data frames from a previous request that remained in `#queue`
3960
+ * after the consumer broke out on the terminal event. Preserves any queued
3961
+ * transport error (from `onerror` / `onclose` / `#failQueue`) so the next
3962
+ * `#nextMessage` surfaces the death signal instead of waiting it out.
3963
+ *
3964
+ * Returns the number of frames dropped (test/debug visibility only).
3965
+ */
3966
+ #dropStaleFrames(): number {
3967
+ if (this.#queue.length === 0) return 0;
3968
+ const surviving = this.#queue.filter(item => item instanceof Error);
3969
+ const dropped = this.#queue.length - surviving.length;
3970
+ if (dropped === 0) return 0;
3971
+ this.#queue.length = 0;
3972
+ for (const item of surviving) this.#queue.push(item);
3973
+ CODEX_DEBUG && logger.debug("[codex] codex websocket dropped stale frames before request", { dropped });
3974
+ return dropped;
3975
+ }
3976
+
3977
+ #wakeWaiters(): void {
3978
+ for (;;) {
3979
+ const waiter = this.#waiters.shift();
3980
+ if (!waiter) break;
3981
+ waiter();
3982
+ }
3983
+ }
3984
+
3985
+ #push(item: Record<string, unknown> | Error | null): void {
3986
+ if (item instanceof Error) {
3987
+ // Append after frames already received instead of wiping them: a queued
3988
+ // terminal event (e.g. `response.completed` followed by an eager server
3989
+ // close) must still reach the consumer rather than morph into a spurious
3990
+ // transport failure. `#dropStaleFrames` keeps errors across requests, so
3991
+ // the death signal still surfaces if the data frames go unconsumed.
3992
+ this.#queue.push(item);
3993
+ this.#wakeWaiters();
3994
+ return;
3995
+ }
3996
+ if (item !== null && this.#queue.length >= CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY) {
3997
+ this.#failQueue(
3998
+ new CodexWebSocketTransportError(
3999
+ `websocket message queue exceeded ${CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY} items`,
4000
+ ),
4001
+ "queue-overflow",
4002
+ );
4003
+ return;
4004
+ }
4005
+ this.#queue.push(item);
4006
+ const waiter = this.#waiters.shift();
4007
+ if (waiter) waiter();
4008
+ }
4009
+
4010
+ async #nextMessage(
4011
+ timeoutMs: number | undefined,
4012
+ timeoutReason: string,
4013
+ ): Promise<Record<string, unknown> | Error | null> {
4014
+ while (this.#queue.length === 0) {
4015
+ const { promise, resolve } = Promise.withResolvers<void>();
4016
+ this.#waiters.push(resolve);
4017
+ let timedOut = false;
4018
+ let timeout: NodeJS.Timeout | undefined;
4019
+ if (timeoutMs !== undefined && timeoutMs > 0) {
4020
+ timeout = setTimeout(() => {
4021
+ timedOut = true;
4022
+ const waiterIndex = this.#waiters.indexOf(resolve);
4023
+ if (waiterIndex >= 0) {
4024
+ this.#waiters.splice(waiterIndex, 1);
4025
+ }
4026
+ resolve();
4027
+ }, timeoutMs);
4028
+ }
4029
+ await promise;
4030
+ if (timeout) clearTimeout(timeout);
4031
+ if (timedOut && this.#queue.length === 0) {
4032
+ return new CodexWebSocketTransportError(`${timeoutReason}`);
4033
+ }
4034
+ }
4035
+ return this.#queue.shift() ?? null;
4036
+ }
4037
+ }
4038
+
4039
+ async function getOrCreateCodexWebSocketConnection(
4040
+ state: CodexWebSocketSessionState,
4041
+ url: string,
4042
+ headers: Headers,
4043
+ provider: string,
4044
+ signal?: AbortSignal,
4045
+ ): Promise<CodexWebSocketConnection> {
4046
+ const proxy = getProxyForUrl(provider, new URL(url));
4047
+ const headerRecord = headersToRecord(headers);
4048
+ // Join an in-flight handshake instead of tearing it down: closing a
4049
+ // CONNECTING socket rejects the concurrent caller (prewarm racing the first
4050
+ // request) with a fatal "websocket closed before open", which would disable
4051
+ // websockets for the entire session.
4052
+ // Bounded re-join: a fresh handshake may have been started by yet another
4053
+ // caller while we awaited the previous one.
4054
+ for (let joinAttempt = 0; joinAttempt < 3; joinAttempt += 1) {
4055
+ const pending = state.connection;
4056
+ if (!pending || pending.isOpen() || !pending.isConnecting()) break;
4057
+ try {
4058
+ await pending.connect(signal);
4059
+ } catch {
4060
+ // The handshake owner surfaces its own failure; re-evaluate below
4061
+ // (state.connection may have been replaced or cleared).
4062
+ }
4063
+ }
4064
+ if (state.connection?.isOpen()) {
4065
+ if (!state.connection.matchesAuth(headerRecord)) {
4066
+ state.connection.close("token-refresh");
4067
+ resetCodexWebSocketAppendState(state);
4068
+ } else if (state.connection.isHealthyForReuse()) {
4069
+ logger.time("codexWs:reuseOpenSocket");
4070
+ return state.connection;
4071
+ } else {
4072
+ // Open in readyState but no inbound traffic recently — likely server-
4073
+ // evicted (issue #1450). Force a fresh handshake instead of writing
4074
+ // `response.create` into a half-open buffer and waiting out the
4075
+ // first-event timeout. Drop append state because the new socket
4076
+ // won't carry the prior `previous_response_id` context.
4077
+ CODEX_DEBUG && logger.debug("[codex] codex websocket reuse rejected by health check", {});
4078
+ state.connection.close("stale-reuse");
4079
+ resetCodexWebSocketAppendState(state);
4080
+ }
4081
+ }
4082
+ state.connection?.close("reconnect");
4083
+ resetCodexWebSocketAppendState(state);
4084
+ logger.time("codexWs:newSocket");
4085
+ state.connection = new CodexWebSocketConnection(url, headerRecord, {
4086
+ onHandshakeHeaders: handshakeHeaders => {
4087
+ updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
4088
+ },
4089
+ proxy,
4090
+ });
4091
+ await state.connection.connect(signal);
4092
+ return state.connection;
4093
+ }
4094
+
4095
+ /**
4096
+ * Compress an SSE request body with zstd. Returns `undefined` when
4097
+ * compression is disabled or fails, in which case the caller sends the
4098
+ * plain JSON string without a `content-encoding` header.
4099
+ */
4100
+ function compressCodexRequestBody(bodyJson: string, baseUrl: string): Uint8Array | undefined {
4101
+ if (!isOfficialCodexApiUrl(baseUrl) || !$flag("PI_CODEX_ZSTD", true)) return undefined;
4102
+ try {
4103
+ return Bun.zstdCompressSync(bodyJson, { level: 3 });
4104
+ } catch (error) {
4105
+ CODEX_DEBUG &&
4106
+ logger.debug("[codex] codex request body compression failed", {
4107
+ error: error instanceof Error ? error.message : String(error),
4108
+ });
4109
+ return undefined;
4110
+ }
4111
+ }
4112
+
4113
+ async function openCodexSseEventStream(
4114
+ url: string,
4115
+ requestHeaders: Record<string, string> | undefined,
4116
+ accountId: string | undefined,
4117
+ apiKey: string,
4118
+ sessionId: string | undefined,
4119
+ body: RequestBody,
4120
+ state: CodexWebSocketSessionState | undefined,
4121
+ responsesLite: boolean,
4122
+ codexClientVersion: string,
4123
+ requestMetadata: CodexRequestMetadata | undefined,
4124
+ signal: AbortSignal | undefined,
4125
+ firstEventTimeoutMs: number | undefined,
4126
+ codexSseMaxAttempts: number | undefined,
4127
+ onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
4128
+ fetchOverride?: FetchImpl,
4129
+ ): Promise<AsyncGenerator<Record<string, unknown>>> {
4130
+ const headers = createCodexHeaders(
4131
+ requestHeaders,
4132
+ accountId,
4133
+ apiKey,
4134
+ codexClientVersion,
4135
+ sessionId,
4136
+ "sse",
4137
+ state,
4138
+ responsesLite,
4139
+ requestMetadata,
4140
+ await getCodexAttestationHeader(accountId),
4141
+ );
4142
+ // `wrapCodexSseStream` arms the iterator-level idle watchdog only after this
4143
+ // fetch resolves. Each transport attempt needs its own pre-response timer:
4144
+ // the retry loop's base signal remains reserved for caller cancellation, so
4145
+ // an internal timeout stays retryable while an explicit abort fails fast.
4146
+ let clearPreResponseTimeout: (() => void) | undefined;
4147
+ const fetchAttempt: FetchImpl = async (input, init) => {
4148
+ try {
4149
+ return await (fetchOverride ?? fetch)(input, init);
4150
+ } finally {
4151
+ clearPreResponseTimeout?.();
4152
+ clearPreResponseTimeout = undefined;
4153
+ }
4154
+ };
4155
+ const bodyJson = JSON.stringify(body);
4156
+ const compressedBody = compressCodexRequestBody(bodyJson, url);
4157
+ if (compressedBody !== undefined) {
4158
+ headers.set("content-encoding", "zstd");
4159
+ }
4160
+ CODEX_DEBUG &&
4161
+ logger.debug("[codex] codex request", {
4162
+ url,
4163
+ model: body.model,
4164
+ headers: redactHeaders(headers),
4165
+ sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
4166
+ sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
4167
+ });
4168
+
4169
+ const send = (requestBody: string | Uint8Array): Promise<Response> =>
4170
+ fetchWithRetry(url, {
4171
+ method: "POST",
4172
+ headers,
4173
+ body: requestBody,
4174
+ signal,
4175
+ prepareInit: () => {
4176
+ const watchdog = armPreResponseTimeout(signal, firstEventTimeoutMs);
4177
+ clearPreResponseTimeout = watchdog.clear;
4178
+ return { signal: watchdog.signal };
4179
+ },
4180
+ maxAttempts: resolveCodexSseMaxAttempts(codexSseMaxAttempts),
4181
+ defaultDelayMs: attempt => CODEX_RETRY_DELAY_MS * (attempt + 1),
4182
+ maxDelayMs: CODEX_RATE_LIMIT_BUDGET_MS,
4183
+ fetch: fetchAttempt,
4184
+ timeout: false,
4185
+ });
4186
+ let response: Response;
4187
+ try {
4188
+ response = await send(compressedBody ?? bodyJson);
4189
+ if (compressedBody !== undefined && (response.status === 400 || response.status === 415)) {
4190
+ const rejectedStatus = response.status;
4191
+ await response.body?.cancel();
4192
+ headers.delete("content-encoding");
4193
+ CODEX_DEBUG &&
4194
+ logger.debug("[codex] retrying request without zstd after encoding rejection", {
4195
+ url,
4196
+ status: rejectedStatus,
4197
+ });
4198
+ response = await send(bodyJson);
4199
+ }
4200
+ } finally {
4201
+ clearPreResponseTimeout?.();
4202
+ }
4203
+ CODEX_DEBUG &&
4204
+ logger.debug("[codex] codex response", {
4205
+ url: response.url,
4206
+ status: response.status,
4207
+ statusText: response.statusText,
4208
+ contentType: response.headers.get("content-type") || null,
4209
+ cfRay: response.headers.get("cf-ray") || null,
4210
+ });
4211
+ if (!response.ok) {
4212
+ throw await CodexApiError.fromResponse(response);
4213
+ }
4214
+ updateCodexSessionMetadataFromHeaders(state, response.headers);
4215
+ if (!response.body) {
4216
+ throw new CodexProviderStreamError("No response body", false);
4217
+ }
4218
+ return readSseJson<Record<string, unknown>>(response.body, signal, event =>
4219
+ onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, undefined),
4220
+ );
4221
+ }
4222
+
4223
+ function createCodexHeaders(
4224
+ initHeaders: Record<string, string> | undefined,
4225
+ accountId: string | undefined,
4226
+ accessToken: string,
4227
+ codexClientVersion: string,
4228
+ sessionId?: string,
4229
+ transport: CodexTransport = "sse",
4230
+ state?: CodexWebSocketSessionState,
4231
+ responsesLite = false,
4232
+ requestMetadata?: CodexCompatibilityIdentity,
4233
+ attestation?: string,
4234
+ ): Headers {
4235
+ const headers = new Headers(initHeaders ?? {});
4236
+ headers.delete("x-api-key");
4237
+ headers.set("Authorization", `Bearer ${accessToken}`);
4238
+ if (accountId) headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
4239
+ if (attestation) {
4240
+ headers.set(OPENAI_HEADERS.ATTESTATION, attestation);
4241
+ } else {
4242
+ headers.delete(OPENAI_HEADERS.ATTESTATION);
4243
+ }
4244
+ const betaHeader =
4245
+ transport === "websocket"
4246
+ ? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2
4247
+ : OPENAI_HEADER_VALUES.BETA_RESPONSES;
4248
+ headers.delete(OPENAI_HEADERS.BETA);
4249
+ headers.delete("openai-beta");
4250
+ headers.set(OPENAI_HEADERS.BETA, betaHeader);
4251
+ headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
4252
+ headers.set(OPENAI_HEADERS.VERSION, codexClientVersion);
4253
+ headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
4254
+ if (sessionId) {
4255
+ headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
4256
+ headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
4257
+ headers.set("x-client-request-id", sessionId);
4258
+ } else {
4259
+ headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
4260
+ headers.delete(OPENAI_HEADERS.SESSION_ID);
4261
+ headers.delete("x-client-request-id");
4262
+ }
4263
+ headers.delete(OPENAI_HEADERS.INSTALLATION_ID);
4264
+ if (requestMetadata) {
4265
+ applyCodexCompatibilityHeaders(headers, requestMetadata);
4266
+ } else {
4267
+ headers.delete(OPENAI_HEADERS.SCOPED_SESSION_ID);
4268
+ headers.delete(OPENAI_HEADERS.THREAD_ID);
4269
+ headers.delete(OPENAI_HEADERS.WINDOW_ID);
4270
+ headers.delete(OPENAI_HEADERS.TURN_METADATA);
4271
+ }
4272
+ if (state?.turnState) {
4273
+ headers.set(X_CODEX_TURN_STATE_HEADER, state.turnState);
4274
+ } else {
4275
+ headers.delete(X_CODEX_TURN_STATE_HEADER);
4276
+ }
4277
+ if (state?.modelsEtag) {
4278
+ headers.set(X_MODELS_ETAG_HEADER, state.modelsEtag);
4279
+ } else {
4280
+ headers.delete(X_MODELS_ETAG_HEADER);
4281
+ }
4282
+ if (responsesLite) {
4283
+ headers.set(OPENAI_HEADERS.RESPONSES_LITE, "true");
4284
+ } else {
4285
+ headers.delete(OPENAI_HEADERS.RESPONSES_LITE);
4286
+ }
4287
+ if (transport === "sse") {
4288
+ headers.set("accept", "text/event-stream");
4289
+ headers.set("content-type", "application/json");
4290
+ } else {
4291
+ headers.delete("accept");
4292
+ headers.delete("content-type");
4293
+ }
4294
+ return headers;
4295
+ }
4296
+
4297
+ function redactHeaders(headers: Headers): Record<string, string> {
4298
+ const redacted: Record<string, string> = {};
4299
+ for (const [key, value] of headers.entries()) {
4300
+ const lower = key.toLowerCase();
4301
+ if (lower === "authorization") {
4302
+ redacted[key] = "Bearer [redacted]";
4303
+ continue;
4304
+ }
4305
+ if (
4306
+ lower.includes("account") ||
4307
+ lower.includes("session") ||
4308
+ lower.includes("conversation") ||
4309
+ lower.includes("thread") ||
4310
+ lower.includes("window") ||
4311
+ lower.includes("installation") ||
4312
+ lower.startsWith("x-codex-turn") ||
4313
+ lower === "x-client-request-id" ||
4314
+ lower === "cookie"
4315
+ ) {
4316
+ redacted[key] = "[redacted]";
4317
+ continue;
4318
+ }
4319
+ redacted[key] = value;
4320
+ }
4321
+ return redacted;
4322
+ }
4323
+
4324
+ /** Resolve a Codex Responses endpoint exactly as the chat and compaction transports do. */
4325
+ export function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
4326
+ const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : CODEX_BASE_URL;
4327
+ const normalized = raw.replace(/\/+$/, "");
4328
+ if (normalized.endsWith("/codex/responses")) return normalized;
4329
+ if (normalized.endsWith("/codex")) return `${normalized}/responses`;
4330
+ return `${normalized}/codex/responses`;
4331
+ }
4332
+
4333
+ function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
4334
+ const messages: ResponseInput = [];
4335
+
4336
+ const normalizeToolCallId = (id: string): string => {
4337
+ if (!id.includes("|")) return id;
4338
+ const [callId, itemId] = id.split("|");
4339
+ const sanitizedCallId = callId.replace(/[^a-zA-Z0-9_-]/g, "_");
4340
+ let sanitizedItemId = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
4341
+ if (!sanitizedItemId.startsWith("fc")) {
4342
+ sanitizedItemId = `fc_${sanitizedItemId}`;
4343
+ }
4344
+ let normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
4345
+ let normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
4346
+ normalizedCallId = normalizedCallId.replace(/_+$/, "");
4347
+ normalizedItemId = normalizedItemId.replace(/_+$/, "");
4348
+ return `${normalizedCallId}|${normalizedItemId}`;
4349
+ };
4350
+
4351
+ const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
4352
+ // gpt-5.x reject raw Harmony control-token spellings anywhere in replayed
4353
+ // input, including the model's own tool-call arguments (#6913).
4354
+ const escapeControlTokens = isHarmonyDialectModel(model);
4355
+ let msgIndex = 0;
4356
+ // Track call_ids that originated as custom tool calls so paired tool-result
4357
+ // messages can be replayed as `custom_tool_call_output` rather than
4358
+ // `function_call_output` (OpenAI rejects mismatched pairs).
4359
+ const customCallIds = new Set<string>();
4360
+ const knownCallIds = new Set<string>();
4361
+ const computerCallIds = new Set<string>();
4362
+
4363
+ for (const msg of transformedMessages) {
4364
+ if (msg.role === "user" || msg.role === "developer") {
4365
+ const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
4366
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider) as
4367
+ | Array<ResponseInput[number]>
4368
+ | undefined;
4369
+ if (historyItems) {
4370
+ const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array<ResponseInput[number]>;
4371
+ const replayItems =
4372
+ model.supportsComputerUse === true
4373
+ ? redactedHistoryItems
4374
+ : unrollCodexComputerItems(redactedHistoryItems, model.compat.supportsImageDetailOriginal);
4375
+ for (const item of replayItems) {
4376
+ if (item.type === "custom_tool_call") {
4377
+ customCallIds.add(item.call_id);
4378
+ }
4379
+ if (item.type === "computer_call") computerCallIds.add(item.call_id);
4380
+ if ((item.type === "function_call" || item.type === "custom_tool_call") && item.call_id) {
4381
+ knownCallIds.add(item.call_id);
4382
+ }
4383
+ }
4384
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
4385
+ msgIndex += 1;
4386
+ continue;
4387
+ }
4388
+
4389
+ const normalizedContent = normalizeInputMessageContent(model, msg.content);
4390
+ if (normalizedContent.length === 0) continue;
4391
+ messages.push({ role: msg.role, content: normalizedContent });
4392
+ msgIndex += 1;
4393
+ continue;
4394
+ }
4395
+
4396
+ if (msg.role === "assistant") {
4397
+ const assistantMsg = msg as AssistantMessage;
4398
+ // Native items are model-bound (reasoning carries encrypted content
4399
+ // minted by the producing model); after a mid-session model switch fall
4400
+ // back to block re-encode, which strips foreign signatures.
4401
+ const providerPayload =
4402
+ assistantMsg.api === model.api && assistantMsg.model === model.id
4403
+ ? getOpenAIResponsesHistoryPayload(assistantMsg.providerPayload, model.provider, assistantMsg.provider)
4404
+ : undefined;
4405
+ const historyItems = providerPayload?.items as Array<Record<string, unknown>> | undefined;
4406
+ let suppressHiddenEmptyFallback = false;
4407
+ if (historyItems) {
4408
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems);
4409
+ if (sanitizedHistoryItems) {
4410
+ const rawReplayItems =
4411
+ model.supportsComputerUse === true
4412
+ ? sanitizedHistoryItems
4413
+ : unrollCodexComputerItems(sanitizedHistoryItems, model.compat.supportsImageDetailOriginal);
4414
+ const replayItems = escapeControlTokens ? escapeReplayedControlTokens(rawReplayItems) : rawReplayItems;
4415
+ for (const item of replayItems) {
4416
+ if (item.type === "custom_tool_call") {
4417
+ customCallIds.add(item.call_id);
4418
+ }
4419
+ if (item.type === "computer_call") computerCallIds.add(item.call_id);
4420
+ if ((item.type === "function_call" || item.type === "custom_tool_call") && item.call_id) {
4421
+ knownCallIds.add(item.call_id);
4422
+ }
4423
+ }
4424
+ if (providerPayload?.dt) {
4425
+ messages.push(...replayItems);
4426
+ } else {
4427
+ messages.splice(0, messages.length, ...replayItems);
4428
+ // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
4429
+ }
4430
+ msgIndex += 1;
4431
+ continue;
4432
+ }
4433
+ suppressHiddenEmptyFallback = true;
4434
+ }
4435
+
4436
+ const convertedOutputItems = convertResponsesAssistantMessage(
4437
+ model.supportsComputerUse === true ? assistantMsg : unrollCodexComputerAssistantMessage(assistantMsg),
4438
+ model,
4439
+ msgIndex,
4440
+ knownCallIds,
4441
+ !suppressHiddenEmptyFallback,
4442
+ customCallIds,
4443
+ false,
4444
+ true,
4445
+ undefined,
4446
+ computerCallIds,
4447
+ );
4448
+ const outputItems = suppressHiddenEmptyFallback
4449
+ ? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
4450
+ : convertedOutputItems;
4451
+ if (outputItems.length > 0) {
4452
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
4453
+ }
4454
+ msgIndex += 1;
4455
+ continue;
4456
+ }
4457
+
4458
+ if (msg.role === "toolResult") {
4459
+ appendResponsesToolResultMessages(
4460
+ messages,
4461
+ model.supportsComputerUse === true ? msg : unrollCodexComputerToolResult(msg),
4462
+ model,
4463
+ false,
4464
+ model.compat.supportsImageDetailOriginal,
4465
+ knownCallIds,
4466
+ customCallIds,
4467
+ true,
4468
+ computerCallIds,
4469
+ );
4470
+ }
4471
+
4472
+ msgIndex += 1;
4473
+ }
4474
+
4475
+ return messages;
4476
+ }
4477
+
4478
+ function normalizeInputMessageContent(
4479
+ model: Model<"openai-codex-responses">,
4480
+ content: string | Array<{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string }>,
4481
+ ): ResponseInputContent[] {
4482
+ // gpt-5.x codex rejects reserved Harmony control-token spellings in input
4483
+ // data; escape the transport copy of untrusted user text so ordinary docs or
4484
+ // grep results cannot poison the session (#6913). History is left untouched.
4485
+ const escapeControlTokens = isHarmonyDialectModel(model);
4486
+ if (typeof content === "string") {
4487
+ if (!content || content.trim() === "") return [];
4488
+ const text = content.toWellFormed();
4489
+ return [{ type: "input_text", text: escapeControlTokens ? escapeHarmonyControlTokens(text) : text }];
4490
+ }
4491
+
4492
+ return (
4493
+ convertResponsesInputContent(
4494
+ content,
4495
+ model.input.includes("image"),
4496
+ model.compat.supportsImageDetailOriginal,
4497
+ escapeControlTokens,
4498
+ ) ?? []
4499
+ );
4500
+ }
4501
+
4502
+ /** @internal Exported for tests. */
4503
+ export { convertMessages as convertCodexResponsesMessages };
4504
+
4505
+ type CodexToolPayload =
4506
+ | { type: "computer"; name?: never }
4507
+ | {
4508
+ type: "function";
4509
+ name: string;
4510
+ description: string;
4511
+ parameters: Record<string, unknown>;
4512
+ strict?: boolean;
4513
+ }
4514
+ | {
4515
+ type: "custom";
4516
+ name: string;
4517
+ description: string;
4518
+ format: { type: "grammar"; syntax: "lark" | "regex"; definition: string };
4519
+ };
4520
+
4521
+ /** @internal Exported for tests. */
4522
+ export function convertOpenAICodexResponsesTools(
4523
+ tools: Tool[],
4524
+ model: Model<"openai-codex-responses">,
4525
+ ): CodexToolPayload[] {
4526
+ const allowFreeform = model.applyPatchToolType === "freeform";
4527
+ const payloads: CodexToolPayload[] = [];
4528
+ for (const tool of tools) {
4529
+ // Subscription models default to the function fallback. Explicit metadata
4530
+ // remains authoritative for future Codex endpoints that implement GA computer use.
4531
+ if (tool.native?.type === "computer" && model.supportsComputerUse === true) {
4532
+ payloads.push({ type: "computer" });
4533
+ continue;
4534
+ }
4535
+ if (allowFreeform && tool.customFormat) {
4536
+ payloads.push({
4537
+ type: "custom",
4538
+ name: tool.customWireName ?? tool.name,
4539
+ description: tool.description || "",
4540
+ format: {
4541
+ type: "grammar",
4542
+ syntax: tool.customFormat.syntax,
4543
+ definition: compactGrammarDefinition(tool.customFormat.syntax, tool.customFormat.definition),
4544
+ },
4545
+ });
4546
+ continue;
4547
+ }
4548
+ const strict = !!(!NO_STRICT && tool.strict);
4549
+ const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
4550
+ const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
4551
+ payloads.push({
4552
+ type: "function",
4553
+ name: tool.name,
4554
+ description: tool.description || "",
4555
+ parameters,
4556
+ ...(effectiveStrict ? { strict: true } : !NO_STRICT && tool.strict === false ? { strict: false } : {}),
4557
+ });
4558
+ }
4559
+ return payloads;
4560
+ }
4561
+
4562
+ export class CodexWebSocketTransportError extends Error {
4563
+ constructor(detail: string) {
4564
+ super(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${detail}`);
4565
+ this.name = "CodexWebSocketTransportError";
4566
+ }
4567
+ }
4568
+ class CodexWhitespaceToolCallLoopError extends Error {
4569
+ constructor(message: string) {
4570
+ super(message);
4571
+ this.name = "CodexWhitespaceToolCallLoopError";
4572
+ }
4573
+ }
4574
+
4575
+ class CodexProviderStreamError extends Error {
4576
+ readonly retryable: boolean;
4577
+ readonly code?: string;
4578
+
4579
+ constructor(message: string, retryable: boolean, code?: string) {
4580
+ super(message);
4581
+ this.name = "CodexProviderStreamError";
4582
+ this.retryable = retryable;
4583
+ this.code = code;
4584
+ }
4585
+ }
4586
+
4587
+ const optionalCodexString = type("unknown").pipe(raw => {
4588
+ const out = type("string")(raw);
4589
+ return out instanceof type.errors ? undefined : out;
4590
+ });
4591
+
4592
+ const innerErrorDetailSchema = type({
4593
+ "code?": optionalCodexString,
4594
+ "type?": optionalCodexString,
4595
+ "message?": optionalCodexString,
4596
+ });
4597
+
4598
+ const codexErrorDetailSchema = type("unknown").pipe(raw => {
4599
+ const out = innerErrorDetailSchema(raw);
4600
+ return out instanceof type.errors ? undefined : out;
4601
+ });
4602
+
4603
+ const innerFailureEventSchema = type({
4604
+ "type?": optionalCodexString,
4605
+ "code?": optionalCodexString,
4606
+ "message?": optionalCodexString,
4607
+ "status?": optionalCodexString,
4608
+ "error?": codexErrorDetailSchema,
4609
+ "response?": type("unknown").pipe(raw => {
4610
+ const out = type({
4611
+ "error?": codexErrorDetailSchema,
4612
+ "message?": optionalCodexString,
4613
+ "status?": optionalCodexString,
4614
+ })(raw);
4615
+ return out instanceof type.errors ? undefined : out;
4616
+ }),
4617
+ });
4618
+
4619
+ const codexFailureEventSchema = type("unknown").pipe(raw => {
4620
+ const out = innerFailureEventSchema(raw);
4621
+ return out instanceof type.errors
4622
+ ? {
4623
+ type: undefined,
4624
+ code: undefined,
4625
+ message: undefined,
4626
+ status: undefined,
4627
+ error: undefined,
4628
+ response: undefined,
4629
+ }
4630
+ : out;
4631
+ });
4632
+
4633
+ export function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
4634
+ const event = codexFailureEventSchema(rawEvent);
4635
+ if (event instanceof type.errors) {
4636
+ return false;
4637
+ }
4638
+ const error = event.error ?? event.response?.error;
4639
+ const code = error?.code ?? error?.type ?? event.code;
4640
+ if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
4641
+ return true;
4642
+ }
4643
+ const message = error?.message ?? event.message ?? event.response?.message;
4644
+ return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
4645
+ }
4646
+
4647
+ export function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
4648
+ const event = codexFailureEventSchema(rawEvent);
4649
+ if (event instanceof type.errors) {
4650
+ return new CodexProviderStreamError("Codex response failed", false);
4651
+ }
4652
+ const nestedError = event.error ?? event.response?.error;
4653
+ const code = nestedError?.code ?? nestedError?.type ?? event.code ?? "";
4654
+ const message = event.message ?? "";
4655
+ const formattedMessage =
4656
+ event.type === "error"
4657
+ ? formatCodexErrorEvent(rawEvent, code, message)
4658
+ : (formatCodexFailure(rawEvent) ?? "Codex response failed");
4659
+ return new CodexProviderStreamError(formattedMessage, isRetryableCodexFailureEvent(rawEvent), code || undefined);
4660
+ }
4661
+
4662
+ function formatCodexFailure(rawEvent: Record<string, unknown>): string | null {
4663
+ const event = codexFailureEventSchema(rawEvent);
4664
+ if (event instanceof type.errors) {
4665
+ return null;
4666
+ }
4667
+ const error = event.error ?? event.response?.error;
4668
+ const message = error?.message ?? event.message ?? event.response?.message;
4669
+ const code = error?.code ?? error?.type ?? event.code;
4670
+ const status = event.response?.status ?? event.status;
4671
+
4672
+ const meta: string[] = [];
4673
+ if (code) meta.push(`code=${code}`);
4674
+ if (status) meta.push(`status=${status}`);
4675
+
4676
+ if (message) {
4677
+ const metaText = meta.length ? ` (${meta.join(", ")})` : "";
4678
+ return `Codex response failed: ${message}${metaText}`;
4679
+ }
4680
+ if (meta.length) {
4681
+ return `Codex response failed (${meta.join(", ")})`;
4682
+ }
4683
+ try {
4684
+ const rawEventJson = JSON.stringify(rawEvent);
4685
+ const truncatedRawEventJson =
4686
+ rawEventJson.length <= 800
4687
+ ? rawEventJson
4688
+ : `${rawEventJson.slice(0, 800)}…[truncated ${rawEventJson.length - 800}]`;
4689
+ return `Codex response failed: ${truncatedRawEventJson}`;
4690
+ } catch {
4691
+ return "Codex response failed";
4692
+ }
4693
+ }
4694
+
4695
+ function formatCodexErrorEvent(rawEvent: Record<string, unknown>, code: string, message: string): string {
4696
+ const detail = formatCodexFailure(rawEvent);
4697
+ if (detail) {
4698
+ return detail.replace("response failed", "error event");
4699
+ }
4700
+ const meta: string[] = [];
4701
+ if (code) meta.push(`code=${code}`);
4702
+ if (message) meta.push(`message=${message}`);
4703
+ if (meta.length > 0) {
4704
+ return `Codex error event (${meta.join(", ")})`;
4705
+ }
4706
+ try {
4707
+ const rawEventJson = JSON.stringify(rawEvent);
4708
+ const truncatedRawEventJson =
4709
+ rawEventJson.length <= 800
4710
+ ? rawEventJson
4711
+ : `${rawEventJson.slice(0, 800)}…[truncated ${rawEventJson.length - 800}]`;
4712
+ return `Codex error event: ${truncatedRawEventJson}`;
4713
+ } catch {
4714
+ return "Codex error event";
4715
+ }
4716
+ }