jeopi-ai 16.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +4347 -0
  2. package/README.md +1193 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +73 -0
  5. package/dist/types/auth-broker/discover.d.ts +35 -0
  6. package/dist/types/auth-broker/index.d.ts +7 -0
  7. package/dist/types/auth-broker/refresher.d.ts +25 -0
  8. package/dist/types/auth-broker/remote-store.d.ts +102 -0
  9. package/dist/types/auth-broker/server.d.ts +43 -0
  10. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  11. package/dist/types/auth-broker/types.d.ts +107 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +411 -0
  13. package/dist/types/auth-gateway/http.d.ts +39 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +36 -0
  16. package/dist/types/auth-gateway/types.d.ts +123 -0
  17. package/dist/types/auth-retry.d.ts +124 -0
  18. package/dist/types/auth-storage.d.ts +1026 -0
  19. package/dist/types/dialect/anthropic.d.ts +15 -0
  20. package/dist/types/dialect/catalog.d.ts +3 -0
  21. package/dist/types/dialect/coercion.d.ts +23 -0
  22. package/dist/types/dialect/deepseek.d.ts +14 -0
  23. package/dist/types/dialect/demotion.d.ts +23 -0
  24. package/dist/types/dialect/examples.d.ts +2 -0
  25. package/dist/types/dialect/factory.d.ts +3 -0
  26. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  27. package/dist/types/dialect/gemini.d.ts +17 -0
  28. package/dist/types/dialect/gemma.d.ts +15 -0
  29. package/dist/types/dialect/glm.d.ts +9 -0
  30. package/dist/types/dialect/harmony.d.ts +8 -0
  31. package/dist/types/dialect/hermes.d.ts +9 -0
  32. package/dist/types/dialect/history.d.ts +3 -0
  33. package/dist/types/dialect/index.d.ts +11 -0
  34. package/dist/types/dialect/inventory.d.ts +12 -0
  35. package/dist/types/dialect/kimi.d.ts +14 -0
  36. package/dist/types/dialect/minimax.d.ts +3 -0
  37. package/dist/types/dialect/owned-stream.d.ts +4 -0
  38. package/dist/types/dialect/qwen3.d.ts +9 -0
  39. package/dist/types/dialect/rendering.d.ts +45 -0
  40. package/dist/types/dialect/thinking.d.ts +6 -0
  41. package/dist/types/dialect/types.d.ts +69 -0
  42. package/dist/types/dialect/xml.d.ts +9 -0
  43. package/dist/types/error/abort.d.ts +14 -0
  44. package/dist/types/error/auth-classify.d.ts +16 -0
  45. package/dist/types/error/auth.d.ts +27 -0
  46. package/dist/types/error/aws.d.ts +23 -0
  47. package/dist/types/error/classes.d.ts +102 -0
  48. package/dist/types/error/finalize.d.ts +39 -0
  49. package/dist/types/error/flags.d.ts +79 -0
  50. package/dist/types/error/format.d.ts +20 -0
  51. package/dist/types/error/gateway.d.ts +20 -0
  52. package/dist/types/error/index.d.ts +13 -0
  53. package/dist/types/error/oauth.d.ts +43 -0
  54. package/dist/types/error/provider.d.ts +42 -0
  55. package/dist/types/error/rate-limit.d.ts +59 -0
  56. package/dist/types/error/retryable.d.ts +27 -0
  57. package/dist/types/error/validation.d.ts +32 -0
  58. package/dist/types/index.d.ts +49 -0
  59. package/dist/types/provider-details.d.ts +24 -0
  60. package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
  61. package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
  62. package/dist/types/providers/__tests__/openai-codex-error.test.d.ts +1 -0
  63. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  64. package/dist/types/providers/anthropic-client.d.ts +94 -0
  65. package/dist/types/providers/anthropic-messages-server-schema.d.ts +497 -0
  66. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  67. package/dist/types/providers/anthropic-wire.d.ts +318 -0
  68. package/dist/types/providers/anthropic.d.ts +248 -0
  69. package/dist/types/providers/aws-credentials.d.ts +53 -0
  70. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  71. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  72. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  73. package/dist/types/providers/cursor.d.ts +91 -0
  74. package/dist/types/providers/devin.d.ts +12 -0
  75. package/dist/types/providers/error-message.d.ts +27 -0
  76. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  77. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  78. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  79. package/dist/types/providers/google-auth.d.ts +32 -0
  80. package/dist/types/providers/google-gemini-cli.d.ts +118 -0
  81. package/dist/types/providers/google-interactions.d.ts +65 -0
  82. package/dist/types/providers/google-shared.d.ts +203 -0
  83. package/dist/types/providers/google-types.d.ts +155 -0
  84. package/dist/types/providers/google-vertex.d.ts +7 -0
  85. package/dist/types/providers/google.d.ts +4 -0
  86. package/dist/types/providers/grammar.d.ts +1 -0
  87. package/dist/types/providers/kimi.d.ts +27 -0
  88. package/dist/types/providers/mock.d.ts +178 -0
  89. package/dist/types/providers/ollama.d.ts +7 -0
  90. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  91. package/dist/types/providers/openai-chat-server-schema.d.ts +695 -0
  92. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  93. package/dist/types/providers/openai-chat-wire.d.ts +644 -0
  94. package/dist/types/providers/openai-codex/request-transformer.d.ts +54 -0
  95. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  96. package/dist/types/providers/openai-codex-responses.d.ts +108 -0
  97. package/dist/types/providers/openai-completions.d.ts +45 -0
  98. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  99. package/dist/types/providers/openai-responses-server-schema.d.ts +349 -0
  100. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  101. package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
  102. package/dist/types/providers/openai-responses.d.ts +126 -0
  103. package/dist/types/providers/openai-shared.d.ts +506 -0
  104. package/dist/types/providers/pi-native-client.d.ts +13 -0
  105. package/dist/types/providers/pi-native-server.d.ts +69 -0
  106. package/dist/types/providers/register-builtins.d.ts +32 -0
  107. package/dist/types/providers/synthetic.d.ts +26 -0
  108. package/dist/types/providers/transform-messages.d.ts +11 -0
  109. package/dist/types/providers/vision-guard.d.ts +20 -0
  110. package/dist/types/registry/aimlapi.d.ts +4 -0
  111. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  112. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  113. package/dist/types/registry/anthropic.d.ts +10 -0
  114. package/dist/types/registry/api-key-login.d.ts +42 -0
  115. package/dist/types/registry/api-key-validation.d.ts +43 -0
  116. package/dist/types/registry/azure.d.ts +4 -0
  117. package/dist/types/registry/cerebras.d.ts +7 -0
  118. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  119. package/dist/types/registry/coreweave.d.ts +7 -0
  120. package/dist/types/registry/cursor.d.ts +7 -0
  121. package/dist/types/registry/deepseek.d.ts +8 -0
  122. package/dist/types/registry/derived.d.ts +5 -0
  123. package/dist/types/registry/devin.d.ts +8 -0
  124. package/dist/types/registry/firepass.d.ts +16 -0
  125. package/dist/types/registry/fireworks.d.ts +7 -0
  126. package/dist/types/registry/github-copilot.d.ts +7 -0
  127. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  128. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  129. package/dist/types/registry/google-antigravity.d.ts +9 -0
  130. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  131. package/dist/types/registry/google-vertex.d.ts +5 -0
  132. package/dist/types/registry/google.d.ts +4 -0
  133. package/dist/types/registry/groq.d.ts +4 -0
  134. package/dist/types/registry/huggingface.d.ts +7 -0
  135. package/dist/types/registry/index.d.ts +4 -0
  136. package/dist/types/registry/kagi.d.ts +14 -0
  137. package/dist/types/registry/kilo.d.ts +7 -0
  138. package/dist/types/registry/kimi-code.d.ts +7 -0
  139. package/dist/types/registry/litellm.d.ts +13 -0
  140. package/dist/types/registry/llama-cpp.d.ts +8 -0
  141. package/dist/types/registry/lm-studio.d.ts +8 -0
  142. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  143. package/dist/types/registry/minimax-code.d.ts +6 -0
  144. package/dist/types/registry/minimax.d.ts +4 -0
  145. package/dist/types/registry/mistral.d.ts +4 -0
  146. package/dist/types/registry/moonshot.d.ts +7 -0
  147. package/dist/types/registry/nanogpt.d.ts +7 -0
  148. package/dist/types/registry/nvidia.d.ts +7 -0
  149. package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
  150. package/dist/types/registry/oauth/anthropic.d.ts +23 -0
  151. package/dist/types/registry/oauth/callback-server.d.ts +72 -0
  152. package/dist/types/registry/oauth/cursor.d.ts +15 -0
  153. package/dist/types/registry/oauth/devin.d.ts +5 -0
  154. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  155. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  156. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  157. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  158. package/dist/types/registry/oauth/google-gemini-cli.d.ts +10 -0
  159. package/dist/types/registry/oauth/google-oauth-shared.d.ts +22 -0
  160. package/dist/types/registry/oauth/index.d.ts +64 -0
  161. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  162. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  163. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  164. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  165. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  166. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  167. package/dist/types/registry/oauth/types.d.ts +56 -0
  168. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  169. package/dist/types/registry/oauth/xai-oauth.d.ts +52 -0
  170. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  171. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  172. package/dist/types/registry/ollama.d.ts +12 -0
  173. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  174. package/dist/types/registry/openai-codex.d.ts +9 -0
  175. package/dist/types/registry/openai.d.ts +4 -0
  176. package/dist/types/registry/opencode-go.d.ts +6 -0
  177. package/dist/types/registry/opencode-zen.d.ts +6 -0
  178. package/dist/types/registry/openrouter.d.ts +13 -0
  179. package/dist/types/registry/parallel.d.ts +14 -0
  180. package/dist/types/registry/perplexity.d.ts +7 -0
  181. package/dist/types/registry/qianfan.d.ts +7 -0
  182. package/dist/types/registry/qwen-portal.d.ts +7 -0
  183. package/dist/types/registry/registry.d.ts +303 -0
  184. package/dist/types/registry/sakana.d.ts +7 -0
  185. package/dist/types/registry/synthetic.d.ts +6 -0
  186. package/dist/types/registry/tavily.d.ts +14 -0
  187. package/dist/types/registry/together.d.ts +6 -0
  188. package/dist/types/registry/types.d.ts +51 -0
  189. package/dist/types/registry/umans.d.ts +7 -0
  190. package/dist/types/registry/venice.d.ts +13 -0
  191. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  192. package/dist/types/registry/vllm.d.ts +7 -0
  193. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  194. package/dist/types/registry/xai-oauth.d.ts +7 -0
  195. package/dist/types/registry/xai.d.ts +4 -0
  196. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  197. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  198. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  199. package/dist/types/registry/xiaomi.d.ts +6 -0
  200. package/dist/types/registry/zai.d.ts +7 -0
  201. package/dist/types/registry/zenmux.d.ts +7 -0
  202. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  203. package/dist/types/stream.d.ts +44 -0
  204. package/dist/types/types.d.ts +715 -0
  205. package/dist/types/usage/claude.d.ts +4 -0
  206. package/dist/types/usage/gemini.d.ts +2 -0
  207. package/dist/types/usage/github-copilot.d.ts +7 -0
  208. package/dist/types/usage/google-antigravity.d.ts +15 -0
  209. package/dist/types/usage/kimi.d.ts +2 -0
  210. package/dist/types/usage/minimax-code.d.ts +2 -0
  211. package/dist/types/usage/ollama.d.ts +5 -0
  212. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  213. package/dist/types/usage/openai-codex-reset.d.ts +79 -0
  214. package/dist/types/usage/openai-codex.d.ts +3 -0
  215. package/dist/types/usage/opencode-go.d.ts +2 -0
  216. package/dist/types/usage/shared.d.ts +1 -0
  217. package/dist/types/usage/zai.d.ts +2 -0
  218. package/dist/types/usage.d.ts +346 -0
  219. package/dist/types/utils/abort.d.ts +25 -0
  220. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  221. package/dist/types/utils/block-symbols.d.ts +20 -0
  222. package/dist/types/utils/deterministic-id.d.ts +16 -0
  223. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  224. package/dist/types/utils/event-stream.d.ts +30 -0
  225. package/dist/types/utils/foundry.d.ts +1 -0
  226. package/dist/types/utils/google-validation.d.ts +2 -0
  227. package/dist/types/utils/harmony-leak.d.ts +118 -0
  228. package/dist/types/utils/http-inspector.d.ts +30 -0
  229. package/dist/types/utils/idle-iterator.d.ts +137 -0
  230. package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
  231. package/dist/types/utils/openai-http.d.ts +54 -0
  232. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  233. package/dist/types/utils/parse-bind.d.ts +23 -0
  234. package/dist/types/utils/provider-response.d.ts +3 -0
  235. package/dist/types/utils/proxy.d.ts +29 -0
  236. package/dist/types/utils/request-debug.d.ts +29 -0
  237. package/dist/types/utils/retry-after.d.ts +4 -0
  238. package/dist/types/utils/retry.d.ts +14 -0
  239. package/dist/types/utils/schema/adapt.d.ts +24 -0
  240. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  241. package/dist/types/utils/schema/dereference.d.ts +11 -0
  242. package/dist/types/utils/schema/draft.d.ts +10 -0
  243. package/dist/types/utils/schema/equality.d.ts +4 -0
  244. package/dist/types/utils/schema/fields.d.ts +54 -0
  245. package/dist/types/utils/schema/index.d.ts +15 -0
  246. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  247. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  248. package/dist/types/utils/schema/normalize.d.ts +124 -0
  249. package/dist/types/utils/schema/spill.d.ts +8 -0
  250. package/dist/types/utils/schema/stamps.d.ts +17 -0
  251. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  252. package/dist/types/utils/schema/types.d.ts +4 -0
  253. package/dist/types/utils/schema/typescript.d.ts +18 -0
  254. package/dist/types/utils/schema/wire.d.ts +92 -0
  255. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  256. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  257. package/dist/types/utils/sse-debug.d.ts +5 -0
  258. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  259. package/dist/types/utils/thinking-loop.d.ts +102 -0
  260. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  261. package/dist/types/utils/tool-choice.d.ts +50 -0
  262. package/dist/types/utils/validation.d.ts +42 -0
  263. package/dist/types/utils.d.ts +24 -0
  264. package/package.json +139 -0
  265. package/src/api-registry.ts +109 -0
  266. package/src/auth-broker/client.ts +359 -0
  267. package/src/auth-broker/discover.ts +222 -0
  268. package/src/auth-broker/index.ts +7 -0
  269. package/src/auth-broker/refresher.ts +117 -0
  270. package/src/auth-broker/remote-store.ts +657 -0
  271. package/src/auth-broker/server.ts +646 -0
  272. package/src/auth-broker/snapshot-cache.ts +191 -0
  273. package/src/auth-broker/types.ts +130 -0
  274. package/src/auth-broker/wire-schemas.ts +249 -0
  275. package/src/auth-gateway/http.ts +194 -0
  276. package/src/auth-gateway/index.ts +3 -0
  277. package/src/auth-gateway/server.ts +802 -0
  278. package/src/auth-gateway/types.ts +151 -0
  279. package/src/auth-retry.ts +250 -0
  280. package/src/auth-storage.ts +5576 -0
  281. package/src/dialect/anthropic.md +31 -0
  282. package/src/dialect/anthropic.ts +608 -0
  283. package/src/dialect/catalog.ts +29 -0
  284. package/src/dialect/coercion.ts +136 -0
  285. package/src/dialect/deepseek.md +24 -0
  286. package/src/dialect/deepseek.ts +609 -0
  287. package/src/dialect/demotion.ts +36 -0
  288. package/src/dialect/examples.ts +33 -0
  289. package/src/dialect/factory.ts +34 -0
  290. package/src/dialect/fenced-thinking.ts +184 -0
  291. package/src/dialect/gemini.md +44 -0
  292. package/src/dialect/gemini.ts +597 -0
  293. package/src/dialect/gemma.md +33 -0
  294. package/src/dialect/gemma.ts +387 -0
  295. package/src/dialect/glm.md +32 -0
  296. package/src/dialect/glm.ts +456 -0
  297. package/src/dialect/harmony.md +31 -0
  298. package/src/dialect/harmony.ts +346 -0
  299. package/src/dialect/hermes.md +25 -0
  300. package/src/dialect/hermes.ts +206 -0
  301. package/src/dialect/history.ts +81 -0
  302. package/src/dialect/index.ts +15 -0
  303. package/src/dialect/inventory.ts +73 -0
  304. package/src/dialect/kimi.md +24 -0
  305. package/src/dialect/kimi.ts +340 -0
  306. package/src/dialect/minimax.md +31 -0
  307. package/src/dialect/minimax.ts +95 -0
  308. package/src/dialect/owned-stream.ts +470 -0
  309. package/src/dialect/prompt-template.md +12 -0
  310. package/src/dialect/qwen3.md +28 -0
  311. package/src/dialect/qwen3.ts +240 -0
  312. package/src/dialect/rendering.ts +249 -0
  313. package/src/dialect/thinking.ts +122 -0
  314. package/src/dialect/types.ts +57 -0
  315. package/src/dialect/xml.md +22 -0
  316. package/src/dialect/xml.ts +90 -0
  317. package/src/error/abort.ts +18 -0
  318. package/src/error/auth-classify.ts +30 -0
  319. package/src/error/auth.ts +48 -0
  320. package/src/error/aws.ts +31 -0
  321. package/src/error/classes.ts +186 -0
  322. package/src/error/finalize.ts +69 -0
  323. package/src/error/flags.ts +506 -0
  324. package/src/error/format.ts +45 -0
  325. package/src/error/gateway.ts +96 -0
  326. package/src/error/index.ts +13 -0
  327. package/src/error/oauth.ts +58 -0
  328. package/src/error/provider.ts +62 -0
  329. package/src/error/rate-limit.ts +161 -0
  330. package/src/error/retryable.ts +70 -0
  331. package/src/error/validation.ts +44 -0
  332. package/src/index.ts +49 -0
  333. package/src/provider-details.ts +90 -0
  334. package/src/providers/__tests__/google-auth.test.ts +144 -0
  335. package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
  336. package/src/providers/__tests__/openai-codex-error.test.ts +84 -0
  337. package/src/providers/amazon-bedrock.ts +1042 -0
  338. package/src/providers/anthropic-client.ts +295 -0
  339. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  340. package/src/providers/anthropic-messages-server.ts +756 -0
  341. package/src/providers/anthropic-wire.ts +318 -0
  342. package/src/providers/anthropic.ts +4078 -0
  343. package/src/providers/aws-credentials.ts +586 -0
  344. package/src/providers/aws-eventstream.ts +181 -0
  345. package/src/providers/aws-sigv4.ts +218 -0
  346. package/src/providers/azure-openai-responses.ts +382 -0
  347. package/src/providers/cursor/proto/agent.proto +3526 -0
  348. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  349. package/src/providers/cursor/proto/buf.yaml +17 -0
  350. package/src/providers/cursor.ts +2695 -0
  351. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  352. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  353. package/src/providers/devin/proto/buf.yaml +17 -0
  354. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  355. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  356. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  357. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  358. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  359. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  360. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  361. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  362. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  363. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  364. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  365. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  366. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  367. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  368. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  369. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  370. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  371. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  372. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  373. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  374. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  375. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  376. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  377. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  378. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  379. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  380. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  381. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  382. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  383. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  384. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  385. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  386. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  387. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  388. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  389. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  390. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  391. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  392. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  393. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  394. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  395. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  396. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  397. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  398. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  399. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  400. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  401. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  402. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  403. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  404. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  405. package/src/providers/devin.ts +577 -0
  406. package/src/providers/error-message.ts +21 -0
  407. package/src/providers/github-copilot-headers.ts +141 -0
  408. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  409. package/src/providers/gitlab-duo-workflow.ts +3058 -0
  410. package/src/providers/gitlab-duo.ts +395 -0
  411. package/src/providers/google-auth.ts +350 -0
  412. package/src/providers/google-gemini-cli.ts +1362 -0
  413. package/src/providers/google-interactions.ts +753 -0
  414. package/src/providers/google-shared.ts +1103 -0
  415. package/src/providers/google-types.ts +180 -0
  416. package/src/providers/google-vertex.ts +183 -0
  417. package/src/providers/google.ts +87 -0
  418. package/src/providers/grammar.ts +70 -0
  419. package/src/providers/kimi.ts +52 -0
  420. package/src/providers/mock.ts +507 -0
  421. package/src/providers/ollama.ts +773 -0
  422. package/src/providers/openai-anthropic-shim.ts +152 -0
  423. package/src/providers/openai-chat-server-schema.ts +242 -0
  424. package/src/providers/openai-chat-server.ts +715 -0
  425. package/src/providers/openai-chat-wire.ts +847 -0
  426. package/src/providers/openai-codex/request-transformer.ts +295 -0
  427. package/src/providers/openai-codex/response-handler.ts +102 -0
  428. package/src/providers/openai-codex-responses.ts +3468 -0
  429. package/src/providers/openai-completions.ts +2173 -0
  430. package/src/providers/openai-reasoning-fallback.ts +269 -0
  431. package/src/providers/openai-responses-reasoning-suppression.md +1 -0
  432. package/src/providers/openai-responses-server-schema.ts +282 -0
  433. package/src/providers/openai-responses-server.ts +1280 -0
  434. package/src/providers/openai-responses-wire.ts +6391 -0
  435. package/src/providers/openai-responses.ts +1022 -0
  436. package/src/providers/openai-shared.ts +2648 -0
  437. package/src/providers/pi-native-client.ts +266 -0
  438. package/src/providers/pi-native-server.ts +242 -0
  439. package/src/providers/register-builtins.ts +475 -0
  440. package/src/providers/synthetic.ts +50 -0
  441. package/src/providers/transform-messages.ts +787 -0
  442. package/src/providers/vision-guard.ts +54 -0
  443. package/src/registry/aimlapi.ts +6 -0
  444. package/src/registry/alibaba-coding-plan.ts +95 -0
  445. package/src/registry/amazon-bedrock.ts +22 -0
  446. package/src/registry/anthropic.ts +26 -0
  447. package/src/registry/api-key-login.ts +112 -0
  448. package/src/registry/api-key-validation.ts +161 -0
  449. package/src/registry/azure.ts +6 -0
  450. package/src/registry/cerebras.ts +23 -0
  451. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  452. package/src/registry/coreweave.ts +40 -0
  453. package/src/registry/cursor.ts +20 -0
  454. package/src/registry/deepseek.ts +46 -0
  455. package/src/registry/derived.ts +9 -0
  456. package/src/registry/devin.ts +15 -0
  457. package/src/registry/firepass.ts +32 -0
  458. package/src/registry/fireworks.ts +28 -0
  459. package/src/registry/github-copilot.ts +22 -0
  460. package/src/registry/gitlab-duo-workflow.ts +20 -0
  461. package/src/registry/gitlab-duo.ts +19 -0
  462. package/src/registry/google-antigravity.ts +22 -0
  463. package/src/registry/google-gemini-cli.ts +22 -0
  464. package/src/registry/google-vertex.ts +38 -0
  465. package/src/registry/google.ts +6 -0
  466. package/src/registry/groq.ts +6 -0
  467. package/src/registry/huggingface.ts +29 -0
  468. package/src/registry/index.ts +4 -0
  469. package/src/registry/kagi.ts +46 -0
  470. package/src/registry/kilo.ts +114 -0
  471. package/src/registry/kimi-code.ts +17 -0
  472. package/src/registry/litellm.ts +45 -0
  473. package/src/registry/llama-cpp.ts +35 -0
  474. package/src/registry/lm-studio.ts +31 -0
  475. package/src/registry/minimax-code-cn.ts +12 -0
  476. package/src/registry/minimax-code.ts +12 -0
  477. package/src/registry/minimax.ts +6 -0
  478. package/src/registry/mistral.ts +6 -0
  479. package/src/registry/moonshot.ts +22 -0
  480. package/src/registry/nanogpt.ts +22 -0
  481. package/src/registry/nvidia.ts +61 -0
  482. package/src/registry/oauth/__tests__/xai-oauth.test.ts +104 -0
  483. package/src/registry/oauth/anthropic.ts +311 -0
  484. package/src/registry/oauth/callback-server.ts +315 -0
  485. package/src/registry/oauth/cursor.ts +171 -0
  486. package/src/registry/oauth/devin.ts +124 -0
  487. package/src/registry/oauth/github-copilot.ts +369 -0
  488. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  489. package/src/registry/oauth/gitlab-duo.ts +222 -0
  490. package/src/registry/oauth/google-antigravity.ts +209 -0
  491. package/src/registry/oauth/google-gemini-cli.ts +273 -0
  492. package/src/registry/oauth/google-oauth-shared.ts +125 -0
  493. package/src/registry/oauth/index.ts +269 -0
  494. package/src/registry/oauth/kimi.ts +289 -0
  495. package/src/registry/oauth/minimax-code.ts +53 -0
  496. package/src/registry/oauth/oauth.html +311 -0
  497. package/src/registry/oauth/openai-codex.ts +364 -0
  498. package/src/registry/oauth/opencode.ts +50 -0
  499. package/src/registry/oauth/perplexity.ts +228 -0
  500. package/src/registry/oauth/pkce.ts +18 -0
  501. package/src/registry/oauth/types.ts +65 -0
  502. package/src/registry/oauth/wafer.ts +24 -0
  503. package/src/registry/oauth/xai-oauth.ts +394 -0
  504. package/src/registry/oauth/xiaomi.ts +211 -0
  505. package/src/registry/ollama-cloud.ts +36 -0
  506. package/src/registry/ollama.ts +43 -0
  507. package/src/registry/openai-codex-device.ts +18 -0
  508. package/src/registry/openai-codex.ts +19 -0
  509. package/src/registry/openai.ts +6 -0
  510. package/src/registry/opencode-go.ts +12 -0
  511. package/src/registry/opencode-zen.ts +12 -0
  512. package/src/registry/openrouter.ts +28 -0
  513. package/src/registry/parallel.ts +45 -0
  514. package/src/registry/perplexity.ts +13 -0
  515. package/src/registry/qianfan.ts +27 -0
  516. package/src/registry/qwen-portal.ts +50 -0
  517. package/src/registry/registry.ts +161 -0
  518. package/src/registry/sakana.ts +22 -0
  519. package/src/registry/synthetic.ts +21 -0
  520. package/src/registry/tavily.ts +45 -0
  521. package/src/registry/together.ts +22 -0
  522. package/src/registry/types.ts +56 -0
  523. package/src/registry/umans.ts +23 -0
  524. package/src/registry/venice.ts +33 -0
  525. package/src/registry/vercel-ai-gateway.ts +38 -0
  526. package/src/registry/vllm.ts +34 -0
  527. package/src/registry/wafer-serverless.ts +12 -0
  528. package/src/registry/xai-oauth.ts +17 -0
  529. package/src/registry/xai.ts +6 -0
  530. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  531. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  532. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  533. package/src/registry/xiaomi.ts +12 -0
  534. package/src/registry/zai.ts +27 -0
  535. package/src/registry/zenmux.ts +22 -0
  536. package/src/registry/zhipu-coding-plan.ts +27 -0
  537. package/src/stream.ts +1778 -0
  538. package/src/types.ts +856 -0
  539. package/src/usage/claude.ts +485 -0
  540. package/src/usage/gemini.ts +258 -0
  541. package/src/usage/github-copilot.ts +424 -0
  542. package/src/usage/google-antigravity.ts +497 -0
  543. package/src/usage/kimi.ts +271 -0
  544. package/src/usage/minimax-code.ts +30 -0
  545. package/src/usage/ollama.ts +41 -0
  546. package/src/usage/openai-codex-base-url.ts +35 -0
  547. package/src/usage/openai-codex-reset.ts +174 -0
  548. package/src/usage/openai-codex.ts +535 -0
  549. package/src/usage/opencode-go.ts +89 -0
  550. package/src/usage/shared.ts +10 -0
  551. package/src/usage/zai.ts +321 -0
  552. package/src/usage.ts +333 -0
  553. package/src/utils/abort.ts +67 -0
  554. package/src/utils/anthropic-auth.ts +93 -0
  555. package/src/utils/block-symbols.ts +32 -0
  556. package/src/utils/deterministic-id.ts +20 -0
  557. package/src/utils/empty-completion-retry.ts +159 -0
  558. package/src/utils/event-stream.ts +171 -0
  559. package/src/utils/foundry.ts +8 -0
  560. package/src/utils/google-validation.ts +25 -0
  561. package/src/utils/harmony-leak.ts +456 -0
  562. package/src/utils/http-inspector.ts +168 -0
  563. package/src/utils/idle-iterator.ts +473 -0
  564. package/src/utils/leaked-thinking-stream.ts +294 -0
  565. package/src/utils/openai-http.ts +122 -0
  566. package/src/utils/openrouter-headers.ts +12 -0
  567. package/src/utils/parse-bind.ts +56 -0
  568. package/src/utils/provider-response.ts +30 -0
  569. package/src/utils/proxy.ts +240 -0
  570. package/src/utils/request-debug.ts +351 -0
  571. package/src/utils/retry-after.ts +110 -0
  572. package/src/utils/retry.ts +59 -0
  573. package/src/utils/schema/CONSTRAINTS.md +166 -0
  574. package/src/utils/schema/adapt.ts +36 -0
  575. package/src/utils/schema/compatibility.ts +435 -0
  576. package/src/utils/schema/dereference.ts +98 -0
  577. package/src/utils/schema/draft.ts +341 -0
  578. package/src/utils/schema/equality.ts +97 -0
  579. package/src/utils/schema/fields.ts +207 -0
  580. package/src/utils/schema/index.ts +15 -0
  581. package/src/utils/schema/json-schema-validator.ts +595 -0
  582. package/src/utils/schema/meta-validator.ts +167 -0
  583. package/src/utils/schema/normalize.ts +1901 -0
  584. package/src/utils/schema/spill.ts +43 -0
  585. package/src/utils/schema/stamps.ts +109 -0
  586. package/src/utils/schema/strict-tool-validation.ts +117 -0
  587. package/src/utils/schema/types.ts +10 -0
  588. package/src/utils/schema/typescript.ts +198 -0
  589. package/src/utils/schema/wire.ts +789 -0
  590. package/src/utils/schema/zod-decontaminate.ts +331 -0
  591. package/src/utils/sdk-stream-timeout.ts +43 -0
  592. package/src/utils/sse-debug.ts +18 -0
  593. package/src/utils/stream-markup-healing.ts +247 -0
  594. package/src/utils/thinking-loop.ts +552 -0
  595. package/src/utils/tool-call-loop-guard.ts +107 -0
  596. package/src/utils/tool-choice.ts +99 -0
  597. package/src/utils/validation.ts +1507 -0
  598. package/src/utils.ts +171 -0
@@ -0,0 +1,3468 @@
1
+ import * as os from "node:os";
2
+ import { scheduler } from "node:timers/promises";
3
+ import { type } from "arktype";
4
+ import { calculateCost } from "jeopi-catalog/models";
5
+ import { CODEX_BASE_URL, getCodexAccountId, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "jeopi-catalog/wire/codex";
6
+ import {
7
+ $env,
8
+ $flag,
9
+ asRecord,
10
+ fetchWithRetry,
11
+ logger,
12
+ parseStreamingJson,
13
+ readSseJson,
14
+ structuredCloneJSON,
15
+ } from "jeopi-utils";
16
+ import packageJson from "../../package.json" with { type: "json" };
17
+ import * as AIError from "../error";
18
+ import { getEnvApiKey } from "../stream";
19
+ import type {
20
+ Api,
21
+ AssistantMessage,
22
+ Context,
23
+ FetchImpl,
24
+ Model,
25
+ ProviderSessionState,
26
+ RawSseEvent,
27
+ ServiceTier,
28
+ StreamFunction,
29
+ StreamOptions,
30
+ TextContent,
31
+ ThinkingContent,
32
+ Tool,
33
+ ToolCall,
34
+ ToolChoice,
35
+ } from "../types";
36
+ import {
37
+ createOpenAIResponsesHistoryPayload,
38
+ getOpenAIResponsesHistoryItems,
39
+ getOpenAIResponsesHistoryPayload,
40
+ normalizeSystemPrompts,
41
+ } from "../utils";
42
+ import { clearStreamingPartialJson, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols";
43
+ import { AssistantMessageEventStream } from "../utils/event-stream";
44
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
45
+ import {
46
+ armPreResponseTimeout,
47
+ getOpenAIStreamFirstEventTimeoutMs,
48
+ getOpenAIStreamIdleTimeoutMs,
49
+ iterateWithIdleTimeout,
50
+ } from "../utils/idle-iterator";
51
+ import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
52
+ import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
53
+ import { notifyRawSseEvent } from "../utils/sse-debug";
54
+ import { compactGrammarDefinition } from "./grammar";
55
+ import {
56
+ type CodexReasoningContext,
57
+ type CodexRequestOptions,
58
+ type InputItem,
59
+ type RequestBody,
60
+ shouldUseCodexResponsesLite,
61
+ transformRequestBody,
62
+ } from "./openai-codex/request-transformer";
63
+ import { CodexApiError } from "./openai-codex/response-handler";
64
+ import type {
65
+ ResponseCustomToolCall,
66
+ ResponseFunctionToolCall,
67
+ ResponseInput,
68
+ ResponseInputContent,
69
+ ResponseOutputMessage,
70
+ ResponseReasoningItem,
71
+ ResponseStatus,
72
+ } from "./openai-responses-wire";
73
+ import {
74
+ accumulateCustomToolCallInputDelta,
75
+ accumulateToolCallArgumentsDelta,
76
+ appendMessageContentPart,
77
+ appendMessageTextDelta,
78
+ appendReasoningSummaryPart,
79
+ appendReasoningSummaryPartDone,
80
+ appendReasoningSummaryTextDelta,
81
+ appendResponsesToolResultMessages,
82
+ applyOpenAIServiceTier,
83
+ buildResponsesDeltaInput,
84
+ convertResponsesAssistantMessage,
85
+ convertResponsesInputContent,
86
+ encodeResponsesToolCallId,
87
+ encodeTextSignatureV1,
88
+ finalizeCustomToolCallInputDone,
89
+ finalizePendingResponsesToolCalls,
90
+ finalizeToolCallArgumentsDone,
91
+ isOpenAIResponsesProgressEvent,
92
+ mapOpenAIResponsesStopReason,
93
+ normalizeOpenAIResponsesPromptCacheKey,
94
+ populateResponsesUsageFromResponse,
95
+ promoteResponsesToolUseStopReason,
96
+ } from "./openai-shared";
97
+ import { transformMessages } from "./transform-messages";
98
+
99
+ export interface OpenAICodexResponsesOptions extends StreamOptions {
100
+ reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
101
+ reasoningSummary?: "auto" | "concise" | "detailed" | null;
102
+ /** `reasoning.context` replay scope; defaults to `all_turns` when unset. The `all_turns` value is gated to gpt-5.4+ Codex models — older ids reject it, so it is suppressed and `context` omitted. */
103
+ reasoningContext?: CodexReasoningContext;
104
+ textVerbosity?: "low" | "medium" | "high";
105
+ include?: string[];
106
+ codexMode?: boolean;
107
+ toolChoice?: ToolChoice;
108
+ preferWebsockets?: boolean;
109
+ serviceTier?: ServiceTier;
110
+ /**
111
+ * Opt into the Responses Lite transport contract. Sends
112
+ * `x-openai-internal-codex-responses-lite: true` on HTTP requests and on the
113
+ * WebSocket upgrade (the marker is connection-scoped there, so lite and
114
+ * non-lite turns never share a pooled socket), strips image detail from
115
+ * input, and disables parallel tool calling — mirroring codex-rs.
116
+ */
117
+ responsesLite?: boolean;
118
+ /**
119
+ * Extra `client_metadata` to include in the request body on both transports.
120
+ * The canonical Codex envelope is `client_metadata["x-codex-turn-metadata"]`
121
+ * (JSON string of thread/turn identifiers); flat keys are also accepted.
122
+ */
123
+ clientMetadata?: Record<string, string>;
124
+ /**
125
+ * Invoked when the server streams a `response.metadata` event carrying
126
+ * ChatGPT moderation metadata (`metadata.openai_chatgpt_moderation_metadata`)
127
+ * for first-party presentation parity. Diagnostic observer: failures are
128
+ * swallowed and must not alter the stream.
129
+ */
130
+ onModerationMetadata?: (metadata: unknown) => void;
131
+ }
132
+
133
+ const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
134
+ const CODEX_MAX_RETRIES = 5;
135
+ const CODEX_RETRY_DELAY_MS = 500;
136
+ const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
137
+ const CODEX_WEBSOCKET_PING_INTERVAL_MS = Number($env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS || 10_000);
138
+ const CODEX_WEBSOCKET_PONG_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS || 60_000);
139
+ const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = Number($env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY || 4096);
140
+ /**
141
+ * Maximum quiet period (no inbound frames AND no observed pong) we'll trust a
142
+ * reused WebSocket for before forcing a fresh handshake. Codex backends and
143
+ * intermediaries occasionally evict idle sockets server-side without sending a
144
+ * FIN, leaving the local `readyState` as OPEN while the next `send()` becomes a
145
+ * write into a half-open buffer. Reusing such a socket parks the next request
146
+ * at `#nextMessage` until the first-event/idle timeout fires (issue #1450). The
147
+ * heartbeat below also catches dead sockets, but only after `pongTimeoutMs`
148
+ * (default 60s) and only while a request is active — this gate closes the door
149
+ * earlier and even when the gap between requests is purely client-side (tool
150
+ * execution, user typing, etc.). Set `PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS=0`
151
+ * to disable.
152
+ */
153
+ const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = Number($env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS || 30_000);
154
+ /**
155
+ * Steady-state liveness ceiling for the Codex WebSocket transport. Distinct from
156
+ * the OMP-wide stream watchdog removed in #1392: a WebSocket can stay TCP-open
157
+ * indefinitely without exchanging frames (server crash after upgrade, half-open
158
+ * network path), so we still need a transport-internal cap to detect those
159
+ * states and trigger the WS→SSE fallback. Only applies AFTER the first event
160
+ * has arrived — slow first-token paths wait as long as the caller permits.
161
+ */
162
+ const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS || 300_000);
163
+ /**
164
+ * Maximum wait for the first WebSocket event before falling back to SSE.
165
+ * Unlike a stream watchdog, this triggers a transport switch (not a request
166
+ * failure) — the outer retry loop catches the timeout error and re-runs on
167
+ * SSE. Generous default so legitimately slow first-token providers still get
168
+ * a chance on the WS transport before falling through.
169
+ */
170
+ const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS || 60_000);
171
+ const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
172
+ const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
173
+ const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
174
+ const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
175
+ const CODEX_RETRYABLE_EVENT_MESSAGE =
176
+ /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
177
+ const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
178
+ const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
179
+ const X_MODELS_ETAG_HEADER = "x-models-etag";
180
+ const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
181
+ /** WebSocket frames cannot carry per-request HTTP headers; codex-rs mirrors the lite marker into `client_metadata` under this key. */
182
+ const CODEX_WS_RESPONSES_LITE_CLIENT_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
183
+ /** `response.metadata` payload key carrying ChatGPT moderation metadata. */
184
+ const CODEX_MODERATION_METADATA_KEY = "openai_chatgpt_moderation_metadata";
185
+ /** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
186
+ const CODEX_WEBSOCKET_FATAL_PATTERNS = ["websocket error:", "websocket closed before open", "connection timeout"];
187
+ /** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
188
+ const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
189
+ const CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES = new Set(["response.done", "response.incomplete"]);
190
+ // Provider/model failure mode: Codex can keep a response alive by streaming
191
+ // whitespace-only function-call argument deltas forever. Those frames count as
192
+ // transport activity, so idle timers never fire; cap the run before raw debug
193
+ // buffers and partial JSON grow without semantic progress.
194
+ const CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT = 256;
195
+ const CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT = 16 * 1024;
196
+ const CODEX_WHITESPACE_LOOP_RETRY_LIMIT = 2;
197
+ const CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS = 250;
198
+
199
+ function isCodexStreamProgressEvent(event: unknown): boolean {
200
+ if (isOpenAIResponsesProgressEvent(event)) return true;
201
+ if (!event || typeof event !== "object") return false;
202
+ const type = (event as { type?: unknown }).type;
203
+ return typeof type === "string" && CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES.has(type);
204
+ }
205
+
206
+ function extractCodexFrameResponseId(frame: Record<string, unknown>): string | undefined {
207
+ const response = (frame as { response?: { id?: unknown } }).response;
208
+ const id = response?.id;
209
+ return typeof id === "string" && id.length > 0 ? id : undefined;
210
+ }
211
+
212
+ function extractCodexFrameSequenceNumber(frame: Record<string, unknown>): number | undefined {
213
+ const raw = (frame as { sequence_number?: unknown }).sequence_number;
214
+ return typeof raw === "number" && Number.isFinite(raw) ? Math.trunc(raw) : undefined;
215
+ }
216
+
217
+ type CodexWebSocketTimeoutDetails = {
218
+ lastEventAt: number;
219
+ lastEventType?: string;
220
+ lastProgressAt: number;
221
+ lastProgressEventType?: string;
222
+ };
223
+
224
+ function createCodexWebSocketTimeoutMessage(reason: string, details: CodexWebSocketTimeoutDetails): string {
225
+ const now = Date.now();
226
+ const lastEvent = details.lastEventType
227
+ ? `${details.lastEventType} ${Math.max(0, now - details.lastEventAt)}ms ago`
228
+ : "none";
229
+ const lastProgress = details.lastProgressEventType
230
+ ? `${details.lastProgressEventType} ${Math.max(0, now - details.lastProgressAt)}ms ago`
231
+ : "none";
232
+ return `${reason} (last event: ${lastEvent}; last progress: ${lastProgress})`;
233
+ }
234
+
235
+ type CodexTransport = "sse" | "websocket";
236
+ type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
237
+ type CodexOutputBlock =
238
+ | ThinkingContent
239
+ | TextContent
240
+ | (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number });
241
+
242
+ /**
243
+ * Per-session request-shape counters. Despite the name, these cover both
244
+ * transports: once stateful SSE chaining is enabled, SSE requests are counted
245
+ * too (the shared chained-request builder records every request it shapes).
246
+ */
247
+ export interface OpenAICodexWebSocketDebugStats {
248
+ fullContextRequests: number;
249
+ deltaRequests: number;
250
+ lastInputItems: number;
251
+ lastDeltaInputItems?: number;
252
+ lastPreviousResponseId?: string;
253
+ }
254
+
255
+ /**
256
+ * Per-session transport state shared by BOTH transports: websocket turn
257
+ * chaining (`previous_response_id` baseline), turn-state/models-etag headers,
258
+ * websocket connection pooling, and debug stats. The name is historical — SSE-only
259
+ * sessions use it too.
260
+ */
261
+ type CodexWebSocketSessionState = {
262
+ disableWebsocket: boolean;
263
+ lastRequest?: RequestBody;
264
+ lastResponseId?: string;
265
+ lastResponseItems?: InputItem[];
266
+ canAppend: boolean;
267
+ turnState?: string;
268
+ modelsEtag?: string;
269
+ connection?: CodexWebSocketConnection;
270
+ lastTransport?: CodexTransport;
271
+ fallbackCount: number;
272
+ lastFallbackAt?: number;
273
+ prewarmed: boolean;
274
+ stats: OpenAICodexWebSocketDebugStats;
275
+ };
276
+
277
+ interface CodexProviderSessionState extends ProviderSessionState {
278
+ webSocketSessions: Map<string, CodexWebSocketSessionState>;
279
+ webSocketPublicToPrivate: Map<string, string>;
280
+ }
281
+
282
+ interface CodexRequestContext {
283
+ apiKey: string;
284
+ accountId: string;
285
+ baseUrl: string;
286
+ url: string;
287
+ requestHeaders: Record<string, string>;
288
+ transportSessionId?: string;
289
+ providerSessionState?: CodexProviderSessionState;
290
+ websocketState?: CodexWebSocketSessionState;
291
+ responsesLite: boolean;
292
+ transformedBody: RequestBody;
293
+ rawRequestDump: RawHttpRequestDump;
294
+ }
295
+
296
+ interface CodexRequestSetup {
297
+ requestSignal: AbortSignal;
298
+ wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
299
+ requestAbortController: AbortController;
300
+ firstEventTimeoutMs: number | undefined;
301
+ websocketIdleTimeoutMs: number | undefined;
302
+ websocketFirstEventTimeoutMs: number | undefined;
303
+ }
304
+
305
+ interface CodexOpenItem {
306
+ item: CodexEventItem;
307
+ block: CodexOutputBlock | null;
308
+ /** Index of {@link block} in `output.content`; `-1` when no block was created for this item. */
309
+ contentIndex: number;
310
+ itemId?: string;
311
+ outputIndex?: number;
312
+ }
313
+
314
+ class CodexStreamRuntime {
315
+ eventStream: AsyncGenerator<Record<string, unknown>>;
316
+ requestBodyForState: RequestBody;
317
+ transport: CodexTransport;
318
+ websocketState?: CodexWebSocketSessionState;
319
+ /**
320
+ * Items open on the wire keyed by `item.id`. `response.output_item.added`
321
+ * registers here; `output_item.done` removes. A keyed event whose `item_id`
322
+ * is not present is dropped rather than appended to a sibling.
323
+ */
324
+ openItems = new Map<string, CodexOpenItem>();
325
+ /**
326
+ * Items open on the wire keyed by `output_index` for streams whose function
327
+ * call items omit `id`; these still carry `output_index` on deltas/done.
328
+ */
329
+ openItemsByOutputIndex = new Map<number, CodexOpenItem>();
330
+ /**
331
+ * Most recently added open item for events that omit both `item_id` and
332
+ * `output_index`. Always tracks the latest `output_item.added`, including
333
+ * fully keyless items that never make it into the keyed maps; cleared when
334
+ * its item closes.
335
+ */
336
+ currentEntry: CodexOpenItem | null = null;
337
+ /** Convenience mirrors of {@link currentEntry} for legacy singleton handlers. */
338
+ currentItem: CodexEventItem | null = null;
339
+ currentBlock: CodexOutputBlock | null = null;
340
+ nativeOutputItems: Array<Record<string, unknown>> = [];
341
+ websocketStreamRetries = 0;
342
+ providerRetryAttempt = 0;
343
+ sawTerminalEvent = false;
344
+ canSafelyReplayWebsocketOverSse = true;
345
+ whitespaceToolCallArgumentsDelta?: CodexWhitespaceToolCallArgumentsDeltaState;
346
+ whitespaceLoopRetries = 0;
347
+
348
+ constructor(initial: {
349
+ eventStream: AsyncGenerator<Record<string, unknown>>;
350
+ requestBodyForState: RequestBody;
351
+ transport: CodexTransport;
352
+ websocketState?: CodexWebSocketSessionState;
353
+ }) {
354
+ this.eventStream = initial.eventStream;
355
+ this.requestBodyForState = initial.requestBodyForState;
356
+ this.transport = initial.transport;
357
+ this.websocketState = initial.websocketState;
358
+ }
359
+
360
+ /**
361
+ * Wipe per-attempt accumulator state before a recovery path replays the turn.
362
+ * Keeps {@link openItems} and the legacy singleton-current pointers in lockstep
363
+ * with {@link nativeOutputItems} so a stale delta from the failed attempt can't
364
+ * bind to a sibling on the retry.
365
+ */
366
+ resetAccumulators(): void {
367
+ this.openItems.clear();
368
+ this.openItemsByOutputIndex.clear();
369
+ this.currentEntry = null;
370
+ this.currentItem = null;
371
+ this.currentBlock = null;
372
+ this.nativeOutputItems.length = 0;
373
+ }
374
+
375
+ /**
376
+ * Look up the open item a Codex stream event targets. `item_id` wins because it
377
+ * uniquely identifies a response item; `output_index` covers idless function
378
+ * call items. A keyed event whose target is already closed is dropped instead
379
+ * of being routed to a sibling. Only streams that omit both keys fall back to
380
+ * {@link currentEntry} — the most recently added item, including fully keyless
381
+ * ones that never reached the keyed maps.
382
+ */
383
+ openItemForEvent(rawEvent: Record<string, unknown>): CodexOpenItem | null {
384
+ const itemId = typeof rawEvent.item_id === "string" ? rawEvent.item_id : "";
385
+ if (itemId) return this.openItems.get(itemId) ?? null;
386
+ const outputIndex =
387
+ typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
388
+ ? Math.trunc(rawEvent.output_index)
389
+ : undefined;
390
+ if (outputIndex !== undefined) return this.openItemsByOutputIndex.get(outputIndex) ?? null;
391
+ return this.currentEntry;
392
+ }
393
+
394
+ closeOpenItem(entry: CodexOpenItem | null | undefined): void {
395
+ if (!entry) return;
396
+ if (entry.itemId) this.openItems.delete(entry.itemId);
397
+ if (entry.outputIndex !== undefined) this.openItemsByOutputIndex.delete(entry.outputIndex);
398
+ if (this.currentEntry === entry) {
399
+ this.currentEntry = null;
400
+ this.currentItem = null;
401
+ this.currentBlock = null;
402
+ }
403
+ }
404
+
405
+ observeWhitespaceToolCallArgumentsDelta(
406
+ rawEvent: Record<string, unknown>,
407
+ delta: string,
408
+ ): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
409
+ if (!isJsonWhitespaceOnly(delta)) {
410
+ this.whitespaceToolCallArgumentsDelta = undefined;
411
+ return undefined;
412
+ }
413
+
414
+ const itemId =
415
+ typeof rawEvent.item_id === "string" && rawEvent.item_id.length > 0
416
+ ? rawEvent.item_id
417
+ : (this.currentItem?.id ?? "");
418
+ const outputIndex =
419
+ typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
420
+ ? Math.trunc(rawEvent.output_index)
421
+ : undefined;
422
+ const sequenceNumber =
423
+ typeof rawEvent.sequence_number === "number" && Number.isFinite(rawEvent.sequence_number)
424
+ ? Math.trunc(rawEvent.sequence_number)
425
+ : undefined;
426
+ let state = this.whitespaceToolCallArgumentsDelta;
427
+ if (!state || state.itemId !== itemId || state.outputIndex !== outputIndex) {
428
+ state = {
429
+ itemId,
430
+ outputIndex,
431
+ consecutiveEvents: 0,
432
+ consecutiveChars: 0,
433
+ firstSequenceNumber: sequenceNumber,
434
+ };
435
+ this.whitespaceToolCallArgumentsDelta = state;
436
+ }
437
+
438
+ state.consecutiveEvents += 1;
439
+ state.consecutiveChars += delta.length;
440
+ state.lastSequenceNumber = sequenceNumber;
441
+ if (
442
+ state.consecutiveEvents < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT &&
443
+ state.consecutiveChars < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT
444
+ ) {
445
+ return undefined;
446
+ }
447
+
448
+ const itemLabel = itemId ? ` for item ${itemId}` : "";
449
+ const sequenceLabel =
450
+ state.firstSequenceNumber === undefined || state.lastSequenceNumber === undefined
451
+ ? ""
452
+ : `, sequence ${state.firstSequenceNumber}..${state.lastSequenceNumber}`;
453
+ return {
454
+ message: `Interrupted OpenAI Codex response after ${state.consecutiveEvents} consecutive whitespace-only tool-call argument delta events (${state.consecutiveChars} chars${sequenceLabel})${itemLabel}.`,
455
+ };
456
+ }
457
+
458
+ handleToolCallArgumentsDelta(
459
+ rawEvent: Record<string, unknown>,
460
+ stream: AssistantMessageEventStream,
461
+ output: AssistantMessage,
462
+ ): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
463
+ const delta = (rawEvent as { delta?: string }).delta || "";
464
+ // Observe BEFORE the item/block guard: degenerate whitespace frames can keep
465
+ // arriving after the item closed (entry detached) and still count as
466
+ // progress for the idle watchdogs — dropping them unobserved would reopen
467
+ // the infinite-loop hole the breaker exists for.
468
+ const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
469
+ if (interruption) return interruption;
470
+ // Route to the entry the event keys to; a delta whose item already closed
471
+ // is dropped instead of leaking into a sibling tool call (#2619).
472
+ const entry = this.openItemForEvent(rawEvent);
473
+ if (!entry) return undefined;
474
+ if (entry.item.type !== "function_call" || entry.block?.type !== "toolCall") return undefined;
475
+ accumulateToolCallArgumentsDelta(entry.block, delta, stream, output, entry.contentIndex);
476
+ return undefined;
477
+ }
478
+
479
+ handleToolCallArgumentsDone(rawEvent: Record<string, unknown>): void {
480
+ const entry = this.openItemForEvent(rawEvent);
481
+ if (entry?.item.type !== "function_call" || entry.block?.type !== "toolCall") return;
482
+ const args = (rawEvent as { arguments?: string }).arguments;
483
+ if (typeof args === "string") finalizeToolCallArgumentsDone(entry.block, args);
484
+ }
485
+
486
+ handleCustomToolCallInputDelta(
487
+ rawEvent: Record<string, unknown>,
488
+ stream: AssistantMessageEventStream,
489
+ output: AssistantMessage,
490
+ ): CodexWhitespaceToolCallArgumentsDeltaInterruption | undefined {
491
+ const delta = (rawEvent as { delta?: string }).delta || "";
492
+ // Observe BEFORE the item/block guard — see handleToolCallArgumentsDelta.
493
+ const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
494
+ if (interruption) return interruption;
495
+ const entry = this.openItemForEvent(rawEvent);
496
+ if (!entry) return undefined;
497
+ if (entry.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return undefined;
498
+ accumulateCustomToolCallInputDelta(entry.block, delta, stream, output, entry.contentIndex);
499
+ return undefined;
500
+ }
501
+
502
+ handleCustomToolCallInputDone(rawEvent: Record<string, unknown>): void {
503
+ const entry = this.openItemForEvent(rawEvent);
504
+ if (entry?.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return;
505
+ const input = (rawEvent as { input?: string }).input;
506
+ if (typeof input === "string") finalizeCustomToolCallInputDone(entry.block, input);
507
+ }
508
+
509
+ handleResponseCreated(rawEvent: Record<string, unknown>): void {
510
+ const response = (rawEvent as { response?: { id?: string } }).response;
511
+ const state = this.websocketState;
512
+ if (state && this.transport === "websocket" && typeof response?.id === "string" && response.id.length > 0) {
513
+ state.lastResponseId = response.id;
514
+ }
515
+ }
516
+ }
517
+
518
+ interface CodexWhitespaceToolCallArgumentsDeltaState {
519
+ itemId: string;
520
+ outputIndex?: number;
521
+ consecutiveEvents: number;
522
+ consecutiveChars: number;
523
+ firstSequenceNumber?: number;
524
+ lastSequenceNumber?: number;
525
+ }
526
+
527
+ interface CodexWhitespaceToolCallArgumentsDeltaInterruption {
528
+ message: string;
529
+ }
530
+
531
+ interface CodexStreamFailureContext {
532
+ model: Model<"openai-codex-responses">;
533
+ output: AssistantMessage;
534
+ options: OpenAICodexResponsesOptions | undefined;
535
+ requestContext: CodexRequestContext;
536
+ startTime: number;
537
+ firstTokenTime?: number;
538
+ }
539
+
540
+ interface CodexStreamCompletion {
541
+ firstTokenTime?: number;
542
+ }
543
+
544
+ function createCodexProviderSessionState(): CodexProviderSessionState {
545
+ const state: CodexProviderSessionState = {
546
+ webSocketSessions: new Map(),
547
+ webSocketPublicToPrivate: new Map(),
548
+ close: () => {
549
+ for (const session of state.webSocketSessions.values()) {
550
+ session.connection?.close("session_disposed");
551
+ }
552
+ state.webSocketSessions.clear();
553
+ state.webSocketPublicToPrivate.clear();
554
+ },
555
+ };
556
+ return state;
557
+ }
558
+
559
+ function getCodexProviderSessionState(
560
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
561
+ ): CodexProviderSessionState | undefined {
562
+ if (!providerSessionState) return undefined;
563
+ const existing = providerSessionState.get(CODEX_PROVIDER_SESSION_STATE_KEY) as CodexProviderSessionState | undefined;
564
+ if (existing) return existing;
565
+ const created = createCodexProviderSessionState();
566
+ providerSessionState.set(CODEX_PROVIDER_SESSION_STATE_KEY, created);
567
+ return created;
568
+ }
569
+
570
+ function isCodexWebSocketRetryableStreamError(error: unknown): boolean {
571
+ if (!(error instanceof CodexWebSocketTransportError)) return false;
572
+ const message = error.message.toLowerCase();
573
+ return (
574
+ message.includes("websocket closed (") ||
575
+ message.includes("websocket closed before response completion") ||
576
+ message.includes("websocket connection is unavailable") ||
577
+ message.includes("websocket send failed") ||
578
+ message.includes("websocket ping failed") ||
579
+ message.includes("websocket pong timeout") ||
580
+ message.includes("websocket message queue exceeded") ||
581
+ message.includes("websocket request already in progress") ||
582
+ message.includes("idle timeout waiting for websocket") ||
583
+ message.includes("timeout waiting for first websocket event") ||
584
+ message.includes("syntaxerror") ||
585
+ message.includes("json")
586
+ );
587
+ }
588
+ function toCodexHeaderRecord(value: unknown): Record<string, string> | null {
589
+ if (!value || typeof value !== "object") return null;
590
+ const headers: Record<string, string> = {};
591
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
592
+ if (typeof entry === "string") {
593
+ headers[key] = entry;
594
+ } else if (Array.isArray(entry) && entry.every(item => typeof item === "string")) {
595
+ headers[key] = entry.join(",");
596
+ } else if (typeof entry === "number" || typeof entry === "boolean") {
597
+ headers[key] = String(entry);
598
+ }
599
+ }
600
+ return Object.keys(headers).length > 0 ? headers : null;
601
+ }
602
+
603
+ function toCodexHeaders(value: unknown): Headers | undefined {
604
+ if (!value) return undefined;
605
+ if (value instanceof Headers) return value;
606
+ if (Array.isArray(value)) {
607
+ try {
608
+ return new Headers(value as Array<[string, string]>);
609
+ } catch {
610
+ return undefined;
611
+ }
612
+ }
613
+ const record = toCodexHeaderRecord(value);
614
+ if (!record) return undefined;
615
+ return new Headers(record);
616
+ }
617
+
618
+ function updateCodexSessionMetadataFromHeaders(
619
+ state: CodexWebSocketSessionState | undefined,
620
+ headers: Headers | Record<string, string> | null | undefined,
621
+ ): void {
622
+ if (!state || !headers) return;
623
+ const resolvedHeaders = headers instanceof Headers ? headers : new Headers(headers);
624
+ const turnState = resolvedHeaders.get(X_CODEX_TURN_STATE_HEADER);
625
+ if (turnState && turnState.length > 0) {
626
+ state.turnState = turnState;
627
+ }
628
+ const modelsEtag = resolvedHeaders.get(X_MODELS_ETAG_HEADER);
629
+ if (modelsEtag && modelsEtag.length > 0) {
630
+ state.modelsEtag = modelsEtag;
631
+ }
632
+ }
633
+
634
+ function extractCodexWebSocketHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): Headers | undefined {
635
+ const eventRecord = openEvent as Record<string, unknown> | undefined;
636
+ const eventResponse = eventRecord?.response as Record<string, unknown> | undefined;
637
+ const socketRecord = socket as unknown as Record<string, unknown>;
638
+ const socketResponse = socketRecord.response as Record<string, unknown> | undefined;
639
+ const socketHandshake = socketRecord.handshake as Record<string, unknown> | undefined;
640
+ return (
641
+ toCodexHeaders(eventRecord?.responseHeaders) ??
642
+ toCodexHeaders(eventRecord?.headers) ??
643
+ toCodexHeaders(eventResponse?.headers) ??
644
+ toCodexHeaders(socketRecord.responseHeaders) ??
645
+ toCodexHeaders(socketRecord.handshakeHeaders) ??
646
+ toCodexHeaders(socketResponse?.headers) ??
647
+ toCodexHeaders(socketHandshake?.headers)
648
+ );
649
+ }
650
+
651
+ // Synthesizes a `RawSseEvent` for a Codex WebSocket frame so the same debug
652
+ // pipeline used for HTTP SSE (`onSseEvent` → `RawSseDebugBuffer.recordEvent`)
653
+ // also captures WebSocket traffic. The `raw` array mirrors SSE wire format
654
+ // (one line per field) so the existing TUI viewer renders it identically:
655
+ // : ws ← <type>
656
+ // event: <type>
657
+ // data: <json>
658
+ // Outbound (client → server) uses `: ws → <type>`. The viewer pretty-prints
659
+ // `data:` JSON lines, so we keep the wire JSON single-line here and let the
660
+ // renderer expand it.
661
+ function notifyCodexWebSocketInbound(
662
+ observer: ((event: RawSseEvent) => void) | undefined,
663
+ parsed: Record<string, unknown>,
664
+ text: string,
665
+ ): void {
666
+ const type = typeof parsed.type === "string" ? parsed.type : null;
667
+ const raw: string[] = [`: ws ← ${type ?? "(untyped)"}`];
668
+ if (type) raw.push(`event: ${type}`);
669
+ raw.push(`data: ${text}`);
670
+ notifyRawSseEvent(observer, { event: type, data: text, raw });
671
+ }
672
+
673
+ function notifyCodexWebSocketOutbound(
674
+ observer: ((event: RawSseEvent) => void) | undefined,
675
+ request: Record<string, unknown>,
676
+ payload: string,
677
+ ): void {
678
+ const type = typeof request.type === "string" ? request.type : null;
679
+ const raw: string[] = [`: ws → ${type ?? "(untyped)"}`];
680
+ if (type) raw.push(`event: ${type}`);
681
+ raw.push(`data: ${payload}`);
682
+ notifyRawSseEvent(observer, { event: type, data: payload, raw });
683
+ }
684
+
685
+ function notifyCodexWebSocketMalformed(
686
+ observer: ((event: RawSseEvent) => void) | undefined,
687
+ data: unknown,
688
+ error: unknown,
689
+ ): void {
690
+ const text = typeof data === "string" ? data : "";
691
+ const reason = error instanceof Error ? error.message : String(error);
692
+ const raw: string[] = [`: ws ← (parse-error: ${reason})`];
693
+ if (text) raw.push(`data: ${text}`);
694
+ notifyRawSseEvent(observer, { event: "parse_error", data: text, raw });
695
+ }
696
+
697
+ /** @internal Exported for tests. */
698
+ export function normalizeCodexToolChoice(
699
+ choice: ToolChoice | undefined,
700
+ tools: Tool[] = [],
701
+ model?: Model<"openai-codex-responses">,
702
+ ): string | Record<string, unknown> | undefined {
703
+ if (!choice) return undefined;
704
+ if (typeof choice === "string") return choice;
705
+ const allowFreeform = model ? model.applyPatchToolType === "freeform" : false;
706
+ const mapName = (name: string): Record<string, string> | undefined => {
707
+ const directTool = tools.find(tool => tool.name === name);
708
+ const customTool = allowFreeform
709
+ ? tools.find(tool => tool.customFormat && (tool.name === name || tool.customWireName === name))
710
+ : undefined;
711
+ const offeredTool = customTool ?? directTool;
712
+ if (!offeredTool) return undefined;
713
+ return customTool
714
+ ? { type: "custom", name: customTool.customWireName ?? customTool.name }
715
+ : { type: "function", name: offeredTool.name };
716
+ };
717
+ if (choice.type === "function") {
718
+ if ("function" in choice && choice.function?.name) {
719
+ return mapName(choice.function.name);
720
+ }
721
+ if ("name" in choice && choice.name) {
722
+ return mapName(choice.name);
723
+ }
724
+ }
725
+ if (choice.type === "tool" && choice.name) {
726
+ return mapName(choice.name);
727
+ }
728
+ return undefined;
729
+ }
730
+
731
+ function getCodexServiceTierCostMultiplier(
732
+ model: Pick<Model<"openai-codex-responses">, "id">,
733
+ serviceTier: ServiceTier | "default" | undefined,
734
+ ): number {
735
+ switch (serviceTier) {
736
+ case "flex":
737
+ return 0.5;
738
+ case "priority":
739
+ return model.id === "gpt-5.5" ? 2.5 : 2;
740
+ default:
741
+ return 1;
742
+ }
743
+ }
744
+
745
+ function resolveCodexCostServiceTier(res: unknown, req?: unknown): ServiceTier | "default" | undefined {
746
+ switch (res) {
747
+ case "flex":
748
+ return "flex";
749
+ case "priority":
750
+ return "priority";
751
+ default:
752
+ if (req === "flex" || req === "priority") {
753
+ return req;
754
+ }
755
+ return "default";
756
+ }
757
+ }
758
+
759
+ function applyCodexServiceTierPricing(
760
+ model: Pick<Model<"openai-codex-responses">, "id">,
761
+ usage: AssistantMessage["usage"],
762
+ resTier: unknown,
763
+ reqTier: unknown,
764
+ ): void {
765
+ const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
766
+ const multiplier = getCodexServiceTierCostMultiplier(model, resolvedTier);
767
+ if (multiplier === 1) return;
768
+ usage.cost.input *= multiplier;
769
+ usage.cost.output *= multiplier;
770
+ usage.cost.cacheRead *= multiplier;
771
+ usage.cost.cacheWrite *= multiplier;
772
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
773
+ }
774
+
775
+ function resetOutputState(output: AssistantMessage): void {
776
+ output.content.length = 0;
777
+ output.usage = {
778
+ input: 0,
779
+ output: 0,
780
+ cacheRead: 0,
781
+ cacheWrite: 0,
782
+ totalTokens: 0,
783
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
784
+ };
785
+ output.stopReason = "stop";
786
+ output.stopDetails = undefined;
787
+ }
788
+
789
+ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): CodexRequestSetup {
790
+ const requestAbortController = new AbortController();
791
+ const requestSignal = options?.signal
792
+ ? AbortSignal.any([options.signal, requestAbortController.signal])
793
+ : requestAbortController.signal;
794
+ const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
795
+ const websocketIdleTimeoutMs = options?.streamIdleTimeoutMs ?? CODEX_WEBSOCKET_IDLE_TIMEOUT_MS;
796
+ const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
797
+ const websocketFirstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS;
798
+ const wrapCodexSseStream = (
799
+ source: AsyncGenerator<Record<string, unknown>>,
800
+ ): AsyncGenerator<Record<string, unknown>> =>
801
+ iterateWithIdleTimeout(source, {
802
+ idleTimeoutMs,
803
+ firstItemTimeoutMs: firstEventTimeoutMs,
804
+ firstItemErrorMessage: "OpenAI Codex SSE stream timed out while waiting for the first event",
805
+ errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
806
+ onIdle: () => requestAbortController.abort(),
807
+ onFirstItemTimeout: () => requestAbortController.abort(),
808
+ abortSignal: options?.signal,
809
+ isProgressItem: isCodexStreamProgressEvent,
810
+ });
811
+ return {
812
+ requestAbortController,
813
+ requestSignal,
814
+ wrapCodexSseStream,
815
+ firstEventTimeoutMs,
816
+ websocketIdleTimeoutMs,
817
+ websocketFirstEventTimeoutMs,
818
+ };
819
+ }
820
+
821
+ async function buildCodexRequestContext(
822
+ model: Model<"openai-codex-responses">,
823
+ context: Context,
824
+ options: OpenAICodexResponsesOptions | undefined,
825
+ output: AssistantMessage,
826
+ ): Promise<CodexRequestContext> {
827
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
828
+ if (!apiKey) {
829
+ throw new AIError.MissingApiKeyError(model.provider);
830
+ }
831
+
832
+ const accountId = getAccountId(apiKey);
833
+ const baseUrl = model.baseUrl || CODEX_BASE_URL;
834
+ const url = resolveCodexResponsesUrl(baseUrl);
835
+ const promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
836
+ const transportSessionId = normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
837
+ const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
838
+
839
+ const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
840
+ const rawRequestDump: RawHttpRequestDump = {
841
+ provider: model.provider,
842
+ api: output.api,
843
+ model: model.id,
844
+ method: "POST",
845
+ url,
846
+ body: transformedBody,
847
+ };
848
+
849
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
850
+ const responsesLite = shouldUseCodexResponsesLite(transformedBody, options?.responsesLite);
851
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, baseUrl, responsesLite);
852
+ const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : undefined;
853
+ if (sessionKey && publicSessionKey) {
854
+ providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
855
+ }
856
+ const websocketState =
857
+ sessionKey && providerSessionState ? getCodexWebSocketSessionState(sessionKey, providerSessionState) : undefined;
858
+ if (websocketState && !isCodexWithinTurnContinuation(context)) {
859
+ // codex-rs scopes `x-codex-turn-state` to a single user turn: tool-loop
860
+ // follow-ups echo it, a new user turn starts without it.
861
+ websocketState.turnState = undefined;
862
+ }
863
+ return {
864
+ apiKey,
865
+ accountId,
866
+ baseUrl,
867
+ url,
868
+ requestHeaders,
869
+ transportSessionId,
870
+ providerSessionState,
871
+ websocketState,
872
+ responsesLite,
873
+ transformedBody,
874
+ rawRequestDump,
875
+ };
876
+ }
877
+
878
+ /** @internal Exported for tests. */
879
+ export async function buildTransformedCodexRequestBody(
880
+ model: Model<"openai-codex-responses">,
881
+ context: Context,
882
+ options: OpenAICodexResponsesOptions | undefined,
883
+ promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(options?.promptCacheKey ?? options?.sessionId),
884
+ ): Promise<RequestBody> {
885
+ const params: RequestBody = {
886
+ model: model.id,
887
+ input: convertMessages(model, context),
888
+ stream: true,
889
+ prompt_cache_key: promptCacheKey,
890
+ };
891
+
892
+ // `maxTokens` is intentionally not forwarded: transformRequestBody strips
893
+ // `max_output_tokens`/`max_completion_tokens` (the Codex backend rejects
894
+ // caller-supplied output caps). Sampling controls (`temperature`, `top_p`,
895
+ // `top_k`, `min_p`, `presence_penalty`, `repetition_penalty`,
896
+ // `frequency_penalty`, `stop`) are likewise refused with
897
+ // `{"detail":"Unsupported parameter: temperature"}` etc., so we drop
898
+ // everything from `StreamOptions` rather than forwarding any of them.
899
+ // (#3117 — codex-rs sends none of these either.)
900
+ applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
901
+ if (context.tools && context.tools.length > 0) {
902
+ params.tools = convertOpenAICodexResponsesTools(context.tools, model);
903
+ if (options?.toolChoice) {
904
+ const toolChoice = normalizeCodexToolChoice(options.toolChoice, context.tools, model);
905
+ if (toolChoice) {
906
+ params.tool_choice = toolChoice;
907
+ }
908
+ }
909
+ }
910
+
911
+ const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
912
+ if (systemPrompts.length > 0) {
913
+ params.instructions = systemPrompts[0];
914
+ }
915
+ const developerMessages = systemPrompts.slice(1);
916
+ if (options?.clientMetadata && Object.keys(options.clientMetadata).length > 0) {
917
+ params.client_metadata = { ...options.clientMetadata };
918
+ }
919
+ const codexOptions: CodexRequestOptions = {
920
+ reasoningEffort: options?.reasoning,
921
+ reasoningSummary: options?.reasoningSummary === undefined ? "auto" : options.reasoningSummary,
922
+ reasoningContext: options?.reasoningContext,
923
+ textVerbosity: options?.textVerbosity,
924
+ include: options?.include,
925
+ responsesLite: options?.responsesLite,
926
+ };
927
+
928
+ return transformRequestBody(params, model, codexOptions, { developerMessages });
929
+ }
930
+
931
+ async function openInitialCodexEventStream(
932
+ model: Model<"openai-codex-responses">,
933
+ options: OpenAICodexResponsesOptions | undefined,
934
+ requestSetup: CodexRequestSetup,
935
+ requestContext: CodexRequestContext,
936
+ ): Promise<{
937
+ eventStream: AsyncGenerator<Record<string, unknown>>;
938
+ requestBodyForState: RequestBody;
939
+ transport: CodexTransport;
940
+ }> {
941
+ const { transformedBody, websocketState } = requestContext;
942
+ if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
943
+ const websocketRetryBudget = CODEX_WEBSOCKET_RETRY_BUDGET;
944
+ let websocketRetries = 0;
945
+ while (true) {
946
+ try {
947
+ return await openCodexWebSocketTransport(
948
+ model,
949
+ options,
950
+ requestContext,
951
+ requestSetup,
952
+ websocketState,
953
+ websocketRetries,
954
+ options ? event => options.onSseEvent?.(event, model) : undefined,
955
+ );
956
+ } catch (error) {
957
+ if (!(error instanceof CodexWebSocketTransportError)) throw error;
958
+ const fatalWebSocketMessage = error.message.toLowerCase();
959
+ const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern =>
960
+ fatalWebSocketMessage.includes(pattern.toLowerCase()),
961
+ );
962
+ const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
963
+ recordCodexWebSocketFailure(websocketState, activateFallback);
964
+ CODEX_DEBUG &&
965
+ logger.debug("[codex] codex websocket fallback", {
966
+ error: error.message,
967
+ retry: websocketRetries,
968
+ retryBudget: websocketRetryBudget,
969
+ activated: activateFallback,
970
+ fatal: isFatal,
971
+ });
972
+ if (!activateFallback) {
973
+ websocketRetries += 1;
974
+ await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, websocketRetries), {
975
+ signal: requestSetup.requestSignal,
976
+ });
977
+ continue;
978
+ }
979
+ break;
980
+ }
981
+ }
982
+ }
983
+ return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
984
+ }
985
+ async function openCodexWebSocketTransport(
986
+ model: Model<"openai-codex-responses">,
987
+ options: OpenAICodexResponsesOptions | undefined,
988
+ requestContext: CodexRequestContext,
989
+ requestSetup: CodexRequestSetup,
990
+ websocketState: CodexWebSocketSessionState,
991
+ retry: number,
992
+ onSseEvent?: (event: RawSseEvent) => void,
993
+ ): Promise<{
994
+ eventStream: AsyncGenerator<Record<string, unknown>>;
995
+ requestBodyForState: RequestBody;
996
+ transport: CodexTransport;
997
+ }> {
998
+ const chainedBody = buildCodexChainedRequestBody(requestContext.transformedBody, websocketState);
999
+ // WebSocket frames cannot carry per-request HTTP headers, so the Responses
1000
+ // Lite marker rides in `client_metadata` on every `response.create`.
1001
+ let websocketRequest = {
1002
+ type: "response.create",
1003
+ ...chainedBody,
1004
+ ...(requestContext.responsesLite
1005
+ ? {
1006
+ client_metadata: {
1007
+ ...(chainedBody.client_metadata ?? {}),
1008
+ [CODEX_WS_RESPONSES_LITE_CLIENT_METADATA_KEY]: "true",
1009
+ },
1010
+ }
1011
+ : {}),
1012
+ };
1013
+ const replacementWebsocketRequest = await options?.onPayload?.(websocketRequest, model);
1014
+ if (replacementWebsocketRequest !== undefined) {
1015
+ websocketRequest = replacementWebsocketRequest as typeof websocketRequest;
1016
+ }
1017
+ const websocketHeaders = createCodexHeaders(
1018
+ requestContext.requestHeaders,
1019
+ requestContext.accountId,
1020
+ requestContext.apiKey,
1021
+ requestContext.transportSessionId,
1022
+ "websocket",
1023
+ websocketState,
1024
+ requestContext.responsesLite,
1025
+ );
1026
+ const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
1027
+ requestContext.rawRequestDump.body = websocketRequest;
1028
+ CODEX_DEBUG &&
1029
+ logger.debug("[codex] codex websocket request", {
1030
+ url: toWebSocketUrl(requestContext.url),
1031
+ model: requestContext.transformedBody.model,
1032
+ reasoningEffort: requestContext.transformedBody.reasoning?.effort ?? null,
1033
+ headers: redactHeaders(websocketHeaders),
1034
+ sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
1035
+ sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
1036
+ requestType: websocketRequest.type,
1037
+ retry,
1038
+ retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
1039
+ });
1040
+ const websocketConnection = await getOrCreateCodexWebSocketConnection(
1041
+ websocketState,
1042
+ toWebSocketUrl(requestContext.url),
1043
+ websocketHeaders,
1044
+ requestSetup.requestSignal,
1045
+ );
1046
+ const eventStream = websocketConnection.streamRequest(
1047
+ websocketRequest,
1048
+ {
1049
+ idleTimeoutMs: requestSetup.websocketIdleTimeoutMs,
1050
+ firstEventTimeoutMs: requestSetup.websocketFirstEventTimeoutMs,
1051
+ },
1052
+ requestSetup.requestSignal,
1053
+ onSseEvent,
1054
+ );
1055
+ return {
1056
+ eventStream,
1057
+ requestBodyForState,
1058
+ transport: "websocket",
1059
+ };
1060
+ }
1061
+
1062
+ /**
1063
+ * True when the request continues the current turn (everything after the
1064
+ * last assistant message is tool results), false when a new user turn starts.
1065
+ * Mirrors codex-rs, which scopes `x-codex-turn-state` to a single turn and
1066
+ * clears it when the next one begins.
1067
+ */
1068
+ function isCodexWithinTurnContinuation(context: Context): boolean {
1069
+ for (let i = context.messages.length - 1; i >= 0; i--) {
1070
+ const role = context.messages[i]?.role;
1071
+ if (role === "toolResult") continue;
1072
+ return role === "assistant";
1073
+ }
1074
+ return false;
1075
+ }
1076
+
1077
+ async function openCodexSseTransport(
1078
+ model: Model<"openai-codex-responses">,
1079
+ requestContext: CodexRequestContext,
1080
+ requestSetup: CodexRequestSetup,
1081
+ options: OpenAICodexResponsesOptions | undefined,
1082
+ state: CodexWebSocketSessionState | undefined,
1083
+ body = requestContext.transformedBody,
1084
+ ): Promise<{
1085
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1086
+ requestBodyForState: RequestBody;
1087
+ transport: CodexTransport;
1088
+ }> {
1089
+ const open = async (wireBody: RequestBody) => {
1090
+ // Keep the 400 dump honest: record the body actually sent on the wire.
1091
+ requestContext.rawRequestDump.body = wireBody;
1092
+ return requestSetup.wrapCodexSseStream(
1093
+ await openCodexSseEventStream(
1094
+ requestContext.url,
1095
+ requestContext.requestHeaders,
1096
+ requestContext.accountId,
1097
+ requestContext.apiKey,
1098
+ requestContext.transportSessionId,
1099
+ wireBody,
1100
+ state,
1101
+ requestContext.responsesLite,
1102
+ requestSetup.requestSignal,
1103
+ requestSetup.firstEventTimeoutMs,
1104
+ event => options?.onSseEvent?.(event, model),
1105
+ options?.fetch,
1106
+ ),
1107
+ );
1108
+ };
1109
+ let wireBody = body;
1110
+ const replacementWireBody = await options?.onPayload?.(wireBody, model);
1111
+ if (replacementWireBody !== undefined) {
1112
+ wireBody = replacementWireBody as RequestBody;
1113
+ }
1114
+ recordCodexWebSocketRequestStats(state, wireBody);
1115
+ return { eventStream: await open(wireBody), requestBodyForState: structuredCloneJSON(wireBody), transport: "sse" };
1116
+ }
1117
+
1118
+ function isJsonWhitespaceOnly(value: string): boolean {
1119
+ for (let index = 0; index < value.length; index += 1) {
1120
+ const code = value.charCodeAt(index);
1121
+ if (code !== 0x09 && code !== 0x0a && code !== 0x0d && code !== 0x20) {
1122
+ return false;
1123
+ }
1124
+ }
1125
+ return true;
1126
+ }
1127
+
1128
+ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null {
1129
+ if (item.type === "reasoning") {
1130
+ return { type: "thinking", thinking: "" };
1131
+ }
1132
+ if (item.type === "message") {
1133
+ const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
1134
+ return { type: "text", text: "", textSignature: encodeTextSignatureV1(item.id, phase) };
1135
+ }
1136
+ if (item.type === "function_call") {
1137
+ return {
1138
+ type: "toolCall",
1139
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1140
+ name: item.name,
1141
+ arguments: {},
1142
+ [kStreamingPartialJson]: item.arguments || "",
1143
+ };
1144
+ }
1145
+ if (item.type === "custom_tool_call") {
1146
+ // Wire name flows through unchanged; the agent-loop dispatcher also
1147
+ // matches `Tool.customWireName`. Reuse `partialJson` as the
1148
+ // accumulation buffer for the raw input string.
1149
+ return {
1150
+ type: "toolCall",
1151
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1152
+ name: item.name,
1153
+ arguments: { input: item.input ?? "" },
1154
+ customWireName: item.name,
1155
+ [kStreamingPartialJson]: item.input ?? "",
1156
+ };
1157
+ }
1158
+ return null;
1159
+ }
1160
+
1161
+ function getOutputBlockStartEventType(block: CodexOutputBlock): "thinking_start" | "text_start" | "toolcall_start" {
1162
+ if (block.type === "thinking") return "thinking_start";
1163
+ if (block.type === "text") return "text_start";
1164
+ return "toolcall_start";
1165
+ }
1166
+
1167
+ function isCodexStalePreviousResponseError(error: unknown): boolean {
1168
+ if (error instanceof CodexProviderStreamError) return error.code === "previous_response_not_found";
1169
+ if (!(error instanceof Error)) return false;
1170
+ if ((error as { code?: string }).code === "previous_response_not_found") return true;
1171
+ // "unsupported": the backend intermittently rejects the parameter outright
1172
+ // with `{"detail":"Unsupported parameter: previous_response_id"}` (no
1173
+ // `error.code`); treat it like a stale chain so the turn replays with full
1174
+ // context instead of surfacing the 400.
1175
+ return (
1176
+ /previous[ _]?response/i.test(error.message) &&
1177
+ /not[ _]?found|invalid|expired|stale|unsupported/i.test(error.message)
1178
+ );
1179
+ }
1180
+
1181
+ async function handleCodexStreamFailure(context: CodexStreamFailureContext, error: unknown): Promise<AssistantMessage> {
1182
+ const { output } = context;
1183
+ if (context.requestContext.websocketState) {
1184
+ resetCodexWebSocketAppendState(context.requestContext.websocketState);
1185
+ context.requestContext.websocketState.turnState = undefined;
1186
+ context.requestContext.websocketState.modelsEtag = undefined;
1187
+ }
1188
+ const result = await AIError.finalize(error, {
1189
+ api: context.model.api,
1190
+ signal: context.options?.signal,
1191
+ rawRequestDump: context.requestContext.rawRequestDump,
1192
+ });
1193
+ output.stopReason = result.stopReason;
1194
+ output.errorStatus = result.status;
1195
+ output.errorId = result.id;
1196
+ output.errorMessage = result.message;
1197
+ output.duration = performance.now() - context.startTime;
1198
+ if (context.firstTokenTime) {
1199
+ output.ttft = context.firstTokenTime - context.startTime;
1200
+ }
1201
+ return output;
1202
+ }
1203
+
1204
+ /**
1205
+ * Owns one `streamOpenAICodexResponses` call: the request scaffolding
1206
+ * (model/output/stream/options/request context) plus the per-attempt
1207
+ * {@link CodexStreamRuntime}. Drives the event loop in {@link process}, applies
1208
+ * the transport-fallback / retry recovery ladder, and emits the final message
1209
+ * in {@link finalize}. The runtime object is mutated in place across retries
1210
+ * (event stream and accumulators are swapped/reset), never reassigned.
1211
+ */
1212
+ class CodexStreamProcessor {
1213
+ runtime: CodexStreamRuntime;
1214
+ model: Model<"openai-codex-responses">;
1215
+ output: AssistantMessage;
1216
+ stream: AssistantMessageEventStream;
1217
+ options: OpenAICodexResponsesOptions | undefined;
1218
+ requestSetup: CodexRequestSetup;
1219
+ requestContext: CodexRequestContext;
1220
+ startTime: number;
1221
+ firstTokenTime?: number;
1222
+
1223
+ constructor(init: {
1224
+ runtime: CodexStreamRuntime;
1225
+ model: Model<"openai-codex-responses">;
1226
+ output: AssistantMessage;
1227
+ stream: AssistantMessageEventStream;
1228
+ options: OpenAICodexResponsesOptions | undefined;
1229
+ requestSetup: CodexRequestSetup;
1230
+ requestContext: CodexRequestContext;
1231
+ startTime: number;
1232
+ }) {
1233
+ this.runtime = init.runtime;
1234
+ this.model = init.model;
1235
+ this.output = init.output;
1236
+ this.stream = init.stream;
1237
+ this.options = init.options;
1238
+ this.requestSetup = init.requestSetup;
1239
+ this.requestContext = init.requestContext;
1240
+ this.startTime = init.startTime;
1241
+ }
1242
+
1243
+ async process(): Promise<CodexStreamCompletion> {
1244
+ const { output, stream } = this;
1245
+ stream.push({ type: "start", partial: output });
1246
+
1247
+ while (true) {
1248
+ try {
1249
+ let firstTokenTime = this.firstTokenTime;
1250
+ for await (const rawEvent of this.runtime.eventStream) {
1251
+ firstTokenTime = this.#handleStreamEvent(rawEvent, firstTokenTime);
1252
+ if (this.runtime.sawTerminalEvent) break;
1253
+ }
1254
+ return { firstTokenTime };
1255
+ } catch (error) {
1256
+ const recovered = await this.#recoverStreamError(error);
1257
+ if (!recovered) {
1258
+ throw error;
1259
+ }
1260
+ stream.push({ type: "start", partial: output });
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ #handleStreamEvent(rawEvent: Record<string, unknown>, firstTokenTime: number | undefined): number | undefined {
1266
+ const { output, stream } = this;
1267
+ const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
1268
+ if (!eventType) return firstTokenTime;
1269
+
1270
+ if (eventType === "response.output_item.added") {
1271
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
1272
+ if (!firstTokenTime) firstTokenTime = performance.now();
1273
+ const item = rawEvent.item as CodexEventItem;
1274
+ this.runtime.currentItem = item;
1275
+ this.runtime.currentBlock = createOutputBlockForItem(item);
1276
+ let contentIndex = -1;
1277
+ if (this.runtime.currentBlock) {
1278
+ output.content.push(this.runtime.currentBlock);
1279
+ contentIndex = output.content.length - 1;
1280
+ }
1281
+ // Track every open item by every stable key the wire gives us. `item.id`
1282
+ // is best; `output_index` preserves idless function/custom tool calls and
1283
+ // keeps their final args authoritative when only `output_item.done`
1284
+ // carries the full payload.
1285
+ const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : undefined;
1286
+ const outputIndex =
1287
+ typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index)
1288
+ ? Math.trunc(rawEvent.output_index)
1289
+ : undefined;
1290
+ const entry: CodexOpenItem = { item, block: this.runtime.currentBlock, contentIndex, itemId, outputIndex };
1291
+ this.runtime.currentEntry = entry;
1292
+ if (itemId) this.runtime.openItems.set(itemId, entry);
1293
+ if (outputIndex !== undefined) this.runtime.openItemsByOutputIndex.set(outputIndex, entry);
1294
+ if (!this.runtime.currentBlock) return firstTokenTime;
1295
+ stream.push({
1296
+ type: getOutputBlockStartEventType(this.runtime.currentBlock),
1297
+ contentIndex,
1298
+ partial: output,
1299
+ });
1300
+ return firstTokenTime;
1301
+ }
1302
+
1303
+ if (eventType === "response.reasoning_summary_part.added") {
1304
+ if (this.runtime.currentItem?.type === "reasoning") {
1305
+ appendReasoningSummaryPart(
1306
+ this.runtime.currentItem,
1307
+ (rawEvent as { part: ResponseReasoningItem["summary"][number] }).part,
1308
+ );
1309
+ }
1310
+ return firstTokenTime;
1311
+ }
1312
+
1313
+ if (eventType === "response.reasoning_summary_text.delta") {
1314
+ if (this.runtime.currentItem?.type === "reasoning" && this.runtime.currentBlock?.type === "thinking") {
1315
+ appendReasoningSummaryTextDelta(
1316
+ this.runtime.currentItem,
1317
+ this.runtime.currentBlock,
1318
+ (rawEvent as { delta?: string }).delta || "",
1319
+ stream,
1320
+ output,
1321
+ output.content.length - 1,
1322
+ );
1323
+ }
1324
+ return firstTokenTime;
1325
+ }
1326
+
1327
+ if (eventType === "response.reasoning_summary_part.done") {
1328
+ if (this.runtime.currentItem?.type === "reasoning" && this.runtime.currentBlock?.type === "thinking") {
1329
+ appendReasoningSummaryPartDone(
1330
+ this.runtime.currentItem,
1331
+ this.runtime.currentBlock,
1332
+ stream,
1333
+ output,
1334
+ output.content.length - 1,
1335
+ );
1336
+ }
1337
+ return firstTokenTime;
1338
+ }
1339
+
1340
+ if (eventType === "response.content_part.added") {
1341
+ if (this.runtime.currentItem?.type === "message") {
1342
+ appendMessageContentPart(
1343
+ this.runtime.currentItem,
1344
+ (rawEvent as { part?: ResponseOutputMessage["content"][number] }).part,
1345
+ );
1346
+ }
1347
+ return firstTokenTime;
1348
+ }
1349
+
1350
+ if (eventType === "response.output_text.delta" || eventType === "response.refusal.delta") {
1351
+ if (this.runtime.currentItem?.type === "message" && this.runtime.currentBlock?.type === "text") {
1352
+ appendMessageTextDelta(
1353
+ this.runtime.currentItem,
1354
+ this.runtime.currentBlock,
1355
+ (rawEvent as { delta?: string }).delta || "",
1356
+ stream,
1357
+ output,
1358
+ output.content.length - 1,
1359
+ eventType === "response.refusal.delta" ? "refusal" : "output_text",
1360
+ );
1361
+ }
1362
+ return firstTokenTime;
1363
+ }
1364
+
1365
+ if (eventType === "response.function_call_arguments.delta") {
1366
+ const interruption = this.runtime.handleToolCallArgumentsDelta(rawEvent, stream, output);
1367
+ if (interruption) {
1368
+ this.runtime.websocketState?.connection?.close("degenerate-tool-call");
1369
+ throw new CodexWhitespaceToolCallLoopError(interruption.message);
1370
+ }
1371
+ return firstTokenTime;
1372
+ }
1373
+
1374
+ if (eventType === "response.function_call_arguments.done") {
1375
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
1376
+ this.runtime.handleToolCallArgumentsDone(rawEvent);
1377
+ return firstTokenTime;
1378
+ }
1379
+
1380
+ if (eventType === "response.custom_tool_call_input.delta") {
1381
+ const interruption = this.runtime.handleCustomToolCallInputDelta(rawEvent, stream, output);
1382
+ if (interruption) {
1383
+ this.runtime.websocketState?.connection?.close("degenerate-tool-call");
1384
+ throw new CodexWhitespaceToolCallLoopError(interruption.message);
1385
+ }
1386
+ return firstTokenTime;
1387
+ }
1388
+
1389
+ if (eventType === "response.custom_tool_call_input.done") {
1390
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
1391
+ this.runtime.handleCustomToolCallInputDone(rawEvent);
1392
+ return firstTokenTime;
1393
+ }
1394
+
1395
+ if (eventType === "response.output_item.done") {
1396
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
1397
+ this.#handleOutputItemDone(rawEvent);
1398
+ return firstTokenTime;
1399
+ }
1400
+
1401
+ if (eventType === "response.created") {
1402
+ this.runtime.handleResponseCreated(rawEvent);
1403
+ return firstTokenTime;
1404
+ }
1405
+
1406
+ if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
1407
+ this.#handleResponseCompleted(rawEvent);
1408
+ return firstTokenTime;
1409
+ }
1410
+
1411
+ if (eventType === "response.metadata") {
1412
+ const moderation = asRecord(rawEvent.metadata)?.[CODEX_MODERATION_METADATA_KEY];
1413
+ if (moderation !== undefined) {
1414
+ try {
1415
+ this.options?.onModerationMetadata?.(moderation);
1416
+ } catch {
1417
+ // Diagnostic observer: failures must not disturb the stream.
1418
+ }
1419
+ }
1420
+ return firstTokenTime;
1421
+ }
1422
+
1423
+ if (eventType === "error" || eventType === "response.failed") {
1424
+ throw createCodexProviderStreamError(rawEvent);
1425
+ }
1426
+
1427
+ return firstTokenTime;
1428
+ }
1429
+
1430
+ #handleOutputItemDone(rawEvent: Record<string, unknown>): void {
1431
+ const { runtime, output, stream } = this;
1432
+ const rawItem = rawEvent.item;
1433
+ if (!rawItem || typeof rawItem !== "object") return;
1434
+ const item = structuredCloneJSON(rawItem) as CodexEventItem;
1435
+ runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
1436
+
1437
+ // Match the finalization to the OPEN ITEM that started this block, not the
1438
+ // singleton current — interleaved items can finish out of order, so the
1439
+ // most-recently-added block may belong to a sibling (#2619). Some Codex
1440
+ // function/custom tool items omit `id`; in that case `output_index` still
1441
+ // routes `output_item.done` to the block that received `output_item.added`.
1442
+ const itemId = typeof (item as { id?: string }).id === "string" ? (item as { id: string }).id : "";
1443
+ const entry = (itemId ? runtime.openItems.get(itemId) : null) ?? runtime.openItemForEvent(rawEvent);
1444
+ const block = entry?.block ?? null;
1445
+ const contentIndex = entry?.contentIndex ?? output.content.length - 1;
1446
+
1447
+ if (item.type === "reasoning" && block?.type === "thinking") {
1448
+ block.thinking = item.summary?.map(summary => summary.text).join("\n\n") || "";
1449
+ block.thinkingSignature = JSON.stringify(item);
1450
+ stream.push({
1451
+ type: "thinking_end",
1452
+ contentIndex,
1453
+ content: block.thinking,
1454
+ partial: output,
1455
+ });
1456
+ runtime.closeOpenItem(entry);
1457
+ return;
1458
+ }
1459
+
1460
+ if (item.type === "message" && block?.type === "text") {
1461
+ block.text = item.content
1462
+ .map(content => (content.type === "output_text" ? content.text : content.refusal))
1463
+ .join("");
1464
+ const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
1465
+ block.textSignature = encodeTextSignatureV1(item.id, phase);
1466
+ stream.push({
1467
+ type: "text_end",
1468
+ contentIndex,
1469
+ content: block.text,
1470
+ partial: output,
1471
+ });
1472
+ runtime.closeOpenItem(entry);
1473
+ return;
1474
+ }
1475
+
1476
+ if (item.type === "function_call") {
1477
+ const toolCall: ToolCall = {
1478
+ type: "toolCall",
1479
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1480
+ name: item.name,
1481
+ arguments: parseStreamingJson(item.arguments || "{}"),
1482
+ };
1483
+ if (block?.type === "toolCall") {
1484
+ // Persist the authoritative final args on the stored block; the throttled
1485
+ // delta parser may have left block.arguments stale (often `{}`).
1486
+ block.arguments = toolCall.arguments;
1487
+ clearStreamingPartialJson(block);
1488
+ }
1489
+ // Detach so a late/duplicate arguments.delta cannot append to the
1490
+ // finished block or trip the whitespace-loop guard against it.
1491
+ runtime.closeOpenItem(entry);
1492
+ runtime.canSafelyReplayWebsocketOverSse = false;
1493
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
1494
+ return;
1495
+ }
1496
+
1497
+ if (item.type === "custom_tool_call") {
1498
+ const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : undefined;
1499
+ const rawInput = partial && partial.length > 0 ? partial : (item.input ?? "");
1500
+ const toolCall: ToolCall = {
1501
+ type: "toolCall",
1502
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1503
+ name: item.name,
1504
+ arguments: { input: rawInput },
1505
+ customWireName: item.name,
1506
+ };
1507
+ if (block?.type === "toolCall") {
1508
+ block.arguments = { input: rawInput };
1509
+ clearStreamingPartialJson(block);
1510
+ }
1511
+ runtime.closeOpenItem(entry);
1512
+ runtime.canSafelyReplayWebsocketOverSse = false;
1513
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
1514
+ return;
1515
+ }
1516
+ }
1517
+
1518
+ #handleResponseCompleted(rawEvent: Record<string, unknown>): void {
1519
+ const { runtime, model, output } = this;
1520
+ runtime.sawTerminalEvent = true;
1521
+ const response = (
1522
+ rawEvent as {
1523
+ response?: {
1524
+ id?: string;
1525
+ usage?: {
1526
+ input_tokens?: number;
1527
+ output_tokens?: number;
1528
+ total_tokens?: number;
1529
+ input_tokens_details?: { cached_tokens?: number };
1530
+ output_tokens_details?: { reasoning_tokens?: number };
1531
+ };
1532
+ status?: string;
1533
+ service_tier?: ServiceTier | "default";
1534
+ end_turn?: boolean;
1535
+ };
1536
+ }
1537
+ ).response;
1538
+
1539
+ populateResponsesUsageFromResponse(output, response?.usage);
1540
+ if (typeof response?.id === "string" && response.id.length > 0) {
1541
+ output.responseId = response.id;
1542
+ }
1543
+
1544
+ const state = runtime.websocketState;
1545
+ if (state) {
1546
+ if (runtime.transport !== "websocket") {
1547
+ // SSE turns never chain (previous_response_id is websocket-only on this
1548
+ // endpoint); a completed SSE turn also invalidates any websocket append
1549
+ // baseline, which no longer matches the transcript.
1550
+ resetCodexWebSocketAppendState(state);
1551
+ } else {
1552
+ state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
1553
+ if (typeof response?.id === "string" && response.id.length > 0) {
1554
+ state.lastResponseId = response.id;
1555
+ state.lastResponseItems = stripInputItemIds(structuredCloneJSON(runtime.nativeOutputItems));
1556
+ state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
1557
+ } else {
1558
+ // Without a response id the append baseline cannot be trusted.
1559
+ state.canAppend = false;
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ finalizePendingResponsesToolCalls(output);
1565
+
1566
+ calculateCost(model, output.usage);
1567
+ applyCodexServiceTierPricing(
1568
+ model,
1569
+ output.usage,
1570
+ response?.service_tier,
1571
+ runtime.requestBodyForState.service_tier,
1572
+ );
1573
+ output.stopReason = mapOpenAIResponsesStopReason(response?.status as ResponseStatus | undefined);
1574
+ promoteResponsesToolUseStopReason(output, response?.end_turn);
1575
+ }
1576
+
1577
+ async #recoverStreamError(error: unknown): Promise<boolean> {
1578
+ if (await this.#tryRecoverWhitespaceToolCallLoop(error)) {
1579
+ return true;
1580
+ }
1581
+ if (await this.#tryReconnectWebSocketOnConnectionLimit(error)) {
1582
+ return true;
1583
+ }
1584
+ if (await this.#tryRecoverPreviousResponseNotFound(error)) {
1585
+ return true;
1586
+ }
1587
+ if (await this.#tryReplayWebsocketFailureOverSse(error)) {
1588
+ return true;
1589
+ }
1590
+ if (await this.#tryRetryProviderError(error)) {
1591
+ return true;
1592
+ }
1593
+ return false;
1594
+ }
1595
+
1596
+ /**
1597
+ * Recover from the degenerate whitespace-only tool-call argument loop
1598
+ * ({@link CodexWhitespaceToolCallLoopError}). The interrupted function call has
1599
+ * no usable arguments, so drop the partial turn and replay the request from
1600
+ * scratch — bounded by {@link CODEX_WHITESPACE_LOOP_RETRY_LIMIT}. Sampling
1601
+ * nondeterminism usually breaks the loop on a fresh attempt; once the budget is
1602
+ * exhausted the original error is surfaced (now without the junk tool call
1603
+ * polluting the message). Replay is refused once any visible content was already
1604
+ * delivered to the consumer — a finished tool call (`canSafelyReplayWebsocketOverSse`),
1605
+ * or any streamed text/commentary block still in `output.content` after the degenerate
1606
+ * tool call is dropped — because replaying re-emits already-streamed deltas.
1607
+ */
1608
+ async #tryRecoverWhitespaceToolCallLoop(error: unknown): Promise<boolean> {
1609
+ if (!(error instanceof CodexWhitespaceToolCallLoopError)) {
1610
+ return false;
1611
+ }
1612
+ // Drop the half-built degenerate tool call whether or not we retry, so it
1613
+ // never reaches the caller's message.
1614
+ this.#dropTrailingDegenerateToolCall();
1615
+ if (
1616
+ this.runtime.whitespaceLoopRetries >= CODEX_WHITESPACE_LOOP_RETRY_LIMIT ||
1617
+ !this.runtime.canSafelyReplayWebsocketOverSse ||
1618
+ this.output.content.some(block => block.type !== "thinking") ||
1619
+ this.options?.signal?.aborted
1620
+ ) {
1621
+ return false;
1622
+ }
1623
+
1624
+ this.runtime.whitespaceLoopRetries += 1;
1625
+ const websocketState = this.requestContext.websocketState;
1626
+ if (websocketState) {
1627
+ resetCodexWebSocketAppendState(websocketState);
1628
+ websocketState.turnState = undefined;
1629
+ websocketState.modelsEtag = undefined;
1630
+ }
1631
+
1632
+ CODEX_DEBUG &&
1633
+ logger.debug("[codex] retrying codex turn after whitespace-only tool-call argument loop", {
1634
+ retry: this.runtime.whitespaceLoopRetries,
1635
+ retryBudget: CODEX_WHITESPACE_LOOP_RETRY_LIMIT,
1636
+ transport: this.runtime.transport,
1637
+ });
1638
+
1639
+ this.runtime.resetAccumulators();
1640
+ this.runtime.sawTerminalEvent = false;
1641
+ this.runtime.whitespaceToolCallArgumentsDelta = undefined;
1642
+ resetOutputState(this.output);
1643
+ this.firstTokenTime = undefined;
1644
+ await scheduler.wait(CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS * this.runtime.whitespaceLoopRetries, {
1645
+ signal: this.requestSetup.requestSignal,
1646
+ });
1647
+
1648
+ if (this.runtime.transport === "websocket" && websocketState) {
1649
+ await this.#reopenWebSocketStream(websocketState);
1650
+ return true;
1651
+ }
1652
+
1653
+ await this.#reopenSseStream(websocketState);
1654
+ return true;
1655
+ }
1656
+
1657
+ /**
1658
+ * Pop the half-built degenerate tool-call block (the one whose arguments were
1659
+ * nothing but whitespace) off the output accumulator so it never surfaces in the
1660
+ * caller's message. Any legitimate content produced before it is preserved.
1661
+ */
1662
+ #dropTrailingDegenerateToolCall(): void {
1663
+ const { runtime, output } = this;
1664
+ const block = runtime.currentBlock;
1665
+ if (block && block.type === "toolCall" && output.content[output.content.length - 1] === block) {
1666
+ output.content.pop();
1667
+ }
1668
+ runtime.closeOpenItem(runtime.currentEntry);
1669
+ }
1670
+
1671
+ /**
1672
+ * Handles `websocket_connection_limit_reached` errors by closing the stale connection
1673
+ * and opening a fresh websocket. If content has already been emitted to the caller,
1674
+ * falls back to SSE replay (same as other WS failures) since we cannot safely
1675
+ * continue a partial response on a new connection. If a tool call was already
1676
+ * delivered (`canSafelyReplayWebsocketOverSse` is false), the error surfaces
1677
+ * instead — replaying would re-emit the same tool calls.
1678
+ */
1679
+ async #tryReconnectWebSocketOnConnectionLimit(error: unknown): Promise<boolean> {
1680
+ if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") {
1681
+ return false;
1682
+ }
1683
+ const websocketState = this.requestContext.websocketState;
1684
+ if (!websocketState || this.runtime.transport !== "websocket" || this.options?.signal?.aborted) {
1685
+ return false;
1686
+ }
1687
+
1688
+ // Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
1689
+ websocketState.connection?.close("connection_limit");
1690
+ websocketState.connection = undefined;
1691
+ resetCodexWebSocketAppendState(websocketState);
1692
+
1693
+ if (this.output.content.length > 0 && !this.runtime.canSafelyReplayWebsocketOverSse) {
1694
+ // A toolcall_end already reached the consumer; a full replay would emit
1695
+ // the same tool calls a second time. Let the error surface instead.
1696
+ return false;
1697
+ }
1698
+
1699
+ CODEX_DEBUG &&
1700
+ logger.debug("[codex] codex websocket connection limit reached, reconnecting", {
1701
+ hadContent: this.output.content.length > 0,
1702
+ retry: this.runtime.websocketStreamRetries,
1703
+ });
1704
+
1705
+ if (this.output.content.length > 0) {
1706
+ // Content already emitted to the caller — cannot safely continue on a new WS.
1707
+ // Reset and replay the full request over SSE.
1708
+ this.runtime.resetAccumulators();
1709
+ resetOutputState(this.output);
1710
+ this.firstTokenTime = undefined;
1711
+ recordCodexWebSocketFailure(websocketState, true);
1712
+ await this.#reopenSseStream(websocketState);
1713
+ return true;
1714
+ }
1715
+
1716
+ // No content emitted yet — clear accumulator state from the failed attempt
1717
+ // (blockless native items can exist even with empty content) and reconnect
1718
+ // over websocket, bounded by the shared retry budget: an account-scoped
1719
+ // limit can reject every fresh connection, and an unbounded loop would
1720
+ // hammer the endpoint with zero backoff.
1721
+ this.runtime.resetAccumulators();
1722
+ this.firstTokenTime = undefined;
1723
+ if (this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET) {
1724
+ recordCodexWebSocketFailure(websocketState, true);
1725
+ await this.#reopenSseStream(websocketState);
1726
+ return true;
1727
+ }
1728
+ this.runtime.websocketStreamRetries += 1;
1729
+ await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), {
1730
+ signal: this.requestSetup.requestSignal,
1731
+ });
1732
+ await this.#reopenWebSocketStream(websocketState);
1733
+ return true;
1734
+ }
1735
+
1736
+ async #tryRecoverPreviousResponseNotFound(error: unknown): Promise<boolean> {
1737
+ const websocketState = this.requestContext.websocketState;
1738
+ if (
1739
+ !isCodexStalePreviousResponseError(error) ||
1740
+ !websocketState ||
1741
+ this.output.content.length > 0 ||
1742
+ this.options?.signal?.aborted ||
1743
+ this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
1744
+ ) {
1745
+ return false;
1746
+ }
1747
+ if (this.runtime.transport !== "websocket") {
1748
+ // SSE never sends previous_response_id; let other recovery handle it.
1749
+ return false;
1750
+ }
1751
+
1752
+ this.runtime.providerRetryAttempt += 1;
1753
+ resetCodexWebSocketAppendState(websocketState);
1754
+ websocketState.turnState = undefined;
1755
+ websocketState.modelsEtag = undefined;
1756
+ this.runtime.resetAccumulators();
1757
+ this.runtime.sawTerminalEvent = false;
1758
+ resetOutputState(this.output);
1759
+ this.firstTokenTime = undefined;
1760
+
1761
+ CODEX_DEBUG &&
1762
+ logger.debug("[codex] codex previous_response_id expired; retrying with full context", {
1763
+ retry: this.runtime.providerRetryAttempt,
1764
+ });
1765
+ await this.#reopenWebSocketStream(websocketState);
1766
+ return true;
1767
+ }
1768
+
1769
+ async #tryReplayWebsocketFailureOverSse(error: unknown): Promise<boolean> {
1770
+ const websocketState = this.requestContext.websocketState;
1771
+ const canReplay =
1772
+ this.runtime.transport === "websocket" &&
1773
+ websocketState &&
1774
+ isCodexWebSocketRetryableStreamError(error) &&
1775
+ this.runtime.canSafelyReplayWebsocketOverSse &&
1776
+ !this.runtime.sawTerminalEvent &&
1777
+ !this.options?.signal?.aborted;
1778
+ if (!canReplay) return false;
1779
+
1780
+ const state = websocketState;
1781
+ const streamError = error instanceof Error ? error : new Error(String(error));
1782
+ const replayingBufferedOutputOverSse = this.output.content.length > 0;
1783
+ const fatalWebSocketMessage = streamError.message.toLowerCase();
1784
+ const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern =>
1785
+ fatalWebSocketMessage.includes(pattern.toLowerCase()),
1786
+ );
1787
+ const activateFallback =
1788
+ replayingBufferedOutputOverSse ||
1789
+ isFatal ||
1790
+ this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET;
1791
+ recordCodexWebSocketFailure(state, activateFallback);
1792
+ CODEX_DEBUG &&
1793
+ logger.debug("[codex] codex websocket stream fallback", {
1794
+ error: streamError.message,
1795
+ retry: this.runtime.websocketStreamRetries,
1796
+ retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
1797
+ activated: activateFallback,
1798
+ fatal: isFatal,
1799
+ replayedBufferedOutput: replayingBufferedOutputOverSse,
1800
+ });
1801
+
1802
+ if (!activateFallback) {
1803
+ this.runtime.websocketStreamRetries += 1;
1804
+ // Full re-send on a fresh socket: clear accumulator state from the failed
1805
+ // attempt. Content is empty here, but blockless native items (e.g.
1806
+ // web_search_call) may already have accumulated.
1807
+ this.runtime.resetAccumulators();
1808
+ this.firstTokenTime = undefined;
1809
+ await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), {
1810
+ signal: this.requestSetup.requestSignal,
1811
+ });
1812
+ await this.#reopenWebSocketStream(state);
1813
+ return true;
1814
+ }
1815
+
1816
+ this.runtime.resetAccumulators();
1817
+ resetOutputState(this.output);
1818
+ this.firstTokenTime = undefined;
1819
+
1820
+ await this.#reopenSseStream(state);
1821
+ return true;
1822
+ }
1823
+
1824
+ async #tryRetryProviderError(error: unknown): Promise<boolean> {
1825
+ if (
1826
+ !(error instanceof CodexProviderStreamError && error.retryable) ||
1827
+ this.output.content.length > 0 ||
1828
+ this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
1829
+ this.options?.signal?.aborted
1830
+ ) {
1831
+ return false;
1832
+ }
1833
+
1834
+ this.runtime.providerRetryAttempt += 1;
1835
+ const websocketState = this.requestContext.websocketState;
1836
+ if (websocketState) {
1837
+ resetCodexWebSocketAppendState(websocketState);
1838
+ websocketState.turnState = undefined;
1839
+ websocketState.modelsEtag = undefined;
1840
+ }
1841
+
1842
+ CODEX_DEBUG &&
1843
+ logger.debug("[codex] retrying codex provider stream error", {
1844
+ error: error instanceof Error ? error.message : String(error),
1845
+ retry: this.runtime.providerRetryAttempt,
1846
+ retryBudget: CODEX_MAX_RETRIES,
1847
+ transport: this.runtime.transport,
1848
+ });
1849
+
1850
+ this.runtime.resetAccumulators();
1851
+ this.runtime.sawTerminalEvent = false;
1852
+ resetOutputState(this.output);
1853
+ this.firstTokenTime = undefined;
1854
+ await scheduler.wait(CODEX_RETRY_DELAY_MS * this.runtime.providerRetryAttempt, {
1855
+ signal: this.requestSetup.requestSignal,
1856
+ });
1857
+
1858
+ if (this.runtime.transport === "websocket" && websocketState) {
1859
+ await this.#reopenWebSocketStream(websocketState);
1860
+ return true;
1861
+ }
1862
+
1863
+ await this.#reopenSseStream(websocketState);
1864
+ return true;
1865
+ }
1866
+
1867
+ async #reopenWebSocketStream(state: CodexWebSocketSessionState): Promise<void> {
1868
+ try {
1869
+ const next = await openCodexWebSocketTransport(
1870
+ this.model,
1871
+ this.options,
1872
+ this.requestContext,
1873
+ this.requestSetup,
1874
+ state,
1875
+ this.runtime.websocketStreamRetries,
1876
+ this.options ? event => this.options?.onSseEvent?.(event, this.model) : undefined,
1877
+ );
1878
+ this.runtime.eventStream = next.eventStream;
1879
+ this.runtime.requestBodyForState = next.requestBodyForState;
1880
+ this.runtime.transport = next.transport;
1881
+ state.lastTransport = next.transport;
1882
+ } catch (error) {
1883
+ if (!(error instanceof CodexWebSocketTransportError)) throw error;
1884
+ // Reopen failed at the websocket layer (handshake refused, connect timeout, etc.).
1885
+ // Activate fallback so subsequent turns use SSE, and replay this turn over SSE
1886
+ // instead of surfacing a raw transport error to the caller.
1887
+ recordCodexWebSocketFailure(state, true);
1888
+ CODEX_DEBUG &&
1889
+ logger.debug("[codex] codex websocket reopen failed, falling back to SSE", {
1890
+ error: error.message,
1891
+ retry: this.runtime.websocketStreamRetries,
1892
+ });
1893
+ await this.#reopenSseStream(state);
1894
+ }
1895
+ }
1896
+
1897
+ async #reopenSseStream(state: CodexWebSocketSessionState | undefined): Promise<void> {
1898
+ const next = await openCodexSseTransport(this.model, this.requestContext, this.requestSetup, this.options, state);
1899
+ this.runtime.eventStream = next.eventStream;
1900
+ this.runtime.requestBodyForState = next.requestBodyForState;
1901
+ this.runtime.transport = next.transport;
1902
+ if (state) {
1903
+ state.lastTransport = next.transport;
1904
+ }
1905
+ }
1906
+
1907
+ finalize(completion: CodexStreamCompletion): AssistantMessage {
1908
+ const { output } = this;
1909
+ if (this.options?.signal?.aborted) {
1910
+ throw new AIError.AbortError();
1911
+ }
1912
+ if (!this.runtime.sawTerminalEvent) {
1913
+ if (this.requestContext.websocketState) {
1914
+ resetCodexWebSocketAppendState(this.requestContext.websocketState);
1915
+ this.requestContext.websocketState.turnState = undefined;
1916
+ this.requestContext.websocketState.modelsEtag = undefined;
1917
+ }
1918
+ CODEX_DEBUG &&
1919
+ logger.debug("[codex] codex stream ended unexpectedly", {
1920
+ transport: this.runtime.transport,
1921
+ terminalEventSeen: this.runtime.sawTerminalEvent,
1922
+ unexpectedStreamEnd: true,
1923
+ sentTurnStateHeader: Boolean(this.requestContext.websocketState?.turnState),
1924
+ sentModelsEtagHeader: Boolean(this.requestContext.websocketState?.modelsEtag),
1925
+ });
1926
+ throw new CodexProviderStreamError("Codex stream ended before terminal completion event", false);
1927
+ }
1928
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
1929
+ throw new CodexProviderStreamError("Codex response failed", false);
1930
+ }
1931
+
1932
+ output.providerPayload = createOpenAIResponsesHistoryPayload(this.model.provider, this.runtime.nativeOutputItems);
1933
+ output.duration = performance.now() - this.startTime;
1934
+ if (completion.firstTokenTime) {
1935
+ output.ttft = completion.firstTokenTime - this.startTime;
1936
+ }
1937
+ return output;
1938
+ }
1939
+ }
1940
+
1941
+ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
1942
+ model: Model<"openai-codex-responses">,
1943
+ context: Context,
1944
+ options?: OpenAICodexResponsesOptions,
1945
+ ): AssistantMessageEventStream => {
1946
+ const stream = new AssistantMessageEventStream();
1947
+
1948
+ (async () => {
1949
+ const startTime = performance.now();
1950
+ const output: AssistantMessage = {
1951
+ role: "assistant",
1952
+ content: [],
1953
+ api: "openai-codex-responses" as Api,
1954
+ provider: model.provider,
1955
+ model: model.id,
1956
+ usage: {
1957
+ input: 0,
1958
+ output: 0,
1959
+ cacheRead: 0,
1960
+ cacheWrite: 0,
1961
+ totalTokens: 0,
1962
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1963
+ },
1964
+ stopReason: "stop",
1965
+ timestamp: Date.now(),
1966
+ };
1967
+ const requestSetup = createRequestSetup(options);
1968
+ let processingContext: CodexStreamProcessor | undefined;
1969
+ let requestContext: CodexRequestContext | undefined;
1970
+
1971
+ try {
1972
+ requestContext = await buildCodexRequestContext(model, context, options, output);
1973
+ const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
1974
+ const runtime = new CodexStreamRuntime({
1975
+ ...initialTransport,
1976
+ websocketState: requestContext.websocketState,
1977
+ });
1978
+ if (requestContext.websocketState) {
1979
+ requestContext.websocketState.lastTransport = initialTransport.transport;
1980
+ }
1981
+
1982
+ processingContext = new CodexStreamProcessor({
1983
+ runtime,
1984
+ model,
1985
+ output,
1986
+ stream,
1987
+ options,
1988
+ requestSetup,
1989
+ requestContext,
1990
+ startTime,
1991
+ });
1992
+
1993
+ const completion = await processingContext.process();
1994
+ processingContext.firstTokenTime = completion.firstTokenTime;
1995
+ const message = processingContext.finalize(completion);
1996
+ stream.push({ type: "done", reason: message.stopReason as "stop" | "length" | "toolUse", message });
1997
+ stream.end();
1998
+ } catch (error) {
1999
+ const failureContext =
2000
+ processingContext ??
2001
+ ({
2002
+ model,
2003
+ output,
2004
+ options,
2005
+ requestContext: requestContext ?? {
2006
+ apiKey: "",
2007
+ accountId: "",
2008
+ baseUrl: model.baseUrl || CODEX_BASE_URL,
2009
+ url: "",
2010
+ requestHeaders: {},
2011
+ responsesLite: options?.responsesLite === true,
2012
+ transformedBody: { model: model.id },
2013
+ rawRequestDump: {
2014
+ provider: model.provider,
2015
+ api: output.api,
2016
+ model: model.id,
2017
+ method: "POST",
2018
+ url: "",
2019
+ body: { model: model.id },
2020
+ },
2021
+ },
2022
+ startTime,
2023
+ } satisfies CodexStreamFailureContext);
2024
+ try {
2025
+ const failure = await handleCodexStreamFailure(failureContext, error);
2026
+ stream.push({ type: "error", reason: failure.stopReason as "error" | "aborted", error: failure });
2027
+ } catch (failureError) {
2028
+ // Last resort — the failure handler itself threw (exotic error object or
2029
+ // request-dump formatting). Never leave the stream un-ended.
2030
+ logger.error("Codex stream failure handler threw", {
2031
+ error: failureError instanceof Error ? failureError.message : String(failureError),
2032
+ });
2033
+ output.stopReason = "error";
2034
+ output.errorMessage ??= error instanceof Error ? error.message : String(error);
2035
+ stream.push({ type: "error", reason: "error", error: output });
2036
+ }
2037
+ stream.end();
2038
+ }
2039
+ })();
2040
+
2041
+ return stream;
2042
+ };
2043
+
2044
+ export async function prewarmOpenAICodexResponses(
2045
+ model: Model<"openai-codex-responses">,
2046
+ options?: Pick<
2047
+ OpenAICodexResponsesOptions,
2048
+ "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite"
2049
+ >,
2050
+ ): Promise<void> {
2051
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
2052
+ if (!apiKey) return;
2053
+ const accountId = getAccountId(apiKey);
2054
+ const baseUrl = model.baseUrl || CODEX_BASE_URL;
2055
+ const url = resolveCodexResponsesUrl(baseUrl);
2056
+ const transportSessionId = normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
2057
+ const promptCacheKey = transportSessionId;
2058
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
2059
+ const responsesLite = options?.responsesLite === true;
2060
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, baseUrl, responsesLite);
2061
+ const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : undefined;
2062
+ if (publicSessionKey && sessionKey) {
2063
+ providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
2064
+ }
2065
+ if (!sessionKey || !providerSessionState) return;
2066
+ const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
2067
+ if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
2068
+ const headers = logger.time(
2069
+ "prewarmCodex:createHeaders",
2070
+ createCodexHeaders,
2071
+ { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
2072
+ accountId,
2073
+ apiKey,
2074
+ promptCacheKey,
2075
+ "websocket",
2076
+ state,
2077
+ responsesLite,
2078
+ );
2079
+ await logger.time(
2080
+ "prewarmCodex:establishWs",
2081
+ getOrCreateCodexWebSocketConnection,
2082
+ state,
2083
+ toWebSocketUrl(url),
2084
+ headers,
2085
+ options?.signal,
2086
+ );
2087
+ state.prewarmed = true;
2088
+ }
2089
+
2090
+ function getCodexWebSocketSessionKey(
2091
+ normalizedSessionId: string | undefined,
2092
+ model: Model<"openai-codex-responses">,
2093
+ accountId: string,
2094
+ baseUrl: string,
2095
+ responsesLite: boolean,
2096
+ ): string | undefined {
2097
+ if (!normalizedSessionId) return undefined;
2098
+ // Responses Lite is connection-scoped on the WebSocket upgrade, so lite and
2099
+ // non-lite turns must never share a pooled socket or append state.
2100
+ const liteSuffix = responsesLite ? ":lite" : "";
2101
+ return `${accountId}:${baseUrl}:${model.id}:${normalizedSessionId}${liteSuffix}`;
2102
+ }
2103
+
2104
+ function getCodexWebSocketSessionState(
2105
+ sessionKey: string,
2106
+ providerSessionState: CodexProviderSessionState,
2107
+ ): CodexWebSocketSessionState {
2108
+ const existing = providerSessionState.webSocketSessions.get(sessionKey);
2109
+ if (existing) return existing;
2110
+ const created: CodexWebSocketSessionState = {
2111
+ disableWebsocket: false,
2112
+ canAppend: false,
2113
+ fallbackCount: 0,
2114
+ prewarmed: false,
2115
+ stats: {
2116
+ fullContextRequests: 0,
2117
+ deltaRequests: 0,
2118
+ lastInputItems: 0,
2119
+ },
2120
+ };
2121
+ providerSessionState.webSocketSessions.set(sessionKey, created);
2122
+ return created;
2123
+ }
2124
+
2125
+ function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void {
2126
+ state.canAppend = false;
2127
+ state.lastRequest = undefined;
2128
+ state.lastResponseId = undefined;
2129
+ state.lastResponseItems = undefined;
2130
+ }
2131
+
2132
+ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activateFallback: boolean): void {
2133
+ resetCodexWebSocketAppendState(state);
2134
+ // Never tear down a CONNECTING socket: it belongs to a concurrent caller's
2135
+ // in-flight handshake (prewarm/request race); closing it would reject that
2136
+ // caller with a fatal "websocket closed before open" and disable websockets
2137
+ // for the whole session.
2138
+ if (state.connection && !state.connection.isConnecting()) {
2139
+ state.connection.close("fallback");
2140
+ state.connection = undefined;
2141
+ }
2142
+ state.lastFallbackAt = Date.now();
2143
+ if (activateFallback && !state.disableWebsocket) {
2144
+ state.disableWebsocket = true;
2145
+ state.fallbackCount += 1;
2146
+ }
2147
+ }
2148
+
2149
+ function getCodexWebSocketEnvValue(): boolean | undefined {
2150
+ const envVal = $env.PI_CODEX_WEBSOCKET;
2151
+ if (envVal !== undefined) {
2152
+ return $flag("PI_CODEX_WEBSOCKET");
2153
+ }
2154
+ return undefined;
2155
+ }
2156
+
2157
+ function shouldUseCodexWebSocket(
2158
+ model: Model<"openai-codex-responses">,
2159
+ state: CodexWebSocketSessionState | undefined,
2160
+ preferWebsockets?: boolean,
2161
+ ): boolean {
2162
+ // Explicitly disabled by the model or session state.
2163
+ if (model.preferWebsockets === false) return false;
2164
+ // Explicitly disabled by the session state.
2165
+ if (!state || state.disableWebsocket) return false;
2166
+ // Env val > Preference
2167
+ const envVal = getCodexWebSocketEnvValue();
2168
+ if (envVal !== undefined) return envVal;
2169
+ // Negative preference overrides model preference; otherwise use the model's preference.
2170
+ if (preferWebsockets === false) return false;
2171
+ return true;
2172
+ }
2173
+
2174
+ export interface OpenAICodexTransportDetails {
2175
+ websocketPreferred: boolean;
2176
+ lastTransport?: CodexTransport;
2177
+ websocketDisabled: boolean;
2178
+ websocketConnected: boolean;
2179
+ fallbackCount: number;
2180
+ canAppend: boolean;
2181
+ prewarmed: boolean;
2182
+ hasSessionState: boolean;
2183
+ lastFallbackAt?: number;
2184
+ }
2185
+
2186
+ function getCodexWebSocketStateForPublicSession(
2187
+ model: Model<"openai-codex-responses">,
2188
+ options:
2189
+ | {
2190
+ sessionId?: string;
2191
+ baseUrl?: string;
2192
+ providerSessionState?: Map<string, ProviderSessionState>;
2193
+ }
2194
+ | undefined,
2195
+ ): CodexWebSocketSessionState | undefined {
2196
+ const baseUrl = options?.baseUrl || model.baseUrl || CODEX_BASE_URL;
2197
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
2198
+ const normalizedSessionId = normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
2199
+ const publicSessionKey = normalizedSessionId ? `${baseUrl}:${model.id}:${normalizedSessionId}` : undefined;
2200
+ const privateSessionKey = publicSessionKey
2201
+ ? providerSessionState?.webSocketPublicToPrivate.get(publicSessionKey)
2202
+ : undefined;
2203
+ return privateSessionKey ? providerSessionState?.webSocketSessions.get(privateSessionKey) : undefined;
2204
+ }
2205
+
2206
+ export function getOpenAICodexWebSocketDebugStats(
2207
+ model: Model<"openai-codex-responses">,
2208
+ options?: {
2209
+ sessionId?: string;
2210
+ baseUrl?: string;
2211
+ providerSessionState?: Map<string, ProviderSessionState>;
2212
+ },
2213
+ ): OpenAICodexWebSocketDebugStats | undefined {
2214
+ const stats = getCodexWebSocketStateForPublicSession(model, options)?.stats;
2215
+ return stats ? { ...stats } : undefined;
2216
+ }
2217
+
2218
+ export function getOpenAICodexTransportDetails(
2219
+ model: Model<"openai-codex-responses">,
2220
+ options?: {
2221
+ sessionId?: string;
2222
+ baseUrl?: string;
2223
+ preferWebsockets?: boolean;
2224
+ providerSessionState?: Map<string, ProviderSessionState>;
2225
+ },
2226
+ ): OpenAICodexTransportDetails {
2227
+ const envVal = getCodexWebSocketEnvValue();
2228
+ const websocketPreferred =
2229
+ envVal !== undefined
2230
+ ? envVal
2231
+ : options?.preferWebsockets === false
2232
+ ? false
2233
+ : options?.preferWebsockets === true || model.preferWebsockets === true;
2234
+ const state = getCodexWebSocketStateForPublicSession(model, options);
2235
+
2236
+ return {
2237
+ websocketPreferred,
2238
+ lastTransport: state?.lastTransport,
2239
+ websocketDisabled: state?.disableWebsocket ?? false,
2240
+ websocketConnected: state?.connection?.isOpen() ?? false,
2241
+ fallbackCount: state?.fallbackCount ?? 0,
2242
+ canAppend: state?.canAppend ?? false,
2243
+ prewarmed: state?.prewarmed ?? false,
2244
+ hasSessionState: state !== undefined,
2245
+ lastFallbackAt: state?.lastFallbackAt,
2246
+ };
2247
+ }
2248
+
2249
+ function stripInputItemIds(items: Array<Record<string, unknown>>): InputItem[] {
2250
+ return items.map(item => {
2251
+ if (item.id == null) return item as InputItem;
2252
+ const { id: _id, ...rest } = item;
2253
+ return rest as InputItem;
2254
+ });
2255
+ }
2256
+
2257
+ function recordCodexWebSocketRequestStats(
2258
+ state: CodexWebSocketSessionState | undefined,
2259
+ request: Record<string, unknown>,
2260
+ ): void {
2261
+ if (!state) return;
2262
+ const input = request.input;
2263
+ state.stats.lastInputItems = Array.isArray(input) ? input.length : 0;
2264
+ if (typeof request.previous_response_id === "string" && request.previous_response_id.length > 0) {
2265
+ state.stats.deltaRequests += 1;
2266
+ state.stats.lastDeltaInputItems = state.stats.lastInputItems;
2267
+ state.stats.lastPreviousResponseId = request.previous_response_id;
2268
+ return;
2269
+ }
2270
+ state.stats.fullContextRequests += 1;
2271
+ state.stats.lastDeltaInputItems = undefined;
2272
+ state.stats.lastPreviousResponseId = undefined;
2273
+ }
2274
+
2275
+ /**
2276
+ * Shape the next websocket turn's request body: when the session's append
2277
+ * baseline is intact (same options, strict history prefix), chain via
2278
+ * `previous_response_id` + delta-only `input`; otherwise break the chain and
2279
+ * replay the full transcript. SSE requests never chain — the HTTP endpoint's
2280
+ * request schema has no `previous_response_id` (codex-rs carries it only on
2281
+ * websocket `response.create` frames) and strict gateway validators 400 it
2282
+ * with `{"detail":"Unsupported parameter: previous_response_id"}`.
2283
+ */
2284
+ function buildCodexChainedRequestBody(
2285
+ requestBody: RequestBody,
2286
+ state: CodexWebSocketSessionState | undefined,
2287
+ ): RequestBody {
2288
+ const chainable = state?.canAppend === true;
2289
+ const appendInput = chainable
2290
+ ? buildResponsesDeltaInput(state.lastRequest, state.lastResponseItems, requestBody)
2291
+ : null;
2292
+ if (appendInput && appendInput.length > 0 && state?.lastResponseId) {
2293
+ const body: RequestBody = { ...requestBody, previous_response_id: state.lastResponseId, input: appendInput };
2294
+ recordCodexWebSocketRequestStats(state, body);
2295
+ return body;
2296
+ }
2297
+ if (chainable && state) {
2298
+ // Chaining was eligible but the prefix/options check failed: history
2299
+ // mutated or options changed — break the chain.
2300
+ CODEX_DEBUG &&
2301
+ logger.debug("[codex] codex append reset", {
2302
+ hadTurnStateHeader: Boolean(state.turnState),
2303
+ hadModelsEtagHeader: Boolean(state.modelsEtag),
2304
+ });
2305
+ resetCodexWebSocketAppendState(state);
2306
+ state.turnState = undefined;
2307
+ state.modelsEtag = undefined;
2308
+ }
2309
+ recordCodexWebSocketRequestStats(state, requestBody);
2310
+ return requestBody;
2311
+ }
2312
+
2313
+ function toWebSocketUrl(url: string): string {
2314
+ const parsed = new URL(url);
2315
+ if (parsed.protocol === "https:") {
2316
+ parsed.protocol = "wss:";
2317
+ } else if (parsed.protocol === "http:") {
2318
+ parsed.protocol = "ws:";
2319
+ }
2320
+ return parsed.toString();
2321
+ }
2322
+
2323
+ function headersToRecord(headers: Headers): Record<string, string> {
2324
+ const result: Record<string, string> = {};
2325
+ for (const [key, value] of headers.entries()) {
2326
+ result[key] = value;
2327
+ }
2328
+ return result;
2329
+ }
2330
+
2331
+ interface CodexWebSocketRequestTimeouts {
2332
+ idleTimeoutMs?: number;
2333
+ firstEventTimeoutMs?: number;
2334
+ }
2335
+
2336
+ interface CodexWebSocketConnectionOptions {
2337
+ onHandshakeHeaders?: (headers: Headers) => void;
2338
+ }
2339
+
2340
+ class CodexWebSocketConnection {
2341
+ #url: string;
2342
+ #headers: Record<string, string>;
2343
+ #onHandshakeHeaders?: (headers: Headers) => void;
2344
+ #socket: Bun.WebSocket | null = null;
2345
+ #queue: Array<Record<string, unknown> | Error | null> = [];
2346
+ #waiters: Array<() => void> = [];
2347
+ #connectPromise?: Promise<void>;
2348
+ #activeRequest = false;
2349
+ #streamObserver?: (event: RawSseEvent) => void;
2350
+ #heartbeatInterval: NodeJS.Timeout | undefined;
2351
+ #removePongListener?: () => void;
2352
+ #handshakeHeaders?: Headers;
2353
+ #debugResponseLog?: RequestDebugResponseLog;
2354
+ /**
2355
+ * Wall-clock of the most recent inbound activity on this socket — any
2356
+ * decoded message, any pong, or the moment the handshake completed. Used
2357
+ * by {@link isHealthyForReuse} so we don't write a continuation frame into
2358
+ * a TCP-open-but-server-evicted socket whose `readyState` still says OPEN.
2359
+ */
2360
+ #lastInboundAt = 0;
2361
+ /** Wall-clock of the last heartbeat ping we issued; 0 if none yet. */
2362
+ #lastPingAt = 0;
2363
+ /**
2364
+ * Most recent `response.id` accepted on this socket, retained across
2365
+ * requests. Lets the next request drop a trailing/duplicate frame from the
2366
+ * previous (cleanly-completed) response that outlived the queue drain.
2367
+ */
2368
+ #lastSeenResponseId?: string;
2369
+
2370
+ constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
2371
+ this.#url = url;
2372
+ this.#headers = headers;
2373
+ this.#onHandshakeHeaders = options.onHandshakeHeaders;
2374
+ }
2375
+
2376
+ isOpen(): boolean {
2377
+ return this.#socket?.readyState === WebSocket.OPEN;
2378
+ }
2379
+
2380
+ /** True while a handshake (possibly started by another caller) is still in flight. */
2381
+ isConnecting(): boolean {
2382
+ return this.#connectPromise !== undefined;
2383
+ }
2384
+
2385
+ /**
2386
+ * Stricter variant of {@link isOpen} for the connection-pool reuse gate.
2387
+ * Refuses sockets that have been silent past {@link CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS}.
2388
+ *
2389
+ * Bun's `WebSocket` does not always surface server-side eviction (no
2390
+ * `onclose`, no `onerror`), so a socket can sit in readyState OPEN long
2391
+ * after the upstream has dropped it. Reusing such a socket sends the next
2392
+ * `response.create` into a half-open write buffer and parks the reader
2393
+ * until the first-event / idle timeout fires (issue #1450). Forcing a
2394
+ * reconnect on any suspect socket trades a sub-second handshake for a
2395
+ * 60–300 s stall.
2396
+ */
2397
+ isHealthyForReuse(): boolean {
2398
+ if (!this.isOpen()) return false;
2399
+ const maxIdleMs = CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS;
2400
+ if (maxIdleMs <= 0) return true;
2401
+ // Initial connect sets #lastInboundAt; any later message or pong refreshes
2402
+ // it. A zero value means the field was never initialized, which itself is
2403
+ // a desync — treat as unhealthy.
2404
+ if (this.#lastInboundAt === 0) return false;
2405
+ return Date.now() - this.#lastInboundAt <= maxIdleMs;
2406
+ }
2407
+
2408
+ matchesAuth(headers: Record<string, string>): boolean {
2409
+ return this.#headers.authorization === headers.authorization;
2410
+ }
2411
+
2412
+ close(reason = "done"): void {
2413
+ if (
2414
+ this.#socket &&
2415
+ (this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)
2416
+ ) {
2417
+ this.#socket.close(1000, reason);
2418
+ }
2419
+ this.#socket = null;
2420
+ this.#stopHeartbeat();
2421
+ }
2422
+
2423
+ async connect(signal?: AbortSignal): Promise<void> {
2424
+ if (this.isOpen()) return;
2425
+ if (this.#connectPromise) {
2426
+ logger.time("codexWs:awaitSharedHandshake");
2427
+ await this.#connectPromise;
2428
+ return;
2429
+ }
2430
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
2431
+ this.#connectPromise = promise;
2432
+ const socket = new (WebSocket as unknown as new (url: string, opts: Bun.WebSocketOptions) => Bun.WebSocket)(
2433
+ this.#url,
2434
+ { headers: this.#headers },
2435
+ );
2436
+ socket.binaryType = "nodebuffer";
2437
+ this.#socket = socket;
2438
+ let settled = false;
2439
+ let timeout: NodeJS.Timeout | undefined;
2440
+ const clearPending = () => {
2441
+ if (timeout !== undefined) {
2442
+ clearTimeout(timeout);
2443
+ timeout = undefined;
2444
+ }
2445
+ if (signal) signal.removeEventListener("abort", onAbort);
2446
+ };
2447
+ const onAbort = () => {
2448
+ socket.close(1000, "aborted");
2449
+ if (!settled) {
2450
+ settled = true;
2451
+ clearPending();
2452
+ reject(new CodexWebSocketTransportError(`request was aborted`));
2453
+ }
2454
+ };
2455
+ if (signal) {
2456
+ if (signal.aborted) {
2457
+ onAbort();
2458
+ } else {
2459
+ signal.addEventListener("abort", onAbort, { once: true });
2460
+ }
2461
+ }
2462
+ if (!settled) {
2463
+ timeout = setTimeout(() => {
2464
+ socket.close(1000, "connect-timeout");
2465
+ if (!settled) {
2466
+ settled = true;
2467
+ clearPending();
2468
+ reject(new CodexWebSocketTransportError(`connection timeout`));
2469
+ }
2470
+ }, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
2471
+ }
2472
+
2473
+ socket.onopen = event => {
2474
+ if (!settled) {
2475
+ settled = true;
2476
+ clearPending();
2477
+ this.#lastInboundAt = Date.now();
2478
+ this.#captureHandshakeHeaders(socket, event);
2479
+ this.#startHeartbeat(socket);
2480
+ resolve();
2481
+ }
2482
+ };
2483
+ socket.onerror = event => {
2484
+ const eventRecord = event as unknown as Record<string, unknown>;
2485
+ const detail =
2486
+ (typeof eventRecord.message === "string" && eventRecord.message) ||
2487
+ (eventRecord.error instanceof Error && eventRecord.error.message) ||
2488
+ String(event.type);
2489
+ const error = new CodexWebSocketTransportError(`websocket error: ${detail}`);
2490
+ if (!settled) {
2491
+ settled = true;
2492
+ clearPending();
2493
+ reject(error);
2494
+ return;
2495
+ }
2496
+ this.#push(error);
2497
+ };
2498
+ socket.onclose = event => {
2499
+ this.#socket = null;
2500
+ this.#stopHeartbeat();
2501
+ if (!settled) {
2502
+ settled = true;
2503
+ clearPending();
2504
+ reject(new CodexWebSocketTransportError(`websocket closed before open (${event.code})`));
2505
+ return;
2506
+ }
2507
+ this.#push(new CodexWebSocketTransportError(`websocket closed (${event.code})`));
2508
+ this.#push(null);
2509
+ };
2510
+ socket.onmessage = event => {
2511
+ // Stamp inbound activity before parsing so even malformed frames refresh
2512
+ // the liveness clock — what matters for reuse health is that the upstream
2513
+ // is still talking to us, not that every frame is well-formed.
2514
+ this.#lastInboundAt = Date.now();
2515
+ this.#writeDebugWebSocketFrame(event.data);
2516
+ try {
2517
+ const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
2518
+ if (!text) return;
2519
+ const parsed = JSON.parse(text) as Record<string, unknown>;
2520
+ if (parsed.type === "error" && typeof parsed.error === "object" && parsed.error) {
2521
+ const inner = parsed.error as Record<string, unknown>;
2522
+ if (typeof parsed.code !== "string" && typeof inner.code === "string") {
2523
+ parsed.code = inner.code;
2524
+ }
2525
+ if (typeof parsed.message !== "string" && typeof inner.message === "string") {
2526
+ parsed.message = inner.message;
2527
+ }
2528
+ }
2529
+ notifyCodexWebSocketInbound(this.#streamObserver, parsed, text);
2530
+ this.#push(parsed);
2531
+ } catch (error) {
2532
+ notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
2533
+ this.#push(new CodexWebSocketTransportError(`${String(error)}`));
2534
+ }
2535
+ };
2536
+
2537
+ logger.time("codexWs:awaitTcpHandshake");
2538
+ try {
2539
+ await promise;
2540
+ } finally {
2541
+ this.#connectPromise = undefined;
2542
+ }
2543
+ }
2544
+
2545
+ async *streamRequest(
2546
+ request: Record<string, unknown>,
2547
+ timeouts: CodexWebSocketRequestTimeouts,
2548
+ signal?: AbortSignal,
2549
+ onSseEvent?: (event: RawSseEvent) => void,
2550
+ ): AsyncGenerator<Record<string, unknown>> {
2551
+ if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
2552
+ throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
2553
+ }
2554
+ if (this.#activeRequest) {
2555
+ throw new CodexWebSocketTransportError(`websocket request already in progress`);
2556
+ }
2557
+ if (signal?.aborted) {
2558
+ throw new CodexWebSocketTransportError(`request was aborted`);
2559
+ }
2560
+ this.#activeRequest = true;
2561
+ this.#streamObserver = onSseEvent;
2562
+ // Drain any non-error frames left over from a prior request before sending.
2563
+ // `CodexStreamProcessor.process` breaks its `for-await` on the terminal event,
2564
+ // which interrupts our generator at `yield next` (the post-yield `break`
2565
+ // never runs). Any frame that landed between the consumer's break and the
2566
+ // generator's `finally` lingers in `#queue` and would otherwise become the
2567
+ // first frame of THIS request — a stale `response.completed` would end the
2568
+ // turn immediately with empty output, and a stale non-progress frame would
2569
+ // flip `sawFirstEvent` and silently downgrade the first-event timeout to
2570
+ // the longer idle timeout. Transport errors are preserved so we surface
2571
+ // the death signal instead of writing into a dead socket.
2572
+ this.#dropStaleFrames();
2573
+ const onAbort = () => {
2574
+ this.close("aborted");
2575
+ this.#push(new CodexWebSocketTransportError(`request was aborted`));
2576
+ };
2577
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
2578
+
2579
+ try {
2580
+ const debugSession = isRequestDebugEnabled()
2581
+ ? await createRequestDebugSession({
2582
+ protocol: "websocket",
2583
+ method: "POST",
2584
+ url: this.#url,
2585
+ headers: this.#headers,
2586
+ body: request,
2587
+ })
2588
+ : undefined;
2589
+ this.#debugResponseLog = debugSession
2590
+ ? await debugSession.openResponseLog("WebSocket 101 Switching Protocols", this.#handshakeHeaders)
2591
+ : undefined;
2592
+
2593
+ const requestPayload = JSON.stringify(request);
2594
+ notifyCodexWebSocketOutbound(onSseEvent, request, requestPayload);
2595
+ // Re-check liveness: the debug-session await above can outlive the socket.
2596
+ const socket = this.#socket;
2597
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
2598
+ throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
2599
+ }
2600
+ try {
2601
+ socket.send(requestPayload);
2602
+ } catch (error) {
2603
+ throw new CodexWebSocketTransportError(
2604
+ `websocket send failed: ${error instanceof Error ? error.message : String(error)}`,
2605
+ );
2606
+ }
2607
+ let sawFirstEvent = false;
2608
+ const { idleTimeoutMs, firstEventTimeoutMs } = timeouts;
2609
+ let lastProgressAt = Date.now();
2610
+ let lastProgressEventType: string | undefined;
2611
+ let lastEventAt = lastProgressAt;
2612
+ let lastEventType: string | undefined;
2613
+ // Cross-request frame guard: lock onto this response's id and reject
2614
+ // frames belonging to another response interleaved on the reused socket.
2615
+ let activeResponseId: string | undefined;
2616
+ let lastSequence: number | undefined;
2617
+ const priorResponseId = this.#lastSeenResponseId;
2618
+ while (true) {
2619
+ let timeoutMs: number | undefined;
2620
+ let timeoutReason: string;
2621
+ if (sawFirstEvent) {
2622
+ timeoutReason = createCodexWebSocketTimeoutMessage("idle timeout waiting for websocket", {
2623
+ lastEventAt,
2624
+ lastEventType,
2625
+ lastProgressAt,
2626
+ lastProgressEventType,
2627
+ });
2628
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
2629
+ timeoutMs = idleTimeoutMs - (Date.now() - lastProgressAt);
2630
+ if (timeoutMs <= 0) {
2631
+ CODEX_DEBUG &&
2632
+ logger.debug("[codex] codex websocket idle timeout", {
2633
+ lastEventType,
2634
+ lastProgressEventType,
2635
+ msSinceLastEvent: Date.now() - lastEventAt,
2636
+ msSinceLastProgress: Date.now() - lastProgressAt,
2637
+ });
2638
+ throw new CodexWebSocketTransportError(`${timeoutReason}`);
2639
+ }
2640
+ }
2641
+ } else {
2642
+ timeoutReason = createCodexWebSocketTimeoutMessage("timeout waiting for first websocket event", {
2643
+ lastEventAt,
2644
+ lastEventType,
2645
+ lastProgressAt,
2646
+ lastProgressEventType,
2647
+ });
2648
+ if (firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0) {
2649
+ timeoutMs = firstEventTimeoutMs;
2650
+ }
2651
+ }
2652
+ const next = await this.#nextMessage(timeoutMs, timeoutReason);
2653
+ if (next instanceof Error) {
2654
+ throw next;
2655
+ }
2656
+ if (next === null) {
2657
+ throw new CodexWebSocketTransportError(`websocket closed before response completion`);
2658
+ }
2659
+ const eventType = typeof next.type === "string" ? next.type : "";
2660
+ // Cross-request frame guard. The socket is reused across turns. Upstream
2661
+ // codex-rs leans on the protocol guarantee that nothing follows a
2662
+ // response's terminal event, but our queue can still surface a trailing
2663
+ // or duplicate frame from a cleanly-completed prior response after
2664
+ // #dropStaleFrames() drained the queue at send time. Attaching such a
2665
+ // frame to THIS turn misattributes an earlier turn's output (a stale
2666
+ // `response.completed` ends the turn early; a stale item makes the model
2667
+ // see an unrelated call). Only lifecycle events (created/completed/
2668
+ // failed/incomplete) carry a `response.id` — exactly the harmful ones —
2669
+ // so key the guard on it and let idless frames (deltas, the rate-limit/
2670
+ // metadata preamble, created-less streams) pass through, matching
2671
+ // upstream rather than gating on `response.created`.
2672
+ const frameResponseId = extractCodexFrameResponseId(next);
2673
+ const frameSequence = extractCodexFrameSequenceNumber(next);
2674
+ if (frameResponseId !== undefined) {
2675
+ if (activeResponseId === undefined) {
2676
+ if (priorResponseId !== undefined && frameResponseId === priorResponseId) {
2677
+ // Trailing/duplicate frame of the previous response that
2678
+ // outlived the drain. Drop without locking or advancing the
2679
+ // first-event clocks so our own response can still start.
2680
+ continue;
2681
+ }
2682
+ activeResponseId = frameResponseId;
2683
+ } else if (frameResponseId !== activeResponseId) {
2684
+ // A different response is interleaving on the socket; the idless
2685
+ // deltas that follow are indistinguishable, so fail closed
2686
+ // (retryable) instead of risking misattribution.
2687
+ this.close("stale-frame");
2688
+ throw new CodexWebSocketTransportError(
2689
+ `websocket frame for response ${frameResponseId} interleaved into active response ${activeResponseId}`,
2690
+ );
2691
+ }
2692
+ this.#lastSeenResponseId = frameResponseId;
2693
+ }
2694
+ if (frameSequence !== undefined) {
2695
+ if (activeResponseId !== undefined && lastSequence !== undefined && frameSequence < lastSequence) {
2696
+ this.close("stale-frame");
2697
+ throw new CodexWebSocketTransportError(
2698
+ `websocket sequence_number ${frameSequence} regressed below ${lastSequence} within response ${activeResponseId}`,
2699
+ );
2700
+ }
2701
+ lastSequence = frameSequence;
2702
+ }
2703
+ sawFirstEvent = true;
2704
+ lastEventAt = Date.now();
2705
+ lastEventType = eventType || undefined;
2706
+ if (isCodexStreamProgressEvent(next)) {
2707
+ lastProgressAt = lastEventAt;
2708
+ lastProgressEventType = lastEventType;
2709
+ }
2710
+ yield next;
2711
+ if (
2712
+ eventType === "response.completed" ||
2713
+ eventType === "response.done" ||
2714
+ eventType === "response.incomplete" ||
2715
+ eventType === "response.failed" ||
2716
+ eventType === "error"
2717
+ ) {
2718
+ break;
2719
+ }
2720
+ }
2721
+ } finally {
2722
+ this.#activeRequest = false;
2723
+ this.#streamObserver = undefined;
2724
+ if (signal) {
2725
+ signal.removeEventListener("abort", onAbort);
2726
+ }
2727
+ const debugResponseLog = this.#debugResponseLog;
2728
+ this.#debugResponseLog = undefined;
2729
+ await debugResponseLog?.close();
2730
+ }
2731
+ }
2732
+
2733
+ #captureHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): void {
2734
+ const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
2735
+ if (!headers) return;
2736
+ this.#handshakeHeaders = headers;
2737
+ this.#onHandshakeHeaders?.(headers);
2738
+ }
2739
+
2740
+ #writeDebugWebSocketFrame(data: unknown): void {
2741
+ const log = this.#debugResponseLog;
2742
+ if (!log) return;
2743
+ if (typeof data === "string") {
2744
+ log.write(data);
2745
+ return;
2746
+ }
2747
+ if (data instanceof Uint8Array) {
2748
+ log.write(data);
2749
+ return;
2750
+ }
2751
+ if (data instanceof ArrayBuffer) {
2752
+ log.write(new Uint8Array(data));
2753
+ return;
2754
+ }
2755
+ log.write(String(data));
2756
+ }
2757
+
2758
+ #startHeartbeat(socket: Bun.WebSocket): void {
2759
+ this.#stopHeartbeat();
2760
+ const intervalMs = CODEX_WEBSOCKET_PING_INTERVAL_MS;
2761
+ if (intervalMs <= 0) return;
2762
+
2763
+ this.#lastPingAt = 0;
2764
+ const socketEventTarget = socket as EventTarget;
2765
+ const onPong = () => {
2766
+ // Pongs are inbound activity — refresh the reuse-health clock so a quiet
2767
+ // but ping-responsive socket stays trustworthy across requests.
2768
+ this.#lastInboundAt = Date.now();
2769
+ };
2770
+ if (
2771
+ typeof socketEventTarget.addEventListener === "function" &&
2772
+ typeof socketEventTarget.removeEventListener === "function"
2773
+ ) {
2774
+ socketEventTarget.addEventListener("pong", onPong);
2775
+ this.#removePongListener = () => socketEventTarget.removeEventListener("pong", onPong);
2776
+ }
2777
+
2778
+ this.#heartbeatInterval = setInterval(() => {
2779
+ if (this.#socket !== socket || socket.readyState !== WebSocket.OPEN) {
2780
+ this.#stopHeartbeat();
2781
+ return;
2782
+ }
2783
+ // Fail-closed on missing pongs even when no pong has ever been observed.
2784
+ // The previous `#observedPong &&` guard disabled the timeout entirely on
2785
+ // runtimes where Bun does not surface a `pong` event for our outgoing
2786
+ // pings (issue #1450) — letting truly dead sockets sail through the
2787
+ // pool until the per-request first-event / idle timeout (60–300 s)
2788
+ // finally fired. Instead, trigger on inbound silence: if we sent a
2789
+ // ping at least `pongTimeoutMs` ago and have received no traffic of
2790
+ // any kind (data frame or pong) since, the socket is unhealthy.
2791
+ const pongTimeoutMs = CODEX_WEBSOCKET_PONG_TIMEOUT_MS;
2792
+ if (
2793
+ pongTimeoutMs > 0 &&
2794
+ this.#lastPingAt > 0 &&
2795
+ this.#lastPingAt > this.#lastInboundAt &&
2796
+ Date.now() - this.#lastPingAt > pongTimeoutMs
2797
+ ) {
2798
+ this.#failQueue(new CodexWebSocketTransportError(`websocket pong timeout`), "pong-timeout");
2799
+ return;
2800
+ }
2801
+ if (typeof socket.ping !== "function") {
2802
+ this.#stopHeartbeat();
2803
+ return;
2804
+ }
2805
+ try {
2806
+ socket.ping();
2807
+ this.#lastPingAt = Date.now();
2808
+ } catch (error) {
2809
+ this.#failQueue(
2810
+ new CodexWebSocketTransportError(
2811
+ `websocket ping failed: ${error instanceof Error ? error.message : String(error)}`,
2812
+ ),
2813
+ "ping-failed",
2814
+ );
2815
+ }
2816
+ }, intervalMs);
2817
+ this.#heartbeatInterval.unref();
2818
+ }
2819
+
2820
+ #stopHeartbeat(): void {
2821
+ if (this.#heartbeatInterval) {
2822
+ clearInterval(this.#heartbeatInterval);
2823
+ this.#heartbeatInterval = undefined;
2824
+ }
2825
+ if (this.#removePongListener) {
2826
+ this.#removePongListener();
2827
+ this.#removePongListener = undefined;
2828
+ }
2829
+ this.#lastPingAt = 0;
2830
+ }
2831
+
2832
+ #failQueue(error: Error, closeReason: string): void {
2833
+ CODEX_DEBUG && logger.debug("[codex] codex websocket transport failure", { error: error.message, closeReason });
2834
+ this.#queue.length = 0;
2835
+ this.#queue.push(error);
2836
+ this.close(closeReason);
2837
+ this.#wakeWaiters();
2838
+ }
2839
+
2840
+ /**
2841
+ * Discard data frames from a previous request that remained in `#queue`
2842
+ * after the consumer broke out on the terminal event. Preserves any queued
2843
+ * transport error (from `onerror` / `onclose` / `#failQueue`) so the next
2844
+ * `#nextMessage` surfaces the death signal instead of waiting it out.
2845
+ *
2846
+ * Returns the number of frames dropped (test/debug visibility only).
2847
+ */
2848
+ #dropStaleFrames(): number {
2849
+ if (this.#queue.length === 0) return 0;
2850
+ const surviving = this.#queue.filter(item => item instanceof Error);
2851
+ const dropped = this.#queue.length - surviving.length;
2852
+ if (dropped === 0) return 0;
2853
+ this.#queue.length = 0;
2854
+ for (const item of surviving) this.#queue.push(item);
2855
+ CODEX_DEBUG && logger.debug("[codex] codex websocket dropped stale frames before request", { dropped });
2856
+ return dropped;
2857
+ }
2858
+
2859
+ #wakeWaiters(): void {
2860
+ for (;;) {
2861
+ const waiter = this.#waiters.shift();
2862
+ if (!waiter) break;
2863
+ waiter();
2864
+ }
2865
+ }
2866
+
2867
+ #push(item: Record<string, unknown> | Error | null): void {
2868
+ if (item instanceof Error) {
2869
+ // Append after frames already received instead of wiping them: a queued
2870
+ // terminal event (e.g. `response.completed` followed by an eager server
2871
+ // close) must still reach the consumer rather than morph into a spurious
2872
+ // transport failure. `#dropStaleFrames` keeps errors across requests, so
2873
+ // the death signal still surfaces if the data frames go unconsumed.
2874
+ this.#queue.push(item);
2875
+ this.#wakeWaiters();
2876
+ return;
2877
+ }
2878
+ if (item !== null && this.#queue.length >= CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY) {
2879
+ this.#failQueue(
2880
+ new CodexWebSocketTransportError(
2881
+ `websocket message queue exceeded ${CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY} items`,
2882
+ ),
2883
+ "queue-overflow",
2884
+ );
2885
+ return;
2886
+ }
2887
+ this.#queue.push(item);
2888
+ const waiter = this.#waiters.shift();
2889
+ if (waiter) waiter();
2890
+ }
2891
+
2892
+ async #nextMessage(
2893
+ timeoutMs: number | undefined,
2894
+ timeoutReason: string,
2895
+ ): Promise<Record<string, unknown> | Error | null> {
2896
+ while (this.#queue.length === 0) {
2897
+ const { promise, resolve } = Promise.withResolvers<void>();
2898
+ this.#waiters.push(resolve);
2899
+ let timedOut = false;
2900
+ let timeout: NodeJS.Timeout | undefined;
2901
+ if (timeoutMs !== undefined && timeoutMs > 0) {
2902
+ timeout = setTimeout(() => {
2903
+ timedOut = true;
2904
+ const waiterIndex = this.#waiters.indexOf(resolve);
2905
+ if (waiterIndex >= 0) {
2906
+ this.#waiters.splice(waiterIndex, 1);
2907
+ }
2908
+ resolve();
2909
+ }, timeoutMs);
2910
+ }
2911
+ await promise;
2912
+ if (timeout) clearTimeout(timeout);
2913
+ if (timedOut && this.#queue.length === 0) {
2914
+ return new CodexWebSocketTransportError(`${timeoutReason}`);
2915
+ }
2916
+ }
2917
+ return this.#queue.shift() ?? null;
2918
+ }
2919
+ }
2920
+
2921
+ async function getOrCreateCodexWebSocketConnection(
2922
+ state: CodexWebSocketSessionState,
2923
+ url: string,
2924
+ headers: Headers,
2925
+ signal?: AbortSignal,
2926
+ ): Promise<CodexWebSocketConnection> {
2927
+ const headerRecord = headersToRecord(headers);
2928
+ // Join an in-flight handshake instead of tearing it down: closing a
2929
+ // CONNECTING socket rejects the concurrent caller (prewarm racing the first
2930
+ // request) with a fatal "websocket closed before open", which would disable
2931
+ // websockets for the entire session.
2932
+ // Bounded re-join: a fresh handshake may have been started by yet another
2933
+ // caller while we awaited the previous one.
2934
+ for (let joinAttempt = 0; joinAttempt < 3; joinAttempt += 1) {
2935
+ const pending = state.connection;
2936
+ if (!pending || pending.isOpen() || !pending.isConnecting()) break;
2937
+ try {
2938
+ await pending.connect(signal);
2939
+ } catch {
2940
+ // The handshake owner surfaces its own failure; re-evaluate below
2941
+ // (state.connection may have been replaced or cleared).
2942
+ }
2943
+ }
2944
+ if (state.connection?.isOpen()) {
2945
+ if (!state.connection.matchesAuth(headerRecord)) {
2946
+ state.connection.close("token-refresh");
2947
+ resetCodexWebSocketAppendState(state);
2948
+ } else if (state.connection.isHealthyForReuse()) {
2949
+ logger.time("codexWs:reuseOpenSocket");
2950
+ return state.connection;
2951
+ } else {
2952
+ // Open in readyState but no inbound traffic recently — likely server-
2953
+ // evicted (issue #1450). Force a fresh handshake instead of writing
2954
+ // `response.create` into a half-open buffer and waiting out the
2955
+ // first-event timeout. Drop append state because the new socket
2956
+ // won't carry the prior `previous_response_id` context.
2957
+ CODEX_DEBUG && logger.debug("[codex] codex websocket reuse rejected by health check", {});
2958
+ state.connection.close("stale-reuse");
2959
+ resetCodexWebSocketAppendState(state);
2960
+ }
2961
+ }
2962
+ state.connection?.close("reconnect");
2963
+ resetCodexWebSocketAppendState(state);
2964
+ logger.time("codexWs:newSocket");
2965
+ state.connection = new CodexWebSocketConnection(url, headerRecord, {
2966
+ onHandshakeHeaders: handshakeHeaders => {
2967
+ updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
2968
+ },
2969
+ });
2970
+ await state.connection.connect(signal);
2971
+ return state.connection;
2972
+ }
2973
+
2974
+ async function openCodexSseEventStream(
2975
+ url: string,
2976
+ requestHeaders: Record<string, string> | undefined,
2977
+ accountId: string,
2978
+ apiKey: string,
2979
+ sessionId: string | undefined,
2980
+ body: RequestBody,
2981
+ state: CodexWebSocketSessionState | undefined,
2982
+ responsesLite: boolean,
2983
+ signal: AbortSignal | undefined,
2984
+ firstEventTimeoutMs: number | undefined,
2985
+ onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
2986
+ fetchOverride?: FetchImpl,
2987
+ ): Promise<AsyncGenerator<Record<string, unknown>>> {
2988
+ const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state, responsesLite);
2989
+ CODEX_DEBUG &&
2990
+ logger.debug("[codex] codex request", {
2991
+ url,
2992
+ model: body.model,
2993
+ headers: redactHeaders(headers),
2994
+ sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
2995
+ sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
2996
+ });
2997
+ // `wrapCodexSseStream` arms the iterator-level idle watchdog only after this
2998
+ // fetch resolves. A pre-response timer still bounds time-to-first-byte (a
2999
+ // proxy that accepts the POST but never sends headers would otherwise hang
3000
+ // forever, since `timeout: false` disables Bun's native ceiling — issue
3001
+ // #2422). It MUST be cleared the instant headers arrive: an absolute
3002
+ // `AbortSignal.timeout` would keep aborting the actively-streaming body.
3003
+ const watchdog = armPreResponseTimeout(signal, firstEventTimeoutMs);
3004
+ let response: Response;
3005
+ try {
3006
+ response = await fetchWithRetry(url, {
3007
+ method: "POST",
3008
+ headers,
3009
+ body: JSON.stringify(body),
3010
+ signal: watchdog.signal,
3011
+ maxAttempts: CODEX_MAX_RETRIES + 1,
3012
+ defaultDelayMs: attempt => CODEX_RETRY_DELAY_MS * (attempt + 1),
3013
+ maxDelayMs: CODEX_RATE_LIMIT_BUDGET_MS,
3014
+ fetch: fetchOverride,
3015
+ timeout: false,
3016
+ });
3017
+ } finally {
3018
+ watchdog.clear();
3019
+ }
3020
+ CODEX_DEBUG &&
3021
+ logger.debug("[codex] codex response", {
3022
+ url: response.url,
3023
+ status: response.status,
3024
+ statusText: response.statusText,
3025
+ contentType: response.headers.get("content-type") || null,
3026
+ cfRay: response.headers.get("cf-ray") || null,
3027
+ });
3028
+ if (!response.ok) {
3029
+ throw await CodexApiError.fromResponse(response);
3030
+ }
3031
+ updateCodexSessionMetadataFromHeaders(state, response.headers);
3032
+ if (!response.body) {
3033
+ throw new CodexProviderStreamError("No response body", false);
3034
+ }
3035
+ return readSseJson<Record<string, unknown>>(response.body, signal, event =>
3036
+ onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, undefined),
3037
+ );
3038
+ }
3039
+
3040
+ function createCodexHeaders(
3041
+ initHeaders: Record<string, string> | undefined,
3042
+ accountId: string,
3043
+ accessToken: string,
3044
+ sessionId?: string,
3045
+ transport: CodexTransport = "sse",
3046
+ state?: CodexWebSocketSessionState,
3047
+ responsesLite = false,
3048
+ ): Headers {
3049
+ const headers = new Headers(initHeaders ?? {});
3050
+ headers.delete("x-api-key");
3051
+ headers.set("Authorization", `Bearer ${accessToken}`);
3052
+ headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
3053
+ const betaHeader =
3054
+ transport === "websocket"
3055
+ ? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2
3056
+ : OPENAI_HEADER_VALUES.BETA_RESPONSES;
3057
+ headers.delete(OPENAI_HEADERS.BETA);
3058
+ headers.delete("openai-beta");
3059
+ headers.set(OPENAI_HEADERS.BETA, betaHeader);
3060
+ headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
3061
+ headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
3062
+ if (sessionId) {
3063
+ headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
3064
+ headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
3065
+ headers.set("x-client-request-id", sessionId);
3066
+ } else {
3067
+ headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
3068
+ headers.delete(OPENAI_HEADERS.SESSION_ID);
3069
+ headers.delete("x-client-request-id");
3070
+ }
3071
+ if (state?.turnState) {
3072
+ headers.set(X_CODEX_TURN_STATE_HEADER, state.turnState);
3073
+ } else {
3074
+ headers.delete(X_CODEX_TURN_STATE_HEADER);
3075
+ }
3076
+ if (state?.modelsEtag) {
3077
+ headers.set(X_MODELS_ETAG_HEADER, state.modelsEtag);
3078
+ } else {
3079
+ headers.delete(X_MODELS_ETAG_HEADER);
3080
+ }
3081
+ if (responsesLite) {
3082
+ headers.set(X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER, "true");
3083
+ } else {
3084
+ headers.delete(X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER);
3085
+ }
3086
+ if (transport === "sse") {
3087
+ headers.set("accept", "text/event-stream");
3088
+ headers.set("content-type", "application/json");
3089
+ } else {
3090
+ headers.delete("accept");
3091
+ headers.delete("content-type");
3092
+ }
3093
+ return headers;
3094
+ }
3095
+
3096
+ function redactHeaders(headers: Headers): Record<string, string> {
3097
+ const redacted: Record<string, string> = {};
3098
+ for (const [key, value] of headers.entries()) {
3099
+ const lower = key.toLowerCase();
3100
+ if (lower === "authorization") {
3101
+ redacted[key] = "Bearer [redacted]";
3102
+ continue;
3103
+ }
3104
+ if (
3105
+ lower.includes("account") ||
3106
+ lower.includes("session") ||
3107
+ lower.includes("conversation") ||
3108
+ lower === "x-client-request-id" ||
3109
+ lower === "cookie"
3110
+ ) {
3111
+ redacted[key] = "[redacted]";
3112
+ continue;
3113
+ }
3114
+ redacted[key] = value;
3115
+ }
3116
+ return redacted;
3117
+ }
3118
+
3119
+ function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
3120
+ const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : CODEX_BASE_URL;
3121
+ const normalized = raw.replace(/\/+$/, "");
3122
+ if (normalized.endsWith("/codex/responses")) return normalized;
3123
+ if (normalized.endsWith("/codex")) return `${normalized}/responses`;
3124
+ return `${normalized}/codex/responses`;
3125
+ }
3126
+
3127
+ function getAccountId(accessToken: string): string {
3128
+ const accountId = getCodexAccountId(accessToken);
3129
+ if (!accountId) {
3130
+ throw new AIError.OAuthError("Failed to extract accountId from token", {
3131
+ kind: "validation",
3132
+ provider: "openai",
3133
+ });
3134
+ }
3135
+ return accountId;
3136
+ }
3137
+
3138
+ function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
3139
+ const messages: ResponseInput = [];
3140
+
3141
+ const normalizeToolCallId = (id: string): string => {
3142
+ if (!id.includes("|")) return id;
3143
+ const [callId, itemId] = id.split("|");
3144
+ const sanitizedCallId = callId.replace(/[^a-zA-Z0-9_-]/g, "_");
3145
+ let sanitizedItemId = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
3146
+ if (!sanitizedItemId.startsWith("fc")) {
3147
+ sanitizedItemId = `fc_${sanitizedItemId}`;
3148
+ }
3149
+ let normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
3150
+ let normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
3151
+ normalizedCallId = normalizedCallId.replace(/_+$/, "");
3152
+ normalizedItemId = normalizedItemId.replace(/_+$/, "");
3153
+ return `${normalizedCallId}|${normalizedItemId}`;
3154
+ };
3155
+
3156
+ const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
3157
+ let msgIndex = 0;
3158
+ // Track call_ids that originated as custom tool calls so paired tool-result
3159
+ // messages can be replayed as `custom_tool_call_output` rather than
3160
+ // `function_call_output` (OpenAI rejects mismatched pairs).
3161
+ const customCallIds = new Set<string>();
3162
+ const knownCallIds = new Set<string>();
3163
+
3164
+ for (const msg of transformedMessages) {
3165
+ if (msg.role === "user" || msg.role === "developer") {
3166
+ const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
3167
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider) as
3168
+ | Array<ResponseInput[number]>
3169
+ | undefined;
3170
+ if (historyItems) {
3171
+ for (const item of historyItems) {
3172
+ const maybe = item as { type?: string; call_id?: string };
3173
+ if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
3174
+ customCallIds.add(maybe.call_id);
3175
+ }
3176
+ }
3177
+ messages.push(...historyItems);
3178
+ msgIndex += 1;
3179
+ continue;
3180
+ }
3181
+
3182
+ const normalizedContent = normalizeInputMessageContent(model, msg.content);
3183
+ if (normalizedContent.length === 0) continue;
3184
+ messages.push({ role: msg.role, content: normalizedContent });
3185
+ msgIndex += 1;
3186
+ continue;
3187
+ }
3188
+
3189
+ if (msg.role === "assistant") {
3190
+ const assistantMsg = msg as AssistantMessage;
3191
+ // Native items are model-bound (reasoning carries encrypted content
3192
+ // minted by the producing model); after a mid-session model switch fall
3193
+ // back to block re-encode, which strips foreign signatures.
3194
+ const providerPayload =
3195
+ assistantMsg.api === model.api && assistantMsg.model === model.id
3196
+ ? getOpenAIResponsesHistoryPayload(assistantMsg.providerPayload, model.provider, assistantMsg.provider)
3197
+ : undefined;
3198
+ const historyItems = providerPayload?.items as Array<ResponseInput[number]> | undefined;
3199
+ if (historyItems) {
3200
+ for (const item of historyItems) {
3201
+ const maybe = item as { type?: string; call_id?: string };
3202
+ if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
3203
+ customCallIds.add(maybe.call_id);
3204
+ }
3205
+ }
3206
+ if (providerPayload?.dt) {
3207
+ messages.push(...historyItems);
3208
+ } else {
3209
+ messages.splice(0, messages.length, ...historyItems);
3210
+ // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
3211
+ }
3212
+ msgIndex += 1;
3213
+ continue;
3214
+ }
3215
+
3216
+ const outputItems = convertResponsesAssistantMessage(
3217
+ msg as AssistantMessage,
3218
+ model,
3219
+ msgIndex,
3220
+ knownCallIds,
3221
+ true,
3222
+ customCallIds,
3223
+ );
3224
+ if (outputItems.length > 0) {
3225
+ messages.push(...outputItems);
3226
+ }
3227
+ msgIndex += 1;
3228
+ continue;
3229
+ }
3230
+
3231
+ if (msg.role === "toolResult") {
3232
+ appendResponsesToolResultMessages(
3233
+ messages,
3234
+ msg,
3235
+ model,
3236
+ false,
3237
+ model.compat.supportsImageDetailOriginal,
3238
+ knownCallIds,
3239
+ customCallIds,
3240
+ );
3241
+ }
3242
+
3243
+ msgIndex += 1;
3244
+ }
3245
+
3246
+ return messages;
3247
+ }
3248
+
3249
+ function normalizeInputMessageContent(
3250
+ model: Model<"openai-codex-responses">,
3251
+ content: string | Array<{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string }>,
3252
+ ): ResponseInputContent[] {
3253
+ if (typeof content === "string") {
3254
+ if (!content || content.trim() === "") return [];
3255
+ return [{ type: "input_text", text: content.toWellFormed() }];
3256
+ }
3257
+
3258
+ return (
3259
+ convertResponsesInputContent(content, model.input.includes("image"), model.compat.supportsImageDetailOriginal) ??
3260
+ []
3261
+ );
3262
+ }
3263
+
3264
+ /** @internal Exported for tests. */
3265
+ export { convertMessages as convertCodexResponsesMessages };
3266
+
3267
+ type CodexToolPayload =
3268
+ | {
3269
+ type: "function";
3270
+ name: string;
3271
+ description: string;
3272
+ parameters: Record<string, unknown>;
3273
+ strict?: boolean;
3274
+ }
3275
+ | {
3276
+ type: "custom";
3277
+ name: string;
3278
+ description: string;
3279
+ format: { type: "grammar"; syntax: "lark" | "regex"; definition: string };
3280
+ };
3281
+
3282
+ /** @internal Exported for tests. */
3283
+ export function convertOpenAICodexResponsesTools(
3284
+ tools: Tool[],
3285
+ model: Model<"openai-codex-responses">,
3286
+ ): CodexToolPayload[] {
3287
+ const allowFreeform = model.applyPatchToolType === "freeform";
3288
+ return tools.map((tool): CodexToolPayload => {
3289
+ if (allowFreeform && tool.customFormat) {
3290
+ return {
3291
+ type: "custom",
3292
+ name: tool.customWireName ?? tool.name,
3293
+ description: tool.description || "",
3294
+ format: {
3295
+ type: "grammar",
3296
+ syntax: tool.customFormat.syntax,
3297
+ definition: compactGrammarDefinition(tool.customFormat.syntax, tool.customFormat.definition),
3298
+ },
3299
+ };
3300
+ }
3301
+ const strict = !!(!NO_STRICT && tool.strict);
3302
+ const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
3303
+ const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
3304
+ return {
3305
+ type: "function",
3306
+ name: tool.name,
3307
+ description: tool.description || "",
3308
+ parameters,
3309
+ ...(effectiveStrict && { strict: true }),
3310
+ };
3311
+ });
3312
+ }
3313
+
3314
+ export class CodexWebSocketTransportError extends Error {
3315
+ constructor(detail: string) {
3316
+ super(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${detail}`);
3317
+ this.name = "CodexWebSocketTransportError";
3318
+ }
3319
+ }
3320
+ class CodexWhitespaceToolCallLoopError extends Error {
3321
+ constructor(message: string) {
3322
+ super(message);
3323
+ this.name = "CodexWhitespaceToolCallLoopError";
3324
+ }
3325
+ }
3326
+
3327
+ class CodexProviderStreamError extends Error {
3328
+ readonly retryable: boolean;
3329
+ readonly code?: string;
3330
+
3331
+ constructor(message: string, retryable: boolean, code?: string) {
3332
+ super(message);
3333
+ this.name = "CodexProviderStreamError";
3334
+ this.retryable = retryable;
3335
+ this.code = code;
3336
+ }
3337
+ }
3338
+
3339
+ const optionalCodexString = type("unknown").pipe(raw => {
3340
+ const out = type("string")(raw);
3341
+ return out instanceof type.errors ? undefined : out;
3342
+ });
3343
+
3344
+ const innerErrorDetailSchema = type({
3345
+ "code?": optionalCodexString,
3346
+ "type?": optionalCodexString,
3347
+ "message?": optionalCodexString,
3348
+ });
3349
+
3350
+ const codexErrorDetailSchema = type("unknown").pipe(raw => {
3351
+ const out = innerErrorDetailSchema(raw);
3352
+ return out instanceof type.errors ? undefined : out;
3353
+ });
3354
+
3355
+ const innerFailureEventSchema = type({
3356
+ "type?": optionalCodexString,
3357
+ "code?": optionalCodexString,
3358
+ "message?": optionalCodexString,
3359
+ "status?": optionalCodexString,
3360
+ "error?": codexErrorDetailSchema,
3361
+ "response?": type("unknown").pipe(raw => {
3362
+ const out = type({
3363
+ "error?": codexErrorDetailSchema,
3364
+ "message?": optionalCodexString,
3365
+ "status?": optionalCodexString,
3366
+ })(raw);
3367
+ return out instanceof type.errors ? undefined : out;
3368
+ }),
3369
+ });
3370
+
3371
+ const codexFailureEventSchema = type("unknown").pipe(raw => {
3372
+ const out = innerFailureEventSchema(raw);
3373
+ return out instanceof type.errors
3374
+ ? {
3375
+ type: undefined,
3376
+ code: undefined,
3377
+ message: undefined,
3378
+ status: undefined,
3379
+ error: undefined,
3380
+ response: undefined,
3381
+ }
3382
+ : out;
3383
+ });
3384
+
3385
+ export function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
3386
+ const event = codexFailureEventSchema(rawEvent);
3387
+ if (event instanceof type.errors) {
3388
+ return false;
3389
+ }
3390
+ const error = event.error ?? event.response?.error;
3391
+ const code = error?.code ?? error?.type ?? event.code;
3392
+ if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
3393
+ return true;
3394
+ }
3395
+ const message = error?.message ?? event.message ?? event.response?.message;
3396
+ return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
3397
+ }
3398
+
3399
+ export function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
3400
+ const event = codexFailureEventSchema(rawEvent);
3401
+ if (event instanceof type.errors) {
3402
+ return new CodexProviderStreamError("Codex response failed", false);
3403
+ }
3404
+ const nestedError = event.error ?? event.response?.error;
3405
+ const code = nestedError?.code ?? nestedError?.type ?? event.code ?? "";
3406
+ const message = event.message ?? "";
3407
+ const formattedMessage =
3408
+ event.type === "error"
3409
+ ? formatCodexErrorEvent(rawEvent, code, message)
3410
+ : (formatCodexFailure(rawEvent) ?? "Codex response failed");
3411
+ return new CodexProviderStreamError(formattedMessage, isRetryableCodexFailureEvent(rawEvent), code || undefined);
3412
+ }
3413
+
3414
+ function formatCodexFailure(rawEvent: Record<string, unknown>): string | null {
3415
+ const event = codexFailureEventSchema(rawEvent);
3416
+ if (event instanceof type.errors) {
3417
+ return null;
3418
+ }
3419
+ const error = event.error ?? event.response?.error;
3420
+ const message = error?.message ?? event.message ?? event.response?.message;
3421
+ const code = error?.code ?? error?.type ?? event.code;
3422
+ const status = event.response?.status ?? event.status;
3423
+
3424
+ const meta: string[] = [];
3425
+ if (code) meta.push(`code=${code}`);
3426
+ if (status) meta.push(`status=${status}`);
3427
+
3428
+ if (message) {
3429
+ const metaText = meta.length ? ` (${meta.join(", ")})` : "";
3430
+ return `Codex response failed: ${message}${metaText}`;
3431
+ }
3432
+ if (meta.length) {
3433
+ return `Codex response failed (${meta.join(", ")})`;
3434
+ }
3435
+ try {
3436
+ const rawEventJson = JSON.stringify(rawEvent);
3437
+ const truncatedRawEventJson =
3438
+ rawEventJson.length <= 800
3439
+ ? rawEventJson
3440
+ : `${rawEventJson.slice(0, 800)}…[truncated ${rawEventJson.length - 800}]`;
3441
+ return `Codex response failed: ${truncatedRawEventJson}`;
3442
+ } catch {
3443
+ return "Codex response failed";
3444
+ }
3445
+ }
3446
+
3447
+ function formatCodexErrorEvent(rawEvent: Record<string, unknown>, code: string, message: string): string {
3448
+ const detail = formatCodexFailure(rawEvent);
3449
+ if (detail) {
3450
+ return detail.replace("response failed", "error event");
3451
+ }
3452
+ const meta: string[] = [];
3453
+ if (code) meta.push(`code=${code}`);
3454
+ if (message) meta.push(`message=${message}`);
3455
+ if (meta.length > 0) {
3456
+ return `Codex error event (${meta.join(", ")})`;
3457
+ }
3458
+ try {
3459
+ const rawEventJson = JSON.stringify(rawEvent);
3460
+ const truncatedRawEventJson =
3461
+ rawEventJson.length <= 800
3462
+ ? rawEventJson
3463
+ : `${rawEventJson.slice(0, 800)}…[truncated ${rawEventJson.length - 800}]`;
3464
+ return `Codex error event: ${truncatedRawEventJson}`;
3465
+ } catch {
3466
+ return "Codex error event";
3467
+ }
3468
+ }