@gajae-code/ai 0.1.1

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