@aryee337/aery-ai 0.1.148 → 0.2.10

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 (592) hide show
  1. package/CHANGELOG.md +2914 -0
  2. package/README.md +614 -813
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +5 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +96 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/types.d.ts +105 -0
  10. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  11. package/dist/types/auth-gateway/http.d.ts +39 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -0
  13. package/dist/types/auth-gateway/server.d.ts +36 -0
  14. package/dist/types/auth-gateway/types.d.ts +117 -0
  15. package/dist/types/auth-storage.d.ts +739 -0
  16. package/dist/types/index.d.ts +49 -0
  17. package/dist/types/model-cache.d.ts +17 -0
  18. package/dist/types/model-manager.d.ts +64 -0
  19. package/dist/types/model-thinking.d.ts +100 -0
  20. package/dist/types/models.d.ts +12 -0
  21. package/dist/types/provider-details.d.ts +24 -0
  22. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  23. package/dist/types/provider-models/descriptors.d.ts +50 -0
  24. package/dist/types/provider-models/google.d.ts +24 -0
  25. package/dist/types/provider-models/index.d.ts +5 -0
  26. package/dist/types/provider-models/ollama.d.ts +7 -0
  27. package/dist/types/provider-models/openai-compat.d.ts +296 -0
  28. package/dist/types/provider-models/special.d.ts +16 -0
  29. package/dist/types/providers/aery-native-client.d.ts +13 -0
  30. package/dist/types/providers/aery-native-server.d.ts +68 -0
  31. package/dist/types/providers/amazon-bedrock.d.ts +38 -0
  32. package/dist/types/providers/anthropic-client.d.ts +99 -0
  33. package/dist/types/providers/anthropic-messages-server-schema.d.ts +465 -0
  34. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  35. package/dist/types/providers/anthropic-wire.d.ts +262 -0
  36. package/dist/types/providers/anthropic.d.ts +206 -0
  37. package/dist/types/providers/aws-credentials.d.ts +43 -0
  38. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  39. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  40. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  41. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  42. package/dist/types/providers/cursor.d.ts +43 -0
  43. package/dist/types/providers/error-message.d.ts +27 -0
  44. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  45. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  46. package/dist/types/providers/google-auth.d.ts +24 -0
  47. package/dist/types/providers/google-gemini-cli.d.ts +81 -0
  48. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  49. package/dist/types/providers/google-shared.d.ts +171 -0
  50. package/dist/types/providers/google-types.d.ts +138 -0
  51. package/dist/types/providers/google-vertex.d.ts +7 -0
  52. package/dist/types/providers/google.d.ts +4 -0
  53. package/dist/types/providers/grammar.d.ts +1 -0
  54. package/dist/types/providers/kimi.d.ts +27 -0
  55. package/dist/types/providers/mock.d.ts +173 -0
  56. package/dist/types/providers/ollama.d.ts +6 -0
  57. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  58. package/dist/types/providers/openai-chat-server-schema.d.ts +817 -0
  59. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  60. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  61. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  62. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  63. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  64. package/dist/types/providers/openai-completions-compat.d.ts +25 -0
  65. package/dist/types/providers/openai-completions.d.ts +54 -0
  66. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  67. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  68. package/dist/types/providers/openai-responses-shared.d.ts +100 -0
  69. package/dist/types/providers/openai-responses.d.ts +66 -0
  70. package/dist/types/providers/register-builtins.d.ts +31 -0
  71. package/dist/types/providers/synthetic.d.ts +26 -0
  72. package/dist/{providers → types/providers}/transform-messages.d.ts +6 -2
  73. package/dist/types/providers/vision-guard.d.ts +8 -0
  74. package/dist/types/providers/xai-responses.d.ts +23 -0
  75. package/dist/types/rate-limit-utils.d.ts +19 -0
  76. package/dist/types/stream.d.ts +28 -0
  77. package/dist/types/types.d.ts +801 -0
  78. package/dist/types/usage/claude.d.ts +4 -0
  79. package/dist/types/usage/gemini.d.ts +2 -0
  80. package/dist/types/usage/github-copilot.d.ts +7 -0
  81. package/dist/types/usage/google-antigravity.d.ts +2 -0
  82. package/dist/types/usage/kimi.d.ts +2 -0
  83. package/dist/types/usage/minimax-code.d.ts +2 -0
  84. package/dist/types/usage/openai-codex.d.ts +3 -0
  85. package/dist/types/usage/shared.d.ts +1 -0
  86. package/dist/types/usage/zai.d.ts +2 -0
  87. package/dist/types/usage.d.ts +260 -0
  88. package/dist/types/utils/abort.d.ts +19 -0
  89. package/dist/types/utils/abortable-iterator.d.ts +4 -0
  90. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  91. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  92. package/dist/types/utils/discovery/codex.d.ts +38 -0
  93. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  94. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  95. package/dist/types/utils/discovery/index.d.ts +4 -0
  96. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  97. package/dist/types/utils/event-stream.d.ts +28 -0
  98. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  99. package/dist/types/utils/foundry.d.ts +1 -0
  100. package/dist/types/utils/http-inspector.d.ts +31 -0
  101. package/dist/types/utils/idle-iterator.d.ts +78 -0
  102. package/dist/types/utils/json-parse.d.ts +37 -0
  103. package/dist/types/utils/oauth/__tests__/xai-oauth.test.d.ts +1 -0
  104. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  105. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  106. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  107. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  108. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  109. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  110. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  111. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  112. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  113. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  114. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  115. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  116. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  117. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  118. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  119. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  120. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  121. package/dist/types/utils/oauth/index.d.ts +38 -0
  122. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  123. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  124. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  125. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  126. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  127. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  128. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  129. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  130. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  131. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  132. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  133. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  134. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  135. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  136. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  137. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  138. package/dist/{utils → types/utils}/oauth/pkce.d.ts +0 -5
  139. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  140. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  141. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  142. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  143. package/dist/types/utils/oauth/together.d.ts +1 -0
  144. package/dist/types/utils/oauth/types.d.ts +44 -0
  145. package/dist/types/utils/oauth/venice.d.ts +18 -0
  146. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  147. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  148. package/dist/types/utils/oauth/wafer.d.ts +2 -0
  149. package/dist/types/utils/oauth/xai-oauth.d.ts +60 -0
  150. package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
  151. package/dist/types/utils/oauth/zai.d.ts +18 -0
  152. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  153. package/dist/types/utils/oauth/zhipu.d.ts +18 -0
  154. package/dist/{utils → types/utils}/overflow.d.ts +9 -11
  155. package/dist/types/utils/parse-bind.d.ts +23 -0
  156. package/dist/types/utils/provider-response.d.ts +3 -0
  157. package/dist/types/utils/request-debug.d.ts +29 -0
  158. package/dist/types/utils/retry-after.d.ts +3 -0
  159. package/dist/types/utils/retry.d.ts +26 -0
  160. package/dist/types/utils/schema/adapt.d.ts +24 -0
  161. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  162. package/dist/types/utils/schema/dereference.d.ts +11 -0
  163. package/dist/types/utils/schema/draft.d.ts +10 -0
  164. package/dist/types/utils/schema/equality.d.ts +4 -0
  165. package/dist/types/utils/schema/fields.d.ts +49 -0
  166. package/dist/types/utils/schema/index.d.ts +13 -0
  167. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  168. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  169. package/dist/types/utils/schema/normalize.d.ts +93 -0
  170. package/dist/types/utils/schema/spill.d.ts +8 -0
  171. package/dist/types/utils/schema/stamps.d.ts +25 -0
  172. package/dist/types/utils/schema/types.d.ts +4 -0
  173. package/dist/types/utils/schema/wire.d.ts +53 -0
  174. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  175. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  176. package/dist/types/utils/sse-debug.d.ts +10 -0
  177. package/dist/types/utils/stream-markup-healing.d.ts +80 -0
  178. package/dist/types/utils/tool-choice.d.ts +50 -0
  179. package/dist/types/utils/validation.d.ts +17 -0
  180. package/dist/types/utils.d.ts +28 -0
  181. package/package.json +139 -105
  182. package/src/api-registry.ts +96 -0
  183. package/src/auth-broker/client.ts +358 -0
  184. package/src/auth-broker/index.ts +5 -0
  185. package/src/auth-broker/refresher.ts +117 -0
  186. package/src/auth-broker/remote-store.ts +623 -0
  187. package/src/auth-broker/server.ts +644 -0
  188. package/src/auth-broker/types.ts +127 -0
  189. package/src/auth-broker/wire-schemas.ts +200 -0
  190. package/src/auth-gateway/http.ts +194 -0
  191. package/src/auth-gateway/index.ts +3 -0
  192. package/src/auth-gateway/server.ts +818 -0
  193. package/src/auth-gateway/types.ts +143 -0
  194. package/src/auth-storage.ts +4422 -0
  195. package/src/index.ts +54 -0
  196. package/src/model-cache.ts +129 -0
  197. package/src/model-manager.ts +469 -0
  198. package/src/model-thinking.ts +782 -0
  199. package/src/models.json +83530 -0
  200. package/src/models.json.d.ts +9 -0
  201. package/src/models.ts +56 -0
  202. package/src/prompts/turn-aborted-guidance.md +4 -0
  203. package/src/provider-details.ts +90 -0
  204. package/src/provider-models/bundled-references.ts +38 -0
  205. package/src/provider-models/descriptors.ts +355 -0
  206. package/src/provider-models/google.ts +88 -0
  207. package/src/provider-models/index.ts +5 -0
  208. package/src/provider-models/ollama.ts +153 -0
  209. package/src/provider-models/openai-compat.ts +2817 -0
  210. package/src/provider-models/special.ts +67 -0
  211. package/src/providers/aery-native-client.ts +228 -0
  212. package/src/providers/aery-native-server.ts +212 -0
  213. package/src/providers/amazon-bedrock.ts +873 -0
  214. package/src/providers/anthropic-client.ts +318 -0
  215. package/src/providers/anthropic-messages-server-schema.ts +243 -0
  216. package/src/providers/anthropic-messages-server.ts +683 -0
  217. package/src/providers/anthropic-wire.ts +268 -0
  218. package/src/providers/anthropic.ts +3094 -0
  219. package/src/providers/aws-credentials.ts +501 -0
  220. package/src/providers/aws-eventstream.ts +185 -0
  221. package/src/providers/aws-sigv4.ts +218 -0
  222. package/src/providers/azure-openai-responses.ts +361 -0
  223. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  224. package/src/providers/cursor/proto/agent.proto +3526 -0
  225. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  226. package/src/providers/cursor/proto/buf.yaml +17 -0
  227. package/src/providers/cursor.ts +2621 -0
  228. package/src/providers/error-message.ts +21 -0
  229. package/src/providers/github-copilot-headers.ts +140 -0
  230. package/src/providers/gitlab-duo.ts +372 -0
  231. package/src/providers/google-auth.ts +252 -0
  232. package/src/providers/google-gemini-cli.ts +809 -0
  233. package/src/providers/google-gemini-headers.ts +41 -0
  234. package/src/providers/google-shared.ts +917 -0
  235. package/src/providers/google-types.ts +167 -0
  236. package/src/providers/google-vertex.ts +91 -0
  237. package/src/providers/google.ts +41 -0
  238. package/src/providers/grammar.ts +70 -0
  239. package/src/providers/kimi.ts +52 -0
  240. package/src/providers/mock.ts +496 -0
  241. package/src/providers/ollama.ts +644 -0
  242. package/src/providers/openai-anthropic-shim.ts +138 -0
  243. package/src/providers/openai-chat-server-schema.ts +252 -0
  244. package/src/providers/openai-chat-server.ts +647 -0
  245. package/src/providers/openai-codex/constants.ts +43 -0
  246. package/src/providers/openai-codex/request-transformer.ts +161 -0
  247. package/src/providers/openai-codex/response-handler.ts +81 -0
  248. package/src/providers/openai-codex-responses.ts +3018 -0
  249. package/src/providers/openai-completions-compat.ts +300 -0
  250. package/src/providers/openai-completions.ts +1979 -0
  251. package/src/providers/openai-responses-server-schema.ts +290 -0
  252. package/src/providers/openai-responses-server.ts +1183 -0
  253. package/src/providers/openai-responses-shared.ts +873 -0
  254. package/src/providers/openai-responses.ts +679 -0
  255. package/src/providers/register-builtins.ts +436 -0
  256. package/src/providers/synthetic.ts +50 -0
  257. package/src/providers/transform-messages.ts +382 -0
  258. package/src/providers/vision-guard.ts +31 -0
  259. package/src/providers/xai-responses.ts +82 -0
  260. package/src/rate-limit-utils.ts +84 -0
  261. package/src/stream.ts +1065 -0
  262. package/src/types.ts +944 -0
  263. package/src/usage/claude.ts +482 -0
  264. package/src/usage/gemini.ts +250 -0
  265. package/src/usage/github-copilot.ts +421 -0
  266. package/src/usage/google-antigravity.ts +201 -0
  267. package/src/usage/kimi.ts +271 -0
  268. package/src/usage/minimax-code.ts +31 -0
  269. package/src/usage/openai-codex.ts +503 -0
  270. package/src/usage/shared.ts +10 -0
  271. package/src/usage/zai.ts +247 -0
  272. package/src/usage.ts +185 -0
  273. package/src/utils/abort.ts +51 -0
  274. package/src/utils/abortable-iterator.ts +69 -0
  275. package/src/utils/anthropic-auth.ts +93 -0
  276. package/src/utils/discovery/antigravity.ts +261 -0
  277. package/src/utils/discovery/codex.ts +371 -0
  278. package/src/utils/discovery/cursor.ts +306 -0
  279. package/src/utils/discovery/gemini.ts +248 -0
  280. package/src/utils/discovery/index.ts +4 -0
  281. package/src/utils/discovery/openai-compatible.ts +224 -0
  282. package/src/utils/event-stream.ts +142 -0
  283. package/src/utils/fireworks-model-id.ts +30 -0
  284. package/src/utils/foundry.ts +8 -0
  285. package/src/utils/http-inspector.ts +176 -0
  286. package/src/utils/idle-iterator.ts +267 -0
  287. package/src/utils/json-parse.ts +182 -0
  288. package/src/utils/oauth/__tests__/xai-oauth.test.ts +107 -0
  289. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  290. package/src/utils/oauth/anthropic.ts +273 -0
  291. package/src/utils/oauth/api-key-login.ts +87 -0
  292. package/src/utils/oauth/api-key-validation.ts +92 -0
  293. package/src/utils/oauth/callback-server.ts +276 -0
  294. package/src/utils/oauth/cerebras.ts +16 -0
  295. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  296. package/src/utils/oauth/cursor.ts +157 -0
  297. package/src/utils/oauth/deepseek.ts +53 -0
  298. package/src/utils/oauth/firepass.ts +24 -0
  299. package/src/utils/oauth/fireworks.ts +15 -0
  300. package/src/utils/oauth/github-copilot.ts +362 -0
  301. package/src/utils/oauth/gitlab-duo.ts +123 -0
  302. package/src/utils/oauth/google-antigravity.ts +200 -0
  303. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  304. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  305. package/src/utils/oauth/huggingface.ts +62 -0
  306. package/src/utils/oauth/index.ts +484 -0
  307. package/src/utils/oauth/kagi.ts +47 -0
  308. package/src/utils/oauth/kilo.ts +87 -0
  309. package/src/utils/oauth/kimi.ts +254 -0
  310. package/src/utils/oauth/litellm.ts +47 -0
  311. package/src/utils/oauth/lm-studio.ts +38 -0
  312. package/src/utils/oauth/minimax-code.ts +78 -0
  313. package/src/utils/oauth/moonshot.ts +23 -0
  314. package/src/utils/oauth/nanogpt.ts +15 -0
  315. package/src/utils/oauth/nvidia.ts +70 -0
  316. package/src/utils/oauth/oauth.html +203 -0
  317. package/src/utils/oauth/ollama-cloud.ts +28 -0
  318. package/src/utils/oauth/ollama.ts +47 -0
  319. package/src/utils/oauth/openai-codex.ts +299 -0
  320. package/src/utils/oauth/opencode.ts +49 -0
  321. package/src/utils/oauth/openrouter.ts +20 -0
  322. package/src/utils/oauth/parallel.ts +46 -0
  323. package/src/utils/oauth/perplexity.ts +206 -0
  324. package/src/utils/oauth/pkce.ts +18 -0
  325. package/src/utils/oauth/qianfan.ts +58 -0
  326. package/src/utils/oauth/qwen-portal.ts +60 -0
  327. package/src/utils/oauth/synthetic.ts +15 -0
  328. package/src/utils/oauth/tavily.ts +46 -0
  329. package/src/utils/oauth/together.ts +16 -0
  330. package/src/utils/oauth/types.ts +99 -0
  331. package/src/utils/oauth/venice.ts +59 -0
  332. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  333. package/src/utils/oauth/vllm.ts +40 -0
  334. package/src/utils/oauth/wafer.ts +50 -0
  335. package/src/utils/oauth/xai-oauth.ts +342 -0
  336. package/src/utils/oauth/xiaomi.ts +139 -0
  337. package/src/utils/oauth/zai.ts +60 -0
  338. package/src/utils/oauth/zenmux.ts +15 -0
  339. package/src/utils/oauth/zhipu.ts +60 -0
  340. package/src/utils/overflow.ts +137 -0
  341. package/src/utils/parse-bind.ts +54 -0
  342. package/src/utils/provider-response.ts +30 -0
  343. package/src/utils/request-debug.ts +336 -0
  344. package/src/utils/retry-after.ts +110 -0
  345. package/src/utils/retry.ts +54 -0
  346. package/src/utils/schema/CONSTRAINTS.md +164 -0
  347. package/src/utils/schema/adapt.ts +36 -0
  348. package/src/utils/schema/compatibility.ts +435 -0
  349. package/src/utils/schema/dereference.ts +98 -0
  350. package/src/utils/schema/draft.ts +341 -0
  351. package/src/utils/schema/equality.ts +97 -0
  352. package/src/utils/schema/fields.ts +191 -0
  353. package/src/utils/schema/index.ts +13 -0
  354. package/src/utils/schema/json-schema-validator.ts +577 -0
  355. package/src/utils/schema/meta-validator.ts +167 -0
  356. package/src/utils/schema/normalize.ts +1588 -0
  357. package/src/utils/schema/spill.ts +43 -0
  358. package/src/utils/schema/stamps.ts +97 -0
  359. package/src/utils/schema/types.ts +10 -0
  360. package/src/utils/schema/wire.ts +293 -0
  361. package/src/utils/schema/zod-decontaminate.ts +331 -0
  362. package/src/utils/sdk-stream-timeout.ts +43 -0
  363. package/src/utils/sse-debug.ts +289 -0
  364. package/src/utils/stream-markup-healing.ts +612 -0
  365. package/src/utils/tool-choice.ts +99 -0
  366. package/src/utils/validation.ts +1024 -0
  367. package/src/utils.ts +166 -0
  368. package/dist/api-registry.d.ts +0 -20
  369. package/dist/api-registry.d.ts.map +0 -1
  370. package/dist/api-registry.js +0 -44
  371. package/dist/api-registry.js.map +0 -1
  372. package/dist/bedrock-provider.d.ts +0 -5
  373. package/dist/bedrock-provider.d.ts.map +0 -1
  374. package/dist/bedrock-provider.js +0 -6
  375. package/dist/bedrock-provider.js.map +0 -1
  376. package/dist/cli.d.ts +0 -3
  377. package/dist/cli.d.ts.map +0 -1
  378. package/dist/cli.js +0 -130
  379. package/dist/cli.js.map +0 -1
  380. package/dist/env-api-keys.d.ts +0 -18
  381. package/dist/env-api-keys.d.ts.map +0 -1
  382. package/dist/env-api-keys.js +0 -178
  383. package/dist/env-api-keys.js.map +0 -1
  384. package/dist/image-models.d.ts +0 -10
  385. package/dist/image-models.d.ts.map +0 -1
  386. package/dist/image-models.generated.d.ts +0 -440
  387. package/dist/image-models.generated.d.ts.map +0 -1
  388. package/dist/image-models.generated.js +0 -442
  389. package/dist/image-models.generated.js.map +0 -1
  390. package/dist/image-models.js +0 -23
  391. package/dist/image-models.js.map +0 -1
  392. package/dist/images-api-registry.d.ts +0 -14
  393. package/dist/images-api-registry.d.ts.map +0 -1
  394. package/dist/images-api-registry.js +0 -22
  395. package/dist/images-api-registry.js.map +0 -1
  396. package/dist/images.d.ts +0 -4
  397. package/dist/images.d.ts.map +0 -1
  398. package/dist/images.js +0 -14
  399. package/dist/images.js.map +0 -1
  400. package/dist/index.d.ts +0 -32
  401. package/dist/index.d.ts.map +0 -1
  402. package/dist/index.js +0 -20
  403. package/dist/index.js.map +0 -1
  404. package/dist/models.d.ts +0 -18
  405. package/dist/models.d.ts.map +0 -1
  406. package/dist/models.generated.d.ts +0 -17480
  407. package/dist/models.generated.d.ts.map +0 -1
  408. package/dist/models.generated.js +0 -16339
  409. package/dist/models.generated.js.map +0 -1
  410. package/dist/models.js +0 -71
  411. package/dist/models.js.map +0 -1
  412. package/dist/oauth.d.ts +0 -2
  413. package/dist/oauth.d.ts.map +0 -1
  414. package/dist/oauth.js +0 -2
  415. package/dist/oauth.js.map +0 -1
  416. package/dist/providers/aery-error-formatting.d.ts +0 -13
  417. package/dist/providers/aery-error-formatting.d.ts.map +0 -1
  418. package/dist/providers/aery-error-formatting.js +0 -112
  419. package/dist/providers/aery-error-formatting.js.map +0 -1
  420. package/dist/providers/amazon-bedrock.d.ts +0 -38
  421. package/dist/providers/amazon-bedrock.d.ts.map +0 -1
  422. package/dist/providers/amazon-bedrock.js +0 -763
  423. package/dist/providers/amazon-bedrock.js.map +0 -1
  424. package/dist/providers/anthropic.d.ts +0 -71
  425. package/dist/providers/anthropic.d.ts.map +0 -1
  426. package/dist/providers/anthropic.js +0 -949
  427. package/dist/providers/anthropic.js.map +0 -1
  428. package/dist/providers/azure-openai-responses.d.ts +0 -15
  429. package/dist/providers/azure-openai-responses.d.ts.map +0 -1
  430. package/dist/providers/azure-openai-responses.js +0 -225
  431. package/dist/providers/azure-openai-responses.js.map +0 -1
  432. package/dist/providers/cloudflare.d.ts +0 -13
  433. package/dist/providers/cloudflare.d.ts.map +0 -1
  434. package/dist/providers/cloudflare.js +0 -26
  435. package/dist/providers/cloudflare.js.map +0 -1
  436. package/dist/providers/faux.d.ts +0 -56
  437. package/dist/providers/faux.d.ts.map +0 -1
  438. package/dist/providers/faux.js +0 -368
  439. package/dist/providers/faux.js.map +0 -1
  440. package/dist/providers/github-copilot-headers.d.ts +0 -8
  441. package/dist/providers/github-copilot-headers.d.ts.map +0 -1
  442. package/dist/providers/github-copilot-headers.js +0 -29
  443. package/dist/providers/github-copilot-headers.js.map +0 -1
  444. package/dist/providers/google-gemini-cli.d.ts +0 -74
  445. package/dist/providers/google-gemini-cli.d.ts.map +0 -1
  446. package/dist/providers/google-gemini-cli.js +0 -779
  447. package/dist/providers/google-gemini-cli.js.map +0 -1
  448. package/dist/providers/google-shared.d.ts +0 -70
  449. package/dist/providers/google-shared.d.ts.map +0 -1
  450. package/dist/providers/google-shared.js +0 -329
  451. package/dist/providers/google-shared.js.map +0 -1
  452. package/dist/providers/google-vertex.d.ts +0 -15
  453. package/dist/providers/google-vertex.d.ts.map +0 -1
  454. package/dist/providers/google-vertex.js +0 -442
  455. package/dist/providers/google-vertex.js.map +0 -1
  456. package/dist/providers/google.d.ts +0 -13
  457. package/dist/providers/google.d.ts.map +0 -1
  458. package/dist/providers/google.js +0 -400
  459. package/dist/providers/google.js.map +0 -1
  460. package/dist/providers/images/openrouter.d.ts +0 -3
  461. package/dist/providers/images/openrouter.d.ts.map +0 -1
  462. package/dist/providers/images/openrouter.js +0 -129
  463. package/dist/providers/images/openrouter.js.map +0 -1
  464. package/dist/providers/images/register-builtins.d.ts +0 -4
  465. package/dist/providers/images/register-builtins.d.ts.map +0 -1
  466. package/dist/providers/images/register-builtins.js +0 -34
  467. package/dist/providers/images/register-builtins.js.map +0 -1
  468. package/dist/providers/mistral.d.ts +0 -25
  469. package/dist/providers/mistral.d.ts.map +0 -1
  470. package/dist/providers/mistral.js +0 -535
  471. package/dist/providers/mistral.js.map +0 -1
  472. package/dist/providers/openai-codex-responses.d.ts +0 -30
  473. package/dist/providers/openai-codex-responses.d.ts.map +0 -1
  474. package/dist/providers/openai-codex-responses.js +0 -1090
  475. package/dist/providers/openai-codex-responses.js.map +0 -1
  476. package/dist/providers/openai-completions.d.ts +0 -19
  477. package/dist/providers/openai-completions.d.ts.map +0 -1
  478. package/dist/providers/openai-completions.js +0 -950
  479. package/dist/providers/openai-completions.js.map +0 -1
  480. package/dist/providers/openai-prompt-cache.d.ts +0 -3
  481. package/dist/providers/openai-prompt-cache.d.ts.map +0 -1
  482. package/dist/providers/openai-prompt-cache.js +0 -10
  483. package/dist/providers/openai-prompt-cache.js.map +0 -1
  484. package/dist/providers/openai-responses-shared.d.ts +0 -18
  485. package/dist/providers/openai-responses-shared.d.ts.map +0 -1
  486. package/dist/providers/openai-responses-shared.js +0 -492
  487. package/dist/providers/openai-responses-shared.js.map +0 -1
  488. package/dist/providers/openai-responses.d.ts +0 -13
  489. package/dist/providers/openai-responses.d.ts.map +0 -1
  490. package/dist/providers/openai-responses.js +0 -237
  491. package/dist/providers/openai-responses.js.map +0 -1
  492. package/dist/providers/register-builtins.d.ts +0 -38
  493. package/dist/providers/register-builtins.d.ts.map +0 -1
  494. package/dist/providers/register-builtins.js +0 -278
  495. package/dist/providers/register-builtins.js.map +0 -1
  496. package/dist/providers/simple-options.d.ts +0 -8
  497. package/dist/providers/simple-options.d.ts.map +0 -1
  498. package/dist/providers/simple-options.js +0 -41
  499. package/dist/providers/simple-options.js.map +0 -1
  500. package/dist/providers/transform-messages.d.ts.map +0 -1
  501. package/dist/providers/transform-messages.js +0 -184
  502. package/dist/providers/transform-messages.js.map +0 -1
  503. package/dist/session-resources.d.ts +0 -4
  504. package/dist/session-resources.d.ts.map +0 -1
  505. package/dist/session-resources.js +0 -22
  506. package/dist/session-resources.js.map +0 -1
  507. package/dist/stream.d.ts +0 -8
  508. package/dist/stream.d.ts.map +0 -1
  509. package/dist/stream.js +0 -27
  510. package/dist/stream.js.map +0 -1
  511. package/dist/types.d.ts +0 -498
  512. package/dist/types.d.ts.map +0 -1
  513. package/dist/types.js +0 -2
  514. package/dist/types.js.map +0 -1
  515. package/dist/utils/diagnostics.d.ts +0 -19
  516. package/dist/utils/diagnostics.d.ts.map +0 -1
  517. package/dist/utils/diagnostics.js +0 -25
  518. package/dist/utils/diagnostics.js.map +0 -1
  519. package/dist/utils/event-stream.d.ts +0 -21
  520. package/dist/utils/event-stream.d.ts.map +0 -1
  521. package/dist/utils/event-stream.js +0 -81
  522. package/dist/utils/event-stream.js.map +0 -1
  523. package/dist/utils/hash.d.ts +0 -3
  524. package/dist/utils/hash.d.ts.map +0 -1
  525. package/dist/utils/hash.js +0 -14
  526. package/dist/utils/hash.js.map +0 -1
  527. package/dist/utils/headers.d.ts +0 -2
  528. package/dist/utils/headers.d.ts.map +0 -1
  529. package/dist/utils/headers.js +0 -8
  530. package/dist/utils/headers.js.map +0 -1
  531. package/dist/utils/json-parse.d.ts +0 -16
  532. package/dist/utils/json-parse.d.ts.map +0 -1
  533. package/dist/utils/json-parse.js +0 -113
  534. package/dist/utils/json-parse.js.map +0 -1
  535. package/dist/utils/node-http-proxy.d.ts +0 -10
  536. package/dist/utils/node-http-proxy.d.ts.map +0 -1
  537. package/dist/utils/node-http-proxy.js +0 -97
  538. package/dist/utils/node-http-proxy.js.map +0 -1
  539. package/dist/utils/oauth/anthropic.d.ts +0 -25
  540. package/dist/utils/oauth/anthropic.d.ts.map +0 -1
  541. package/dist/utils/oauth/anthropic.js +0 -335
  542. package/dist/utils/oauth/anthropic.js.map +0 -1
  543. package/dist/utils/oauth/device-code.d.ts +0 -19
  544. package/dist/utils/oauth/device-code.d.ts.map +0 -1
  545. package/dist/utils/oauth/device-code.js +0 -55
  546. package/dist/utils/oauth/device-code.js.map +0 -1
  547. package/dist/utils/oauth/github-copilot.d.ts +0 -30
  548. package/dist/utils/oauth/github-copilot.d.ts.map +0 -1
  549. package/dist/utils/oauth/github-copilot.js +0 -268
  550. package/dist/utils/oauth/github-copilot.js.map +0 -1
  551. package/dist/utils/oauth/google-antigravity.d.ts +0 -26
  552. package/dist/utils/oauth/google-antigravity.d.ts.map +0 -1
  553. package/dist/utils/oauth/google-antigravity.js +0 -377
  554. package/dist/utils/oauth/google-antigravity.js.map +0 -1
  555. package/dist/utils/oauth/google-gemini-cli.d.ts +0 -26
  556. package/dist/utils/oauth/google-gemini-cli.d.ts.map +0 -1
  557. package/dist/utils/oauth/google-gemini-cli.js +0 -482
  558. package/dist/utils/oauth/google-gemini-cli.js.map +0 -1
  559. package/dist/utils/oauth/index.d.ts +0 -63
  560. package/dist/utils/oauth/index.d.ts.map +0 -1
  561. package/dist/utils/oauth/index.js +0 -131
  562. package/dist/utils/oauth/index.js.map +0 -1
  563. package/dist/utils/oauth/oauth-page.d.ts +0 -3
  564. package/dist/utils/oauth/oauth-page.d.ts.map +0 -1
  565. package/dist/utils/oauth/oauth-page.js +0 -105
  566. package/dist/utils/oauth/oauth-page.js.map +0 -1
  567. package/dist/utils/oauth/openai-codex.d.ts +0 -34
  568. package/dist/utils/oauth/openai-codex.d.ts.map +0 -1
  569. package/dist/utils/oauth/openai-codex.js +0 -385
  570. package/dist/utils/oauth/openai-codex.js.map +0 -1
  571. package/dist/utils/oauth/pkce.d.ts.map +0 -1
  572. package/dist/utils/oauth/pkce.js +0 -31
  573. package/dist/utils/oauth/pkce.js.map +0 -1
  574. package/dist/utils/oauth/types.d.ts +0 -64
  575. package/dist/utils/oauth/types.d.ts.map +0 -1
  576. package/dist/utils/oauth/types.js +0 -2
  577. package/dist/utils/oauth/types.js.map +0 -1
  578. package/dist/utils/overflow.d.ts.map +0 -1
  579. package/dist/utils/overflow.js +0 -151
  580. package/dist/utils/overflow.js.map +0 -1
  581. package/dist/utils/sanitize-unicode.d.ts +0 -22
  582. package/dist/utils/sanitize-unicode.d.ts.map +0 -1
  583. package/dist/utils/sanitize-unicode.js +0 -26
  584. package/dist/utils/sanitize-unicode.js.map +0 -1
  585. package/dist/utils/typebox-helpers.d.ts +0 -17
  586. package/dist/utils/typebox-helpers.d.ts.map +0 -1
  587. package/dist/utils/typebox-helpers.js +0 -21
  588. package/dist/utils/typebox-helpers.js.map +0 -1
  589. package/dist/utils/validation.d.ts +0 -18
  590. package/dist/utils/validation.d.ts.map +0 -1
  591. package/dist/utils/validation.js +0 -281
  592. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,3018 @@
1
+ import * as os from "node:os";
2
+ import { scheduler } from "node:timers/promises";
3
+ import {
4
+ $env,
5
+ $flag,
6
+ asRecord,
7
+ extractHttpStatusFromError,
8
+ fetchWithRetry,
9
+ logger,
10
+ readSseJson,
11
+ structuredCloneJSON,
12
+ } from "@aryee337/aery-utils";
13
+ import type OpenAI from "openai";
14
+ import type {
15
+ ResponseCustomToolCall,
16
+ ResponseFunctionToolCall,
17
+ ResponseInput,
18
+ ResponseInputContent,
19
+ ResponseOutputMessage,
20
+ ResponseReasoningItem,
21
+ } from "openai/resources/responses/responses";
22
+ import packageJson from "../../package.json" with { type: "json" };
23
+ import { calculateCost } from "../models";
24
+ import { getEnvApiKey } from "../stream";
25
+ import {
26
+ type Api,
27
+ type AssistantMessage,
28
+ type Context,
29
+ type FetchImpl,
30
+ type Model,
31
+ type ProviderSessionState,
32
+ type RawSseEvent,
33
+ resolveServiceTier,
34
+ type ServiceTier,
35
+ type StreamFunction,
36
+ type StreamOptions,
37
+ type TextContent,
38
+ type ThinkingContent,
39
+ type Tool,
40
+ type ToolCall,
41
+ type ToolChoice,
42
+ } from "../types";
43
+ import {
44
+ createOpenAIResponsesHistoryPayload,
45
+ getOpenAIResponsesHistoryItems,
46
+ getOpenAIResponsesHistoryPayload,
47
+ normalizeSystemPrompts,
48
+ } from "../utils";
49
+ import { AssistantMessageEventStream } from "../utils/event-stream";
50
+ import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
51
+ import {
52
+ getOpenAIStreamFirstEventTimeoutMs,
53
+ getOpenAIStreamIdleTimeoutMs,
54
+ iterateWithIdleTimeout,
55
+ } from "../utils/idle-iterator";
56
+ import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
57
+ import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
58
+ import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
59
+ import { notifyRawSseEvent } from "../utils/sse-debug";
60
+ import { compactGrammarDefinition } from "./grammar";
61
+ import { CODEX_BASE_URL, getCodexAccountId, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "./openai-codex/constants";
62
+ import {
63
+ type CodexRequestOptions,
64
+ type InputItem,
65
+ type RequestBody,
66
+ transformRequestBody,
67
+ } from "./openai-codex/request-transformer";
68
+ import { parseCodexError } from "./openai-codex/response-handler";
69
+ import { normalizeOpenAIResponsesPromptCacheKey } from "./openai-responses";
70
+ import {
71
+ appendResponsesToolResultMessages,
72
+ convertResponsesAssistantMessage,
73
+ convertResponsesInputContent,
74
+ encodeResponsesToolCallId,
75
+ encodeTextSignatureV1,
76
+ isOpenAIResponsesProgressEvent,
77
+ mapOpenAIResponsesStopReason,
78
+ populateResponsesUsageFromResponse,
79
+ } from "./openai-responses-shared";
80
+ import { transformMessages } from "./transform-messages";
81
+
82
+ export interface OpenAICodexResponsesOptions extends StreamOptions {
83
+ reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
84
+ reasoningSummary?: "auto" | "concise" | "detailed" | null;
85
+ textVerbosity?: "low" | "medium" | "high";
86
+ include?: string[];
87
+ codexMode?: boolean;
88
+ toolChoice?: ToolChoice;
89
+ preferWebsockets?: boolean;
90
+ serviceTier?: ServiceTier;
91
+ }
92
+
93
+ const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
94
+ const CODEX_MAX_RETRIES = 5;
95
+ const CODEX_RETRY_DELAY_MS = 500;
96
+ const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
97
+ const CODEX_WEBSOCKET_PING_INTERVAL_MS = 10_000;
98
+ const CODEX_WEBSOCKET_PONG_TIMEOUT_MS = 60_000;
99
+ const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = 4096;
100
+ /**
101
+ * Maximum quiet period (no inbound frames AND no observed pong) we'll trust a
102
+ * reused WebSocket for before forcing a fresh handshake. Codex backends and
103
+ * intermediaries occasionally evict idle sockets server-side without sending a
104
+ * FIN, leaving the local `readyState` as OPEN while the next `send()` becomes a
105
+ * write into a half-open buffer. Reusing such a socket parks the next request
106
+ * at `#nextMessage` until the first-event/idle timeout fires (issue #1450). The
107
+ * heartbeat below also catches dead sockets, but only after `pongTimeoutMs`
108
+ * (default 60s) and only while a request is active — this gate closes the door
109
+ * earlier and even when the gap between requests is purely client-side (tool
110
+ * execution, user typing, etc.). Set `PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS=0`
111
+ * to disable.
112
+ */
113
+ const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = 30_000;
114
+ /**
115
+ * Steady-state liveness ceiling for the Codex WebSocket transport. Distinct from
116
+ * the AERY-wide stream watchdog removed in #1392: a WebSocket can stay TCP-open
117
+ * indefinitely without exchanging frames (server crash after upgrade, half-open
118
+ * network path), so we still need a transport-internal cap to detect those
119
+ * states and trigger the WS→SSE fallback. Only applies AFTER the first event
120
+ * has arrived — slow first-token paths wait as long as the caller permits.
121
+ */
122
+ const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
123
+ /**
124
+ * Maximum wait for the first WebSocket event before falling back to SSE.
125
+ * Unlike a stream watchdog, this triggers a transport switch (not a request
126
+ * failure) — the outer retry loop catches the timeout error and re-runs on
127
+ * SSE. Generous default so legitimately slow first-token providers still get
128
+ * a chance on the WS transport before falling through.
129
+ */
130
+ const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = 60_000;
131
+ const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
132
+ const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
133
+ const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
134
+ const CODEX_RETRYABLE_EVENT_MESSAGE =
135
+ /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
136
+ const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
137
+ const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
138
+ const X_MODELS_ETAG_HEADER = "x-models-etag";
139
+ const X_REASONING_INCLUDED_HEADER = "x-reasoning-included";
140
+ /** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
141
+ const CODEX_WEBSOCKET_FATAL_PATTERNS = ["websocket error:", "websocket closed before open", "connection timeout"];
142
+ /** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
143
+ const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
144
+ const CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES = new Set(["response.done", "response.incomplete"]);
145
+
146
+ function isCodexStreamProgressEvent(event: unknown): boolean {
147
+ if (isOpenAIResponsesProgressEvent(event)) return true;
148
+ if (!event || typeof event !== "object") return false;
149
+ const type = (event as { type?: unknown }).type;
150
+ return typeof type === "string" && CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES.has(type);
151
+ }
152
+
153
+ type CodexWebSocketTimeoutDetails = {
154
+ lastEventAt: number;
155
+ lastEventType?: string;
156
+ lastProgressAt: number;
157
+ lastProgressEventType?: string;
158
+ };
159
+
160
+ function createCodexWebSocketTimeoutMessage(reason: string, details: CodexWebSocketTimeoutDetails): string {
161
+ const now = Date.now();
162
+ const lastEvent = details.lastEventType
163
+ ? `${details.lastEventType} ${Math.max(0, now - details.lastEventAt)}ms ago`
164
+ : "none";
165
+ const lastProgress = details.lastProgressEventType
166
+ ? `${details.lastProgressEventType} ${Math.max(0, now - details.lastProgressAt)}ms ago`
167
+ : "none";
168
+ return `${reason} (last event: ${lastEvent}; last progress: ${lastProgress})`;
169
+ }
170
+
171
+ type CodexTransport = "sse" | "websocket";
172
+ type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
173
+ type CodexOutputBlock = ThinkingContent | TextContent | (ToolCall & { partialJson: string; lastParseLen?: number });
174
+
175
+ export interface OpenAICodexWebSocketDebugStats {
176
+ fullContextRequests: number;
177
+ deltaRequests: number;
178
+ lastInputItems: number;
179
+ lastDeltaInputItems?: number;
180
+ lastPreviousResponseId?: string;
181
+ }
182
+
183
+ type CodexWebSocketSessionState = {
184
+ disableWebsocket: boolean;
185
+ lastRequest?: RequestBody;
186
+ lastResponseId?: string;
187
+ lastResponseItems?: InputItem[];
188
+ canAppend: boolean;
189
+ turnState?: string;
190
+ modelsEtag?: string;
191
+ reasoningIncluded?: boolean;
192
+ connection?: CodexWebSocketConnection;
193
+ lastTransport?: CodexTransport;
194
+ fallbackCount: number;
195
+ lastFallbackAt?: number;
196
+ prewarmed: boolean;
197
+ stats: OpenAICodexWebSocketDebugStats;
198
+ };
199
+
200
+ interface CodexProviderSessionState extends ProviderSessionState {
201
+ webSocketSessions: Map<string, CodexWebSocketSessionState>;
202
+ webSocketPublicToPrivate: Map<string, string>;
203
+ }
204
+
205
+ interface CodexRequestContext {
206
+ apiKey: string;
207
+ accountId: string;
208
+ baseUrl: string;
209
+ url: string;
210
+ requestHeaders: Record<string, string>;
211
+ transportSessionId?: string;
212
+ providerSessionState?: CodexProviderSessionState;
213
+ websocketState?: CodexWebSocketSessionState;
214
+ transformedBody: RequestBody;
215
+ rawRequestDump: RawHttpRequestDump;
216
+ }
217
+
218
+ interface CodexRequestSetup {
219
+ requestSignal: AbortSignal;
220
+ wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
221
+ requestAbortController: AbortController;
222
+ websocketIdleTimeoutMs: number | undefined;
223
+ websocketFirstEventTimeoutMs: number | undefined;
224
+ }
225
+
226
+ interface CodexStreamRuntime {
227
+ eventStream: AsyncGenerator<Record<string, unknown>>;
228
+ requestBodyForState: RequestBody;
229
+ transport: CodexTransport;
230
+ websocketState?: CodexWebSocketSessionState;
231
+ currentItem: CodexEventItem | null;
232
+ currentBlock: CodexOutputBlock | null;
233
+ nativeOutputItems: Array<Record<string, unknown>>;
234
+ websocketStreamRetries: number;
235
+ providerRetryAttempt: number;
236
+ sawTerminalEvent: boolean;
237
+ canSafelyReplayWebsocketOverSse: boolean;
238
+ }
239
+
240
+ interface CodexStreamProcessingContext {
241
+ model: Model<"openai-codex-responses">;
242
+ output: AssistantMessage;
243
+ stream: AssistantMessageEventStream;
244
+ options: OpenAICodexResponsesOptions | undefined;
245
+ requestSetup: CodexRequestSetup;
246
+ requestContext: CodexRequestContext;
247
+ startTime: number;
248
+ firstTokenTime?: number;
249
+ }
250
+
251
+ interface CodexStreamCompletion {
252
+ firstTokenTime?: number;
253
+ }
254
+
255
+ function parseCodexNonNegativeInteger(value: string | undefined, fallback: number): number {
256
+ if (!value) return fallback;
257
+ const parsed = Number(value);
258
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
259
+ return Math.trunc(parsed);
260
+ }
261
+
262
+ function parseCodexPositiveInteger(value: string | undefined, fallback: number): number {
263
+ if (!value) return fallback;
264
+ const parsed = Number(value);
265
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
266
+ return Math.trunc(parsed);
267
+ }
268
+
269
+ function isCodexWebSocketEnvEnabled(): boolean {
270
+ return $flag("PI_CODEX_WEBSOCKET");
271
+ }
272
+
273
+ function getCodexWebSocketRetryBudget(): number {
274
+ return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET, CODEX_WEBSOCKET_RETRY_BUDGET);
275
+ }
276
+
277
+ function getCodexWebSocketRetryDelayMs(retry: number): number {
278
+ const baseDelay = parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS, CODEX_RETRY_DELAY_MS);
279
+ return baseDelay * Math.max(1, retry);
280
+ }
281
+
282
+ function getCodexWebSocketIdleTimeoutMs(): number {
283
+ return parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS, CODEX_WEBSOCKET_IDLE_TIMEOUT_MS);
284
+ }
285
+
286
+ function getCodexWebSocketFirstEventTimeoutMs(): number {
287
+ return parseCodexPositiveInteger(
288
+ $env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS,
289
+ CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS,
290
+ );
291
+ }
292
+
293
+ function getCodexWebSocketPingIntervalMs(): number {
294
+ return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS, CODEX_WEBSOCKET_PING_INTERVAL_MS);
295
+ }
296
+
297
+ function getCodexWebSocketPongTimeoutMs(): number {
298
+ return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS, CODEX_WEBSOCKET_PONG_TIMEOUT_MS);
299
+ }
300
+
301
+ function getCodexWebSocketMessageQueueCapacity(): number {
302
+ return parseCodexPositiveInteger(
303
+ $env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY,
304
+ CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY,
305
+ );
306
+ }
307
+
308
+ function getCodexWebSocketMaxIdleReuseMs(): number {
309
+ return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS, CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS);
310
+ }
311
+
312
+ function createCodexProviderSessionState(): CodexProviderSessionState {
313
+ const state: CodexProviderSessionState = {
314
+ webSocketSessions: new Map(),
315
+ webSocketPublicToPrivate: new Map(),
316
+ close: () => {
317
+ for (const session of state.webSocketSessions.values()) {
318
+ session.connection?.close("session_disposed");
319
+ }
320
+ state.webSocketSessions.clear();
321
+ state.webSocketPublicToPrivate.clear();
322
+ },
323
+ };
324
+ return state;
325
+ }
326
+
327
+ function getCodexProviderSessionState(
328
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
329
+ ): CodexProviderSessionState | undefined {
330
+ if (!providerSessionState) return undefined;
331
+ const existing = providerSessionState.get(CODEX_PROVIDER_SESSION_STATE_KEY) as CodexProviderSessionState | undefined;
332
+ if (existing) return existing;
333
+ const created = createCodexProviderSessionState();
334
+ providerSessionState.set(CODEX_PROVIDER_SESSION_STATE_KEY, created);
335
+ return created;
336
+ }
337
+
338
+ function createCodexWebSocketTransportError(message: string): Error {
339
+ return new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`);
340
+ }
341
+
342
+ function isCodexWebSocketFatalError(error: Error): boolean {
343
+ const msg = error.message.toLowerCase();
344
+ return CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern => msg.includes(pattern.toLowerCase()));
345
+ }
346
+
347
+ function isCodexWebSocketTransportError(error: unknown): boolean {
348
+ if (!(error instanceof Error)) return false;
349
+ return error.message.startsWith(CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX);
350
+ }
351
+
352
+ function isCodexWebSocketRetryableStreamError(error: unknown): boolean {
353
+ if (!(error instanceof Error) || !isCodexWebSocketTransportError(error)) return false;
354
+ const message = error.message.toLowerCase();
355
+ return (
356
+ message.includes("websocket closed (") ||
357
+ message.includes("websocket closed before response completion") ||
358
+ message.includes("websocket connection is unavailable") ||
359
+ message.includes("websocket send failed") ||
360
+ message.includes("websocket ping failed") ||
361
+ message.includes("websocket pong timeout") ||
362
+ message.includes("websocket message queue exceeded") ||
363
+ message.includes("idle timeout waiting for websocket") ||
364
+ message.includes("timeout waiting for first websocket event") ||
365
+ message.includes("syntaxerror") ||
366
+ message.includes("json")
367
+ );
368
+ }
369
+
370
+ function toCodexHeaderRecord(value: unknown): Record<string, string> | null {
371
+ if (!value || typeof value !== "object") return null;
372
+ const headers: Record<string, string> = {};
373
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
374
+ if (typeof entry === "string") {
375
+ headers[key] = entry;
376
+ } else if (Array.isArray(entry) && entry.every(item => typeof item === "string")) {
377
+ headers[key] = entry.join(",");
378
+ } else if (typeof entry === "number" || typeof entry === "boolean") {
379
+ headers[key] = String(entry);
380
+ }
381
+ }
382
+ return Object.keys(headers).length > 0 ? headers : null;
383
+ }
384
+
385
+ function toCodexHeaders(value: unknown): Headers | undefined {
386
+ if (!value) return undefined;
387
+ if (value instanceof Headers) return value;
388
+ if (Array.isArray(value)) {
389
+ try {
390
+ return new Headers(value as Array<[string, string]>);
391
+ } catch {
392
+ return undefined;
393
+ }
394
+ }
395
+ const record = toCodexHeaderRecord(value);
396
+ if (!record) return undefined;
397
+ return new Headers(record);
398
+ }
399
+
400
+ function updateCodexSessionMetadataFromHeaders(
401
+ state: CodexWebSocketSessionState | undefined,
402
+ headers: Headers | Record<string, string> | null | undefined,
403
+ ): void {
404
+ if (!state || !headers) return;
405
+ const resolvedHeaders = headers instanceof Headers ? headers : new Headers(headers);
406
+ const turnState = resolvedHeaders.get(X_CODEX_TURN_STATE_HEADER);
407
+ if (turnState && turnState.length > 0) {
408
+ state.turnState = turnState;
409
+ }
410
+ const modelsEtag = resolvedHeaders.get(X_MODELS_ETAG_HEADER);
411
+ if (modelsEtag && modelsEtag.length > 0) {
412
+ state.modelsEtag = modelsEtag;
413
+ }
414
+ const reasoningIncluded = resolvedHeaders.get(X_REASONING_INCLUDED_HEADER);
415
+ if (reasoningIncluded !== null) {
416
+ const normalized = reasoningIncluded.trim().toLowerCase();
417
+ state.reasoningIncluded = normalized.length === 0 ? true : normalized !== "false";
418
+ }
419
+ }
420
+
421
+ function extractCodexWebSocketHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): Headers | undefined {
422
+ const eventRecord = openEvent as Record<string, unknown> | undefined;
423
+ const eventResponse = eventRecord?.response as Record<string, unknown> | undefined;
424
+ const socketRecord = socket as unknown as Record<string, unknown>;
425
+ const socketResponse = socketRecord.response as Record<string, unknown> | undefined;
426
+ const socketHandshake = socketRecord.handshake as Record<string, unknown> | undefined;
427
+ return (
428
+ toCodexHeaders(eventRecord?.responseHeaders) ??
429
+ toCodexHeaders(eventRecord?.headers) ??
430
+ toCodexHeaders(eventResponse?.headers) ??
431
+ toCodexHeaders(socketRecord.responseHeaders) ??
432
+ toCodexHeaders(socketRecord.handshakeHeaders) ??
433
+ toCodexHeaders(socketResponse?.headers) ??
434
+ toCodexHeaders(socketHandshake?.headers)
435
+ );
436
+ }
437
+
438
+ // Synthesizes a `RawSseEvent` for a Codex WebSocket frame so the same debug
439
+ // pipeline used for HTTP SSE (`onSseEvent` → `RawSseDebugBuffer.recordEvent`)
440
+ // also captures WebSocket traffic. The `raw` array mirrors SSE wire format
441
+ // (one line per field) so the existing TUI viewer renders it identically:
442
+ // : ws ← <type>
443
+ // event: <type>
444
+ // data: <json>
445
+ // Outbound (client → server) uses `: ws → <type>`. The viewer pretty-prints
446
+ // `data:` JSON lines, so we keep the wire JSON single-line here and let the
447
+ // renderer expand it.
448
+ function notifyCodexWebSocketInbound(
449
+ observer: ((event: RawSseEvent) => void) | undefined,
450
+ parsed: Record<string, unknown>,
451
+ text: string,
452
+ ): void {
453
+ const type = typeof parsed.type === "string" ? parsed.type : null;
454
+ const raw: string[] = [`: ws ← ${type ?? "(untyped)"}`];
455
+ if (type) raw.push(`event: ${type}`);
456
+ raw.push(`data: ${text}`);
457
+ notifyRawSseEvent(observer, { event: type, data: text, raw });
458
+ }
459
+
460
+ function notifyCodexWebSocketOutbound(
461
+ observer: ((event: RawSseEvent) => void) | undefined,
462
+ request: Record<string, unknown>,
463
+ payload: string,
464
+ ): void {
465
+ const type = typeof request.type === "string" ? request.type : null;
466
+ const raw: string[] = [`: ws → ${type ?? "(untyped)"}`];
467
+ if (type) raw.push(`event: ${type}`);
468
+ raw.push(`data: ${payload}`);
469
+ notifyRawSseEvent(observer, { event: type, data: payload, raw });
470
+ }
471
+
472
+ function notifyCodexWebSocketMalformed(
473
+ observer: ((event: RawSseEvent) => void) | undefined,
474
+ data: unknown,
475
+ error: unknown,
476
+ ): void {
477
+ const text = typeof data === "string" ? data : "";
478
+ const reason = error instanceof Error ? error.message : String(error);
479
+ const raw: string[] = [`: ws ← (parse-error: ${reason})`];
480
+ if (text) raw.push(`data: ${text}`);
481
+ notifyRawSseEvent(observer, { event: "parse_error", data: text, raw });
482
+ }
483
+
484
+ /** @internal Exported for tests. */
485
+ export function normalizeCodexToolChoice(
486
+ choice: ToolChoice | undefined,
487
+ tools: Tool[] = [],
488
+ model?: Model<"openai-codex-responses">,
489
+ ): string | Record<string, unknown> | undefined {
490
+ if (!choice) return undefined;
491
+ if (typeof choice === "string") return choice;
492
+ const allowFreeform = model ? supportsFreeformApplyPatchCodex(model) : false;
493
+ const mapName = (name: string): Record<string, string> => {
494
+ const customTool = allowFreeform
495
+ ? tools.find(tool => tool.customFormat && (tool.name === name || tool.customWireName === name))
496
+ : undefined;
497
+ return customTool
498
+ ? { type: "custom", name: customTool.customWireName ?? customTool.name }
499
+ : { type: "function", name };
500
+ };
501
+ if (choice.type === "function") {
502
+ if ("function" in choice && choice.function?.name) {
503
+ return mapName(choice.function.name);
504
+ }
505
+ if ("name" in choice && choice.name) {
506
+ return mapName(choice.name);
507
+ }
508
+ }
509
+ if (choice.type === "tool" && choice.name) {
510
+ return mapName(choice.name);
511
+ }
512
+ return undefined;
513
+ }
514
+
515
+ function createEmptyUsage(): AssistantMessage["usage"] {
516
+ return {
517
+ input: 0,
518
+ output: 0,
519
+ cacheRead: 0,
520
+ cacheWrite: 0,
521
+ totalTokens: 0,
522
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
523
+ };
524
+ }
525
+
526
+ function getCodexUserAgent(): string {
527
+ return `aery/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`;
528
+ }
529
+
530
+ function getCodexServiceTierCostMultiplier(
531
+ model: Pick<Model<"openai-codex-responses">, "id">,
532
+ serviceTier: ServiceTier | "default" | undefined,
533
+ ): number {
534
+ switch (serviceTier) {
535
+ case "flex":
536
+ return 0.5;
537
+ case "priority":
538
+ return model.id === "gpt-5.5" ? 2.5 : 2;
539
+ default:
540
+ return 1;
541
+ }
542
+ }
543
+
544
+ function resolveCodexCostServiceTier(res: unknown, req?: unknown): ServiceTier | "default" | undefined {
545
+ switch (res) {
546
+ case "flex":
547
+ return "flex";
548
+ case "priority":
549
+ return "priority";
550
+ default:
551
+ if (req === "flex" || req === "priority") {
552
+ return req;
553
+ }
554
+ return "default";
555
+ }
556
+ }
557
+
558
+ function applyCodexServiceTierPricing(
559
+ model: Pick<Model<"openai-codex-responses">, "id">,
560
+ usage: AssistantMessage["usage"],
561
+ resTier: unknown,
562
+ reqTier: unknown,
563
+ ): void {
564
+ const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
565
+ const multiplier = getCodexServiceTierCostMultiplier(model, resolvedTier);
566
+ if (multiplier === 1) return;
567
+ usage.cost.input *= multiplier;
568
+ usage.cost.output *= multiplier;
569
+ usage.cost.cacheRead *= multiplier;
570
+ usage.cost.cacheWrite *= multiplier;
571
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
572
+ }
573
+
574
+ function createAssistantOutput(model: Model<"openai-codex-responses">): AssistantMessage {
575
+ return {
576
+ role: "assistant",
577
+ content: [],
578
+ api: "openai-codex-responses" as Api,
579
+ provider: model.provider,
580
+ model: model.id,
581
+ usage: createEmptyUsage(),
582
+ stopReason: "stop",
583
+ timestamp: Date.now(),
584
+ };
585
+ }
586
+
587
+ function resetOutputState(output: AssistantMessage): void {
588
+ output.content.length = 0;
589
+ output.usage = createEmptyUsage();
590
+ output.stopReason = "stop";
591
+ }
592
+
593
+ function removeTransientBlockIndices(output: AssistantMessage): void {
594
+ for (const block of output.content) {
595
+ delete (block as { index?: number }).index;
596
+ }
597
+ }
598
+
599
+ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): CodexRequestSetup {
600
+ const requestAbortController = new AbortController();
601
+ const requestSignal = options?.signal
602
+ ? AbortSignal.any([options.signal, requestAbortController.signal])
603
+ : requestAbortController.signal;
604
+ const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
605
+ const websocketIdleTimeoutMs = options?.streamIdleTimeoutMs ?? getCodexWebSocketIdleTimeoutMs();
606
+ const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
607
+ const websocketFirstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getCodexWebSocketFirstEventTimeoutMs();
608
+ const wrapCodexSseStream = (
609
+ source: AsyncGenerator<Record<string, unknown>>,
610
+ ): AsyncGenerator<Record<string, unknown>> =>
611
+ iterateWithIdleTimeout(source, {
612
+ idleTimeoutMs,
613
+ firstItemTimeoutMs: firstEventTimeoutMs,
614
+ firstItemErrorMessage: "OpenAI Codex SSE stream timed out while waiting for the first event",
615
+ errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
616
+ onIdle: () => requestAbortController.abort(),
617
+ onFirstItemTimeout: () => requestAbortController.abort(),
618
+ abortSignal: options?.signal,
619
+ isProgressItem: isCodexStreamProgressEvent,
620
+ });
621
+ return {
622
+ requestAbortController,
623
+ requestSignal,
624
+ wrapCodexSseStream,
625
+ websocketIdleTimeoutMs,
626
+ websocketFirstEventTimeoutMs,
627
+ };
628
+ }
629
+
630
+ async function buildCodexRequestContext(
631
+ model: Model<"openai-codex-responses">,
632
+ context: Context,
633
+ options: OpenAICodexResponsesOptions | undefined,
634
+ output: AssistantMessage,
635
+ ): Promise<CodexRequestContext> {
636
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
637
+ if (!apiKey) {
638
+ throw new Error(`No API key for provider: ${model.provider}`);
639
+ }
640
+
641
+ const accountId = getAccountId(apiKey);
642
+ const baseUrl = model.baseUrl || CODEX_BASE_URL;
643
+ const url = resolveCodexResponsesUrl(baseUrl);
644
+ const promptCacheKey = resolveCodexPromptCacheKey(options);
645
+ const transportSessionId = resolveCodexTransportSessionId(options);
646
+ const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
647
+ options?.onPayload?.(transformedBody);
648
+
649
+ const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
650
+ const rawRequestDump: RawHttpRequestDump = {
651
+ provider: model.provider,
652
+ api: output.api,
653
+ model: model.id,
654
+ method: "POST",
655
+ url,
656
+ body: transformedBody,
657
+ };
658
+
659
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
660
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, baseUrl);
661
+ const publicSessionKey = getCodexPublicSessionKey(transportSessionId, model, baseUrl);
662
+ if (sessionKey && publicSessionKey) {
663
+ providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
664
+ }
665
+ const websocketState =
666
+ sessionKey && providerSessionState ? getCodexWebSocketSessionState(sessionKey, providerSessionState) : undefined;
667
+ return {
668
+ apiKey,
669
+ accountId,
670
+ baseUrl,
671
+ url,
672
+ requestHeaders,
673
+ transportSessionId,
674
+ providerSessionState,
675
+ websocketState,
676
+ transformedBody,
677
+ rawRequestDump,
678
+ };
679
+ }
680
+
681
+ async function buildTransformedCodexRequestBody(
682
+ model: Model<"openai-codex-responses">,
683
+ context: Context,
684
+ options: OpenAICodexResponsesOptions | undefined,
685
+ promptCacheKey = resolveCodexPromptCacheKey(options),
686
+ ): Promise<RequestBody> {
687
+ const params: RequestBody = {
688
+ model: model.id,
689
+ input: [...convertMessages(model, context)],
690
+ stream: true,
691
+ prompt_cache_key: promptCacheKey,
692
+ };
693
+
694
+ if (options?.maxTokens) {
695
+ params.max_output_tokens = options.maxTokens;
696
+ }
697
+ if (options?.temperature !== undefined) {
698
+ params.temperature = options.temperature;
699
+ }
700
+ if (options?.topP !== undefined) {
701
+ params.top_p = options.topP;
702
+ }
703
+ if (options?.topK !== undefined) {
704
+ params.top_k = options.topK;
705
+ }
706
+ if (options?.minP !== undefined) {
707
+ params.min_p = options.minP;
708
+ }
709
+ if (options?.presencePenalty !== undefined) {
710
+ params.presence_penalty = options.presencePenalty;
711
+ }
712
+ if (options?.repetitionPenalty !== undefined) {
713
+ params.repetition_penalty = options.repetitionPenalty;
714
+ }
715
+ const resolvedServiceTier = resolveServiceTier(options?.serviceTier, model.provider);
716
+ if (resolvedServiceTier === "flex" || resolvedServiceTier === "scale" || resolvedServiceTier === "priority") {
717
+ params.service_tier = resolvedServiceTier;
718
+ }
719
+ if (context.tools && context.tools.length > 0) {
720
+ params.tools = convertOpenAICodexResponsesTools(context.tools, model);
721
+ if (options?.toolChoice) {
722
+ const toolChoice = normalizeCodexToolChoice(options.toolChoice, context.tools, model);
723
+ if (toolChoice) {
724
+ params.tool_choice = toolChoice;
725
+ }
726
+ }
727
+ // When a custom-tool is active, force serial tool-calling. OpenAI's
728
+ // `parallel_tool_calls` is request-scoped — disabling it here affects
729
+ // every tool in the turn, not just the custom one. That's coarser
730
+ // than spec §1's "supports_parallel_tool_calls = false" (which
731
+ // strictly targets `apply_patch`), but the platform API offers no
732
+ // per-tool flag.
733
+ const emittedTools = params.tools as CodexToolPayload[];
734
+ if (emittedTools.some(t => t.type === "custom")) {
735
+ params.parallel_tool_calls = false;
736
+ }
737
+ }
738
+
739
+ const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
740
+ if (systemPrompts.length > 0) {
741
+ params.instructions = systemPrompts[0];
742
+ }
743
+ const developerMessages = systemPrompts.slice(1);
744
+ const codexOptions: CodexRequestOptions = {
745
+ reasoningEffort: options?.reasoning,
746
+ reasoningSummary: options?.reasoningSummary ?? "auto",
747
+ textVerbosity: options?.textVerbosity,
748
+ include: options?.include,
749
+ };
750
+
751
+ return transformRequestBody(params, model, codexOptions, { developerMessages });
752
+ }
753
+
754
+ async function openInitialCodexEventStream(
755
+ model: Model<"openai-codex-responses">,
756
+ options: OpenAICodexResponsesOptions | undefined,
757
+ requestSetup: CodexRequestSetup,
758
+ requestContext: CodexRequestContext,
759
+ ): Promise<{
760
+ eventStream: AsyncGenerator<Record<string, unknown>>;
761
+ requestBodyForState: RequestBody;
762
+ transport: CodexTransport;
763
+ }> {
764
+ const { transformedBody, websocketState } = requestContext;
765
+ if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
766
+ const websocketRetryBudget = getCodexWebSocketRetryBudget();
767
+ let websocketRetries = 0;
768
+ while (true) {
769
+ try {
770
+ return await openCodexWebSocketTransport(
771
+ requestContext,
772
+ requestSetup,
773
+ websocketState,
774
+ websocketRetries,
775
+ options ? event => options.onSseEvent?.(event, model) : undefined,
776
+ );
777
+ } catch (error) {
778
+ const websocketError = error instanceof Error ? error : new Error(String(error));
779
+ const isFatal = isCodexWebSocketFatalError(websocketError);
780
+ const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
781
+ recordCodexWebSocketFailure(websocketState, activateFallback);
782
+ logCodexDebug("codex websocket fallback", {
783
+ error: websocketError.message,
784
+ retry: websocketRetries,
785
+ retryBudget: websocketRetryBudget,
786
+ activated: activateFallback,
787
+ fatal: isFatal,
788
+ });
789
+ if (!activateFallback) {
790
+ websocketRetries += 1;
791
+ await scheduler.wait(getCodexWebSocketRetryDelayMs(websocketRetries), {
792
+ signal: requestSetup.requestSignal,
793
+ });
794
+ continue;
795
+ }
796
+ break;
797
+ }
798
+ }
799
+ }
800
+ return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
801
+ }
802
+ async function openCodexWebSocketTransport(
803
+ requestContext: CodexRequestContext,
804
+ requestSetup: CodexRequestSetup,
805
+ websocketState: CodexWebSocketSessionState,
806
+ retry: number,
807
+ onSseEvent?: (event: RawSseEvent) => void,
808
+ ): Promise<{
809
+ eventStream: AsyncGenerator<Record<string, unknown>>;
810
+ requestBodyForState: RequestBody;
811
+ transport: CodexTransport;
812
+ }> {
813
+ const websocketRequest = buildCodexWebSocketRequest(requestContext.transformedBody, websocketState);
814
+ const websocketHeaders = createCodexHeaders(
815
+ requestContext.requestHeaders,
816
+ requestContext.accountId,
817
+ requestContext.apiKey,
818
+ requestContext.transportSessionId,
819
+ "websocket",
820
+ websocketState,
821
+ );
822
+ const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
823
+ logCodexDebug("codex websocket request", {
824
+ url: toWebSocketUrl(requestContext.url),
825
+ model: requestContext.transformedBody.model,
826
+ reasoningEffort: requestContext.transformedBody.reasoning?.effort ?? null,
827
+ headers: redactHeaders(websocketHeaders),
828
+ sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
829
+ sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
830
+ requestType: websocketRequest.type,
831
+ retry,
832
+ retryBudget: getCodexWebSocketRetryBudget(),
833
+ });
834
+ const eventStream = await openCodexWebSocketEventStream(
835
+ toWebSocketUrl(requestContext.url),
836
+ websocketHeaders,
837
+ websocketRequest,
838
+ websocketState,
839
+ {
840
+ idleTimeoutMs: requestSetup.websocketIdleTimeoutMs,
841
+ firstEventTimeoutMs: requestSetup.websocketFirstEventTimeoutMs,
842
+ },
843
+ requestSetup.requestSignal,
844
+ onSseEvent,
845
+ );
846
+ return { eventStream, requestBodyForState, transport: "websocket" };
847
+ }
848
+
849
+ async function openCodexSseTransport(
850
+ model: Model<"openai-codex-responses">,
851
+ requestContext: CodexRequestContext,
852
+ requestSetup: CodexRequestSetup,
853
+ options: OpenAICodexResponsesOptions | undefined,
854
+ state: CodexWebSocketSessionState | undefined,
855
+ body = requestContext.transformedBody,
856
+ ): Promise<{
857
+ eventStream: AsyncGenerator<Record<string, unknown>>;
858
+ requestBodyForState: RequestBody;
859
+ transport: CodexTransport;
860
+ }> {
861
+ const eventStream = requestSetup.wrapCodexSseStream(
862
+ await openCodexSseEventStream(
863
+ requestContext.url,
864
+ requestContext.requestHeaders,
865
+ requestContext.accountId,
866
+ requestContext.apiKey,
867
+ requestContext.transportSessionId,
868
+ body,
869
+ state,
870
+ requestSetup.requestSignal,
871
+ event => options?.onSseEvent?.(event, model),
872
+ options?.fetch,
873
+ ),
874
+ );
875
+ return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
876
+ }
877
+
878
+ async function reopenCodexWebSocketRuntimeStream(
879
+ context: CodexStreamProcessingContext,
880
+ runtime: CodexStreamRuntime,
881
+ state: CodexWebSocketSessionState,
882
+ ): Promise<void> {
883
+ try {
884
+ const next = await openCodexWebSocketTransport(
885
+ context.requestContext,
886
+ context.requestSetup,
887
+ state,
888
+ runtime.websocketStreamRetries,
889
+ context.options ? event => context.options?.onSseEvent?.(event, context.model) : undefined,
890
+ );
891
+ runtime.eventStream = next.eventStream;
892
+ runtime.requestBodyForState = next.requestBodyForState;
893
+ runtime.transport = next.transport;
894
+ state.lastTransport = next.transport;
895
+ } catch (error) {
896
+ const wsError = error instanceof Error ? error : new Error(String(error));
897
+ if (!isCodexWebSocketTransportError(wsError)) throw error;
898
+ // Reopen failed at the websocket layer (handshake refused, connect timeout, etc.).
899
+ // Activate fallback so subsequent turns use SSE, and replay this turn over SSE
900
+ // instead of surfacing a raw transport error to the caller.
901
+ recordCodexWebSocketFailure(state, true);
902
+ logCodexDebug("codex websocket reopen failed, falling back to SSE", {
903
+ error: wsError.message,
904
+ retry: runtime.websocketStreamRetries,
905
+ });
906
+ await reopenCodexSseRuntimeStream(context, runtime, state);
907
+ }
908
+ }
909
+
910
+ async function reopenCodexSseRuntimeStream(
911
+ context: CodexStreamProcessingContext,
912
+ runtime: CodexStreamRuntime,
913
+ state: CodexWebSocketSessionState | undefined,
914
+ ): Promise<void> {
915
+ const next = await openCodexSseTransport(
916
+ context.model,
917
+ context.requestContext,
918
+ context.requestSetup,
919
+ context.options,
920
+ state,
921
+ );
922
+ runtime.eventStream = next.eventStream;
923
+ runtime.requestBodyForState = next.requestBodyForState;
924
+ runtime.transport = next.transport;
925
+ if (state) {
926
+ state.lastTransport = next.transport;
927
+ }
928
+ }
929
+
930
+ function createCodexStreamRuntime(initial: {
931
+ eventStream: AsyncGenerator<Record<string, unknown>>;
932
+ requestBodyForState: RequestBody;
933
+ transport: CodexTransport;
934
+ websocketState?: CodexWebSocketSessionState;
935
+ }): CodexStreamRuntime {
936
+ return {
937
+ eventStream: initial.eventStream,
938
+ requestBodyForState: initial.requestBodyForState,
939
+ transport: initial.transport,
940
+ websocketState: initial.websocketState,
941
+ currentItem: null,
942
+ currentBlock: null,
943
+ nativeOutputItems: [],
944
+ websocketStreamRetries: 0,
945
+ providerRetryAttempt: 0,
946
+ sawTerminalEvent: false,
947
+ canSafelyReplayWebsocketOverSse: true,
948
+ };
949
+ }
950
+
951
+ async function processCodexResponseStream(
952
+ context: CodexStreamProcessingContext,
953
+ runtime: CodexStreamRuntime,
954
+ ): Promise<CodexStreamCompletion> {
955
+ const { output, stream } = context;
956
+ stream.push({ type: "start", partial: output });
957
+
958
+ while (true) {
959
+ try {
960
+ let firstTokenTime = context.firstTokenTime;
961
+ for await (const rawEvent of runtime.eventStream) {
962
+ firstTokenTime = handleCodexStreamEvent({
963
+ ...context,
964
+ runtime,
965
+ rawEvent,
966
+ firstTokenTime,
967
+ });
968
+ if (runtime.sawTerminalEvent) break;
969
+ }
970
+ return { firstTokenTime };
971
+ } catch (error) {
972
+ const recovered = await recoverCodexStreamError(context, runtime, error);
973
+ if (!recovered) {
974
+ throw error;
975
+ }
976
+ }
977
+ }
978
+ }
979
+
980
+ function handleCodexStreamEvent(args: {
981
+ model: Model<"openai-codex-responses">;
982
+ output: AssistantMessage;
983
+ stream: AssistantMessageEventStream;
984
+ runtime: CodexStreamRuntime;
985
+ rawEvent: Record<string, unknown>;
986
+ firstTokenTime?: number;
987
+ }): number | undefined {
988
+ const { model, output, stream, runtime, rawEvent } = args;
989
+ const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
990
+ if (!eventType) return args.firstTokenTime;
991
+
992
+ const blocks = output.content;
993
+ const blockIndex = () => blocks.length - 1;
994
+ let firstTokenTime = args.firstTokenTime;
995
+
996
+ if (eventType === "response.output_item.added") {
997
+ if (!firstTokenTime) firstTokenTime = Date.now();
998
+ const item = rawEvent.item as CodexEventItem;
999
+ runtime.currentItem = item;
1000
+ runtime.currentBlock = createOutputBlockForItem(item);
1001
+ if (!runtime.currentBlock) return firstTokenTime;
1002
+ output.content.push(runtime.currentBlock);
1003
+ stream.push({
1004
+ type: getOutputBlockStartEventType(runtime.currentBlock),
1005
+ contentIndex: blockIndex(),
1006
+ partial: output,
1007
+ });
1008
+ return firstTokenTime;
1009
+ }
1010
+
1011
+ if (eventType === "response.reasoning_summary_part.added") {
1012
+ handleReasoningSummaryPartAdded(runtime.currentItem, rawEvent);
1013
+ return firstTokenTime;
1014
+ }
1015
+
1016
+ if (eventType === "response.reasoning_summary_text.delta") {
1017
+ handleReasoningSummaryTextDelta(runtime.currentItem, runtime.currentBlock, rawEvent, stream, output, blockIndex);
1018
+ return firstTokenTime;
1019
+ }
1020
+
1021
+ if (eventType === "response.reasoning_summary_part.done") {
1022
+ handleReasoningSummaryPartDone(runtime.currentItem, runtime.currentBlock, stream, output, blockIndex);
1023
+ return firstTokenTime;
1024
+ }
1025
+
1026
+ if (eventType === "response.content_part.added") {
1027
+ handleContentPartAdded(runtime.currentItem, rawEvent);
1028
+ return firstTokenTime;
1029
+ }
1030
+
1031
+ if (eventType === "response.output_text.delta") {
1032
+ handleMessageTextDelta(
1033
+ runtime.currentItem,
1034
+ runtime.currentBlock,
1035
+ rawEvent,
1036
+ stream,
1037
+ output,
1038
+ blockIndex,
1039
+ "output_text",
1040
+ );
1041
+ return firstTokenTime;
1042
+ }
1043
+
1044
+ if (eventType === "response.refusal.delta") {
1045
+ handleMessageTextDelta(
1046
+ runtime.currentItem,
1047
+ runtime.currentBlock,
1048
+ rawEvent,
1049
+ stream,
1050
+ output,
1051
+ blockIndex,
1052
+ "refusal",
1053
+ );
1054
+ return firstTokenTime;
1055
+ }
1056
+
1057
+ if (eventType === "response.function_call_arguments.delta") {
1058
+ handleToolCallArgumentsDelta(runtime.currentItem, runtime.currentBlock, rawEvent, stream, output, blockIndex);
1059
+ return firstTokenTime;
1060
+ }
1061
+
1062
+ if (eventType === "response.function_call_arguments.done") {
1063
+ handleToolCallArgumentsDone(runtime.currentItem, runtime.currentBlock, rawEvent);
1064
+ return firstTokenTime;
1065
+ }
1066
+
1067
+ if (eventType === "response.custom_tool_call_input.delta") {
1068
+ handleCustomToolCallInputDelta(runtime.currentItem, runtime.currentBlock, rawEvent, stream, output, blockIndex);
1069
+ return firstTokenTime;
1070
+ }
1071
+
1072
+ if (eventType === "response.custom_tool_call_input.done") {
1073
+ handleCustomToolCallInputDone(runtime.currentItem, runtime.currentBlock, rawEvent);
1074
+ return firstTokenTime;
1075
+ }
1076
+
1077
+ if (eventType === "response.output_item.done") {
1078
+ handleOutputItemDone(model, output, stream, runtime, rawEvent, blockIndex);
1079
+ return firstTokenTime;
1080
+ }
1081
+
1082
+ if (eventType === "response.created") {
1083
+ return handleResponseCreated(runtime, rawEvent);
1084
+ }
1085
+
1086
+ if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
1087
+ handleResponseCompleted(model, output, runtime, rawEvent);
1088
+ return firstTokenTime;
1089
+ }
1090
+
1091
+ if (eventType === "error" || eventType === "response.failed") {
1092
+ throw createCodexProviderStreamError(rawEvent);
1093
+ }
1094
+
1095
+ return firstTokenTime;
1096
+ }
1097
+
1098
+ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null {
1099
+ if (item.type === "reasoning") {
1100
+ return { type: "thinking", thinking: "" };
1101
+ }
1102
+ if (item.type === "message") {
1103
+ return { type: "text", text: "" };
1104
+ }
1105
+ if (item.type === "function_call") {
1106
+ return {
1107
+ type: "toolCall",
1108
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1109
+ name: item.name,
1110
+ arguments: {},
1111
+ partialJson: item.arguments || "",
1112
+ };
1113
+ }
1114
+ if (item.type === "custom_tool_call") {
1115
+ // Wire name flows through unchanged; the agent-loop dispatcher also
1116
+ // matches `Tool.customWireName`. Reuse `partialJson` as the
1117
+ // accumulation buffer for the raw input string.
1118
+ return {
1119
+ type: "toolCall",
1120
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1121
+ name: item.name,
1122
+ arguments: { input: item.input ?? "" },
1123
+ customWireName: item.name,
1124
+ partialJson: item.input ?? "",
1125
+ };
1126
+ }
1127
+ return null;
1128
+ }
1129
+
1130
+ function getOutputBlockStartEventType(block: CodexOutputBlock): "thinking_start" | "text_start" | "toolcall_start" {
1131
+ if (block.type === "thinking") return "thinking_start";
1132
+ if (block.type === "text") return "text_start";
1133
+ return "toolcall_start";
1134
+ }
1135
+
1136
+ function handleReasoningSummaryPartAdded(currentItem: CodexEventItem | null, rawEvent: Record<string, unknown>): void {
1137
+ if (currentItem?.type !== "reasoning") return;
1138
+ currentItem.summary = currentItem.summary || [];
1139
+ currentItem.summary.push((rawEvent as { part: ResponseReasoningItem["summary"][number] }).part);
1140
+ }
1141
+
1142
+ function handleReasoningSummaryTextDelta(
1143
+ currentItem: CodexEventItem | null,
1144
+ currentBlock: CodexOutputBlock | null,
1145
+ rawEvent: Record<string, unknown>,
1146
+ stream: AssistantMessageEventStream,
1147
+ output: AssistantMessage,
1148
+ blockIndex: () => number,
1149
+ ): void {
1150
+ if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
1151
+ currentItem.summary = currentItem.summary || [];
1152
+ const lastPart = currentItem.summary[currentItem.summary.length - 1];
1153
+ if (!lastPart) return;
1154
+ const delta = (rawEvent as { delta?: string }).delta || "";
1155
+ currentBlock.thinking += delta;
1156
+ lastPart.text += delta;
1157
+ stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta, partial: output });
1158
+ }
1159
+
1160
+ function handleReasoningSummaryPartDone(
1161
+ currentItem: CodexEventItem | null,
1162
+ currentBlock: CodexOutputBlock | null,
1163
+ stream: AssistantMessageEventStream,
1164
+ output: AssistantMessage,
1165
+ blockIndex: () => number,
1166
+ ): void {
1167
+ if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
1168
+ currentItem.summary = currentItem.summary || [];
1169
+ const lastPart = currentItem.summary[currentItem.summary.length - 1];
1170
+ if (!lastPart) return;
1171
+ currentBlock.thinking += "\n\n";
1172
+ lastPart.text += "\n\n";
1173
+ stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta: "\n\n", partial: output });
1174
+ }
1175
+
1176
+ function handleContentPartAdded(currentItem: CodexEventItem | null, rawEvent: Record<string, unknown>): void {
1177
+ if (currentItem?.type !== "message") return;
1178
+ currentItem.content = currentItem.content || [];
1179
+ const part = (rawEvent as { part?: ResponseOutputMessage["content"][number] }).part;
1180
+ if (part && (part.type === "output_text" || part.type === "refusal")) {
1181
+ currentItem.content.push(part);
1182
+ }
1183
+ }
1184
+
1185
+ function handleMessageTextDelta(
1186
+ currentItem: CodexEventItem | null,
1187
+ currentBlock: CodexOutputBlock | null,
1188
+ rawEvent: Record<string, unknown>,
1189
+ stream: AssistantMessageEventStream,
1190
+ output: AssistantMessage,
1191
+ blockIndex: () => number,
1192
+ partType: "output_text" | "refusal",
1193
+ ): void {
1194
+ if (currentItem?.type !== "message" || currentBlock?.type !== "text") return;
1195
+ if (!currentItem.content || currentItem.content.length === 0) return;
1196
+ const lastPart = currentItem.content[currentItem.content.length - 1];
1197
+ if (!lastPart || lastPart.type !== partType) return;
1198
+ const delta = (rawEvent as { delta?: string }).delta || "";
1199
+ currentBlock.text += delta;
1200
+ if (lastPart.type === "output_text") {
1201
+ lastPart.text += delta;
1202
+ } else {
1203
+ lastPart.refusal += delta;
1204
+ }
1205
+ stream.push({ type: "text_delta", contentIndex: blockIndex(), delta, partial: output });
1206
+ }
1207
+
1208
+ function handleToolCallArgumentsDelta(
1209
+ currentItem: CodexEventItem | null,
1210
+ currentBlock: CodexOutputBlock | null,
1211
+ rawEvent: Record<string, unknown>,
1212
+ stream: AssistantMessageEventStream,
1213
+ output: AssistantMessage,
1214
+ blockIndex: () => number,
1215
+ ): void {
1216
+ if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
1217
+ const delta = (rawEvent as { delta?: string }).delta || "";
1218
+ currentBlock.partialJson += delta;
1219
+ const throttled = parseStreamingJsonThrottled(currentBlock.partialJson, currentBlock.lastParseLen ?? 0);
1220
+ if (throttled) {
1221
+ currentBlock.arguments = throttled.value;
1222
+ currentBlock.lastParseLen = throttled.parsedLen;
1223
+ }
1224
+ stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
1225
+ }
1226
+
1227
+ function handleToolCallArgumentsDone(
1228
+ currentItem: CodexEventItem | null,
1229
+ currentBlock: CodexOutputBlock | null,
1230
+ rawEvent: Record<string, unknown>,
1231
+ ): void {
1232
+ if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
1233
+ const args = (rawEvent as { arguments?: string }).arguments;
1234
+ if (typeof args === "string") {
1235
+ currentBlock.partialJson = args;
1236
+ currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
1237
+ delete (currentBlock as { partialJson?: string }).partialJson;
1238
+ delete (currentBlock as { lastParseLen?: number }).lastParseLen;
1239
+ }
1240
+ }
1241
+
1242
+ function handleCustomToolCallInputDelta(
1243
+ currentItem: CodexEventItem | null,
1244
+ currentBlock: CodexOutputBlock | null,
1245
+ rawEvent: Record<string, unknown>,
1246
+ stream: AssistantMessageEventStream,
1247
+ output: AssistantMessage,
1248
+ blockIndex: () => number,
1249
+ ): void {
1250
+ if (currentItem?.type !== "custom_tool_call" || currentBlock?.type !== "toolCall") return;
1251
+ const delta = (rawEvent as { delta?: string }).delta || "";
1252
+ currentBlock.partialJson += delta;
1253
+ currentBlock.arguments = { input: currentBlock.partialJson };
1254
+ stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
1255
+ }
1256
+
1257
+ function handleCustomToolCallInputDone(
1258
+ currentItem: CodexEventItem | null,
1259
+ currentBlock: CodexOutputBlock | null,
1260
+ rawEvent: Record<string, unknown>,
1261
+ ): void {
1262
+ if (currentItem?.type !== "custom_tool_call" || currentBlock?.type !== "toolCall") return;
1263
+ const input = (rawEvent as { input?: string }).input;
1264
+ if (typeof input === "string") {
1265
+ currentBlock.partialJson = input;
1266
+ currentBlock.arguments = { input };
1267
+ }
1268
+ }
1269
+
1270
+ function handleOutputItemDone(
1271
+ model: Model<"openai-codex-responses">,
1272
+ output: AssistantMessage,
1273
+ stream: AssistantMessageEventStream,
1274
+ runtime: CodexStreamRuntime,
1275
+ rawEvent: Record<string, unknown>,
1276
+ blockIndex: () => number,
1277
+ ): void {
1278
+ const item = structuredCloneJSON(rawEvent.item) as CodexEventItem;
1279
+ runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
1280
+
1281
+ if (item.type === "reasoning" && runtime.currentBlock?.type === "thinking") {
1282
+ runtime.currentBlock.thinking = item.summary?.map(summary => summary.text).join("\n\n") || "";
1283
+ runtime.currentBlock.thinkingSignature = JSON.stringify(item);
1284
+ stream.push({
1285
+ type: "thinking_end",
1286
+ contentIndex: blockIndex(),
1287
+ content: runtime.currentBlock.thinking,
1288
+ partial: output,
1289
+ });
1290
+ runtime.currentBlock = null;
1291
+ return;
1292
+ }
1293
+
1294
+ if (item.type === "message" && runtime.currentBlock?.type === "text") {
1295
+ runtime.currentBlock.text = item.content
1296
+ .map(content => (content.type === "output_text" ? content.text : content.refusal))
1297
+ .join("");
1298
+ const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
1299
+ runtime.currentBlock.textSignature = encodeTextSignatureV1(item.id, phase);
1300
+ stream.push({
1301
+ type: "text_end",
1302
+ contentIndex: blockIndex(),
1303
+ content: runtime.currentBlock.text,
1304
+ partial: output,
1305
+ });
1306
+ runtime.currentBlock = null;
1307
+ return;
1308
+ }
1309
+
1310
+ if (item.type === "function_call") {
1311
+ const toolCall: ToolCall = {
1312
+ type: "toolCall",
1313
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1314
+ name: item.name,
1315
+ arguments: parseStreamingJson(item.arguments || "{}"),
1316
+ };
1317
+ if (runtime.currentBlock?.type === "toolCall") {
1318
+ // Persist the authoritative final args on the stored block; the throttled
1319
+ // delta parser may have left currentBlock.arguments stale (often `{}`).
1320
+ runtime.currentBlock.arguments = toolCall.arguments;
1321
+ delete (runtime.currentBlock as { partialJson?: string }).partialJson;
1322
+ delete (runtime.currentBlock as { lastParseLen?: number }).lastParseLen;
1323
+ }
1324
+ runtime.canSafelyReplayWebsocketOverSse = false;
1325
+ stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
1326
+ return;
1327
+ }
1328
+
1329
+ if (item.type === "custom_tool_call") {
1330
+ const rawInput =
1331
+ runtime.currentBlock?.type === "toolCall" && runtime.currentBlock.partialJson
1332
+ ? runtime.currentBlock.partialJson
1333
+ : (item.input ?? "");
1334
+ const toolCall: ToolCall = {
1335
+ type: "toolCall",
1336
+ id: encodeResponsesToolCallId(item.call_id, item.id),
1337
+ name: item.name,
1338
+ arguments: { input: rawInput },
1339
+ customWireName: item.name,
1340
+ };
1341
+ runtime.canSafelyReplayWebsocketOverSse = false;
1342
+ stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
1343
+ return;
1344
+ }
1345
+
1346
+ void model;
1347
+ }
1348
+
1349
+ function handleResponseCreated(runtime: CodexStreamRuntime, rawEvent: Record<string, unknown>): number | undefined {
1350
+ const response = (rawEvent as { response?: { id?: string } }).response;
1351
+ const state = runtime.websocketState;
1352
+ if (runtime.transport === "websocket" && state && typeof response?.id === "string" && response.id.length > 0) {
1353
+ state.lastResponseId = response.id;
1354
+ }
1355
+ return undefined;
1356
+ }
1357
+
1358
+ function handleResponseCompleted(
1359
+ model: Model<"openai-codex-responses">,
1360
+ output: AssistantMessage,
1361
+ runtime: CodexStreamRuntime,
1362
+ rawEvent: Record<string, unknown>,
1363
+ ): void {
1364
+ runtime.sawTerminalEvent = true;
1365
+ const response = (
1366
+ rawEvent as {
1367
+ response?: {
1368
+ id?: string;
1369
+ usage?: {
1370
+ input_tokens?: number;
1371
+ output_tokens?: number;
1372
+ total_tokens?: number;
1373
+ input_tokens_details?: { cached_tokens?: number };
1374
+ output_tokens_details?: { reasoning_tokens?: number };
1375
+ };
1376
+ status?: string;
1377
+ service_tier?: ServiceTier | "default";
1378
+ };
1379
+ }
1380
+ ).response;
1381
+
1382
+ populateResponsesUsageFromResponse(output, response?.usage);
1383
+ if (typeof response?.id === "string" && response.id.length > 0) {
1384
+ output.responseId = response.id;
1385
+ }
1386
+
1387
+ const state = runtime.websocketState;
1388
+ if (runtime.transport === "websocket" && state) {
1389
+ state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
1390
+ if (typeof response?.id === "string" && response.id.length > 0) {
1391
+ state.lastResponseId = response.id;
1392
+ state.lastResponseItems = stripInputItemIds(structuredCloneJSON(runtime.nativeOutputItems));
1393
+ }
1394
+ state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
1395
+ }
1396
+
1397
+ calculateCost(model, output.usage);
1398
+ applyCodexServiceTierPricing(model, output.usage, response?.service_tier, runtime.requestBodyForState.service_tier);
1399
+ output.stopReason = mapOpenAIResponsesStopReason(response?.status as OpenAI.Responses.ResponseStatus | undefined);
1400
+ if (output.content.some(block => block.type === "toolCall") && output.stopReason === "stop") {
1401
+ output.stopReason = "toolUse";
1402
+ }
1403
+ }
1404
+
1405
+ async function recoverCodexStreamError(
1406
+ context: CodexStreamProcessingContext,
1407
+ runtime: CodexStreamRuntime,
1408
+ error: unknown,
1409
+ ): Promise<boolean> {
1410
+ if (await tryReconnectCodexWebSocketOnConnectionLimit(context, runtime, error)) {
1411
+ return true;
1412
+ }
1413
+ if (await tryRecoverCodexPreviousResponseNotFound(context, runtime, error)) {
1414
+ return true;
1415
+ }
1416
+ if (await tryReplayWebsocketFailureOverSse(context, runtime, error)) {
1417
+ return true;
1418
+ }
1419
+ if (await tryRetryCodexProviderError(context, runtime, error)) {
1420
+ return true;
1421
+ }
1422
+ return false;
1423
+ }
1424
+
1425
+ /**
1426
+ * Handles `websocket_connection_limit_reached` errors by closing the stale connection
1427
+ * and opening a fresh websocket. If content has already been emitted to the caller,
1428
+ * falls back to SSE replay (same as other WS failures) since we cannot safely
1429
+ * continue a partial response on a new connection.
1430
+ */
1431
+ async function tryReconnectCodexWebSocketOnConnectionLimit(
1432
+ context: CodexStreamProcessingContext,
1433
+ runtime: CodexStreamRuntime,
1434
+ error: unknown,
1435
+ ): Promise<boolean> {
1436
+ if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") {
1437
+ return false;
1438
+ }
1439
+ const websocketState = context.requestContext.websocketState;
1440
+ if (!websocketState || runtime.transport !== "websocket" || context.options?.signal?.aborted) {
1441
+ return false;
1442
+ }
1443
+
1444
+ // Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
1445
+ websocketState.connection?.close("connection_limit");
1446
+ websocketState.connection = undefined;
1447
+ resetCodexWebSocketAppendState(websocketState);
1448
+
1449
+ logCodexDebug("codex websocket connection limit reached, reconnecting", {
1450
+ hadContent: context.output.content.length > 0,
1451
+ retry: runtime.websocketStreamRetries,
1452
+ });
1453
+
1454
+ if (context.output.content.length > 0) {
1455
+ // Content already emitted to the caller — cannot safely continue on a new WS.
1456
+ // Reset and replay the full request over SSE.
1457
+ runtime.canSafelyReplayWebsocketOverSse = true;
1458
+ runtime.currentItem = null;
1459
+ runtime.currentBlock = null;
1460
+ runtime.nativeOutputItems.length = 0;
1461
+ resetOutputState(context.output);
1462
+ context.firstTokenTime = undefined;
1463
+ recordCodexWebSocketFailure(websocketState, true);
1464
+ await reopenCodexSseRuntimeStream(context, runtime, websocketState);
1465
+ return true;
1466
+ }
1467
+
1468
+ // No content emitted yet — reconnect over websocket.
1469
+ runtime.websocketStreamRetries += 1;
1470
+ await reopenCodexWebSocketRuntimeStream(context, runtime, websocketState);
1471
+ return true;
1472
+ }
1473
+
1474
+ function isCodexPreviousResponseNotFound(error: unknown): boolean {
1475
+ return error instanceof CodexProviderStreamError && error.code === "previous_response_not_found";
1476
+ }
1477
+
1478
+ async function tryRecoverCodexPreviousResponseNotFound(
1479
+ context: CodexStreamProcessingContext,
1480
+ runtime: CodexStreamRuntime,
1481
+ error: unknown,
1482
+ ): Promise<boolean> {
1483
+ const websocketState = context.requestContext.websocketState;
1484
+ if (
1485
+ !isCodexPreviousResponseNotFound(error) ||
1486
+ !websocketState ||
1487
+ runtime.transport !== "websocket" ||
1488
+ context.output.content.length > 0 ||
1489
+ context.options?.signal?.aborted ||
1490
+ runtime.providerRetryAttempt >= CODEX_MAX_RETRIES
1491
+ ) {
1492
+ return false;
1493
+ }
1494
+
1495
+ runtime.providerRetryAttempt += 1;
1496
+ resetCodexWebSocketAppendState(websocketState);
1497
+ resetCodexSessionMetadata(websocketState);
1498
+ runtime.currentItem = null;
1499
+ runtime.currentBlock = null;
1500
+ runtime.sawTerminalEvent = false;
1501
+ runtime.nativeOutputItems.length = 0;
1502
+ resetOutputState(context.output);
1503
+ context.firstTokenTime = undefined;
1504
+
1505
+ logCodexDebug("codex previous_response_id expired; retrying with full context", {
1506
+ retry: runtime.providerRetryAttempt,
1507
+ });
1508
+ await reopenCodexWebSocketRuntimeStream(context, runtime, websocketState);
1509
+ return true;
1510
+ }
1511
+
1512
+ async function tryReplayWebsocketFailureOverSse(
1513
+ context: CodexStreamProcessingContext,
1514
+ runtime: CodexStreamRuntime,
1515
+ error: unknown,
1516
+ ): Promise<boolean> {
1517
+ const websocketState = context.requestContext.websocketState;
1518
+ const canReplay =
1519
+ runtime.transport === "websocket" &&
1520
+ websocketState &&
1521
+ isCodexWebSocketRetryableStreamError(error) &&
1522
+ runtime.canSafelyReplayWebsocketOverSse &&
1523
+ !runtime.sawTerminalEvent &&
1524
+ !context.options?.signal?.aborted;
1525
+ if (!canReplay) return false;
1526
+
1527
+ const state = websocketState;
1528
+ const streamError = error instanceof Error ? error : new Error(String(error));
1529
+ const replayingBufferedOutputOverSse = context.output.content.length > 0;
1530
+ const isFatal = isCodexWebSocketFatalError(streamError);
1531
+ const activateFallback =
1532
+ replayingBufferedOutputOverSse || isFatal || runtime.websocketStreamRetries >= getCodexWebSocketRetryBudget();
1533
+ recordCodexWebSocketFailure(state, activateFallback);
1534
+ logCodexDebug("codex websocket stream fallback", {
1535
+ error: streamError.message,
1536
+ retry: runtime.websocketStreamRetries,
1537
+ retryBudget: getCodexWebSocketRetryBudget(),
1538
+ activated: activateFallback,
1539
+ fatal: isFatal,
1540
+ replayedBufferedOutput: replayingBufferedOutputOverSse,
1541
+ });
1542
+
1543
+ if (!activateFallback) {
1544
+ runtime.websocketStreamRetries += 1;
1545
+ await scheduler.wait(getCodexWebSocketRetryDelayMs(runtime.websocketStreamRetries), {
1546
+ signal: context.requestSetup.requestSignal,
1547
+ });
1548
+ await reopenCodexWebSocketRuntimeStream(context, runtime, state);
1549
+ return true;
1550
+ }
1551
+
1552
+ if (replayingBufferedOutputOverSse) {
1553
+ runtime.canSafelyReplayWebsocketOverSse = true;
1554
+ runtime.currentItem = null;
1555
+ runtime.currentBlock = null;
1556
+ runtime.nativeOutputItems.length = 0;
1557
+ resetOutputState(context.output);
1558
+ context.firstTokenTime = undefined;
1559
+ }
1560
+
1561
+ await reopenCodexSseRuntimeStream(context, runtime, state);
1562
+ return true;
1563
+ }
1564
+
1565
+ async function tryRetryCodexProviderError(
1566
+ context: CodexStreamProcessingContext,
1567
+ runtime: CodexStreamRuntime,
1568
+ error: unknown,
1569
+ ): Promise<boolean> {
1570
+ if (
1571
+ !isRetryableCodexProviderError(error) ||
1572
+ context.output.content.length > 0 ||
1573
+ runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
1574
+ context.options?.signal?.aborted
1575
+ ) {
1576
+ return false;
1577
+ }
1578
+
1579
+ runtime.providerRetryAttempt += 1;
1580
+ const websocketState = context.requestContext.websocketState;
1581
+ if (runtime.transport === "websocket" && websocketState) {
1582
+ resetCodexWebSocketAppendState(websocketState);
1583
+ resetCodexSessionMetadata(websocketState);
1584
+ }
1585
+
1586
+ logCodexDebug("retrying codex provider stream error", {
1587
+ error: error instanceof Error ? error.message : String(error),
1588
+ retry: runtime.providerRetryAttempt,
1589
+ retryBudget: CODEX_MAX_RETRIES,
1590
+ transport: runtime.transport,
1591
+ });
1592
+
1593
+ runtime.currentItem = null;
1594
+ runtime.currentBlock = null;
1595
+ runtime.sawTerminalEvent = false;
1596
+ resetOutputState(context.output);
1597
+ context.firstTokenTime = undefined;
1598
+ await scheduler.wait(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, {
1599
+ signal: context.requestSetup.requestSignal,
1600
+ });
1601
+
1602
+ if (runtime.transport === "websocket" && websocketState) {
1603
+ await reopenCodexWebSocketRuntimeStream(context, runtime, websocketState);
1604
+ return true;
1605
+ }
1606
+
1607
+ await reopenCodexSseRuntimeStream(context, runtime, websocketState);
1608
+ return true;
1609
+ }
1610
+
1611
+ function finalizeCodexResponse(
1612
+ context: CodexStreamProcessingContext,
1613
+ runtime: CodexStreamRuntime,
1614
+ completion: CodexStreamCompletion,
1615
+ ): AssistantMessage {
1616
+ const { output } = context;
1617
+ if (context.options?.signal?.aborted) {
1618
+ throw new Error("Request was aborted");
1619
+ }
1620
+ if (!runtime.sawTerminalEvent) {
1621
+ if (runtime.transport === "websocket" && context.requestContext.websocketState) {
1622
+ resetCodexWebSocketAppendState(context.requestContext.websocketState);
1623
+ resetCodexSessionMetadata(context.requestContext.websocketState);
1624
+ }
1625
+ logCodexDebug("codex stream ended unexpectedly", {
1626
+ transport: runtime.transport,
1627
+ terminalEventSeen: runtime.sawTerminalEvent,
1628
+ unexpectedStreamEnd: true,
1629
+ sentTurnStateHeader: Boolean(context.requestContext.websocketState?.turnState),
1630
+ sentModelsEtagHeader: Boolean(context.requestContext.websocketState?.modelsEtag),
1631
+ });
1632
+ throw new Error("Codex stream ended before terminal completion event");
1633
+ }
1634
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
1635
+ throw new Error("Codex response failed");
1636
+ }
1637
+
1638
+ output.providerPayload = createOpenAIResponsesHistoryPayload(context.model.provider, runtime.nativeOutputItems);
1639
+ output.duration = Date.now() - context.startTime;
1640
+ if (completion.firstTokenTime) {
1641
+ output.ttft = completion.firstTokenTime - context.startTime;
1642
+ }
1643
+ return output;
1644
+ }
1645
+
1646
+ async function handleCodexStreamFailure(
1647
+ context: CodexStreamProcessingContext,
1648
+ error: unknown,
1649
+ ): Promise<AssistantMessage> {
1650
+ const { output } = context;
1651
+ removeTransientBlockIndices(output);
1652
+ if (context.requestContext.websocketState) {
1653
+ resetCodexWebSocketAppendState(context.requestContext.websocketState);
1654
+ resetCodexSessionMetadata(context.requestContext.websocketState);
1655
+ }
1656
+ output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
1657
+ output.errorStatus = extractHttpStatusFromError(error);
1658
+ output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
1659
+ output.duration = Date.now() - context.startTime;
1660
+ if (context.firstTokenTime) {
1661
+ output.ttft = context.firstTokenTime - context.startTime;
1662
+ }
1663
+ return output;
1664
+ }
1665
+
1666
+ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
1667
+ model: Model<"openai-codex-responses">,
1668
+ context: Context,
1669
+ options?: OpenAICodexResponsesOptions,
1670
+ ): AssistantMessageEventStream => {
1671
+ const stream = new AssistantMessageEventStream();
1672
+
1673
+ (async () => {
1674
+ const startTime = Date.now();
1675
+ const output = createAssistantOutput(model);
1676
+ const requestSetup = createRequestSetup(options);
1677
+ let processingContext: CodexStreamProcessingContext | undefined;
1678
+
1679
+ try {
1680
+ const requestContext = await buildCodexRequestContext(model, context, options, output);
1681
+ const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
1682
+ const runtime = createCodexStreamRuntime({
1683
+ ...initialTransport,
1684
+ websocketState: requestContext.websocketState,
1685
+ });
1686
+ if (requestContext.websocketState) {
1687
+ requestContext.websocketState.lastTransport = initialTransport.transport;
1688
+ }
1689
+
1690
+ processingContext = {
1691
+ model,
1692
+ output,
1693
+ stream,
1694
+ options,
1695
+ requestSetup,
1696
+ requestContext,
1697
+ startTime,
1698
+ };
1699
+
1700
+ const completion = await processCodexResponseStream(processingContext, runtime);
1701
+ processingContext.firstTokenTime = completion.firstTokenTime;
1702
+ const message = finalizeCodexResponse(processingContext, runtime, completion);
1703
+ stream.push({ type: "done", reason: message.stopReason as "stop" | "length" | "toolUse", message });
1704
+ stream.end();
1705
+ } catch (error) {
1706
+ const failureContext =
1707
+ processingContext ??
1708
+ ({
1709
+ model,
1710
+ output,
1711
+ stream,
1712
+ options,
1713
+ requestSetup,
1714
+ requestContext: {
1715
+ apiKey: "",
1716
+ accountId: "",
1717
+ baseUrl: model.baseUrl || CODEX_BASE_URL,
1718
+ url: "",
1719
+ requestHeaders: {},
1720
+ transformedBody: { model: model.id },
1721
+ rawRequestDump: {
1722
+ provider: model.provider,
1723
+ api: output.api,
1724
+ model: model.id,
1725
+ method: "POST",
1726
+ url: "",
1727
+ body: { model: model.id },
1728
+ },
1729
+ },
1730
+ startTime,
1731
+ } satisfies CodexStreamProcessingContext);
1732
+ const failure = await handleCodexStreamFailure(failureContext, error);
1733
+ stream.push({ type: "error", reason: failure.stopReason as "error" | "aborted", error: failure });
1734
+ stream.end();
1735
+ }
1736
+ })();
1737
+
1738
+ return stream;
1739
+ };
1740
+
1741
+ export async function prewarmOpenAICodexResponses(
1742
+ model: Model<"openai-codex-responses">,
1743
+ options?: Pick<
1744
+ OpenAICodexResponsesOptions,
1745
+ "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState"
1746
+ >,
1747
+ ): Promise<void> {
1748
+ const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
1749
+ if (!apiKey) return;
1750
+ const accountId = getAccountId(apiKey);
1751
+ const baseUrl = model.baseUrl || CODEX_BASE_URL;
1752
+ const url = resolveCodexResponsesUrl(baseUrl);
1753
+ const promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
1754
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
1755
+ const sessionKey = getCodexWebSocketSessionKey(promptCacheKey, model, accountId, baseUrl);
1756
+ const publicSessionKey = getCodexPublicSessionKey(promptCacheKey, model, baseUrl);
1757
+ if (publicSessionKey && sessionKey) {
1758
+ providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
1759
+ }
1760
+ if (!sessionKey || !providerSessionState) return;
1761
+ const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
1762
+ if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
1763
+ const headers = logger.time(
1764
+ "prewarmCodex:createHeaders",
1765
+ createCodexHeaders,
1766
+ { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
1767
+ accountId,
1768
+ apiKey,
1769
+ promptCacheKey,
1770
+ "websocket",
1771
+ state,
1772
+ );
1773
+ await logger.time(
1774
+ "prewarmCodex:establishWs",
1775
+ getOrCreateCodexWebSocketConnection,
1776
+ state,
1777
+ toWebSocketUrl(url),
1778
+ headers,
1779
+ options?.signal,
1780
+ );
1781
+ state.prewarmed = true;
1782
+ }
1783
+
1784
+ function resolveCodexPromptCacheKey(
1785
+ options: Pick<OpenAICodexResponsesOptions, "promptCacheKey" | "sessionId"> | undefined,
1786
+ ): string | undefined {
1787
+ return normalizeOpenAIResponsesPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
1788
+ }
1789
+
1790
+ function resolveCodexTransportSessionId(
1791
+ options: Pick<OpenAICodexResponsesOptions, "sessionId"> | undefined,
1792
+ ): string | undefined {
1793
+ return normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
1794
+ }
1795
+
1796
+ function getCodexWebSocketSessionKey(
1797
+ sessionId: string | undefined,
1798
+ model: Model<"openai-codex-responses">,
1799
+ accountId: string,
1800
+ baseUrl: string,
1801
+ ): string | undefined {
1802
+ const promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(sessionId);
1803
+ if (!promptCacheKey) return undefined;
1804
+ return `${accountId}:${baseUrl}:${model.id}:${promptCacheKey}`;
1805
+ }
1806
+
1807
+ function getCodexPublicSessionKey(
1808
+ sessionId: string | undefined,
1809
+ model: Model<"openai-codex-responses">,
1810
+ baseUrl: string,
1811
+ ): string | undefined {
1812
+ const promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(sessionId);
1813
+ if (!promptCacheKey) return undefined;
1814
+ return `${baseUrl}:${model.id}:${promptCacheKey}`;
1815
+ }
1816
+
1817
+ function getCodexWebSocketSessionState(
1818
+ sessionKey: string,
1819
+ providerSessionState: CodexProviderSessionState,
1820
+ ): CodexWebSocketSessionState {
1821
+ const existing = providerSessionState.webSocketSessions.get(sessionKey);
1822
+ if (existing) return existing;
1823
+ const created: CodexWebSocketSessionState = {
1824
+ disableWebsocket: false,
1825
+ canAppend: false,
1826
+ fallbackCount: 0,
1827
+ prewarmed: false,
1828
+ stats: {
1829
+ fullContextRequests: 0,
1830
+ deltaRequests: 0,
1831
+ lastInputItems: 0,
1832
+ },
1833
+ };
1834
+ providerSessionState.webSocketSessions.set(sessionKey, created);
1835
+ return created;
1836
+ }
1837
+
1838
+ function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void {
1839
+ state.canAppend = false;
1840
+ state.lastRequest = undefined;
1841
+ state.lastResponseId = undefined;
1842
+ state.lastResponseItems = undefined;
1843
+ }
1844
+
1845
+ function resetCodexSessionMetadata(state: CodexWebSocketSessionState): void {
1846
+ state.turnState = undefined;
1847
+ state.modelsEtag = undefined;
1848
+ state.reasoningIncluded = undefined;
1849
+ }
1850
+
1851
+ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activateFallback: boolean): void {
1852
+ resetCodexWebSocketAppendState(state);
1853
+ state.connection?.close("fallback");
1854
+ state.connection = undefined;
1855
+ state.lastFallbackAt = Date.now();
1856
+ if (activateFallback && !state.disableWebsocket) {
1857
+ state.disableWebsocket = true;
1858
+ state.fallbackCount += 1;
1859
+ }
1860
+ }
1861
+
1862
+ function shouldUseCodexWebSocket(
1863
+ model: Model<"openai-codex-responses">,
1864
+ state: CodexWebSocketSessionState | undefined,
1865
+ preferWebsockets?: boolean,
1866
+ ): boolean {
1867
+ if (!state || state.disableWebsocket) return false;
1868
+ if (preferWebsockets === false) return false;
1869
+ return isCodexWebSocketEnvEnabled() || preferWebsockets === true || model.preferWebsockets === true;
1870
+ }
1871
+
1872
+ export interface OpenAICodexTransportDetails {
1873
+ websocketPreferred: boolean;
1874
+ lastTransport?: CodexTransport;
1875
+ websocketDisabled: boolean;
1876
+ websocketConnected: boolean;
1877
+ fallbackCount: number;
1878
+ canAppend: boolean;
1879
+ prewarmed: boolean;
1880
+ hasSessionState: boolean;
1881
+ lastFallbackAt?: number;
1882
+ }
1883
+
1884
+ function getCodexWebSocketStateForPublicSession(
1885
+ model: Model<"openai-codex-responses">,
1886
+ options:
1887
+ | {
1888
+ sessionId?: string;
1889
+ baseUrl?: string;
1890
+ providerSessionState?: Map<string, ProviderSessionState>;
1891
+ }
1892
+ | undefined,
1893
+ ): CodexWebSocketSessionState | undefined {
1894
+ const baseUrl = options?.baseUrl || model.baseUrl || CODEX_BASE_URL;
1895
+ const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
1896
+ const publicSessionKey = getCodexPublicSessionKey(options?.sessionId, model, baseUrl);
1897
+ const privateSessionKey = publicSessionKey
1898
+ ? providerSessionState?.webSocketPublicToPrivate.get(publicSessionKey)
1899
+ : undefined;
1900
+ return privateSessionKey ? providerSessionState?.webSocketSessions.get(privateSessionKey) : undefined;
1901
+ }
1902
+
1903
+ export function getOpenAICodexWebSocketDebugStats(
1904
+ model: Model<"openai-codex-responses">,
1905
+ options?: {
1906
+ sessionId?: string;
1907
+ baseUrl?: string;
1908
+ providerSessionState?: Map<string, ProviderSessionState>;
1909
+ },
1910
+ ): OpenAICodexWebSocketDebugStats | undefined {
1911
+ const stats = getCodexWebSocketStateForPublicSession(model, options)?.stats;
1912
+ return stats ? { ...stats } : undefined;
1913
+ }
1914
+
1915
+ export function getOpenAICodexTransportDetails(
1916
+ model: Model<"openai-codex-responses">,
1917
+ options?: {
1918
+ sessionId?: string;
1919
+ baseUrl?: string;
1920
+ preferWebsockets?: boolean;
1921
+ providerSessionState?: Map<string, ProviderSessionState>;
1922
+ },
1923
+ ): OpenAICodexTransportDetails {
1924
+ const websocketPreferred =
1925
+ options?.preferWebsockets === false
1926
+ ? false
1927
+ : isCodexWebSocketEnvEnabled() || options?.preferWebsockets === true || model.preferWebsockets === true;
1928
+ const state = getCodexWebSocketStateForPublicSession(model, options);
1929
+
1930
+ return {
1931
+ websocketPreferred,
1932
+ lastTransport: state?.lastTransport,
1933
+ websocketDisabled: state?.disableWebsocket ?? false,
1934
+ websocketConnected: state?.connection?.isOpen() ?? false,
1935
+ fallbackCount: state?.fallbackCount ?? 0,
1936
+ canAppend: state?.canAppend ?? false,
1937
+ prewarmed: state?.prewarmed ?? false,
1938
+ hasSessionState: state !== undefined,
1939
+ lastFallbackAt: state?.lastFallbackAt,
1940
+ };
1941
+ }
1942
+
1943
+ function buildAppendInput(
1944
+ previous: RequestBody | undefined,
1945
+ previousResponseItems: InputItem[] | undefined,
1946
+ current: RequestBody,
1947
+ ): InputItem[] | null {
1948
+ if (!previous) return null;
1949
+ if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
1950
+ const previousWithoutInput = { ...previous, input: undefined };
1951
+ const currentWithoutInput = { ...current, input: undefined };
1952
+ if (JSON.stringify(previousWithoutInput) !== JSON.stringify(currentWithoutInput)) {
1953
+ return null;
1954
+ }
1955
+ const baseline = [...previous.input, ...(previousResponseItems ?? [])];
1956
+ if (current.input.length <= baseline.length) return null;
1957
+ for (let index = 0; index < baseline.length; index += 1) {
1958
+ if (JSON.stringify(baseline[index]) !== JSON.stringify(current.input[index])) {
1959
+ return null;
1960
+ }
1961
+ }
1962
+ return current.input.slice(baseline.length) as InputItem[];
1963
+ }
1964
+
1965
+ function stripInputItemIds(items: Array<Record<string, unknown>>): InputItem[] {
1966
+ return items.map(item => {
1967
+ if (item.id == null) return item as InputItem;
1968
+ const { id: _id, ...rest } = item;
1969
+ return rest as InputItem;
1970
+ });
1971
+ }
1972
+
1973
+ function recordCodexWebSocketRequestStats(
1974
+ state: CodexWebSocketSessionState | undefined,
1975
+ request: Record<string, unknown>,
1976
+ ): void {
1977
+ if (!state) return;
1978
+ const input = request.input;
1979
+ state.stats.lastInputItems = Array.isArray(input) ? input.length : 0;
1980
+ if (typeof request.previous_response_id === "string" && request.previous_response_id.length > 0) {
1981
+ state.stats.deltaRequests += 1;
1982
+ state.stats.lastDeltaInputItems = state.stats.lastInputItems;
1983
+ state.stats.lastPreviousResponseId = request.previous_response_id;
1984
+ return;
1985
+ }
1986
+ state.stats.fullContextRequests += 1;
1987
+ state.stats.lastDeltaInputItems = undefined;
1988
+ state.stats.lastPreviousResponseId = undefined;
1989
+ }
1990
+
1991
+ function buildCodexWebSocketRequest(
1992
+ requestBody: RequestBody,
1993
+ state: CodexWebSocketSessionState | undefined,
1994
+ ): Record<string, unknown> {
1995
+ const appendInput = state?.canAppend
1996
+ ? buildAppendInput(state.lastRequest, state.lastResponseItems, requestBody)
1997
+ : null;
1998
+ if (appendInput && appendInput.length > 0 && state?.lastResponseId) {
1999
+ const request = {
2000
+ type: "response.create",
2001
+ ...requestBody,
2002
+ previous_response_id: state.lastResponseId,
2003
+ input: appendInput,
2004
+ };
2005
+ recordCodexWebSocketRequestStats(state, request);
2006
+ return request;
2007
+ }
2008
+ if (state?.canAppend) {
2009
+ logCodexDebug("codex websocket append reset", {
2010
+ hadTurnStateHeader: Boolean(state.turnState),
2011
+ hadModelsEtagHeader: Boolean(state.modelsEtag),
2012
+ });
2013
+ resetCodexWebSocketAppendState(state);
2014
+ resetCodexSessionMetadata(state);
2015
+ }
2016
+ const request = {
2017
+ type: "response.create",
2018
+ ...requestBody,
2019
+ };
2020
+ recordCodexWebSocketRequestStats(state, request);
2021
+ return request;
2022
+ }
2023
+
2024
+ function toWebSocketUrl(url: string): string {
2025
+ const parsed = new URL(url);
2026
+ if (parsed.protocol === "https:") {
2027
+ parsed.protocol = "wss:";
2028
+ } else if (parsed.protocol === "http:") {
2029
+ parsed.protocol = "ws:";
2030
+ }
2031
+ return parsed.toString();
2032
+ }
2033
+
2034
+ function headersToRecord(headers: Headers): Record<string, string> {
2035
+ const result: Record<string, string> = {};
2036
+ for (const [key, value] of headers.entries()) {
2037
+ result[key] = value;
2038
+ }
2039
+ return result;
2040
+ }
2041
+
2042
+ interface CodexWebSocketRequestTimeouts {
2043
+ idleTimeoutMs?: number;
2044
+ firstEventTimeoutMs?: number;
2045
+ }
2046
+
2047
+ interface CodexWebSocketConnectionOptions {
2048
+ onHandshakeHeaders?: (headers: Headers) => void;
2049
+ }
2050
+
2051
+ class CodexWebSocketConnection {
2052
+ #url: string;
2053
+ #headers: Record<string, string>;
2054
+ #onHandshakeHeaders?: (headers: Headers) => void;
2055
+ #socket: Bun.WebSocket | null = null;
2056
+ #queue: Array<Record<string, unknown> | Error | null> = [];
2057
+ #waiters: Array<() => void> = [];
2058
+ #connectPromise?: Promise<void>;
2059
+ #activeRequest = false;
2060
+ #streamObserver?: (event: RawSseEvent) => void;
2061
+ #heartbeatInterval: NodeJS.Timeout | undefined;
2062
+ #removePongListener?: () => void;
2063
+ #handshakeHeaders?: Headers;
2064
+ #debugResponseLog?: RequestDebugResponseLog;
2065
+ /**
2066
+ * Wall-clock of the most recent inbound activity on this socket — any
2067
+ * decoded message, any pong, or the moment the handshake completed. Used
2068
+ * by {@link isHealthyForReuse} so we don't write a continuation frame into
2069
+ * a TCP-open-but-server-evicted socket whose `readyState` still says OPEN.
2070
+ */
2071
+ #lastInboundAt = 0;
2072
+ /** Wall-clock of the last heartbeat ping we issued; 0 if none yet. */
2073
+ #lastPingAt = 0;
2074
+
2075
+ constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
2076
+ this.#url = url;
2077
+ this.#headers = headers;
2078
+ this.#onHandshakeHeaders = options.onHandshakeHeaders;
2079
+ }
2080
+
2081
+ isOpen(): boolean {
2082
+ return this.#socket?.readyState === WebSocket.OPEN;
2083
+ }
2084
+
2085
+ /**
2086
+ * Stricter variant of {@link isOpen} for the connection-pool reuse gate.
2087
+ * Refuses sockets that have been silent past {@link CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS}.
2088
+ *
2089
+ * Bun's `WebSocket` does not always surface server-side eviction (no
2090
+ * `onclose`, no `onerror`), so a socket can sit in readyState OPEN long
2091
+ * after the upstream has dropped it. Reusing such a socket sends the next
2092
+ * `response.create` into a half-open write buffer and parks the reader
2093
+ * until the first-event / idle timeout fires (issue #1450). Forcing a
2094
+ * reconnect on any suspect socket trades a sub-second handshake for a
2095
+ * 60–300 s stall.
2096
+ */
2097
+ isHealthyForReuse(): boolean {
2098
+ if (!this.isOpen()) return false;
2099
+ const maxIdleMs = getCodexWebSocketMaxIdleReuseMs();
2100
+ if (maxIdleMs <= 0) return true;
2101
+ // Initial connect sets #lastInboundAt; any later message or pong refreshes
2102
+ // it. A zero value means the field was never initialized, which itself is
2103
+ // a desync — treat as unhealthy.
2104
+ if (this.#lastInboundAt === 0) return false;
2105
+ return Date.now() - this.#lastInboundAt <= maxIdleMs;
2106
+ }
2107
+
2108
+ matchesAuth(headers: Record<string, string>): boolean {
2109
+ return this.#headers.authorization === headers.authorization;
2110
+ }
2111
+
2112
+ close(reason = "done"): void {
2113
+ if (
2114
+ this.#socket &&
2115
+ (this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)
2116
+ ) {
2117
+ this.#socket.close(1000, reason);
2118
+ }
2119
+ this.#socket = null;
2120
+ this.#stopHeartbeat();
2121
+ }
2122
+
2123
+ async connect(signal?: AbortSignal): Promise<void> {
2124
+ if (this.isOpen()) return;
2125
+ if (this.#connectPromise) {
2126
+ logger.time("codexWs:awaitSharedHandshake");
2127
+ await this.#connectPromise;
2128
+ return;
2129
+ }
2130
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
2131
+ this.#connectPromise = promise;
2132
+ const socket = new (WebSocket as unknown as new (url: string, opts: Bun.WebSocketOptions) => Bun.WebSocket)(
2133
+ this.#url,
2134
+ { headers: this.#headers },
2135
+ );
2136
+ socket.binaryType = "nodebuffer";
2137
+ this.#socket = socket;
2138
+ let settled = false;
2139
+ let timeout: NodeJS.Timeout | undefined;
2140
+ const onAbort = () => {
2141
+ socket.close(1000, "aborted");
2142
+ if (!settled) {
2143
+ settled = true;
2144
+ reject(createCodexWebSocketTransportError("request was aborted"));
2145
+ }
2146
+ };
2147
+ if (signal) {
2148
+ if (signal.aborted) {
2149
+ onAbort();
2150
+ } else {
2151
+ signal.addEventListener("abort", onAbort, { once: true });
2152
+ }
2153
+ }
2154
+ const clearPending = () => {
2155
+ if (timeout) clearTimeout(timeout);
2156
+ if (signal) signal.removeEventListener("abort", onAbort);
2157
+ };
2158
+ timeout = setTimeout(() => {
2159
+ socket.close(1000, "connect-timeout");
2160
+ if (!settled) {
2161
+ settled = true;
2162
+ reject(createCodexWebSocketTransportError("connection timeout"));
2163
+ }
2164
+ }, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
2165
+
2166
+ socket.onopen = event => {
2167
+ if (!settled) {
2168
+ settled = true;
2169
+ clearPending();
2170
+ this.#lastInboundAt = Date.now();
2171
+ this.#captureHandshakeHeaders(socket, event);
2172
+ this.#startHeartbeat(socket);
2173
+ resolve();
2174
+ }
2175
+ };
2176
+ socket.onerror = event => {
2177
+ const eventRecord = event as unknown as Record<string, unknown>;
2178
+ const detail =
2179
+ (typeof eventRecord.message === "string" && eventRecord.message) ||
2180
+ (eventRecord.error instanceof Error && eventRecord.error.message) ||
2181
+ String(event.type);
2182
+ const error = createCodexWebSocketTransportError(`websocket error: ${detail}`);
2183
+ if (!settled) {
2184
+ settled = true;
2185
+ clearPending();
2186
+ reject(error);
2187
+ return;
2188
+ }
2189
+ this.#push(error);
2190
+ };
2191
+ socket.onclose = event => {
2192
+ this.#socket = null;
2193
+ this.#stopHeartbeat();
2194
+ if (!settled) {
2195
+ settled = true;
2196
+ clearPending();
2197
+ reject(createCodexWebSocketTransportError(`websocket closed before open (${event.code})`));
2198
+ return;
2199
+ }
2200
+ this.#push(createCodexWebSocketTransportError(`websocket closed (${event.code})`));
2201
+ this.#push(null);
2202
+ };
2203
+ socket.onmessage = event => {
2204
+ // Stamp inbound activity before parsing so even malformed frames refresh
2205
+ // the liveness clock — what matters for reuse health is that the upstream
2206
+ // is still talking to us, not that every frame is well-formed.
2207
+ this.#lastInboundAt = Date.now();
2208
+ this.#writeDebugWebSocketFrame(event.data);
2209
+ try {
2210
+ const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
2211
+ if (!text) return;
2212
+ const parsed = JSON.parse(text) as Record<string, unknown>;
2213
+ if (parsed.type === "error" && typeof parsed.error === "object" && parsed.error) {
2214
+ const inner = parsed.error as Record<string, unknown>;
2215
+ if (typeof parsed.code !== "string" && typeof inner.code === "string") {
2216
+ parsed.code = inner.code;
2217
+ }
2218
+ if (typeof parsed.message !== "string" && typeof inner.message === "string") {
2219
+ parsed.message = inner.message;
2220
+ }
2221
+ }
2222
+ notifyCodexWebSocketInbound(this.#streamObserver, parsed, text);
2223
+ this.#push(parsed);
2224
+ } catch (error) {
2225
+ notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
2226
+ this.#push(createCodexWebSocketTransportError(String(error)));
2227
+ }
2228
+ };
2229
+
2230
+ logger.time("codexWs:awaitTcpHandshake");
2231
+ try {
2232
+ await promise;
2233
+ } finally {
2234
+ this.#connectPromise = undefined;
2235
+ }
2236
+ }
2237
+
2238
+ async *streamRequest(
2239
+ request: Record<string, unknown>,
2240
+ timeouts: CodexWebSocketRequestTimeouts,
2241
+ signal?: AbortSignal,
2242
+ onSseEvent?: (event: RawSseEvent) => void,
2243
+ ): AsyncGenerator<Record<string, unknown>> {
2244
+ if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
2245
+ throw createCodexWebSocketTransportError("websocket connection is unavailable");
2246
+ }
2247
+ if (this.#activeRequest) {
2248
+ throw createCodexWebSocketTransportError("websocket request already in progress");
2249
+ }
2250
+ this.#activeRequest = true;
2251
+ this.#streamObserver = onSseEvent;
2252
+ // Drain any non-error frames left over from a prior request before sending.
2253
+ // `processCodexResponseStream` breaks its `for-await` on the terminal event,
2254
+ // which interrupts our generator at `yield next` (the post-yield `break`
2255
+ // never runs). Any frame that landed between the consumer's break and the
2256
+ // generator's `finally` lingers in `#queue` and would otherwise become the
2257
+ // first frame of THIS request — a stale `response.completed` would end the
2258
+ // turn immediately with empty output, and a stale non-progress frame would
2259
+ // flip `sawFirstEvent` and silently downgrade the first-event timeout to
2260
+ // the longer idle timeout. Transport errors are preserved so we surface
2261
+ // the death signal instead of writing into a dead socket.
2262
+ this.#dropStaleFrames();
2263
+ const onAbort = () => {
2264
+ this.close("aborted");
2265
+ this.#push(createCodexWebSocketTransportError("request was aborted"));
2266
+ };
2267
+ if (signal) {
2268
+ if (signal.aborted) {
2269
+ onAbort();
2270
+ } else {
2271
+ signal.addEventListener("abort", onAbort, { once: true });
2272
+ }
2273
+ }
2274
+
2275
+ try {
2276
+ const debugSession = isRequestDebugEnabled()
2277
+ ? await createRequestDebugSession({
2278
+ protocol: "websocket",
2279
+ method: "POST",
2280
+ url: this.#url,
2281
+ headers: this.#headers,
2282
+ body: request,
2283
+ })
2284
+ : undefined;
2285
+ this.#debugResponseLog = debugSession
2286
+ ? await debugSession.openResponseLog("WebSocket 101 Switching Protocols", this.#handshakeHeaders)
2287
+ : undefined;
2288
+
2289
+ const requestPayload = JSON.stringify(request);
2290
+ notifyCodexWebSocketOutbound(onSseEvent, request, requestPayload);
2291
+ try {
2292
+ this.#socket.send(requestPayload);
2293
+ } catch (error) {
2294
+ throw createCodexWebSocketTransportError(
2295
+ `websocket send failed: ${error instanceof Error ? error.message : String(error)}`,
2296
+ );
2297
+ }
2298
+ let sawFirstEvent = false;
2299
+ const { idleTimeoutMs, firstEventTimeoutMs } = timeouts;
2300
+ let lastProgressAt = Date.now();
2301
+ let lastProgressEventType: string | undefined;
2302
+ let lastEventAt = lastProgressAt;
2303
+ let lastEventType: string | undefined;
2304
+ while (true) {
2305
+ let timeoutMs: number | undefined;
2306
+ let timeoutReason: string;
2307
+ if (sawFirstEvent) {
2308
+ timeoutReason = createCodexWebSocketTimeoutMessage("idle timeout waiting for websocket", {
2309
+ lastEventAt,
2310
+ lastEventType,
2311
+ lastProgressAt,
2312
+ lastProgressEventType,
2313
+ });
2314
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
2315
+ timeoutMs = idleTimeoutMs - (Date.now() - lastProgressAt);
2316
+ if (timeoutMs <= 0) {
2317
+ logCodexDebug("codex websocket idle timeout", {
2318
+ lastEventType,
2319
+ lastProgressEventType,
2320
+ msSinceLastEvent: Date.now() - lastEventAt,
2321
+ msSinceLastProgress: Date.now() - lastProgressAt,
2322
+ });
2323
+ throw createCodexWebSocketTransportError(timeoutReason);
2324
+ }
2325
+ }
2326
+ } else {
2327
+ timeoutReason = createCodexWebSocketTimeoutMessage("timeout waiting for first websocket event", {
2328
+ lastEventAt,
2329
+ lastEventType,
2330
+ lastProgressAt,
2331
+ lastProgressEventType,
2332
+ });
2333
+ if (firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0) {
2334
+ timeoutMs = firstEventTimeoutMs;
2335
+ }
2336
+ }
2337
+ const next = await this.#nextMessage(timeoutMs, timeoutReason);
2338
+ if (next instanceof Error) {
2339
+ throw next;
2340
+ }
2341
+ if (next === null) {
2342
+ throw createCodexWebSocketTransportError("websocket closed before response completion");
2343
+ }
2344
+ sawFirstEvent = true;
2345
+ const eventType = typeof next.type === "string" ? next.type : "";
2346
+ lastEventAt = Date.now();
2347
+ lastEventType = eventType || undefined;
2348
+ if (isCodexStreamProgressEvent(next)) {
2349
+ lastProgressAt = lastEventAt;
2350
+ lastProgressEventType = lastEventType;
2351
+ }
2352
+ yield next;
2353
+ if (
2354
+ eventType === "response.completed" ||
2355
+ eventType === "response.done" ||
2356
+ eventType === "response.incomplete" ||
2357
+ eventType === "response.failed" ||
2358
+ eventType === "error"
2359
+ ) {
2360
+ break;
2361
+ }
2362
+ }
2363
+ } finally {
2364
+ this.#activeRequest = false;
2365
+ this.#streamObserver = undefined;
2366
+ if (signal) {
2367
+ signal.removeEventListener("abort", onAbort);
2368
+ }
2369
+ const debugResponseLog = this.#debugResponseLog;
2370
+ this.#debugResponseLog = undefined;
2371
+ await debugResponseLog?.close();
2372
+ }
2373
+ }
2374
+
2375
+ #captureHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): void {
2376
+ const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
2377
+ if (!headers) return;
2378
+ this.#handshakeHeaders = headers;
2379
+ this.#onHandshakeHeaders?.(headers);
2380
+ }
2381
+
2382
+ #writeDebugWebSocketFrame(data: unknown): void {
2383
+ const log = this.#debugResponseLog;
2384
+ if (!log) return;
2385
+ if (typeof data === "string") {
2386
+ log.write(data);
2387
+ return;
2388
+ }
2389
+ if (data instanceof Uint8Array) {
2390
+ log.write(data);
2391
+ return;
2392
+ }
2393
+ if (data instanceof ArrayBuffer) {
2394
+ log.write(new Uint8Array(data));
2395
+ return;
2396
+ }
2397
+ log.write(String(data));
2398
+ }
2399
+
2400
+ #startHeartbeat(socket: Bun.WebSocket): void {
2401
+ this.#stopHeartbeat();
2402
+ const intervalMs = getCodexWebSocketPingIntervalMs();
2403
+ if (intervalMs <= 0) return;
2404
+
2405
+ this.#lastPingAt = 0;
2406
+ const socketEventTarget = socket as EventTarget;
2407
+ const onPong = () => {
2408
+ // Pongs are inbound activity — refresh the reuse-health clock so a quiet
2409
+ // but ping-responsive socket stays trustworthy across requests.
2410
+ this.#lastInboundAt = Date.now();
2411
+ };
2412
+ if (
2413
+ typeof socketEventTarget.addEventListener === "function" &&
2414
+ typeof socketEventTarget.removeEventListener === "function"
2415
+ ) {
2416
+ socketEventTarget.addEventListener("pong", onPong);
2417
+ this.#removePongListener = () => socketEventTarget.removeEventListener("pong", onPong);
2418
+ }
2419
+
2420
+ this.#heartbeatInterval = setInterval(() => {
2421
+ if (this.#socket !== socket || socket.readyState !== WebSocket.OPEN) {
2422
+ this.#stopHeartbeat();
2423
+ return;
2424
+ }
2425
+ // Fail-closed on missing pongs even when no pong has ever been observed.
2426
+ // The previous `#observedPong &&` guard disabled the timeout entirely on
2427
+ // runtimes where Bun does not surface a `pong` event for our outgoing
2428
+ // pings (issue #1450) — letting truly dead sockets sail through the
2429
+ // pool until the per-request first-event / idle timeout (60–300 s)
2430
+ // finally fired. Instead, trigger on inbound silence: if we sent a
2431
+ // ping at least `pongTimeoutMs` ago and have received no traffic of
2432
+ // any kind (data frame or pong) since, the socket is unhealthy.
2433
+ const pongTimeoutMs = getCodexWebSocketPongTimeoutMs();
2434
+ if (
2435
+ pongTimeoutMs > 0 &&
2436
+ this.#lastPingAt > 0 &&
2437
+ this.#lastPingAt > this.#lastInboundAt &&
2438
+ Date.now() - this.#lastPingAt > pongTimeoutMs
2439
+ ) {
2440
+ this.#failQueue(createCodexWebSocketTransportError("websocket pong timeout"), "pong-timeout");
2441
+ return;
2442
+ }
2443
+ if (typeof socket.ping !== "function") {
2444
+ this.#stopHeartbeat();
2445
+ return;
2446
+ }
2447
+ try {
2448
+ socket.ping();
2449
+ this.#lastPingAt = Date.now();
2450
+ } catch (error) {
2451
+ this.#failQueue(
2452
+ createCodexWebSocketTransportError(
2453
+ `websocket ping failed: ${error instanceof Error ? error.message : String(error)}`,
2454
+ ),
2455
+ "ping-failed",
2456
+ );
2457
+ }
2458
+ }, intervalMs);
2459
+ this.#heartbeatInterval.unref();
2460
+ }
2461
+
2462
+ #stopHeartbeat(): void {
2463
+ if (this.#heartbeatInterval) {
2464
+ clearInterval(this.#heartbeatInterval);
2465
+ this.#heartbeatInterval = undefined;
2466
+ }
2467
+ if (this.#removePongListener) {
2468
+ this.#removePongListener();
2469
+ this.#removePongListener = undefined;
2470
+ }
2471
+ this.#lastPingAt = 0;
2472
+ }
2473
+
2474
+ #failQueue(error: Error, closeReason: string): void {
2475
+ logCodexDebug("codex websocket transport failure", { error: error.message, closeReason });
2476
+ this.#queue.length = 0;
2477
+ this.#queue.push(error);
2478
+ this.close(closeReason);
2479
+ this.#wakeWaiters();
2480
+ }
2481
+
2482
+ /**
2483
+ * Discard data frames from a previous request that remained in `#queue`
2484
+ * after the consumer broke out on the terminal event. Preserves any queued
2485
+ * transport error (from `onerror` / `onclose` / `#failQueue`) so the next
2486
+ * `#nextMessage` surfaces the death signal instead of waiting it out.
2487
+ *
2488
+ * Returns the number of frames dropped (test/debug visibility only).
2489
+ */
2490
+ #dropStaleFrames(): number {
2491
+ if (this.#queue.length === 0) return 0;
2492
+ const surviving = this.#queue.filter(item => item instanceof Error);
2493
+ const dropped = this.#queue.length - surviving.length;
2494
+ if (dropped === 0) return 0;
2495
+ this.#queue.length = 0;
2496
+ for (const item of surviving) this.#queue.push(item);
2497
+ logCodexDebug("codex websocket dropped stale frames before request", { dropped });
2498
+ return dropped;
2499
+ }
2500
+
2501
+ #wakeWaiters(): void {
2502
+ for (;;) {
2503
+ const waiter = this.#waiters.shift();
2504
+ if (!waiter) break;
2505
+ waiter();
2506
+ }
2507
+ }
2508
+
2509
+ #push(item: Record<string, unknown> | Error | null): void {
2510
+ if (item instanceof Error) {
2511
+ if (!(this.#queue[0] instanceof Error)) {
2512
+ this.#queue.length = 0;
2513
+ }
2514
+ this.#queue.push(item);
2515
+ this.#wakeWaiters();
2516
+ return;
2517
+ }
2518
+ if (item !== null && this.#queue.length >= getCodexWebSocketMessageQueueCapacity()) {
2519
+ this.#failQueue(
2520
+ createCodexWebSocketTransportError(
2521
+ `websocket message queue exceeded ${getCodexWebSocketMessageQueueCapacity()} items`,
2522
+ ),
2523
+ "queue-overflow",
2524
+ );
2525
+ return;
2526
+ }
2527
+ this.#queue.push(item);
2528
+ const waiter = this.#waiters.shift();
2529
+ if (waiter) waiter();
2530
+ }
2531
+
2532
+ async #nextMessage(
2533
+ timeoutMs: number | undefined,
2534
+ timeoutReason: string,
2535
+ ): Promise<Record<string, unknown> | Error | null> {
2536
+ while (this.#queue.length === 0) {
2537
+ const { promise, resolve } = Promise.withResolvers<void>();
2538
+ this.#waiters.push(resolve);
2539
+ let timedOut = false;
2540
+ let timeout: NodeJS.Timeout | undefined;
2541
+ if (timeoutMs !== undefined && timeoutMs > 0) {
2542
+ timeout = setTimeout(() => {
2543
+ timedOut = true;
2544
+ const waiterIndex = this.#waiters.indexOf(resolve);
2545
+ if (waiterIndex >= 0) {
2546
+ this.#waiters.splice(waiterIndex, 1);
2547
+ }
2548
+ resolve();
2549
+ }, timeoutMs);
2550
+ }
2551
+ await promise;
2552
+ if (timeout) clearTimeout(timeout);
2553
+ if (timedOut && this.#queue.length === 0) {
2554
+ return createCodexWebSocketTransportError(timeoutReason);
2555
+ }
2556
+ }
2557
+ return this.#queue.shift() ?? null;
2558
+ }
2559
+ }
2560
+
2561
+ async function getOrCreateCodexWebSocketConnection(
2562
+ state: CodexWebSocketSessionState,
2563
+ url: string,
2564
+ headers: Headers,
2565
+ signal?: AbortSignal,
2566
+ ): Promise<CodexWebSocketConnection> {
2567
+ const headerRecord = headersToRecord(headers);
2568
+ if (state.connection?.isOpen()) {
2569
+ if (!state.connection.matchesAuth(headerRecord)) {
2570
+ state.connection.close("token-refresh");
2571
+ resetCodexWebSocketAppendState(state);
2572
+ } else if (state.connection.isHealthyForReuse()) {
2573
+ logger.time("codexWs:reuseOpenSocket");
2574
+ return state.connection;
2575
+ } else {
2576
+ // Open in readyState but no inbound traffic recently — likely server-
2577
+ // evicted (issue #1450). Force a fresh handshake instead of writing
2578
+ // `response.create` into a half-open buffer and waiting out the
2579
+ // first-event timeout. Drop append state because the new socket
2580
+ // won't carry the prior `previous_response_id` context.
2581
+ logCodexDebug("codex websocket reuse rejected by health check", {});
2582
+ state.connection.close("stale-reuse");
2583
+ resetCodexWebSocketAppendState(state);
2584
+ }
2585
+ }
2586
+ state.connection?.close("reconnect");
2587
+ resetCodexWebSocketAppendState(state);
2588
+ logger.time("codexWs:newSocket");
2589
+ state.connection = new CodexWebSocketConnection(url, headerRecord, {
2590
+ onHandshakeHeaders: handshakeHeaders => {
2591
+ updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
2592
+ },
2593
+ });
2594
+ await state.connection.connect(signal);
2595
+ return state.connection;
2596
+ }
2597
+
2598
+ async function openCodexSseEventStream(
2599
+ url: string,
2600
+ requestHeaders: Record<string, string> | undefined,
2601
+ accountId: string,
2602
+ apiKey: string,
2603
+ sessionId: string | undefined,
2604
+ body: RequestBody,
2605
+ state: CodexWebSocketSessionState | undefined,
2606
+ signal?: AbortSignal,
2607
+ onSseEvent?: OpenAICodexResponsesOptions["onSseEvent"],
2608
+ fetchOverride?: FetchImpl,
2609
+ ): Promise<AsyncGenerator<Record<string, unknown>>> {
2610
+ const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
2611
+ logCodexDebug("codex request", {
2612
+ url,
2613
+ model: body.model,
2614
+ headers: redactHeaders(headers),
2615
+ sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
2616
+ sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
2617
+ });
2618
+ const response = await fetchWithRetry(url, {
2619
+ method: "POST",
2620
+ headers,
2621
+ body: JSON.stringify(body),
2622
+ signal,
2623
+ maxAttempts: CODEX_MAX_RETRIES + 1,
2624
+ defaultDelayMs: attempt => CODEX_RETRY_DELAY_MS * (attempt + 1),
2625
+ maxDelayMs: CODEX_RATE_LIMIT_BUDGET_MS,
2626
+ fetch: fetchOverride,
2627
+ });
2628
+ logCodexDebug("codex response", {
2629
+ url: response.url,
2630
+ status: response.status,
2631
+ statusText: response.statusText,
2632
+ contentType: response.headers.get("content-type") || null,
2633
+ cfRay: response.headers.get("cf-ray") || null,
2634
+ });
2635
+ updateCodexSessionMetadataFromHeaders(state, response.headers);
2636
+ if (!response.ok) {
2637
+ const info = await parseCodexError(response);
2638
+ const error = new Error(info.friendlyMessage || info.message);
2639
+ (error as { headers?: Headers; status?: number }).headers = response.headers;
2640
+ (error as { headers?: Headers; status?: number }).status = response.status;
2641
+ throw error;
2642
+ }
2643
+ if (!response.body) {
2644
+ throw new Error("No response body");
2645
+ }
2646
+ return readSseJson<Record<string, unknown>>(response.body, signal, event =>
2647
+ onSseEvent?.({ event: event.event, data: event.data, raw: [...event.raw] }, undefined),
2648
+ );
2649
+ }
2650
+
2651
+ async function openCodexWebSocketEventStream(
2652
+ url: string,
2653
+ headers: Headers,
2654
+ request: Record<string, unknown>,
2655
+ state: CodexWebSocketSessionState,
2656
+ timeouts: CodexWebSocketRequestTimeouts,
2657
+ signal?: AbortSignal,
2658
+ onSseEvent?: (event: RawSseEvent) => void,
2659
+ ): Promise<AsyncGenerator<Record<string, unknown>>> {
2660
+ const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal);
2661
+ return connection.streamRequest(request, timeouts, signal, onSseEvent);
2662
+ }
2663
+
2664
+ function createCodexHeaders(
2665
+ initHeaders: Record<string, string> | undefined,
2666
+ accountId: string,
2667
+ accessToken: string,
2668
+ sessionId?: string,
2669
+ transport: CodexTransport = "sse",
2670
+ state?: CodexWebSocketSessionState,
2671
+ ): Headers {
2672
+ const headers = new Headers(initHeaders ?? {});
2673
+ headers.delete("x-api-key");
2674
+ headers.set("Authorization", `Bearer ${accessToken}`);
2675
+ headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
2676
+ const betaHeader =
2677
+ transport === "websocket"
2678
+ ? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2
2679
+ : OPENAI_HEADER_VALUES.BETA_RESPONSES;
2680
+ headers.delete(OPENAI_HEADERS.BETA);
2681
+ headers.delete("openai-beta");
2682
+ headers.set(OPENAI_HEADERS.BETA, betaHeader);
2683
+ headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
2684
+ headers.set("User-Agent", getCodexUserAgent());
2685
+ if (sessionId) {
2686
+ headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
2687
+ headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
2688
+ headers.set("x-client-request-id", sessionId);
2689
+ } else {
2690
+ headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
2691
+ headers.delete(OPENAI_HEADERS.SESSION_ID);
2692
+ }
2693
+ if (state?.turnState) {
2694
+ headers.set(X_CODEX_TURN_STATE_HEADER, state.turnState);
2695
+ } else {
2696
+ headers.delete(X_CODEX_TURN_STATE_HEADER);
2697
+ }
2698
+ if (state?.modelsEtag) {
2699
+ headers.set(X_MODELS_ETAG_HEADER, state.modelsEtag);
2700
+ } else {
2701
+ headers.delete(X_MODELS_ETAG_HEADER);
2702
+ }
2703
+ if (transport === "sse") {
2704
+ headers.set("accept", "text/event-stream");
2705
+ headers.set("content-type", "application/json");
2706
+ } else {
2707
+ headers.delete("accept");
2708
+ headers.delete("content-type");
2709
+ }
2710
+ return headers;
2711
+ }
2712
+
2713
+ function logCodexDebug(message: string, details?: Record<string, unknown>): void {
2714
+ if (!CODEX_DEBUG) return;
2715
+ logger.debug(`[codex] ${message}`, details ?? {});
2716
+ }
2717
+
2718
+ function redactHeaders(headers: Headers): Record<string, string> {
2719
+ const redacted: Record<string, string> = {};
2720
+ for (const [key, value] of headers.entries()) {
2721
+ const lower = key.toLowerCase();
2722
+ if (lower === "authorization") {
2723
+ redacted[key] = "Bearer [redacted]";
2724
+ continue;
2725
+ }
2726
+ if (
2727
+ lower.includes("account") ||
2728
+ lower.includes("session") ||
2729
+ lower.includes("conversation") ||
2730
+ lower === "cookie"
2731
+ ) {
2732
+ redacted[key] = "[redacted]";
2733
+ continue;
2734
+ }
2735
+ redacted[key] = value;
2736
+ }
2737
+ return redacted;
2738
+ }
2739
+
2740
+ function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
2741
+ const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : CODEX_BASE_URL;
2742
+ const normalized = raw.replace(/\/+$/, "");
2743
+ if (normalized.endsWith("/codex/responses")) return normalized;
2744
+ if (normalized.endsWith("/codex")) return `${normalized}/responses`;
2745
+ return `${normalized}/codex/responses`;
2746
+ }
2747
+
2748
+ function getAccountId(accessToken: string): string {
2749
+ const accountId = getCodexAccountId(accessToken);
2750
+ if (!accountId) {
2751
+ throw new Error("Failed to extract accountId from token");
2752
+ }
2753
+ return accountId;
2754
+ }
2755
+
2756
+ function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
2757
+ const messages: ResponseInput = [];
2758
+
2759
+ const normalizeToolCallId = (id: string): string => {
2760
+ if (!id.includes("|")) return id;
2761
+ const [callId, itemId] = id.split("|");
2762
+ const sanitizedCallId = callId.replace(/[^a-zA-Z0-9_-]/g, "_");
2763
+ let sanitizedItemId = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
2764
+ if (!sanitizedItemId.startsWith("fc")) {
2765
+ sanitizedItemId = `fc_${sanitizedItemId}`;
2766
+ }
2767
+ let normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
2768
+ let normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
2769
+ normalizedCallId = normalizedCallId.replace(/_+$/, "");
2770
+ normalizedItemId = normalizedItemId.replace(/_+$/, "");
2771
+ return `${normalizedCallId}|${normalizedItemId}`;
2772
+ };
2773
+
2774
+ const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
2775
+ let msgIndex = 0;
2776
+ // Track call_ids that originated as custom tool calls so paired tool-result
2777
+ // messages can be replayed as `custom_tool_call_output` rather than
2778
+ // `function_call_output` (OpenAI rejects mismatched pairs).
2779
+ const customCallIds = new Set<string>();
2780
+ const knownCallIds = new Set<string>();
2781
+
2782
+ for (const msg of transformedMessages) {
2783
+ if (msg.role === "user" || msg.role === "developer") {
2784
+ const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
2785
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider) as
2786
+ | Array<ResponseInput[number]>
2787
+ | undefined;
2788
+ if (historyItems) {
2789
+ for (const item of historyItems) {
2790
+ const maybe = item as { type?: string; call_id?: string };
2791
+ if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
2792
+ customCallIds.add(maybe.call_id);
2793
+ }
2794
+ }
2795
+ messages.push(...historyItems);
2796
+ msgIndex += 1;
2797
+ continue;
2798
+ }
2799
+
2800
+ const normalizedContent = normalizeInputMessageContent(model, msg.content);
2801
+ if (normalizedContent.length === 0) continue;
2802
+ messages.push({ role: msg.role, content: normalizedContent });
2803
+ msgIndex += 1;
2804
+ continue;
2805
+ }
2806
+
2807
+ if (msg.role === "assistant") {
2808
+ const assistantMsg = msg as AssistantMessage;
2809
+ const providerPayload = getOpenAIResponsesHistoryPayload(
2810
+ assistantMsg.providerPayload,
2811
+ model.provider,
2812
+ assistantMsg.provider,
2813
+ );
2814
+ const historyItems = providerPayload?.items as Array<ResponseInput[number]> | undefined;
2815
+ if (historyItems) {
2816
+ for (const item of historyItems) {
2817
+ const maybe = item as { type?: string; call_id?: string };
2818
+ if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
2819
+ customCallIds.add(maybe.call_id);
2820
+ }
2821
+ }
2822
+ if (providerPayload?.dt) {
2823
+ messages.push(...historyItems);
2824
+ } else {
2825
+ messages.splice(0, messages.length, ...historyItems);
2826
+ // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
2827
+ }
2828
+ msgIndex += 1;
2829
+ continue;
2830
+ }
2831
+
2832
+ const outputItems = convertResponsesAssistantMessage(
2833
+ msg as AssistantMessage,
2834
+ model,
2835
+ msgIndex,
2836
+ knownCallIds,
2837
+ true,
2838
+ customCallIds,
2839
+ );
2840
+ if (outputItems.length > 0) {
2841
+ messages.push(...outputItems);
2842
+ }
2843
+ msgIndex += 1;
2844
+ continue;
2845
+ }
2846
+
2847
+ if (msg.role === "toolResult") {
2848
+ appendResponsesToolResultMessages(messages, msg, model, false, knownCallIds, customCallIds);
2849
+ }
2850
+
2851
+ msgIndex += 1;
2852
+ }
2853
+
2854
+ return messages;
2855
+ }
2856
+
2857
+ function normalizeInputMessageContent(
2858
+ model: Model<"openai-codex-responses">,
2859
+ content: string | Array<{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string }>,
2860
+ ): ResponseInputContent[] {
2861
+ if (typeof content === "string") {
2862
+ if (!content || content.trim() === "") return [];
2863
+ return [{ type: "input_text", text: content.toWellFormed() }];
2864
+ }
2865
+
2866
+ return convertResponsesInputContent(content, model.input.includes("image")) ?? [];
2867
+ }
2868
+
2869
+ /** @internal Exported for tests. */
2870
+ export { convertMessages as convertCodexResponsesMessages };
2871
+
2872
+ /**
2873
+ * Whether this Codex-backend model should get the custom-tool grammar
2874
+ * variant for `apply_patch`. codex-rs uses a single serializer for both
2875
+ * the public Responses endpoint and `chatgpt.com/backend-api`, so the
2876
+ * backend already accepts `{type: "custom"}` tools in production. The
2877
+ * generated model catalog sets `applyPatchToolType` for first-party GPT-5
2878
+ * Codex models; this runtime path only consumes that metadata.
2879
+ */
2880
+ function supportsFreeformApplyPatchCodex(model: Model<"openai-codex-responses">): boolean {
2881
+ return model.applyPatchToolType === "freeform";
2882
+ }
2883
+
2884
+ type CodexToolPayload =
2885
+ | {
2886
+ type: "function";
2887
+ name: string;
2888
+ description: string;
2889
+ parameters: Record<string, unknown>;
2890
+ strict?: boolean;
2891
+ }
2892
+ | {
2893
+ type: "custom";
2894
+ name: string;
2895
+ description: string;
2896
+ format: { type: "grammar"; syntax: "lark" | "regex"; definition: string };
2897
+ };
2898
+
2899
+ /** @internal Exported for tests. */
2900
+ export function convertOpenAICodexResponsesTools(
2901
+ tools: Tool[],
2902
+ model: Model<"openai-codex-responses">,
2903
+ ): CodexToolPayload[] {
2904
+ const allowFreeform = supportsFreeformApplyPatchCodex(model);
2905
+ return tools.map((tool): CodexToolPayload => {
2906
+ if (allowFreeform && tool.customFormat) {
2907
+ return {
2908
+ type: "custom",
2909
+ name: tool.customWireName ?? tool.name,
2910
+ description: tool.description || "",
2911
+ format: {
2912
+ type: "grammar",
2913
+ syntax: tool.customFormat.syntax,
2914
+ definition: compactGrammarDefinition(tool.customFormat.syntax, tool.customFormat.definition),
2915
+ },
2916
+ };
2917
+ }
2918
+ const strict = !!(!NO_STRICT && tool.strict);
2919
+ const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
2920
+ const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
2921
+ return {
2922
+ type: "function",
2923
+ name: tool.name,
2924
+ description: tool.description || "",
2925
+ parameters,
2926
+ ...(effectiveStrict && { strict: true }),
2927
+ };
2928
+ });
2929
+ }
2930
+
2931
+ function getString(value: unknown): string | undefined {
2932
+ return typeof value === "string" ? value : undefined;
2933
+ }
2934
+
2935
+ class CodexProviderStreamError extends Error {
2936
+ readonly retryable: boolean;
2937
+ readonly code?: string;
2938
+
2939
+ constructor(message: string, retryable: boolean, code?: string) {
2940
+ super(message);
2941
+ this.name = "CodexProviderStreamError";
2942
+ this.retryable = retryable;
2943
+ this.code = code;
2944
+ }
2945
+ }
2946
+
2947
+ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
2948
+ const response = asRecord(rawEvent.response);
2949
+ const error = asRecord(rawEvent.error) ?? (response ? asRecord(response.error) : null);
2950
+ const code = getString(error?.code) ?? getString(error?.type) ?? getString(rawEvent.code);
2951
+ if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
2952
+ return true;
2953
+ }
2954
+ const message = getString(error?.message) ?? getString(rawEvent.message) ?? getString(response?.message);
2955
+ return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
2956
+ }
2957
+
2958
+ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
2959
+ const code = getString(rawEvent.code) ?? "";
2960
+ const message = getString(rawEvent.message) ?? "";
2961
+ const formattedMessage =
2962
+ typeof rawEvent.type === "string" && rawEvent.type === "error"
2963
+ ? formatCodexErrorEvent(rawEvent, code, message)
2964
+ : (formatCodexFailure(rawEvent) ?? "Codex response failed");
2965
+ return new CodexProviderStreamError(formattedMessage, isRetryableCodexFailureEvent(rawEvent), code || undefined);
2966
+ }
2967
+
2968
+ function isRetryableCodexProviderError(error: unknown): boolean {
2969
+ return error instanceof CodexProviderStreamError && error.retryable;
2970
+ }
2971
+
2972
+ function truncate(text: string, limit: number): string {
2973
+ if (text.length <= limit) return text;
2974
+ return `${text.slice(0, limit)}…[truncated ${text.length - limit}]`;
2975
+ }
2976
+
2977
+ function formatCodexFailure(rawEvent: Record<string, unknown>): string | null {
2978
+ const response = asRecord(rawEvent.response);
2979
+ const error = asRecord(rawEvent.error) ?? (response ? asRecord(response.error) : null);
2980
+ const message = getString(error?.message) ?? getString(rawEvent.message) ?? getString(response?.message);
2981
+ const code = getString(error?.code) ?? getString(error?.type) ?? getString(rawEvent.code);
2982
+ const status = getString(response?.status) ?? getString(rawEvent.status);
2983
+
2984
+ const meta: string[] = [];
2985
+ if (code) meta.push(`code=${code}`);
2986
+ if (status) meta.push(`status=${status}`);
2987
+
2988
+ if (message) {
2989
+ const metaText = meta.length ? ` (${meta.join(", ")})` : "";
2990
+ return `Codex response failed: ${message}${metaText}`;
2991
+ }
2992
+ if (meta.length) {
2993
+ return `Codex response failed (${meta.join(", ")})`;
2994
+ }
2995
+ try {
2996
+ return `Codex response failed: ${truncate(JSON.stringify(rawEvent), 800)}`;
2997
+ } catch {
2998
+ return "Codex response failed";
2999
+ }
3000
+ }
3001
+
3002
+ function formatCodexErrorEvent(rawEvent: Record<string, unknown>, code: string, message: string): string {
3003
+ const detail = formatCodexFailure(rawEvent);
3004
+ if (detail) {
3005
+ return detail.replace("response failed", "error event");
3006
+ }
3007
+ const meta: string[] = [];
3008
+ if (code) meta.push(`code=${code}`);
3009
+ if (message) meta.push(`message=${message}`);
3010
+ if (meta.length > 0) {
3011
+ return `Codex error event (${meta.join(", ")})`;
3012
+ }
3013
+ try {
3014
+ return `Codex error event: ${truncate(JSON.stringify(rawEvent), 800)}`;
3015
+ } catch {
3016
+ return "Codex error event";
3017
+ }
3018
+ }