@earendil-works/pi-ai 0.81.1 → 0.82.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.
- package/README.md +42 -4
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +26 -10
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.d.ts.map +1 -1
- package/dist/api/azure-openai-responses.js +21 -8
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +25 -13
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/constrained-sampling.d.ts +17 -0
- package/dist/api/constrained-sampling.d.ts.map +1 -0
- package/dist/api/constrained-sampling.js +100 -0
- package/dist/api/constrained-sampling.js.map +1 -0
- package/dist/api/google-generative-ai.d.ts.map +1 -1
- package/dist/api/google-generative-ai.js +7 -11
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-shared.d.ts +4 -3
- package/dist/api/google-shared.d.ts.map +1 -1
- package/dist/api/google-shared.js +17 -3
- package/dist/api/google-shared.js.map +1 -1
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js +7 -11
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/mistral-conversations.d.ts.map +1 -1
- package/dist/api/mistral-conversations.js +13 -9
- package/dist/api/mistral-conversations.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +68 -27
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts +6 -7
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +131 -34
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts +5 -0
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +173 -66
- package/dist/api/openai-responses-shared.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +29 -7
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/openrouter-images.d.ts.map +1 -1
- package/dist/api/openrouter-images.js +8 -3
- package/dist/api/openrouter-images.js.map +1 -1
- package/dist/auth/oauth/kimi-coding.d.ts +10 -0
- package/dist/auth/oauth/kimi-coding.d.ts.map +1 -0
- package/dist/auth/oauth/kimi-coding.js +249 -0
- package/dist/auth/oauth/kimi-coding.js.map +1 -0
- package/dist/auth/oauth/load.d.ts +4 -0
- package/dist/auth/oauth/load.d.ts.map +1 -1
- package/dist/auth/oauth/load.js +10 -0
- package/dist/auth/oauth/load.js.map +1 -1
- package/dist/auth/oauth/openrouter.d.ts +13 -0
- package/dist/auth/oauth/openrouter.d.ts.map +1 -0
- package/dist/auth/oauth/openrouter.js +222 -0
- package/dist/auth/oauth/openrouter.js.map +1 -0
- package/dist/auth/oauth/radius.d.ts +3 -2
- package/dist/auth/oauth/radius.d.ts.map +1 -1
- package/dist/auth/oauth/radius.js +37 -35
- package/dist/auth/oauth/radius.js.map +1 -1
- package/dist/auth/resolve.d.ts.map +1 -1
- package/dist/auth/resolve.js +11 -1
- package/dist/auth/resolve.js.map +1 -1
- package/dist/bun-oauth.d.ts.map +1 -1
- package/dist/bun-oauth.js +4 -0
- package/dist/bun-oauth.js.map +1 -1
- package/dist/env-api-keys.d.ts +3 -0
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +9 -3
- package/dist/env-api-keys.js.map +1 -1
- package/dist/image-models.generated.d.ts +15 -0
- package/dist/image-models.generated.d.ts.map +1 -1
- package/dist/image-models.generated.js +15 -0
- package/dist/image-models.generated.js.map +1 -1
- package/dist/model-catalog.d.ts +17 -0
- package/dist/model-catalog.d.ts.map +1 -0
- package/dist/model-catalog.js +4 -0
- package/dist/model-catalog.js.map +1 -0
- package/dist/models-store.d.ts +5 -0
- package/dist/models-store.d.ts.map +1 -1
- package/dist/models-store.js.map +1 -1
- package/dist/models.generated.d.ts +74 -4466
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js.map +1 -1
- package/dist/providers/all.d.ts +2 -2
- package/dist/providers/all.d.ts.map +1 -1
- package/dist/providers/all.js +5 -3
- package/dist/providers/all.js.map +1 -1
- package/dist/providers/amazon-bedrock.models.d.ts +3 -439
- package/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.models.js +2 -1
- package/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/dist/providers/ant-ling.models.d.ts +3 -15
- package/dist/providers/ant-ling.models.d.ts.map +1 -1
- package/dist/providers/ant-ling.models.js +2 -1
- package/dist/providers/ant-ling.models.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +30 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/anthropic.models.d.ts +3 -59
- package/dist/providers/anthropic.models.d.ts.map +1 -1
- package/dist/providers/anthropic.models.js +2 -1
- package/dist/providers/anthropic.models.js.map +1 -1
- package/dist/providers/azure-openai-responses.models.d.ts +3 -187
- package/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.models.js +2 -1
- package/dist/providers/azure-openai-responses.models.js.map +1 -1
- package/dist/providers/cerebras.models.d.ts +3 -15
- package/dist/providers/cerebras.models.d.ts.map +1 -1
- package/dist/providers/cerebras.models.js +2 -1
- package/dist/providers/cerebras.models.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.d.ts +3 -171
- package/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.js +2 -1
- package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
- package/dist/providers/cloudflare-workers-ai.models.d.ts +3 -55
- package/dist/providers/cloudflare-workers-ai.models.d.ts.map +1 -1
- package/dist/providers/cloudflare-workers-ai.models.js +2 -1
- package/dist/providers/cloudflare-workers-ai.models.js.map +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/ant-ling.json +1 -1
- package/dist/providers/data/anthropic.json +1 -1
- package/dist/providers/data/azure-openai-responses.json +1 -1
- package/dist/providers/data/cerebras.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/dist/providers/data/deepseek.json +1 -1
- package/dist/providers/data/fireworks.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/google-vertex.json +1 -1
- package/dist/providers/data/google.json +1 -1
- package/dist/providers/data/groq.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/kimi-coding.json +1 -1
- package/dist/providers/data/minimax-cn.json +1 -1
- package/dist/providers/data/minimax.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/openai-codex.json +1 -1
- package/dist/providers/data/openai.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/qwen-token-plan-cn.json +1 -1
- package/dist/providers/data/qwen-token-plan.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/data/xai.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
- package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
- package/dist/providers/data/xiaomi.json +1 -1
- package/dist/providers/data/zai-coding-cn.json +1 -1
- package/dist/providers/data/zai.json +1 -1
- package/dist/providers/deepseek.models.d.ts +3 -11
- package/dist/providers/deepseek.models.d.ts.map +1 -1
- package/dist/providers/deepseek.models.js +2 -1
- package/dist/providers/deepseek.models.js.map +1 -1
- package/dist/providers/fireworks.models.d.ts +3 -67
- package/dist/providers/fireworks.models.d.ts.map +1 -1
- package/dist/providers/fireworks.models.js +2 -1
- package/dist/providers/fireworks.models.js.map +1 -1
- package/dist/providers/github-copilot.models.d.ts +3 -115
- package/dist/providers/github-copilot.models.d.ts.map +1 -1
- package/dist/providers/github-copilot.models.js +2 -1
- package/dist/providers/github-copilot.models.js.map +1 -1
- package/dist/providers/google-vertex.models.d.ts +3 -43
- package/dist/providers/google-vertex.models.d.ts.map +1 -1
- package/dist/providers/google-vertex.models.js +2 -1
- package/dist/providers/google-vertex.models.js.map +1 -1
- package/dist/providers/google.models.d.ts +3 -75
- package/dist/providers/google.models.d.ts.map +1 -1
- package/dist/providers/google.models.js +2 -1
- package/dist/providers/google.models.js.map +1 -1
- package/dist/providers/groq.models.d.ts +3 -31
- package/dist/providers/groq.models.d.ts.map +1 -1
- package/dist/providers/groq.models.js +2 -1
- package/dist/providers/groq.models.js.map +1 -1
- package/dist/providers/huggingface.models.d.ts +3 -199
- package/dist/providers/huggingface.models.d.ts.map +1 -1
- package/dist/providers/huggingface.models.js +2 -1
- package/dist/providers/huggingface.models.js.map +1 -1
- package/dist/providers/kimi-coding.d.ts.map +1 -1
- package/dist/providers/kimi-coding.js +10 -2
- package/dist/providers/kimi-coding.js.map +1 -1
- package/dist/providers/kimi-coding.models.d.ts +3 -15
- package/dist/providers/kimi-coding.models.d.ts.map +1 -1
- package/dist/providers/kimi-coding.models.js +2 -1
- package/dist/providers/kimi-coding.models.js.map +1 -1
- package/dist/providers/minimax-cn.models.d.ts +3 -15
- package/dist/providers/minimax-cn.models.d.ts.map +1 -1
- package/dist/providers/minimax-cn.models.js +2 -1
- package/dist/providers/minimax-cn.models.js.map +1 -1
- package/dist/providers/minimax.models.d.ts +3 -15
- package/dist/providers/minimax.models.d.ts.map +1 -1
- package/dist/providers/minimax.models.js +2 -1
- package/dist/providers/minimax.models.js.map +1 -1
- package/dist/providers/mistral.models.d.ts +3 -123
- package/dist/providers/mistral.models.d.ts.map +1 -1
- package/dist/providers/mistral.models.js +2 -1
- package/dist/providers/mistral.models.js.map +1 -1
- package/dist/providers/moonshotai-cn.models.d.ts +3 -43
- package/dist/providers/moonshotai-cn.models.d.ts.map +1 -1
- package/dist/providers/moonshotai-cn.models.js +2 -1
- package/dist/providers/moonshotai-cn.models.js.map +1 -1
- package/dist/providers/moonshotai.models.d.ts +3 -43
- package/dist/providers/moonshotai.models.d.ts.map +1 -1
- package/dist/providers/moonshotai.models.js +2 -1
- package/dist/providers/moonshotai.models.js.map +1 -1
- package/dist/providers/nvidia.models.d.ts +3 -79
- package/dist/providers/nvidia.models.d.ts.map +1 -1
- package/dist/providers/nvidia.models.js +2 -1
- package/dist/providers/nvidia.models.js.map +1 -1
- package/dist/providers/openai-codex.models.d.ts +3 -31
- package/dist/providers/openai-codex.models.d.ts.map +1 -1
- package/dist/providers/openai-codex.models.js +2 -1
- package/dist/providers/openai-codex.models.js.map +1 -1
- package/dist/providers/openai.models.d.ts +3 -187
- package/dist/providers/openai.models.d.ts.map +1 -1
- package/dist/providers/openai.models.js +2 -1
- package/dist/providers/openai.models.js.map +1 -1
- package/dist/providers/opencode-go.models.d.ts +3 -63
- package/dist/providers/opencode-go.models.d.ts.map +1 -1
- package/dist/providers/opencode-go.models.js +2 -1
- package/dist/providers/opencode-go.models.js.map +1 -1
- package/dist/providers/opencode.models.d.ts +3 -223
- package/dist/providers/opencode.models.d.ts.map +1 -1
- package/dist/providers/opencode.models.js +2 -1
- package/dist/providers/opencode.models.js.map +1 -1
- package/dist/providers/openrouter-images.d.ts.map +1 -1
- package/dist/providers/openrouter-images.js +10 -2
- package/dist/providers/openrouter-images.js.map +1 -1
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +10 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/openrouter.models.d.ts +3 -1087
- package/dist/providers/openrouter.models.d.ts.map +1 -1
- package/dist/providers/openrouter.models.js +2 -1
- package/dist/providers/openrouter.models.js.map +1 -1
- package/dist/providers/qwen-token-plan-cn.models.d.ts +3 -63
- package/dist/providers/qwen-token-plan-cn.models.d.ts.map +1 -1
- package/dist/providers/qwen-token-plan-cn.models.js +2 -1
- package/dist/providers/qwen-token-plan-cn.models.js.map +1 -1
- package/dist/providers/qwen-token-plan.models.d.ts +3 -63
- package/dist/providers/qwen-token-plan.models.d.ts.map +1 -1
- package/dist/providers/qwen-token-plan.models.js +2 -1
- package/dist/providers/qwen-token-plan.models.js.map +1 -1
- package/dist/providers/together.models.d.ts +3 -67
- package/dist/providers/together.models.d.ts.map +1 -1
- package/dist/providers/together.models.js +2 -1
- package/dist/providers/together.models.js.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.d.ts +3 -763
- package/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.js +2 -1
- package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/dist/providers/xai.models.d.ts +3 -15
- package/dist/providers/xai.models.d.ts.map +1 -1
- package/dist/providers/xai.models.js +2 -1
- package/dist/providers/xai.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-ams.models.d.ts +3 -15
- package/dist/providers/xiaomi-token-plan-ams.models.d.ts.map +1 -1
- package/dist/providers/xiaomi-token-plan-ams.models.js +2 -1
- package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-cn.models.d.ts +3 -15
- package/dist/providers/xiaomi-token-plan-cn.models.d.ts.map +1 -1
- package/dist/providers/xiaomi-token-plan-cn.models.js +2 -1
- package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.d.ts +3 -15
- package/dist/providers/xiaomi-token-plan-sgp.models.d.ts.map +1 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.js +2 -1
- package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
- package/dist/providers/xiaomi.models.d.ts +3 -27
- package/dist/providers/xiaomi.models.d.ts.map +1 -1
- package/dist/providers/xiaomi.models.js +2 -1
- package/dist/providers/xiaomi.models.js.map +1 -1
- package/dist/providers/zai-coding-cn.models.d.ts +3 -27
- package/dist/providers/zai-coding-cn.models.d.ts.map +1 -1
- package/dist/providers/zai-coding-cn.models.js +2 -1
- package/dist/providers/zai-coding-cn.models.js.map +1 -1
- package/dist/providers/zai.models.d.ts +3 -27
- package/dist/providers/zai.models.d.ts.map +1 -1
- package/dist/providers/zai.models.js +2 -1
- package/dist/providers/zai.models.js.map +1 -1
- package/dist/types.d.ts +35 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/error-body.d.ts.map +1 -1
- package/dist/utils/error-body.js +8 -3
- package/dist/utils/error-body.js.map +1 -1
- package/dist/utils/provider-retry.d.ts +16 -0
- package/dist/utils/provider-retry.d.ts.map +1 -0
- package/dist/utils/provider-retry.js +94 -0
- package/dist/utils/provider-retry.js.map +1 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +3 -0
- package/dist/utils/retry.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-codex-responses.d.ts","sourceRoot":"","sources":["../../src/api/openai-codex-responses.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEX,6BAA6B,EAG7B,MAAM,yCAAyC,CAAC;AAkBjD,OAAO,KAAK,EAOX,mBAAmB,EACnB,cAAc,EACd,aAAa,EAEb,MAAM,aAAa,CAAC;AA+CrB,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IACjE,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;IACnF,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IACzE,WAAW,CAAC,EAAE,6BAA6B,CAAC,cAAc,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;CAC1C;AAsID,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,wBAAwB,EAAE,2BAA2B,CAyOxF,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,wBAAwB,EAAE,mBAAmB,CAkBtF,CAAC;AAoTF,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AA0BD,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,8BAA8B,GAAG,SAAS,CAG/G;AAED,wBAAgB,mCAAmC,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAQ5E;AAED,wBAAgB,iCAAiC,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAe1E","sourcesContent":["import type * as NodeOs from \"node:os\";\nimport type * as NodeZlib from \"node:zlib\";\nimport type {\n\tTool as OpenAITool,\n\tResponseCreateParamsStreaming,\n\tResponseInput,\n\tResponseStreamEvent,\n} from \"openai/resources/responses/responses.js\";\n\ntype ProcessWithOsBuiltinModule = typeof process & {\n\tgetBuiltinModule?: (id: \"node:os\") => typeof NodeOs;\n};\n\nfunction loadNodeOs(): typeof NodeOs | null {\n\tif (typeof process === \"undefined\" || !(process.versions?.node || process.versions?.bun)) {\n\t\treturn null;\n\t}\n\treturn (process as ProcessWithOsBuiltinModule).getBuiltinModule?.(\"node:os\") ?? null;\n}\n\n// NEVER convert to top-level runtime imports - breaks browser/Vite builds\nconst _os: typeof NodeOs | null = loadNodeOs();\n\nimport { clampThinkingLevel } from \"../models.ts\";\nimport { registerSessionResourceCleanup } from \"../session-resources.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tProviderEnv,\n\tProviderHeaders,\n\tSimpleStreamOptions,\n\tStreamFunction,\n\tStreamOptions,\n\tUsage,\n} from \"../types.ts\";\nimport { combineAbortSignals } from \"../utils/abort-signals.ts\";\nimport { splitDeferredTools } from \"../utils/deferred-tools.ts\";\nimport {\n\tappendAssistantMessageDiagnostic,\n\tcreateAssistantMessageDiagnostic,\n\tformatThrownValue,\n} from \"../utils/diagnostics.ts\";\nimport { formatProviderError, normalizeProviderError } from \"../utils/error-body.ts\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.ts\";\nimport { headersToRecord } from \"../utils/headers.ts\";\nimport { resolveHttpProxyUrlForTarget } from \"../utils/node-http-proxy.ts\";\nimport { uuidv7 } from \"../utils/uuid.ts\";\nimport { clampOpenAIPromptCacheKey } from \"./openai-prompt-cache.ts\";\nimport { convertResponsesMessages, convertResponsesTools, processResponsesStream } from \"./openai-responses-shared.ts\";\nimport { buildBaseOptions } from \"./simple-options.ts\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst DEFAULT_CODEX_BASE_URL = \"https://chatgpt.com/backend-api\";\nconst JWT_CLAIM_PATH = \"https://api.openai.com/auth\" as const;\nconst DEFAULT_MAX_RETRIES = 0;\nconst BASE_DELAY_MS = 1000;\nconst DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\nconst DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;\n// The Codex backend accepts zstd-compressed request bodies on the SSE responses\n// endpoint (the same endpoint the official Codex client compresses against).\nconst REQUEST_COMPRESSION_ZSTD_LEVEL = 3;\nconst CODEX_TOOL_CALL_PROVIDERS = new Set([\"openai\", \"openai-codex\", \"opencode\"]);\nconst WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;\nconst WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = \"websocket_connection_limit_reached\";\n\nconst CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([\n\t\"completed\",\n\t\"incomplete\",\n\t\"failed\",\n\t\"cancelled\",\n\t\"queued\",\n\t\"in_progress\",\n]);\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface OpenAICodexResponsesOptions extends StreamOptions {\n\treasoningEffort?: \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\treasoningSummary?: \"auto\" | \"concise\" | \"detailed\" | \"off\" | \"on\" | null;\n\tserviceTier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\ttextVerbosity?: \"low\" | \"medium\" | \"high\";\n\ttoolChoice?: \"auto\" | \"none\" | \"required\";\n}\n\ntype CodexResponseStatus = \"completed\" | \"incomplete\" | \"failed\" | \"cancelled\" | \"queued\" | \"in_progress\";\n\ninterface RequestBody {\n\tmodel: string;\n\tstore?: boolean;\n\tstream?: boolean;\n\tinstructions?: string;\n\tprevious_response_id?: string;\n\tinput?: ResponseInput;\n\ttools?: OpenAITool[];\n\ttool_choice?: OpenAICodexResponsesOptions[\"toolChoice\"];\n\tparallel_tool_calls?: boolean;\n\ttemperature?: number;\n\treasoning?: { effort?: string; summary?: string };\n\tservice_tier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\ttext?: { verbosity?: string };\n\tinclude?: string[];\n\tprompt_cache_key?: string;\n\t[key: string]: unknown;\n}\n\n// ============================================================================\n// Retry Helpers\n// ============================================================================\n\nfunction isTerminalRateLimitError(errorText: string): boolean {\n\treturn /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\terrorText,\n\t);\n}\n\nfunction isRetryableError(status: number, errorText: string): boolean {\n\tif (status === 429 && isTerminalRateLimitError(errorText)) {\n\t\treturn false;\n\t}\n\tif (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {\n\t\treturn true;\n\t}\n\treturn /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);\n}\n\nfunction getRetryAfterDelayMs(headers: Headers): number | undefined {\n\tconst retryAfterMs = headers.get(\"retry-after-ms\");\n\tif (retryAfterMs !== null) {\n\t\tconst millis = Number(retryAfterMs);\n\t\tif (Number.isFinite(millis)) {\n\t\t\treturn Math.max(0, millis);\n\t\t}\n\t}\n\n\tconst retryAfter = headers.get(\"retry-after\");\n\tif (!retryAfter) {\n\t\treturn undefined;\n\t}\n\n\tconst seconds = Number(retryAfter);\n\tif (Number.isFinite(seconds)) {\n\t\treturn Math.max(0, seconds * 1000);\n\t}\n\n\tconst date = Date.parse(retryAfter);\n\tif (!Number.isNaN(date)) {\n\t\treturn Math.max(0, date - Date.now());\n\t}\n\n\treturn undefined;\n}\n\nfunction capRetryDelayMs(delayMs: number, options?: StreamOptions): number {\n\tconst maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;\n\treturn maxRetryDelayMs > 0 ? Math.min(delayMs, maxRetryDelayMs) : delayMs;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\"abort\", () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t});\n\t});\n}\n\nfunction normalizeTimeoutMs(value: number | undefined): number | undefined {\n\tif (value === undefined) return undefined;\n\tif (!Number.isFinite(value) || value < 0) {\n\t\tthrow new Error(`Invalid timeoutMs: ${String(value)}`);\n\t}\n\treturn Math.floor(value);\n}\n\n// ============================================================================\n// Request Compression\n// ============================================================================\n\ntype ProcessWithBuiltinModule = typeof process & {\n\tgetBuiltinModule?: (id: \"node:zlib\") => typeof NodeZlib;\n};\n\nfunction loadNodeZlib(): typeof NodeZlib | null {\n\tif (typeof process === \"undefined\" || !(process.versions?.node || process.versions?.bun)) {\n\t\treturn null;\n\t}\n\treturn (process as ProcessWithBuiltinModule).getBuiltinModule?.(\"node:zlib\") ?? null;\n}\n\n// Returns the zstd-compressed body bytes, or null when compression is\n// unavailable (browser/Vite builds). Callers fall back to sending the\n// uncompressed JSON when this returns null.\nfunction compressRequestBodyZstd(bodyJson: string): Uint8Array | null {\n\tconst zlib = loadNodeZlib();\n\tif (!zlib || typeof zlib.zstdCompressSync !== \"function\") {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst compressed = zlib.zstdCompressSync(bodyJson, {\n\t\t\tparams: { [zlib.constants.ZSTD_c_compressionLevel]: REQUEST_COMPRESSION_ZSTD_LEVEL },\n\t\t});\n\t\treturn new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n// ============================================================================\n// Main Stream Function\n// ============================================================================\n\nexport const stream: StreamFunction<\"openai-codex-responses\", OpenAICodexResponsesOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: \"openai-codex-responses\" as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey;\n\t\t\tif (!apiKey) {\n\t\t\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t\t\t}\n\n\t\t\tconst accountId = extractAccountId(apiKey);\n\t\t\tlet body = buildRequestBody(model, context, options);\n\t\t\tconst nextBody = await options?.onPayload?.(body, model);\n\t\t\tif (nextBody !== undefined) {\n\t\t\t\tbody = nextBody as RequestBody;\n\t\t\t}\n\t\t\tconst codexSessionId = clampOpenAIPromptCacheKey(options?.sessionId);\n\t\t\tconst websocketRequestId = codexSessionId || uuidv7();\n\t\t\tconst sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, codexSessionId);\n\t\t\tconst websocketHeaders = buildWebSocketHeaders(\n\t\t\t\tmodel.headers,\n\t\t\t\toptions?.headers,\n\t\t\t\taccountId,\n\t\t\t\tapiKey,\n\t\t\t\twebsocketRequestId,\n\t\t\t);\n\t\t\tconst bodyJson = JSON.stringify(body);\n\t\t\tconst httpTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);\n\t\t\tconst websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);\n\t\t\tconst transport = options?.transport || \"auto\";\n\t\t\tconst websocketDisabledForSession = transport !== \"sse\" && isWebSocketSseFallbackActive(options?.sessionId);\n\t\t\tif (websocketDisabledForSession) {\n\t\t\t\trecordWebSocketSseFallback(options?.sessionId);\n\t\t\t}\n\n\t\t\tif (transport !== \"sse\" && !websocketDisabledForSession) {\n\t\t\t\tlet websocketStarted = false;\n\t\t\t\tlet retriedWebSocketConnectionLimit = false;\n\t\t\t\twhile (true) {\n\t\t\t\t\twebsocketStarted = false;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait processWebSocketStream(\n\t\t\t\t\t\t\tresolveCodexWebSocketUrl(model.baseUrl),\n\t\t\t\t\t\t\tbody,\n\t\t\t\t\t\t\twebsocketHeaders,\n\t\t\t\t\t\t\toutput,\n\t\t\t\t\t\t\tstream,\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\twebsocketStarted = true;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\thttpTimeoutMs,\n\t\t\t\t\t\t\twebsocketConnectTimeoutMs,\n\t\t\t\t\t\t\toptions,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\t\treason: output.stopReason as \"stop\" | \"length\" | \"toolUse\",\n\t\t\t\t\t\t\tmessage: output,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tstream.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst aborted = options?.signal?.aborted;\n\t\t\t\t\t\tconst connectionLimitBeforeStart = !websocketStarted && isWebSocketConnectionLimitReachedError(error);\n\t\t\t\t\t\tif (!aborted && connectionLimitBeforeStart && !retriedWebSocketConnectionLimit) {\n\t\t\t\t\t\t\tretriedWebSocketConnectionLimit = true;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (aborted || (isCodexNonTransportError(error) && !connectionLimitBeforeStart)) {\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tappendAssistantMessageDiagnostic(\n\t\t\t\t\t\t\toutput,\n\t\t\t\t\t\t\tcreateAssistantMessageDiagnostic(\"provider_transport_failure\", error, {\n\t\t\t\t\t\t\t\tconfiguredTransport: transport,\n\t\t\t\t\t\t\t\tfallbackTransport: websocketStarted ? undefined : \"sse\",\n\t\t\t\t\t\t\t\teventsEmitted: websocketStarted,\n\t\t\t\t\t\t\t\tphase: websocketStarted ? \"after_message_stream_start\" : \"before_message_stream_start\",\n\t\t\t\t\t\t\t\trequestBytes: new TextEncoder().encode(bodyJson).byteLength,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t\trecordWebSocketFailure(options?.sessionId, error);\n\t\t\t\t\t\tif (websocketStarted) {\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\trecordWebSocketSseFallback(options?.sessionId);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Compress the request body once for the SSE path. The Codex backend\n\t\t\t// decodes Content-Encoding: zstd; the WebSocket transport above sends the\n\t\t\t// uncompressed JSON frame, matching the official Codex client.\n\t\t\tconst compressedBody = compressRequestBodyZstd(bodyJson);\n\t\t\tif (compressedBody) {\n\t\t\t\tsseHeaders.set(\"content-encoding\", \"zstd\");\n\t\t\t}\n\t\t\tconst sseBody: Uint8Array | string = compressedBody ?? bodyJson;\n\n\t\t\t// Fetch with retry logic for rate limits and transient errors\n\t\t\tlet response: Response | undefined;\n\t\t\tlet lastError: Error | undefined;\n\t\t\tconst maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;\n\n\t\t\tfor (let attempt = 0; attempt <= maxRetries; attempt++) {\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst headerTimeoutSignal =\n\t\t\t\t\t\thttpTimeoutMs !== undefined && httpTimeoutMs > 0 ? AbortSignal.timeout(httpTimeoutMs) : undefined;\n\t\t\t\t\tconst combinedSignal = combineAbortSignals([options?.signal, headerTimeoutSignal]);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tresponse = await fetch(resolveCodexUrl(model.baseUrl), {\n\t\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t\theaders: sseHeaders,\n\t\t\t\t\t\t\tbody: sseBody,\n\t\t\t\t\t\t\tsignal: combinedSignal.signal,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (headerTimeoutSignal?.aborted && !options?.signal?.aborted) {\n\t\t\t\t\t\t\tthrow new Error(`Codex SSE response headers timed out after ${httpTimeoutMs}ms`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tcombinedSignal.cleanup();\n\t\t\t\t\t}\n\t\t\t\t\tawait options?.onResponse?.(\n\t\t\t\t\t\t{ status: response.status, headers: headersToRecord(response.headers) },\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst errorText = await response.text();\n\t\t\t\t\tif (attempt < maxRetries && isRetryableError(response.status, errorText)) {\n\t\t\t\t\t\tconst retryAfterDelayMs = getRetryAfterDelayMs(response.headers);\n\t\t\t\t\t\tconst delayMs =\n\t\t\t\t\t\t\tretryAfterDelayMs === undefined\n\t\t\t\t\t\t\t\t? BASE_DELAY_MS * 2 ** attempt\n\t\t\t\t\t\t\t\t: response.status === 429\n\t\t\t\t\t\t\t\t\t? capRetryDelayMs(retryAfterDelayMs, options)\n\t\t\t\t\t\t\t\t\t: retryAfterDelayMs;\n\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Parse error for friendly message on final attempt or non-retryable error\n\t\t\t\t\tconst fakeResponse = new Response(errorText, {\n\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\t});\n\t\t\t\t\tconst info = await parseErrorResponse(fakeResponse);\n\t\t\t\t\tthrow new Error(info.friendlyMessage || info.message);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof Error) {\n\t\t\t\t\t\tif (error.name === \"AbortError\" || error.message === \"Request was aborted\") {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t// Network errors are retryable\n\t\t\t\t\tif (attempt < maxRetries && !lastError.message.includes(\"usage limit\")) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthrow lastError;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!response?.ok) {\n\t\t\t\tthrow lastError ?? new Error(\"Failed after retries\");\n\t\t\t}\n\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"No response body\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t\tawait processStream(response, output, stream, model, options);\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason as \"stop\" | \"length\" | \"toolUse\", message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tfor (const block of output.content) {\n\t\t\t\t// partialJson is only a streaming scratch buffer; never persist it.\n\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t}\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = formatProviderError(normalizeProviderError(error));\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\nexport const streamSimple: StreamFunction<\"openai-codex-responses\", SimpleStreamOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey;\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, context, options, apiKey);\n\tconst clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;\n\tconst reasoningEffort = clampedReasoning === \"off\" ? undefined : clampedReasoning;\n\n\treturn stream(model, context, {\n\t\t...base,\n\t\treasoningEffort,\n\t} satisfies OpenAICodexResponsesOptions);\n};\n\n// ============================================================================\n// Request Building\n// ============================================================================\n\nfunction buildRequestBody(\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): RequestBody {\n\tconst toolPlacement = splitDeferredTools(context, model.compat?.supportsToolSearch ?? false);\n\tconst messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {\n\t\tincludeSystemPrompt: false,\n\t\tdeferredTools: toolPlacement.deferred,\n\t});\n\n\tconst body: RequestBody = {\n\t\tmodel: model.id,\n\t\tstore: false,\n\t\tstream: true,\n\t\tinstructions: context.systemPrompt || \"You are a helpful assistant.\",\n\t\tinput: messages,\n\t\ttext: { verbosity: options?.textVerbosity || \"low\" },\n\t\tinclude: [\"reasoning.encrypted_content\"],\n\t\tprompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),\n\t\ttool_choice: options?.toolChoice ?? \"auto\",\n\t\tparallel_tool_calls: true,\n\t};\n\n\tif (options?.temperature !== undefined) {\n\t\tbody.temperature = options.temperature;\n\t}\n\n\tif (options?.serviceTier !== undefined) {\n\t\tbody.service_tier = options.serviceTier;\n\t}\n\n\tif (toolPlacement.immediate.length > 0) {\n\t\tbody.tools = convertResponsesTools(toolPlacement.immediate, { strict: null });\n\t}\n\n\tif (options?.reasoningEffort !== undefined) {\n\t\tconst effort =\n\t\t\toptions.reasoningEffort === \"none\"\n\t\t\t\t? (model.thinkingLevelMap?.off ?? \"none\")\n\t\t\t\t: (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort);\n\t\tif (effort !== null) {\n\t\t\tbody.reasoning = {\n\t\t\t\teffort,\n\t\t\t\tsummary: options.reasoningSummary ?? \"auto\",\n\t\t\t};\n\t\t}\n\t}\n\n\treturn body;\n}\n\nfunction getServiceTierCostMultiplier(\n\tmodel: Pick<Model<\"openai-codex-responses\">, \"id\">,\n\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n): number {\n\tswitch (serviceTier) {\n\t\tcase \"flex\":\n\t\t\treturn 0.5;\n\t\tcase \"priority\":\n\t\t\treturn model.id === \"gpt-5.5\" ? 2.5 : 2;\n\t\tdefault:\n\t\t\treturn 1;\n\t}\n}\n\nfunction applyServiceTierPricing(\n\tusage: Usage,\n\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\tmodel: Pick<Model<\"openai-codex-responses\">, \"id\">,\n) {\n\tconst multiplier = getServiceTierCostMultiplier(model, serviceTier);\n\tif (multiplier === 1) return;\n\n\tusage.cost.input *= multiplier;\n\tusage.cost.output *= multiplier;\n\tusage.cost.cacheRead *= multiplier;\n\tusage.cost.cacheWrite *= multiplier;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n}\n\nfunction resolveCodexServiceTier(\n\tresponseServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\trequestServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n): ResponseCreateParamsStreaming[\"service_tier\"] | undefined {\n\tif (responseServiceTier === \"default\" && (requestServiceTier === \"flex\" || requestServiceTier === \"priority\")) {\n\t\treturn requestServiceTier;\n\t}\n\treturn responseServiceTier ?? requestServiceTier;\n}\n\nfunction resolveCodexUrl(baseUrl?: string): string {\n\tconst raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;\n\tconst normalized = raw.replace(/\\/+$/, \"\");\n\tif (normalized.endsWith(\"/codex/responses\")) return normalized;\n\tif (normalized.endsWith(\"/codex\")) return `${normalized}/responses`;\n\treturn `${normalized}/codex/responses`;\n}\n\nfunction resolveCodexWebSocketUrl(baseUrl?: string): string {\n\tconst url = new URL(resolveCodexUrl(baseUrl));\n\tif (url.protocol === \"https:\") url.protocol = \"wss:\";\n\tif (url.protocol === \"http:\") url.protocol = \"ws:\";\n\treturn url.toString();\n}\n\n// ============================================================================\n// Response Processing\n// ============================================================================\n\nasync function processStream(\n\tresponse: Response,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tawait processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal)), output, stream, model, {\n\t\tserviceTier: options?.serviceTier,\n\t\tresolveServiceTier: resolveCodexServiceTier,\n\t\tapplyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),\n\t});\n}\n\nclass CodexApiError extends Error {\n\treadonly code?: string;\n\treadonly payload?: Record<string, unknown>;\n\n\tconstructor(message: string, options?: { code?: string; payload?: Record<string, unknown>; cause?: unknown }) {\n\t\tsuper(message);\n\t\tthis.name = \"CodexApiError\";\n\t\tthis.code = options?.code;\n\t\tthis.payload = options?.payload;\n\t\tthis.cause = options?.cause;\n\t}\n}\n\nclass CodexProtocolError extends Error {\n\treadonly payload?: unknown;\n\n\tconstructor(message: string, options?: { payload?: unknown; cause?: unknown }) {\n\t\tsuper(message);\n\t\tthis.name = \"CodexProtocolError\";\n\t\tthis.payload = options?.payload;\n\t\tthis.cause = options?.cause;\n\t}\n}\n\nfunction isCodexNonTransportError(error: unknown): boolean {\n\treturn error instanceof CodexApiError || error instanceof CodexProtocolError;\n}\n\nfunction isWebSocketConnectionLimitReachedError(error: unknown): boolean {\n\treturn error instanceof CodexApiError && error.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE;\n}\n\nfunction extractCodexEventError(event: Record<string, unknown>): { code?: string; message?: string } {\n\tconst nested = event.error && typeof event.error === \"object\" ? (event.error as Record<string, unknown>) : undefined;\n\treturn {\n\t\tcode: typeof event.code === \"string\" ? event.code : typeof nested?.code === \"string\" ? nested.code : undefined,\n\t\tmessage:\n\t\t\ttypeof event.message === \"string\"\n\t\t\t\t? event.message\n\t\t\t\t: typeof nested?.message === \"string\"\n\t\t\t\t\t? nested.message\n\t\t\t\t\t: undefined,\n\t};\n}\n\nasync function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>): AsyncGenerator<ResponseStreamEvent> {\n\tfor await (const event of events) {\n\t\tconst type = typeof event.type === \"string\" ? event.type : undefined;\n\t\tif (!type) continue;\n\n\t\tif (type === \"error\") {\n\t\t\tconst { code, message } = extractCodexEventError(event);\n\t\t\tthrow new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {\n\t\t\t\tcode,\n\t\t\t\tpayload: event,\n\t\t\t});\n\t\t}\n\n\t\tif (type === \"response.failed\") {\n\t\t\tconst response = (event as { response?: { error?: { code?: string; message?: string } } }).response;\n\t\t\tconst code = response?.error?.code;\n\t\t\tconst message = response?.error?.message;\n\t\t\tthrow new CodexApiError(message || \"Codex response failed\", { code, payload: event });\n\t\t}\n\n\t\tif (type === \"response.done\" || type === \"response.completed\" || type === \"response.incomplete\") {\n\t\t\tconst response = (event as { response?: { status?: unknown } }).response;\n\t\t\tconst normalizedResponse = response\n\t\t\t\t? { ...response, status: normalizeCodexStatus(response.status) }\n\t\t\t\t: response;\n\t\t\tyield { ...event, type: \"response.completed\", response: normalizedResponse } as ResponseStreamEvent;\n\t\t\treturn;\n\t\t}\n\n\t\tyield event as unknown as ResponseStreamEvent;\n\t}\n}\n\nfunction normalizeCodexStatus(status: unknown): CodexResponseStatus | undefined {\n\tif (typeof status !== \"string\") return undefined;\n\treturn CODEX_RESPONSE_STATUSES.has(status as CodexResponseStatus) ? (status as CodexResponseStatus) : undefined;\n}\n\n// ============================================================================\n// SSE Parsing\n// ============================================================================\n\nasync function* parseSSE(response: Response, signal?: AbortSignal): AsyncGenerator<Record<string, unknown>> {\n\tif (!response.body) return;\n\n\tconst reader = response.body.getReader();\n\tconst decoder = new TextDecoder();\n\tlet buffer = \"\";\n\tconst onAbort = () => {\n\t\tvoid reader.cancel().catch(() => {});\n\t};\n\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\tlet idx = buffer.indexOf(\"\\n\\n\");\n\t\t\twhile (idx !== -1) {\n\t\t\t\tconst chunk = buffer.slice(0, idx);\n\t\t\t\tbuffer = buffer.slice(idx + 2);\n\n\t\t\t\tconst dataLines = chunk\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.filter((l) => l.startsWith(\"data:\"))\n\t\t\t\t\t.map((l) => l.slice(5).trim());\n\t\t\t\tif (dataLines.length > 0) {\n\t\t\t\t\tconst data = dataLines.join(\"\\n\").trim();\n\t\t\t\t\tif (data && data !== \"[DONE]\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tyield JSON.parse(data) as Record<string, unknown>;\n\t\t\t\t\t\t} catch (cause) {\n\t\t\t\t\t\t\tthrow new CodexProtocolError(`Invalid Codex SSE JSON: ${formatThrownValue(cause)}`, {\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\tpayload: data,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tidx = buffer.indexOf(\"\\n\\n\");\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\ttry {\n\t\t\tawait reader.cancel();\n\t\t} catch {}\n\t\ttry {\n\t\t\treader.releaseLock();\n\t\t} catch {}\n\t}\n}\n\n// ============================================================================\n// WebSocket Parsing\n// ============================================================================\n\nconst OPENAI_BETA_RESPONSES_WEBSOCKETS = \"responses_websockets=2026-02-06\";\nconst SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;\nconst SESSION_WEBSOCKET_MAX_AGE_MS = 55 * 60 * 1000;\n\ntype WebSocketEventType = \"open\" | \"message\" | \"error\" | \"close\";\ntype WebSocketListener = (event: unknown) => void;\n\ninterface WebSocketLike {\n\tclose(code?: number, reason?: string): void;\n\tsend(data: string): void;\n\taddEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n\tremoveEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n}\n\ninterface CachedWebSocketContinuationState {\n\tlastRequestBody: RequestBody;\n\tlastResponseId: string;\n\tlastResponseItems: ResponseInput;\n}\n\ninterface CachedWebSocketConnection {\n\tsocket: WebSocketLike;\n\tbusy: boolean;\n\tcreatedAt: number;\n\tidleTimer?: ReturnType<typeof setTimeout>;\n\tcontinuation?: CachedWebSocketContinuationState;\n}\n\nexport interface OpenAICodexWebSocketDebugStats {\n\trequests: number;\n\tconnectionsCreated: number;\n\tconnectionsReused: number;\n\tcachedContextRequests: number;\n\tstoreTrueRequests: number;\n\tfullContextRequests: number;\n\tdeltaRequests: number;\n\tlastInputItems: number;\n\tlastDeltaInputItems?: number;\n\tlastPreviousResponseId?: string;\n\twebsocketFailures: number;\n\tsseFallbacks: number;\n\twebsocketFallbackActive?: boolean;\n\tlastWebSocketError?: string;\n}\n\nconst websocketSessionCache = new Map<string, CachedWebSocketConnection>();\nconst websocketDebugStats = new Map<string, OpenAICodexWebSocketDebugStats>();\nconst websocketSseFallbackSessions = new Set<string>();\n\nfunction getOrCreateWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats {\n\tlet stats = websocketDebugStats.get(sessionId);\n\tif (!stats) {\n\t\tstats = {\n\t\t\trequests: 0,\n\t\t\tconnectionsCreated: 0,\n\t\t\tconnectionsReused: 0,\n\t\t\tcachedContextRequests: 0,\n\t\t\tstoreTrueRequests: 0,\n\t\t\tfullContextRequests: 0,\n\t\t\tdeltaRequests: 0,\n\t\t\tlastInputItems: 0,\n\t\t\twebsocketFailures: 0,\n\t\t\tsseFallbacks: 0,\n\t\t};\n\t\twebsocketDebugStats.set(sessionId, stats);\n\t}\n\treturn stats;\n}\n\nexport function getOpenAICodexWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats | undefined {\n\tconst stats = websocketDebugStats.get(sessionId);\n\treturn stats ? { ...stats } : undefined;\n}\n\nexport function resetOpenAICodexWebSocketDebugStats(sessionId?: string): void {\n\tif (sessionId) {\n\t\twebsocketDebugStats.delete(sessionId);\n\t\twebsocketSseFallbackSessions.delete(sessionId);\n\t\treturn;\n\t}\n\twebsocketDebugStats.clear();\n\twebsocketSseFallbackSessions.clear();\n}\n\nexport function closeOpenAICodexWebSocketSessions(sessionId?: string): void {\n\tconst closeEntry = (entry: CachedWebSocketConnection) => {\n\t\tif (entry.idleTimer) clearTimeout(entry.idleTimer);\n\t\tcloseWebSocketSilently(entry.socket, 1000, \"debug_close\");\n\t};\n\tif (sessionId) {\n\t\tconst entry = websocketSessionCache.get(sessionId);\n\t\tif (entry) closeEntry(entry);\n\t\twebsocketSessionCache.delete(sessionId);\n\t\treturn;\n\t}\n\tfor (const entry of websocketSessionCache.values()) {\n\t\tcloseEntry(entry);\n\t}\n\twebsocketSessionCache.clear();\n}\n\nregisterSessionResourceCleanup(closeOpenAICodexWebSocketSessions);\n\nfunction isWebSocketSseFallbackActive(sessionId: string | undefined): boolean {\n\treturn sessionId ? websocketSseFallbackSessions.has(sessionId) : false;\n}\n\nfunction recordWebSocketSseFallback(sessionId: string | undefined): void {\n\tif (!sessionId) return;\n\tconst stats = getOrCreateWebSocketDebugStats(sessionId);\n\tstats.sseFallbacks++;\n\tstats.websocketFallbackActive = isWebSocketSseFallbackActive(sessionId);\n}\n\nfunction recordWebSocketFailure(sessionId: string | undefined, error: unknown): void {\n\tif (!sessionId) return;\n\twebsocketSseFallbackSessions.add(sessionId);\n\n\tconst stats = getOrCreateWebSocketDebugStats(sessionId);\n\tstats.websocketFailures++;\n\tstats.lastWebSocketError = formatThrownValue(error);\n\tstats.websocketFallbackActive = true;\n}\n\ntype WebSocketConstructor = new (\n\turl: string,\n\tprotocols?: string | string[] | { headers?: Record<string, string> },\n) => WebSocketLike;\n\nlet _cachedWebsocket: WebSocketConstructor | null = null;\nasync function getWebSocketConstructor(env?: ProviderEnv): Promise<WebSocketConstructor | null> {\n\tif (!env && _cachedWebsocket) return _cachedWebsocket;\n\n\t// bun doesn't respect http proxy envs, ref: https://github.com/oven-sh/bun/issues/15489\n\t// TODO: remove this when bun supports proxy envs in websocket.\n\tif (typeof process !== \"undefined\" && process.versions?.bun) {\n\t\tconst WebSocketWithProxy = class extends WebSocket {\n\t\t\tconstructor(url: string | URL, options?: string | string[] | Record<string, unknown>) {\n\t\t\t\tlet _opts: Record<string, unknown> = {};\n\t\t\t\tif (Array.isArray(options) || typeof options === \"string\") {\n\t\t\t\t\t_opts = { protocols: options };\n\t\t\t\t} else {\n\t\t\t\t\t_opts = { ...options };\n\t\t\t\t}\n\n\t\t\t\tconst proxyUrl = resolveHttpProxyUrlForTarget(\n\t\t\t\t\turl.toString().replace(/^wss:/, \"https:\").replace(/^ws:/, \"http:\"),\n\t\t\t\t\tenv,\n\t\t\t\t);\n\t\t\t\tsuper(url, { ..._opts, ...(proxyUrl ? { proxy: proxyUrl.toString() } : {}) } as any);\n\t\t\t}\n\t\t};\n\t\tif (!env) {\n\t\t\t_cachedWebsocket = WebSocketWithProxy;\n\t\t}\n\t\treturn WebSocketWithProxy;\n\t}\n\n\tconst ctor = (globalThis as { WebSocket?: unknown }).WebSocket;\n\tif (typeof ctor !== \"function\") return null;\n\treturn ctor as unknown as WebSocketConstructor;\n}\n\nclass WebSocketCloseError extends Error {\n\treadonly code?: number;\n\treadonly reason?: string;\n\treadonly wasClean?: boolean;\n\n\tconstructor(message: string, options?: { code?: number; reason?: string; wasClean?: boolean }) {\n\t\tsuper(message);\n\t\tthis.name = \"WebSocketCloseError\";\n\t\tthis.code = options?.code;\n\t\tthis.reason = options?.reason;\n\t\tthis.wasClean = options?.wasClean;\n\t}\n}\n\nfunction getWebSocketReadyState(socket: WebSocketLike): number | undefined {\n\tconst readyState = (socket as { readyState?: unknown }).readyState;\n\treturn typeof readyState === \"number\" ? readyState : undefined;\n}\n\nfunction isWebSocketReusable(socket: WebSocketLike): boolean {\n\tconst readyState = getWebSocketReadyState(socket);\n\t// If readyState is unavailable, assume the runtime keeps it open/reusable.\n\treturn readyState === undefined || readyState === 1;\n}\n\nfunction isWebSocketSessionExpired(entry: CachedWebSocketConnection): boolean {\n\treturn Date.now() - entry.createdAt >= SESSION_WEBSOCKET_MAX_AGE_MS;\n}\n\nfunction closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = \"done\"): void {\n\ttry {\n\t\tsocket.close(code, reason);\n\t} catch {}\n}\n\nfunction scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocketConnection): void {\n\tif (entry.idleTimer) {\n\t\tclearTimeout(entry.idleTimer);\n\t}\n\tentry.idleTimer = setTimeout(() => {\n\t\tif (entry.busy) return;\n\t\tcloseWebSocketSilently(entry.socket, 1000, \"idle_timeout\");\n\t\twebsocketSessionCache.delete(sessionId);\n\t}, SESSION_WEBSOCKET_CACHE_TTL_MS);\n}\n\nasync function connectWebSocket(\n\turl: string,\n\theaders: Headers,\n\tsignal?: AbortSignal,\n\tconnectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS,\n\tenv?: ProviderEnv,\n): Promise<WebSocketLike> {\n\tconst WebSocketCtor = await getWebSocketConstructor(env);\n\tif (!WebSocketCtor) {\n\t\tthrow new Error(\"WebSocket transport is not available in this runtime\");\n\t}\n\n\tconst wsHeaders = headersToRecord(headers);\n\tdelete wsHeaders[\"OpenAI-Beta\"];\n\n\treturn new Promise<WebSocketLike>((resolve, reject) => {\n\t\tlet settled = false;\n\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\tlet socket: WebSocketLike;\n\n\t\ttry {\n\t\t\tsocket = new WebSocketCtor(url, { headers: wsHeaders });\n\t\t} catch (error) {\n\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\treturn;\n\t\t}\n\n\t\tconst cleanup = () => {\n\t\t\tif (timeout) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\ttimeout = undefined;\n\t\t\t}\n\t\t\tsocket.removeEventListener(\"open\", onOpen);\n\t\t\tsocket.removeEventListener(\"error\", onError);\n\t\t\tsocket.removeEventListener(\"close\", onClose);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\t\tconst fail = (error: Error, closeReason?: string) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tif (closeReason) {\n\t\t\t\tcloseWebSocketSilently(socket, 1000, closeReason);\n\t\t\t}\n\t\t\treject(error);\n\t\t};\n\t\tconst onOpen: WebSocketListener = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(socket);\n\t\t};\n\t\tconst onError: WebSocketListener = (event) => {\n\t\t\tfail(extractWebSocketError(event));\n\t\t};\n\t\tconst onClose: WebSocketListener = (event) => {\n\t\t\tfail(extractWebSocketCloseError(event));\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tfail(new Error(\"Request was aborted\"), \"aborted\");\n\t\t};\n\n\t\tsocket.addEventListener(\"open\", onOpen);\n\t\tsocket.addEventListener(\"error\", onError);\n\t\tsocket.addEventListener(\"close\", onClose);\n\t\tsignal?.addEventListener(\"abort\", onAbort);\n\n\t\tif (connectTimeoutMs > 0) {\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\tfail(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`), \"connect_timeout\");\n\t\t\t}, connectTimeoutMs);\n\t\t}\n\t\tif (signal?.aborted) {\n\t\t\tonAbort();\n\t\t}\n\t});\n}\n\nasync function acquireWebSocket(\n\turl: string,\n\theaders: Headers,\n\tsessionId: string | undefined,\n\tsignal?: AbortSignal,\n\tconnectTimeoutMs?: number,\n\tenv?: ProviderEnv,\n): Promise<{\n\tsocket: WebSocketLike;\n\tentry?: CachedWebSocketConnection;\n\treused: boolean;\n\trelease: (options?: { keep?: boolean }) => void;\n}> {\n\tif (!sessionId) {\n\t\tconst socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env);\n\t\treturn {\n\t\t\tsocket,\n\t\t\treused: false,\n\t\t\trelease: () => closeWebSocketSilently(socket),\n\t\t};\n\t}\n\n\tconst cached = websocketSessionCache.get(sessionId);\n\tif (cached) {\n\t\tif (cached.idleTimer) {\n\t\t\tclearTimeout(cached.idleTimer);\n\t\t\tcached.idleTimer = undefined;\n\t\t}\n\t\tif (!cached.busy && isWebSocketSessionExpired(cached)) {\n\t\t\tcloseWebSocketSilently(cached.socket, 1000, \"connection_age_limit\");\n\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t} else if (!cached.busy && isWebSocketReusable(cached.socket)) {\n\t\t\tcached.busy = true;\n\t\t\treturn {\n\t\t\t\tsocket: cached.socket,\n\t\t\t\tentry: cached,\n\t\t\t\treused: true,\n\t\t\t\trelease: ({ keep } = {}) => {\n\t\t\t\t\tif (!keep || !isWebSocketReusable(cached.socket)) {\n\t\t\t\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcached.busy = false;\n\t\t\t\t\tscheduleSessionWebSocketExpiry(sessionId, cached);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (cached.busy) {\n\t\t\tconst socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env);\n\t\t\treturn {\n\t\t\t\tsocket,\n\t\t\t\treused: false,\n\t\t\t\trelease: () => {\n\t\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (!isWebSocketReusable(cached.socket)) {\n\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t}\n\t}\n\n\tconst socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env);\n\tconst entry: CachedWebSocketConnection = { socket, busy: true, createdAt: Date.now() };\n\twebsocketSessionCache.set(sessionId, entry);\n\treturn {\n\t\tsocket,\n\t\tentry,\n\t\treused: false,\n\t\trelease: ({ keep } = {}) => {\n\t\t\tif (!keep || !isWebSocketReusable(entry.socket)) {\n\t\t\t\tcloseWebSocketSilently(entry.socket);\n\t\t\t\tif (entry.idleTimer) clearTimeout(entry.idleTimer);\n\t\t\t\tif (websocketSessionCache.get(sessionId) === entry) {\n\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tentry.busy = false;\n\t\t\tscheduleSessionWebSocketExpiry(sessionId, entry);\n\t\t},\n\t};\n}\n\nfunction extractWebSocketError(event: unknown): Error {\n\tif (event && typeof event === \"object\") {\n\t\tconst message = \"message\" in event ? (event as { message?: unknown }).message : undefined;\n\t\tif (typeof message === \"string\" && message.length > 0) {\n\t\t\treturn new Error(message);\n\t\t}\n\n\t\tconst nestedError = \"error\" in event ? (event as { error?: unknown }).error : undefined;\n\t\tif (nestedError instanceof Error && nestedError.message.length > 0) {\n\t\t\treturn nestedError;\n\t\t}\n\t\tif (nestedError && typeof nestedError === \"object\" && \"message\" in nestedError) {\n\t\t\tconst nestedMessage = (nestedError as { message?: unknown }).message;\n\t\t\tif (typeof nestedMessage === \"string\" && nestedMessage.length > 0) {\n\t\t\t\treturn new Error(nestedMessage);\n\t\t\t}\n\t\t}\n\t}\n\treturn new Error(\"WebSocket error\");\n}\n\nfunction extractWebSocketCloseError(event: unknown): Error {\n\tif (event && typeof event === \"object\") {\n\t\tconst code = \"code\" in event ? (event as { code?: unknown }).code : undefined;\n\t\tconst reason = \"reason\" in event ? (event as { reason?: unknown }).reason : undefined;\n\t\tconst wasClean = \"wasClean\" in event ? (event as { wasClean?: unknown }).wasClean : undefined;\n\t\tconst codeText = typeof code === \"number\" ? ` ${code}` : \"\";\n\t\tlet reasonText = typeof reason === \"string\" && reason.length > 0 ? ` ${reason}` : \"\";\n\t\tif (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) {\n\t\t\treasonText = \" message too big\";\n\t\t}\n\t\treturn new WebSocketCloseError(`WebSocket closed${codeText}${reasonText}`.trim(), {\n\t\t\tcode: typeof code === \"number\" ? code : undefined,\n\t\t\treason: typeof reason === \"string\" && reason.length > 0 ? reason : undefined,\n\t\t\twasClean: typeof wasClean === \"boolean\" ? wasClean : undefined,\n\t\t});\n\t}\n\treturn new Error(\"WebSocket closed\");\n}\n\nasync function decodeWebSocketData(data: unknown): Promise<string | null> {\n\tif (typeof data === \"string\") return data;\n\tif (data instanceof ArrayBuffer) {\n\t\treturn new TextDecoder().decode(new Uint8Array(data));\n\t}\n\tif (ArrayBuffer.isView(data)) {\n\t\tconst view = data as ArrayBufferView;\n\t\treturn new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));\n\t}\n\tif (data && typeof data === \"object\" && \"arrayBuffer\" in data) {\n\t\tconst blobLike = data as { arrayBuffer: () => Promise<ArrayBuffer> };\n\t\tconst arrayBuffer = await blobLike.arrayBuffer();\n\t\treturn new TextDecoder().decode(new Uint8Array(arrayBuffer));\n\t}\n\treturn null;\n}\n\nasync function* parseWebSocket(\n\tsocket: WebSocketLike,\n\tsignal?: AbortSignal,\n\tidleTimeoutMs?: number,\n): AsyncGenerator<Record<string, unknown>> {\n\tconst queue: Record<string, unknown>[] = [];\n\tlet pending: (() => void) | null = null;\n\tlet done = false;\n\tlet failed: Error | null = null;\n\tlet sawCompletion = false;\n\n\tconst wake = () => {\n\t\tif (!pending) return;\n\t\tconst resolve = pending;\n\t\tpending = null;\n\t\tresolve();\n\t};\n\n\tconst onMessage: WebSocketListener = (event) => {\n\t\tvoid (async () => {\n\t\t\tlet text: string | null = null;\n\t\t\ttry {\n\t\t\t\tif (!event || typeof event !== \"object\" || !(\"data\" in event)) return;\n\t\t\t\ttext = await decodeWebSocketData((event as { data?: unknown }).data);\n\t\t\t\tif (!text) return;\n\t\t\t\tconst parsed = JSON.parse(text) as Record<string, unknown>;\n\t\t\t\tconst type = typeof parsed.type === \"string\" ? parsed.type : \"\";\n\t\t\t\tif (type === \"response.completed\" || type === \"response.done\" || type === \"response.incomplete\") {\n\t\t\t\t\tsawCompletion = true;\n\t\t\t\t\tdone = true;\n\t\t\t\t}\n\t\t\t\tqueue.push(parsed);\n\t\t\t\twake();\n\t\t\t} catch (cause) {\n\t\t\t\tfailed = new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, {\n\t\t\t\t\tcause,\n\t\t\t\t\tpayload: text,\n\t\t\t\t});\n\t\t\t\tdone = true;\n\t\t\t\twake();\n\t\t\t}\n\t\t})();\n\t};\n\n\tconst onError: WebSocketListener = (event) => {\n\t\tfailed = extractWebSocketError(event);\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onClose: WebSocketListener = (event) => {\n\t\tif (sawCompletion) {\n\t\t\tdone = true;\n\t\t\twake();\n\t\t\treturn;\n\t\t}\n\t\tif (!failed) {\n\t\t\tfailed = extractWebSocketCloseError(event);\n\t\t}\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onAbort = () => {\n\t\tfailed = new Error(\"Request was aborted\");\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tsocket.addEventListener(\"message\", onMessage);\n\tsocket.addEventListener(\"error\", onError);\n\tsocket.addEventListener(\"close\", onClose);\n\tsignal?.addEventListener(\"abort\", onAbort);\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tif (queue.length > 0) {\n\t\t\t\tyield queue.shift()!;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (done) break;\n\t\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tpending = resolve;\n\t\t\t\tif (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {\n\t\t\t\t\ttimeout = setTimeout(() => {\n\t\t\t\t\t\tconst error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);\n\t\t\t\t\t\tfailed = error;\n\t\t\t\t\t\tdone = true;\n\t\t\t\t\t\tpending = null;\n\t\t\t\t\t\tcloseWebSocketSilently(socket, 1000, \"idle_timeout\");\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}, idleTimeoutMs);\n\t\t\t\t}\n\t\t\t}).finally(() => {\n\t\t\t\tif (timeout) {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (failed) {\n\t\t\tthrow failed;\n\t\t}\n\t\tif (!sawCompletion) {\n\t\t\tthrow new Error(\"WebSocket stream closed before response.completed\");\n\t\t}\n\t} finally {\n\t\tsocket.removeEventListener(\"message\", onMessage);\n\t\tsocket.removeEventListener(\"error\", onError);\n\t\tsocket.removeEventListener(\"close\", onClose);\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\nfunction requestBodyWithoutInput(body: RequestBody): RequestBody {\n\tconst { input: _input, previous_response_id: _previousResponseId, ...rest } = body;\n\treturn rest;\n}\n\nfunction responseInputsEqual(a: ResponseInput | undefined, b: ResponseInput | undefined): boolean {\n\treturn JSON.stringify(a ?? []) === JSON.stringify(b ?? []);\n}\n\nfunction requestBodiesMatchExceptInput(a: RequestBody, b: RequestBody): boolean {\n\treturn JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));\n}\n\nfunction getCachedWebSocketInputDelta(\n\tbody: RequestBody,\n\tcontinuation: CachedWebSocketContinuationState,\n): ResponseInput | undefined {\n\tif (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {\n\t\treturn undefined;\n\t}\n\n\tconst currentInput = body.input ?? [];\n\tconst baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];\n\tif (currentInput.length < baseline.length) {\n\t\treturn undefined;\n\t}\n\n\tconst prefix = currentInput.slice(0, baseline.length);\n\tif (!responseInputsEqual(prefix, baseline)) {\n\t\treturn undefined;\n\t}\n\n\treturn currentInput.slice(baseline.length);\n}\n\nfunction buildCachedWebSocketRequestBody(entry: CachedWebSocketConnection, body: RequestBody): RequestBody {\n\tconst continuation = entry.continuation;\n\tif (!continuation) {\n\t\treturn body;\n\t}\n\n\tconst delta = getCachedWebSocketInputDelta(body, continuation);\n\tif (!delta || !continuation.lastResponseId) {\n\t\tentry.continuation = undefined;\n\t\treturn body;\n\t}\n\n\treturn {\n\t\t...body,\n\t\tprevious_response_id: continuation.lastResponseId,\n\t\tinput: delta,\n\t};\n}\n\nasync function* startWebSocketOutputOnFirstEvent(\n\tevents: AsyncIterable<ResponseStreamEvent>,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tonStart: () => void,\n): AsyncGenerator<ResponseStreamEvent> {\n\tlet started = false;\n\tfor await (const event of events) {\n\t\tif (!started) {\n\t\t\tstarted = true;\n\t\t\tonStart();\n\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t}\n\t\tyield event;\n\t}\n}\n\nasync function processWebSocketStream(\n\turl: string,\n\tbody: RequestBody,\n\theaders: Headers,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\tonStart: () => void,\n\tidleTimeoutMs: number | undefined,\n\twebsocketConnectTimeoutMs: number | undefined,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tconst { socket, entry, reused, release } = await acquireWebSocket(\n\t\turl,\n\t\theaders,\n\t\toptions?.sessionId,\n\t\toptions?.signal,\n\t\twebsocketConnectTimeoutMs,\n\t\toptions?.env,\n\t);\n\tlet keepConnection = true;\n\tconst useCachedContext = options?.transport === \"websocket-cached\" || options?.transport === \"auto\";\n\t// ChatGPT Codex Responses rejects `store: true` (\"Store must be set to false\").\n\t// WebSocket continuation still works via connection-scoped previous_response_id state.\n\tconst fullBody = body;\n\tconst requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;\n\tconst stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : undefined;\n\tif (stats) {\n\t\tstats.requests++;\n\t\tif (reused) stats.connectionsReused++;\n\t\telse stats.connectionsCreated++;\n\t\tif (useCachedContext) stats.cachedContextRequests++;\n\t\tif (requestBody.store === true) stats.storeTrueRequests++;\n\t\tstats.lastInputItems = requestBody.input?.length ?? 0;\n\t\tif (requestBody.previous_response_id) {\n\t\t\tstats.deltaRequests++;\n\t\t\tstats.lastDeltaInputItems = requestBody.input?.length ?? 0;\n\t\t\tstats.lastPreviousResponseId = requestBody.previous_response_id;\n\t\t} else {\n\t\t\tstats.fullContextRequests++;\n\t\t\tstats.lastDeltaInputItems = undefined;\n\t\t\tstats.lastPreviousResponseId = undefined;\n\t\t}\n\t}\n\ttry {\n\t\tsocket.send(JSON.stringify({ type: \"response.create\", ...requestBody }));\n\t\tawait processResponsesStream(\n\t\t\tstartWebSocketOutputOnFirstEvent(\n\t\t\t\tmapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)),\n\t\t\t\toutput,\n\t\t\t\tstream,\n\t\t\t\tonStart,\n\t\t\t),\n\t\t\toutput,\n\t\t\tstream,\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tserviceTier: options?.serviceTier,\n\t\t\t\tresolveServiceTier: resolveCodexServiceTier,\n\t\t\t\tapplyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),\n\t\t\t},\n\t\t);\n\t\tif (options?.signal?.aborted) {\n\t\t\tkeepConnection = false;\n\t\t} else if (useCachedContext && entry && output.responseId) {\n\t\t\tconst responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {\n\t\t\t\tincludeSystemPrompt: false,\n\t\t\t}).filter((item) => item.type !== \"function_call_output\");\n\t\t\tentry.continuation = {\n\t\t\t\tlastRequestBody: fullBody,\n\t\t\t\tlastResponseId: output.responseId,\n\t\t\t\tlastResponseItems: responseItems,\n\t\t\t};\n\t\t}\n\t} catch (error) {\n\t\tif (entry) {\n\t\t\tentry.continuation = undefined;\n\t\t}\n\t\tkeepConnection = false;\n\t\tthrow error;\n\t} finally {\n\t\trelease({ keep: keepConnection });\n\t}\n}\n\n// ============================================================================\n// Error Handling\n// ============================================================================\n\nasync function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {\n\tconst raw = await response.text();\n\tlet message = raw || response.statusText || \"Request failed\";\n\tlet friendlyMessage: string | undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as {\n\t\t\terror?: { code?: string; type?: string; message?: string; plan_type?: string; resets_at?: number };\n\t\t};\n\t\tconst err = parsed?.error;\n\t\tif (err) {\n\t\t\tconst code = err.code || err.type || \"\";\n\t\t\tif (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {\n\t\t\t\tconst plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : \"\";\n\t\t\t\tconst mins = err.resets_at\n\t\t\t\t\t? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000))\n\t\t\t\t\t: undefined;\n\t\t\t\tconst when = mins !== undefined ? ` Try again in ~${mins} min.` : \"\";\n\t\t\t\tfriendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();\n\t\t\t}\n\t\t\tmessage = err.message || friendlyMessage || message;\n\t\t}\n\t} catch {}\n\n\treturn { message, friendlyMessage };\n}\n\n// ============================================================================\n// Auth & Headers\n// ============================================================================\n\nfunction extractAccountId(token: string): string {\n\ttry {\n\t\tconst parts = token.split(\".\");\n\t\tif (parts.length !== 3) throw new Error(\"Invalid token\");\n\t\tconst payload = JSON.parse(atob(parts[1]));\n\t\tconst accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;\n\t\tif (!accountId) throw new Error(\"No account ID in token\");\n\t\treturn accountId;\n\t} catch {\n\t\tthrow new Error(\"Failed to extract accountId from token\");\n\t}\n}\n\nfunction buildBaseCodexHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: ProviderHeaders | undefined,\n\taccountId: string,\n\ttoken: string,\n): Headers {\n\tconst headers = new Headers(initHeaders);\n\tfor (const [key, value] of Object.entries(additionalHeaders || {})) {\n\t\tif (value === null) {\n\t\t\theaders.delete(key);\n\t\t} else {\n\t\t\theaders.set(key, value);\n\t\t}\n\t}\n\theaders.set(\"Authorization\", `Bearer ${token}`);\n\theaders.set(\"chatgpt-account-id\", accountId);\n\theaders.set(\"originator\", \"pi\");\n\tconst userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : \"pi (browser)\";\n\theaders.set(\"User-Agent\", userAgent);\n\treturn headers;\n}\n\nfunction buildSSEHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: ProviderHeaders | undefined,\n\taccountId: string,\n\ttoken: string,\n\tsessionId?: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.set(\"OpenAI-Beta\", \"responses=experimental\");\n\theaders.set(\"accept\", \"text/event-stream\");\n\theaders.set(\"content-type\", \"application/json\");\n\n\tif (sessionId) {\n\t\theaders.set(\"session-id\", sessionId);\n\t\theaders.set(\"x-client-request-id\", sessionId);\n\t}\n\n\treturn headers;\n}\n\nfunction buildWebSocketHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: ProviderHeaders | undefined,\n\taccountId: string,\n\ttoken: string,\n\trequestId: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.delete(\"accept\");\n\theaders.delete(\"content-type\");\n\theaders.delete(\"OpenAI-Beta\");\n\theaders.delete(\"openai-beta\");\n\theaders.set(\"OpenAI-Beta\", OPENAI_BETA_RESPONSES_WEBSOCKETS);\n\theaders.set(\"x-client-request-id\", requestId);\n\theaders.set(\"session-id\", requestId);\n\treturn headers;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"openai-codex-responses.d.ts","sourceRoot":"","sources":["../../src/api/openai-codex-responses.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEX,6BAA6B,EAG7B,MAAM,yCAAyC,CAAC;AAkBjD,OAAO,KAAK,EAOX,mBAAmB,EACnB,cAAc,EACd,aAAa,EAEb,MAAM,aAAa,CAAC;AAiDrB,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IACjE,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;IACnF,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IACzE,WAAW,CAAC,EAAE,6BAA6B,CAAC,cAAc,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;CAC1C;AA6ID,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,wBAAwB,EAAE,2BAA2B,CAiQxF,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,wBAAwB,EAAE,mBAAmB,CAkBtF,CAAC;AA2UF,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AA0BD,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,8BAA8B,GAAG,SAAS,CAG/G;AAED,wBAAgB,mCAAmC,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAQ5E;AAED,wBAAgB,iCAAiC,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAe1E","sourcesContent":["import type * as NodeOs from \"node:os\";\nimport type * as NodeZlib from \"node:zlib\";\nimport type {\n\tTool as OpenAITool,\n\tResponseCreateParamsStreaming,\n\tResponseInput,\n\tResponseStreamEvent,\n} from \"openai/resources/responses/responses.js\";\n\ntype ProcessWithOsBuiltinModule = typeof process & {\n\tgetBuiltinModule?: (id: \"node:os\") => typeof NodeOs;\n};\n\nfunction loadNodeOs(): typeof NodeOs | null {\n\tif (typeof process === \"undefined\" || !(process.versions?.node || process.versions?.bun)) {\n\t\treturn null;\n\t}\n\treturn (process as ProcessWithOsBuiltinModule).getBuiltinModule?.(\"node:os\") ?? null;\n}\n\n// NEVER convert to top-level runtime imports - breaks browser/Vite builds\nconst _os: typeof NodeOs | null = loadNodeOs();\n\nimport { clampThinkingLevel } from \"../models.ts\";\nimport { registerSessionResourceCleanup } from \"../session-resources.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tProviderEnv,\n\tProviderHeaders,\n\tSimpleStreamOptions,\n\tStreamFunction,\n\tStreamOptions,\n\tUsage,\n} from \"../types.ts\";\nimport { combineAbortSignals } from \"../utils/abort-signals.ts\";\nimport { splitDeferredTools } from \"../utils/deferred-tools.ts\";\nimport {\n\tappendAssistantMessageDiagnostic,\n\tcreateAssistantMessageDiagnostic,\n\tformatThrownValue,\n} from \"../utils/diagnostics.ts\";\nimport { formatProviderError, normalizeProviderError } from \"../utils/error-body.ts\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.ts\";\nimport { headersToRecord } from \"../utils/headers.ts\";\nimport { resolveHttpProxyUrlForTarget } from \"../utils/node-http-proxy.ts\";\nimport { uuidv7 } from \"../utils/uuid.ts\";\nimport { createGrammarToolInputProperties } from \"./constrained-sampling.ts\";\nimport { clampOpenAIPromptCacheKey } from \"./openai-prompt-cache.ts\";\nimport { convertResponsesMessages, convertResponsesTools, processResponsesStream } from \"./openai-responses-shared.ts\";\nimport { buildBaseOptions } from \"./simple-options.ts\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst DEFAULT_CODEX_BASE_URL = \"https://chatgpt.com/backend-api\";\nconst JWT_CLAIM_PATH = \"https://api.openai.com/auth\" as const;\nconst DEFAULT_MAX_RETRIES = 0;\nconst BASE_DELAY_MS = 1000;\nconst DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\nconst DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;\n// The Codex backend accepts zstd-compressed request bodies on the SSE responses\n// endpoint (the same endpoint the official Codex client compresses against).\nconst REQUEST_COMPRESSION_ZSTD_LEVEL = 3;\nconst CODEX_TOOL_CALL_PROVIDERS = new Set([\"openai\", \"openai-codex\", \"opencode\"]);\nconst WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;\nconst WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = \"websocket_connection_limit_reached\";\nconst PREVIOUS_RESPONSE_NOT_FOUND_CODE = \"previous_response_not_found\";\n\nconst CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([\n\t\"completed\",\n\t\"incomplete\",\n\t\"failed\",\n\t\"cancelled\",\n\t\"queued\",\n\t\"in_progress\",\n]);\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface OpenAICodexResponsesOptions extends StreamOptions {\n\treasoningEffort?: \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\treasoningSummary?: \"auto\" | \"concise\" | \"detailed\" | \"off\" | \"on\" | null;\n\tserviceTier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\ttextVerbosity?: \"low\" | \"medium\" | \"high\";\n\ttoolChoice?: \"auto\" | \"none\" | \"required\";\n}\n\ntype CodexResponseStatus = \"completed\" | \"incomplete\" | \"failed\" | \"cancelled\" | \"queued\" | \"in_progress\";\n\ninterface RequestBody {\n\tmodel: string;\n\tstore?: boolean;\n\tstream?: boolean;\n\tinstructions?: string;\n\tprevious_response_id?: string;\n\tinput?: ResponseInput;\n\ttools?: OpenAITool[];\n\ttool_choice?: OpenAICodexResponsesOptions[\"toolChoice\"];\n\tparallel_tool_calls?: boolean;\n\ttemperature?: number;\n\treasoning?: { effort?: string; summary?: string };\n\tservice_tier?: ResponseCreateParamsStreaming[\"service_tier\"];\n\ttext?: { verbosity?: string };\n\tinclude?: string[];\n\tprompt_cache_key?: string;\n\t[key: string]: unknown;\n}\n\n// ============================================================================\n// Retry Helpers\n// ============================================================================\n\nfunction isTerminalRateLimitError(errorText: string): boolean {\n\treturn /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\terrorText,\n\t);\n}\n\nfunction isRetryableError(status: number, errorText: string): boolean {\n\tif (status === 429 && isTerminalRateLimitError(errorText)) {\n\t\treturn false;\n\t}\n\tif (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {\n\t\treturn true;\n\t}\n\treturn /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);\n}\n\nfunction getRetryAfterDelayMs(headers: Headers): number | undefined {\n\tconst retryAfterMs = headers.get(\"retry-after-ms\");\n\tif (retryAfterMs !== null) {\n\t\tconst millis = Number(retryAfterMs);\n\t\tif (Number.isFinite(millis)) {\n\t\t\treturn Math.max(0, millis);\n\t\t}\n\t}\n\n\tconst retryAfter = headers.get(\"retry-after\");\n\tif (!retryAfter) {\n\t\treturn undefined;\n\t}\n\n\tconst seconds = Number(retryAfter);\n\tif (Number.isFinite(seconds)) {\n\t\treturn Math.max(0, seconds * 1000);\n\t}\n\n\tconst date = Date.parse(retryAfter);\n\tif (!Number.isNaN(date)) {\n\t\treturn Math.max(0, date - Date.now());\n\t}\n\n\treturn undefined;\n}\n\nclass RetryDelayExceededError extends Error {}\n\nfunction validateRetryDelayMs(delayMs: number, options?: StreamOptions): number {\n\tconst maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;\n\tif (maxRetryDelayMs > 0 && delayMs > maxRetryDelayMs) {\n\t\tthrow new RetryDelayExceededError(\n\t\t\t`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxRetryDelayMs / 1000)}s)`,\n\t\t);\n\t}\n\treturn delayMs;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\"abort\", () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t});\n\t});\n}\n\nfunction normalizeTimeoutMs(value: number | undefined): number | undefined {\n\tif (value === undefined) return undefined;\n\tif (!Number.isFinite(value) || value < 0) {\n\t\tthrow new Error(`Invalid timeoutMs: ${String(value)}`);\n\t}\n\treturn Math.floor(value);\n}\n\n// ============================================================================\n// Request Compression\n// ============================================================================\n\ntype ProcessWithBuiltinModule = typeof process & {\n\tgetBuiltinModule?: (id: \"node:zlib\") => typeof NodeZlib;\n};\n\nfunction loadNodeZlib(): typeof NodeZlib | null {\n\tif (typeof process === \"undefined\" || !(process.versions?.node || process.versions?.bun)) {\n\t\treturn null;\n\t}\n\treturn (process as ProcessWithBuiltinModule).getBuiltinModule?.(\"node:zlib\") ?? null;\n}\n\n// Returns the zstd-compressed body bytes, or null when compression is\n// unavailable (browser/Vite builds). Callers fall back to sending the\n// uncompressed JSON when this returns null.\nfunction compressRequestBodyZstd(bodyJson: string): Uint8Array | null {\n\tconst zlib = loadNodeZlib();\n\tif (!zlib || typeof zlib.zstdCompressSync !== \"function\") {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst compressed = zlib.zstdCompressSync(bodyJson, {\n\t\t\tparams: { [zlib.constants.ZSTD_c_compressionLevel]: REQUEST_COMPRESSION_ZSTD_LEVEL },\n\t\t});\n\t\treturn new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n// ============================================================================\n// Main Stream Function\n// ============================================================================\n\nexport const stream: StreamFunction<\"openai-codex-responses\", OpenAICodexResponsesOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: \"openai-codex-responses\" as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey;\n\t\t\tif (!apiKey) {\n\t\t\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t\t\t}\n\n\t\t\tconst accountId = extractAccountId(apiKey);\n\t\t\tconst grammarToolInputProperties = createGrammarToolInputProperties(\n\t\t\t\tcontext.tools,\n\t\t\t\tmodel.compat?.supportsOpenAIGrammarTools ?? false,\n\t\t\t);\n\t\t\tconst cacheSessionId = options?.cacheRetention === \"none\" ? undefined : options?.sessionId;\n\t\t\tconst codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId);\n\t\t\tlet body = buildRequestBody(model, context, options, codexSessionId, grammarToolInputProperties);\n\t\t\tconst nextBody = await options?.onPayload?.(body, model);\n\t\t\tif (nextBody !== undefined) {\n\t\t\t\tbody = nextBody as RequestBody;\n\t\t\t}\n\t\t\tconst websocketRequestId = codexSessionId || uuidv7();\n\t\t\tconst sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, codexSessionId);\n\t\t\tconst websocketHeaders = buildWebSocketHeaders(\n\t\t\t\tmodel.headers,\n\t\t\t\toptions?.headers,\n\t\t\t\taccountId,\n\t\t\t\tapiKey,\n\t\t\t\twebsocketRequestId,\n\t\t\t);\n\t\t\tconst bodyJson = JSON.stringify(body);\n\t\t\tconst httpTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);\n\t\t\tconst websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);\n\t\t\tconst transport = options?.transport || \"auto\";\n\t\t\tlet startEmitted = false;\n\t\t\tconst websocketDisabledForSession = transport !== \"sse\" && isWebSocketSseFallbackActive(cacheSessionId);\n\t\t\tif (websocketDisabledForSession) {\n\t\t\t\trecordWebSocketSseFallback(cacheSessionId);\n\t\t\t}\n\n\t\t\tif (transport !== \"sse\" && !websocketDisabledForSession) {\n\t\t\t\tlet websocketStarted = false;\n\t\t\t\tlet retriedWebSocketConnectionLimit = false;\n\t\t\t\tlet retriedMissingWebSocketContinuation = false;\n\t\t\t\twhile (true) {\n\t\t\t\t\twebsocketStarted = false;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait processWebSocketStream(\n\t\t\t\t\t\t\tresolveCodexWebSocketUrl(model.baseUrl),\n\t\t\t\t\t\t\tbody,\n\t\t\t\t\t\t\twebsocketHeaders,\n\t\t\t\t\t\t\toutput,\n\t\t\t\t\t\t\tstream,\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\twebsocketStarted = true;\n\t\t\t\t\t\t\t\tif (!startEmitted) {\n\t\t\t\t\t\t\t\t\tstartEmitted = true;\n\t\t\t\t\t\t\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\thttpTimeoutMs,\n\t\t\t\t\t\t\twebsocketConnectTimeoutMs,\n\t\t\t\t\t\t\tcacheSessionId,\n\t\t\t\t\t\t\tgrammarToolInputProperties,\n\t\t\t\t\t\t\toptions,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\t\treason: output.stopReason as \"stop\" | \"length\" | \"toolUse\",\n\t\t\t\t\t\t\tmessage: output,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tstream.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconst aborted = options?.signal?.aborted;\n\t\t\t\t\t\tconst connectionLimitBeforeStart = !websocketStarted && isWebSocketConnectionLimitReachedError(error);\n\t\t\t\t\t\tconst previousResponseNotFound = isPreviousResponseNotFoundError(error);\n\t\t\t\t\t\tif (!aborted && previousResponseNotFound && !retriedMissingWebSocketContinuation) {\n\t\t\t\t\t\t\tretriedMissingWebSocketContinuation = true;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!aborted && connectionLimitBeforeStart && !retriedWebSocketConnectionLimit) {\n\t\t\t\t\t\t\tretriedWebSocketConnectionLimit = true;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (aborted || (isCodexNonTransportError(error) && !connectionLimitBeforeStart)) {\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tappendAssistantMessageDiagnostic(\n\t\t\t\t\t\t\toutput,\n\t\t\t\t\t\t\tcreateAssistantMessageDiagnostic(\"provider_transport_failure\", error, {\n\t\t\t\t\t\t\t\tconfiguredTransport: transport,\n\t\t\t\t\t\t\t\tfallbackTransport: websocketStarted ? undefined : \"sse\",\n\t\t\t\t\t\t\t\teventsEmitted: websocketStarted,\n\t\t\t\t\t\t\t\tphase: websocketStarted ? \"after_message_stream_start\" : \"before_message_stream_start\",\n\t\t\t\t\t\t\t\trequestBytes: new TextEncoder().encode(bodyJson).byteLength,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t\trecordWebSocketFailure(cacheSessionId, error);\n\t\t\t\t\t\tif (websocketStarted) {\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\trecordWebSocketSseFallback(cacheSessionId);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Compress the request body once for the SSE path. The Codex backend\n\t\t\t// decodes Content-Encoding: zstd; the WebSocket transport above sends the\n\t\t\t// uncompressed JSON frame, matching the official Codex client.\n\t\t\tconst compressedBody = compressRequestBodyZstd(bodyJson);\n\t\t\tif (compressedBody) {\n\t\t\t\tsseHeaders.set(\"content-encoding\", \"zstd\");\n\t\t\t}\n\t\t\tconst sseBody: Uint8Array | string = compressedBody ?? bodyJson;\n\n\t\t\t// Fetch with retry logic for rate limits and transient errors\n\t\t\tlet response: Response | undefined;\n\t\t\tlet lastError: Error | undefined;\n\t\t\tconst maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;\n\n\t\t\tfor (let attempt = 0; attempt <= maxRetries; attempt++) {\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst headerTimeoutSignal =\n\t\t\t\t\t\thttpTimeoutMs !== undefined && httpTimeoutMs > 0 ? AbortSignal.timeout(httpTimeoutMs) : undefined;\n\t\t\t\t\tconst combinedSignal = combineAbortSignals([options?.signal, headerTimeoutSignal]);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tresponse = await fetch(resolveCodexUrl(model.baseUrl), {\n\t\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t\theaders: sseHeaders,\n\t\t\t\t\t\t\tbody: sseBody,\n\t\t\t\t\t\t\tsignal: combinedSignal.signal,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (headerTimeoutSignal?.aborted && !options?.signal?.aborted) {\n\t\t\t\t\t\t\tthrow new Error(`Codex SSE response headers timed out after ${httpTimeoutMs}ms`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tcombinedSignal.cleanup();\n\t\t\t\t\t}\n\t\t\t\t\tawait options?.onResponse?.(\n\t\t\t\t\t\t{ status: response.status, headers: headersToRecord(response.headers) },\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst errorText = await response.text();\n\t\t\t\t\tif (attempt < maxRetries && isRetryableError(response.status, errorText)) {\n\t\t\t\t\t\tconst retryAfterDelayMs = getRetryAfterDelayMs(response.headers);\n\t\t\t\t\t\tconst delayMs =\n\t\t\t\t\t\t\tretryAfterDelayMs === undefined\n\t\t\t\t\t\t\t\t? BASE_DELAY_MS * 2 ** attempt\n\t\t\t\t\t\t\t\t: validateRetryDelayMs(retryAfterDelayMs, options);\n\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Parse error for friendly message on final attempt or non-retryable error\n\t\t\t\t\tconst fakeResponse = new Response(errorText, {\n\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\t});\n\t\t\t\t\tconst info = await parseErrorResponse(fakeResponse);\n\t\t\t\t\tthrow new Error(info.friendlyMessage || info.message);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof Error) {\n\t\t\t\t\t\tif (error.name === \"AbortError\" || error.message === \"Request was aborted\") {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t// Network errors are retryable\n\t\t\t\t\tif (\n\t\t\t\t\t\tattempt < maxRetries &&\n\t\t\t\t\t\t!(lastError instanceof RetryDelayExceededError) &&\n\t\t\t\t\t\t!lastError.message.includes(\"usage limit\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthrow lastError;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!response?.ok) {\n\t\t\t\tthrow lastError ?? new Error(\"Failed after retries\");\n\t\t\t}\n\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"No response body\");\n\t\t\t}\n\n\t\t\tif (!startEmitted) {\n\t\t\t\tstartEmitted = true;\n\t\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t\t}\n\t\t\tawait processStream(response, output, stream, model, grammarToolInputProperties, options);\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason as \"stop\" | \"length\" | \"toolUse\", message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tfor (const block of output.content) {\n\t\t\t\t// Streaming scratch buffers are only used during parsing; never persist them.\n\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t\tdelete (block as { customInput?: unknown }).customInput;\n\t\t\t}\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = formatProviderError(normalizeProviderError(error));\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\nexport const streamSimple: StreamFunction<\"openai-codex-responses\", SimpleStreamOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey;\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, context, options, apiKey);\n\tconst clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;\n\tconst reasoningEffort = clampedReasoning === \"off\" ? undefined : clampedReasoning;\n\n\treturn stream(model, context, {\n\t\t...base,\n\t\treasoningEffort,\n\t} satisfies OpenAICodexResponsesOptions);\n};\n\n// ============================================================================\n// Request Building\n// ============================================================================\n\nfunction buildRequestBody(\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions: OpenAICodexResponsesOptions | undefined,\n\tcacheSessionId: string | undefined,\n\tgrammarToolInputProperties: ReadonlyMap<string, string> = createGrammarToolInputProperties(\n\t\tcontext.tools,\n\t\tmodel.compat?.supportsOpenAIGrammarTools ?? false,\n\t),\n): RequestBody {\n\tconst supportsStrictMode = model.compat?.supportsStrictMode ?? true;\n\tconst supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false;\n\tconst toolPlacement = splitDeferredTools(context, model.compat?.supportsToolSearch ?? false);\n\tconst messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {\n\t\tincludeSystemPrompt: false,\n\t\tgrammarToolInputProperties,\n\t\tdeferredTools: toolPlacement.deferred,\n\t\ttoolOptions: {\n\t\t\tstrict: null,\n\t\t\tsupportsStrictMode,\n\t\t\tsupportsOpenAIGrammarTools,\n\t\t},\n\t});\n\n\tconst body: RequestBody = {\n\t\tmodel: model.id,\n\t\tstore: false,\n\t\tstream: true,\n\t\tinstructions: context.systemPrompt || \"You are a helpful assistant.\",\n\t\tinput: messages,\n\t\ttext: { verbosity: options?.textVerbosity || \"low\" },\n\t\tinclude: [\"reasoning.encrypted_content\"],\n\t\tprompt_cache_key: cacheSessionId,\n\t\ttool_choice: options?.toolChoice ?? \"auto\",\n\t\tparallel_tool_calls: true,\n\t};\n\n\tif (options?.temperature !== undefined) {\n\t\tbody.temperature = options.temperature;\n\t}\n\n\tif (options?.serviceTier !== undefined) {\n\t\tbody.service_tier = options.serviceTier;\n\t}\n\n\tif (toolPlacement.immediate.length > 0) {\n\t\tbody.tools = convertResponsesTools(toolPlacement.immediate, {\n\t\t\tstrict: null,\n\t\t\tsupportsStrictMode,\n\t\t\tsupportsOpenAIGrammarTools,\n\t\t});\n\t}\n\n\tif (options?.reasoningEffort !== undefined) {\n\t\tconst effort =\n\t\t\toptions.reasoningEffort === \"none\"\n\t\t\t\t? (model.thinkingLevelMap?.off ?? \"none\")\n\t\t\t\t: (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort);\n\t\tif (effort !== null) {\n\t\t\tbody.reasoning = {\n\t\t\t\teffort,\n\t\t\t\tsummary: options.reasoningSummary ?? \"auto\",\n\t\t\t};\n\t\t}\n\t}\n\n\treturn body;\n}\n\nfunction getServiceTierCostMultiplier(\n\tmodel: Pick<Model<\"openai-codex-responses\">, \"id\">,\n\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n): number {\n\tswitch (serviceTier) {\n\t\tcase \"flex\":\n\t\t\treturn 0.5;\n\t\tcase \"priority\":\n\t\t\treturn model.id === \"gpt-5.5\" ? 2.5 : 2;\n\t\tdefault:\n\t\t\treturn 1;\n\t}\n}\n\nfunction applyServiceTierPricing(\n\tusage: Usage,\n\tserviceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\tmodel: Pick<Model<\"openai-codex-responses\">, \"id\">,\n) {\n\tconst multiplier = getServiceTierCostMultiplier(model, serviceTier);\n\tif (multiplier === 1) return;\n\n\tusage.cost.input *= multiplier;\n\tusage.cost.output *= multiplier;\n\tusage.cost.cacheRead *= multiplier;\n\tusage.cost.cacheWrite *= multiplier;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n}\n\nfunction resolveCodexServiceTier(\n\tresponseServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n\trequestServiceTier: ResponseCreateParamsStreaming[\"service_tier\"] | undefined,\n): ResponseCreateParamsStreaming[\"service_tier\"] | undefined {\n\tif (responseServiceTier === \"default\" && (requestServiceTier === \"flex\" || requestServiceTier === \"priority\")) {\n\t\treturn requestServiceTier;\n\t}\n\treturn responseServiceTier ?? requestServiceTier;\n}\n\nfunction resolveCodexUrl(baseUrl?: string): string {\n\tconst raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;\n\tconst normalized = raw.replace(/\\/+$/, \"\");\n\tif (normalized.endsWith(\"/codex/responses\")) return normalized;\n\tif (normalized.endsWith(\"/codex\")) return `${normalized}/responses`;\n\treturn `${normalized}/codex/responses`;\n}\n\nfunction resolveCodexWebSocketUrl(baseUrl?: string): string {\n\tconst url = new URL(resolveCodexUrl(baseUrl));\n\tif (url.protocol === \"https:\") url.protocol = \"wss:\";\n\tif (url.protocol === \"http:\") url.protocol = \"ws:\";\n\treturn url.toString();\n}\n\n// ============================================================================\n// Response Processing\n// ============================================================================\n\nasync function processStream(\n\tresponse: Response,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\tgrammarToolInputProperties: ReadonlyMap<string, string>,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tawait processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal)), output, stream, model, {\n\t\tserviceTier: options?.serviceTier,\n\t\tgrammarToolInputProperties,\n\t\tresolveServiceTier: resolveCodexServiceTier,\n\t\tapplyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),\n\t});\n}\n\nclass CodexApiError extends Error {\n\treadonly code?: string;\n\treadonly payload?: Record<string, unknown>;\n\n\tconstructor(message: string, options?: { code?: string; payload?: Record<string, unknown>; cause?: unknown }) {\n\t\tsuper(message);\n\t\tthis.name = \"CodexApiError\";\n\t\tthis.code = options?.code;\n\t\tthis.payload = options?.payload;\n\t\tthis.cause = options?.cause;\n\t}\n}\n\nclass CodexProtocolError extends Error {\n\treadonly payload?: unknown;\n\n\tconstructor(message: string, options?: { payload?: unknown; cause?: unknown }) {\n\t\tsuper(message);\n\t\tthis.name = \"CodexProtocolError\";\n\t\tthis.payload = options?.payload;\n\t\tthis.cause = options?.cause;\n\t}\n}\n\nfunction isCodexNonTransportError(error: unknown): boolean {\n\treturn error instanceof CodexApiError || error instanceof CodexProtocolError;\n}\n\nfunction isWebSocketConnectionLimitReachedError(error: unknown): boolean {\n\treturn error instanceof CodexApiError && error.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE;\n}\n\nfunction isPreviousResponseNotFoundError(error: unknown): boolean {\n\treturn error instanceof CodexApiError && error.code === PREVIOUS_RESPONSE_NOT_FOUND_CODE;\n}\n\nfunction extractCodexEventError(event: Record<string, unknown>): { code?: string; message?: string } {\n\tconst nested = event.error && typeof event.error === \"object\" ? (event.error as Record<string, unknown>) : undefined;\n\treturn {\n\t\tcode: typeof event.code === \"string\" ? event.code : typeof nested?.code === \"string\" ? nested.code : undefined,\n\t\tmessage:\n\t\t\ttypeof event.message === \"string\"\n\t\t\t\t? event.message\n\t\t\t\t: typeof nested?.message === \"string\"\n\t\t\t\t\t? nested.message\n\t\t\t\t\t: undefined,\n\t};\n}\n\nasync function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>): AsyncGenerator<ResponseStreamEvent> {\n\tfor await (const event of events) {\n\t\tconst type = typeof event.type === \"string\" ? event.type : undefined;\n\t\tif (!type) continue;\n\n\t\tif (type === \"error\") {\n\t\t\tconst { code, message } = extractCodexEventError(event);\n\t\t\tthrow new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {\n\t\t\t\tcode,\n\t\t\t\tpayload: event,\n\t\t\t});\n\t\t}\n\n\t\tif (type === \"response.failed\") {\n\t\t\tconst response = (event as { response?: { error?: { code?: string; message?: string } } }).response;\n\t\t\tconst code = response?.error?.code;\n\t\t\tconst message = response?.error?.message;\n\t\t\tthrow new CodexApiError(message || \"Codex response failed\", { code, payload: event });\n\t\t}\n\n\t\tif (type === \"response.done\" || type === \"response.completed\" || type === \"response.incomplete\") {\n\t\t\tconst response = (event as { response?: { status?: unknown } }).response;\n\t\t\tconst normalizedResponse = response\n\t\t\t\t? { ...response, status: normalizeCodexStatus(response.status) }\n\t\t\t\t: response;\n\t\t\tyield { ...event, type: \"response.completed\", response: normalizedResponse } as ResponseStreamEvent;\n\t\t\treturn;\n\t\t}\n\n\t\tyield event as unknown as ResponseStreamEvent;\n\t}\n}\n\nfunction normalizeCodexStatus(status: unknown): CodexResponseStatus | undefined {\n\tif (typeof status !== \"string\") return undefined;\n\treturn CODEX_RESPONSE_STATUSES.has(status as CodexResponseStatus) ? (status as CodexResponseStatus) : undefined;\n}\n\n// ============================================================================\n// SSE Parsing\n// ============================================================================\n\nasync function* parseSSE(response: Response, signal?: AbortSignal): AsyncGenerator<Record<string, unknown>> {\n\tif (!response.body) return;\n\n\tconst reader = response.body.getReader();\n\tconst decoder = new TextDecoder();\n\tlet buffer = \"\";\n\tconst onAbort = () => {\n\t\tvoid reader.cancel().catch(() => {});\n\t};\n\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\tlet idx = buffer.indexOf(\"\\n\\n\");\n\t\t\twhile (idx !== -1) {\n\t\t\t\tconst chunk = buffer.slice(0, idx);\n\t\t\t\tbuffer = buffer.slice(idx + 2);\n\n\t\t\t\tconst dataLines = chunk\n\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t.filter((l) => l.startsWith(\"data:\"))\n\t\t\t\t\t.map((l) => l.slice(5).trim());\n\t\t\t\tif (dataLines.length > 0) {\n\t\t\t\t\tconst data = dataLines.join(\"\\n\").trim();\n\t\t\t\t\tif (data && data !== \"[DONE]\") {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tyield JSON.parse(data) as Record<string, unknown>;\n\t\t\t\t\t\t} catch (cause) {\n\t\t\t\t\t\t\tthrow new CodexProtocolError(`Invalid Codex SSE JSON: ${formatThrownValue(cause)}`, {\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\tpayload: data,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tidx = buffer.indexOf(\"\\n\\n\");\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\ttry {\n\t\t\tawait reader.cancel();\n\t\t} catch {}\n\t\ttry {\n\t\t\treader.releaseLock();\n\t\t} catch {}\n\t}\n}\n\n// ============================================================================\n// WebSocket Parsing\n// ============================================================================\n\nconst OPENAI_BETA_RESPONSES_WEBSOCKETS = \"responses_websockets=2026-02-06\";\nconst SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;\nconst SESSION_WEBSOCKET_MAX_AGE_MS = 55 * 60 * 1000;\n\ntype WebSocketEventType = \"open\" | \"message\" | \"error\" | \"close\";\ntype WebSocketListener = (event: unknown) => void;\n\ninterface WebSocketLike {\n\tclose(code?: number, reason?: string): void;\n\tsend(data: string): void;\n\taddEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n\tremoveEventListener(type: WebSocketEventType, listener: WebSocketListener): void;\n}\n\ninterface CachedWebSocketContinuationState {\n\tlastRequestBody: RequestBody;\n\tlastResponseId: string;\n\tlastResponseItems: ResponseInput;\n}\n\ninterface CachedWebSocketConnection {\n\tsocket: WebSocketLike;\n\tbusy: boolean;\n\tcreatedAt: number;\n\tidleTimer?: ReturnType<typeof setTimeout>;\n\tcontinuation?: CachedWebSocketContinuationState;\n}\n\nexport interface OpenAICodexWebSocketDebugStats {\n\trequests: number;\n\tconnectionsCreated: number;\n\tconnectionsReused: number;\n\tcachedContextRequests: number;\n\tstoreTrueRequests: number;\n\tfullContextRequests: number;\n\tdeltaRequests: number;\n\tlastInputItems: number;\n\tlastDeltaInputItems?: number;\n\tlastPreviousResponseId?: string;\n\twebsocketFailures: number;\n\tsseFallbacks: number;\n\twebsocketFallbackActive?: boolean;\n\tlastWebSocketError?: string;\n}\n\nconst websocketSessionCache = new Map<string, CachedWebSocketConnection>();\nconst websocketDebugStats = new Map<string, OpenAICodexWebSocketDebugStats>();\nconst websocketSseFallbackSessions = new Set<string>();\n\nfunction getOrCreateWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats {\n\tlet stats = websocketDebugStats.get(sessionId);\n\tif (!stats) {\n\t\tstats = {\n\t\t\trequests: 0,\n\t\t\tconnectionsCreated: 0,\n\t\t\tconnectionsReused: 0,\n\t\t\tcachedContextRequests: 0,\n\t\t\tstoreTrueRequests: 0,\n\t\t\tfullContextRequests: 0,\n\t\t\tdeltaRequests: 0,\n\t\t\tlastInputItems: 0,\n\t\t\twebsocketFailures: 0,\n\t\t\tsseFallbacks: 0,\n\t\t};\n\t\twebsocketDebugStats.set(sessionId, stats);\n\t}\n\treturn stats;\n}\n\nexport function getOpenAICodexWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats | undefined {\n\tconst stats = websocketDebugStats.get(sessionId);\n\treturn stats ? { ...stats } : undefined;\n}\n\nexport function resetOpenAICodexWebSocketDebugStats(sessionId?: string): void {\n\tif (sessionId) {\n\t\twebsocketDebugStats.delete(sessionId);\n\t\twebsocketSseFallbackSessions.delete(sessionId);\n\t\treturn;\n\t}\n\twebsocketDebugStats.clear();\n\twebsocketSseFallbackSessions.clear();\n}\n\nexport function closeOpenAICodexWebSocketSessions(sessionId?: string): void {\n\tconst closeEntry = (entry: CachedWebSocketConnection) => {\n\t\tif (entry.idleTimer) clearTimeout(entry.idleTimer);\n\t\tcloseWebSocketSilently(entry.socket, 1000, \"debug_close\");\n\t};\n\tif (sessionId) {\n\t\tconst entry = websocketSessionCache.get(sessionId);\n\t\tif (entry) closeEntry(entry);\n\t\twebsocketSessionCache.delete(sessionId);\n\t\treturn;\n\t}\n\tfor (const entry of websocketSessionCache.values()) {\n\t\tcloseEntry(entry);\n\t}\n\twebsocketSessionCache.clear();\n}\n\nregisterSessionResourceCleanup(closeOpenAICodexWebSocketSessions);\n\nfunction isWebSocketSseFallbackActive(sessionId: string | undefined): boolean {\n\treturn sessionId ? websocketSseFallbackSessions.has(sessionId) : false;\n}\n\nfunction recordWebSocketSseFallback(sessionId: string | undefined): void {\n\tif (!sessionId) return;\n\tconst stats = getOrCreateWebSocketDebugStats(sessionId);\n\tstats.sseFallbacks++;\n\tstats.websocketFallbackActive = isWebSocketSseFallbackActive(sessionId);\n}\n\nfunction recordWebSocketFailure(sessionId: string | undefined, error: unknown): void {\n\tif (!sessionId) return;\n\twebsocketSseFallbackSessions.add(sessionId);\n\n\tconst stats = getOrCreateWebSocketDebugStats(sessionId);\n\tstats.websocketFailures++;\n\tstats.lastWebSocketError = formatThrownValue(error);\n\tstats.websocketFallbackActive = true;\n}\n\ntype WebSocketConstructor = new (\n\turl: string,\n\tprotocols?: string | string[] | { headers?: Record<string, string> },\n) => WebSocketLike;\n\nlet _cachedWebsocket: WebSocketConstructor | null = null;\nasync function getWebSocketConstructor(env?: ProviderEnv): Promise<WebSocketConstructor | null> {\n\tif (!env && _cachedWebsocket) return _cachedWebsocket;\n\n\t// bun doesn't respect http proxy envs, ref: https://github.com/oven-sh/bun/issues/15489\n\t// TODO: remove this when bun supports proxy envs in websocket.\n\tif (typeof process !== \"undefined\" && process.versions?.bun) {\n\t\tconst WebSocketWithProxy = class extends WebSocket {\n\t\t\tconstructor(url: string | URL, options?: string | string[] | Record<string, unknown>) {\n\t\t\t\tlet _opts: Record<string, unknown> = {};\n\t\t\t\tif (Array.isArray(options) || typeof options === \"string\") {\n\t\t\t\t\t_opts = { protocols: options };\n\t\t\t\t} else {\n\t\t\t\t\t_opts = { ...options };\n\t\t\t\t}\n\n\t\t\t\tconst proxyUrl = resolveHttpProxyUrlForTarget(\n\t\t\t\t\turl.toString().replace(/^wss:/, \"https:\").replace(/^ws:/, \"http:\"),\n\t\t\t\t\tenv,\n\t\t\t\t);\n\t\t\t\tsuper(url, { ..._opts, ...(proxyUrl ? { proxy: proxyUrl.toString() } : {}) } as any);\n\t\t\t}\n\t\t};\n\t\tif (!env) {\n\t\t\t_cachedWebsocket = WebSocketWithProxy;\n\t\t}\n\t\treturn WebSocketWithProxy;\n\t}\n\n\tconst ctor = (globalThis as { WebSocket?: unknown }).WebSocket;\n\tif (typeof ctor !== \"function\") return null;\n\treturn ctor as unknown as WebSocketConstructor;\n}\n\nclass WebSocketCloseError extends Error {\n\treadonly code?: number;\n\treadonly reason?: string;\n\treadonly wasClean?: boolean;\n\n\tconstructor(message: string, options?: { code?: number; reason?: string; wasClean?: boolean }) {\n\t\tsuper(message);\n\t\tthis.name = \"WebSocketCloseError\";\n\t\tthis.code = options?.code;\n\t\tthis.reason = options?.reason;\n\t\tthis.wasClean = options?.wasClean;\n\t}\n}\n\nfunction getWebSocketReadyState(socket: WebSocketLike): number | undefined {\n\tconst readyState = (socket as { readyState?: unknown }).readyState;\n\treturn typeof readyState === \"number\" ? readyState : undefined;\n}\n\nfunction isWebSocketReusable(socket: WebSocketLike): boolean {\n\tconst readyState = getWebSocketReadyState(socket);\n\t// If readyState is unavailable, assume the runtime keeps it open/reusable.\n\treturn readyState === undefined || readyState === 1;\n}\n\nfunction isWebSocketSessionExpired(entry: CachedWebSocketConnection): boolean {\n\treturn Date.now() - entry.createdAt >= SESSION_WEBSOCKET_MAX_AGE_MS;\n}\n\nfunction closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = \"done\"): void {\n\ttry {\n\t\tsocket.close(code, reason);\n\t} catch {}\n}\n\nfunction scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocketConnection): void {\n\tif (entry.idleTimer) {\n\t\tclearTimeout(entry.idleTimer);\n\t}\n\tentry.idleTimer = setTimeout(() => {\n\t\tif (entry.busy) return;\n\t\tcloseWebSocketSilently(entry.socket, 1000, \"idle_timeout\");\n\t\twebsocketSessionCache.delete(sessionId);\n\t}, SESSION_WEBSOCKET_CACHE_TTL_MS);\n}\n\nasync function connectWebSocket(\n\turl: string,\n\theaders: Headers,\n\tsignal?: AbortSignal,\n\tconnectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS,\n\tenv?: ProviderEnv,\n): Promise<WebSocketLike> {\n\tconst WebSocketCtor = await getWebSocketConstructor(env);\n\tif (!WebSocketCtor) {\n\t\tthrow new Error(\"WebSocket transport is not available in this runtime\");\n\t}\n\n\tconst wsHeaders = headersToRecord(headers);\n\tdelete wsHeaders[\"OpenAI-Beta\"];\n\n\treturn new Promise<WebSocketLike>((resolve, reject) => {\n\t\tlet settled = false;\n\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\tlet socket: WebSocketLike;\n\n\t\ttry {\n\t\t\tsocket = new WebSocketCtor(url, { headers: wsHeaders });\n\t\t} catch (error) {\n\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\treturn;\n\t\t}\n\n\t\tconst cleanup = () => {\n\t\t\tif (timeout) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\ttimeout = undefined;\n\t\t\t}\n\t\t\tsocket.removeEventListener(\"open\", onOpen);\n\t\t\tsocket.removeEventListener(\"error\", onError);\n\t\t\tsocket.removeEventListener(\"close\", onClose);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\t\tconst fail = (error: Error, closeReason?: string) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tif (closeReason) {\n\t\t\t\tcloseWebSocketSilently(socket, 1000, closeReason);\n\t\t\t}\n\t\t\treject(error);\n\t\t};\n\t\tconst onOpen: WebSocketListener = () => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(socket);\n\t\t};\n\t\tconst onError: WebSocketListener = (event) => {\n\t\t\tfail(extractWebSocketError(event));\n\t\t};\n\t\tconst onClose: WebSocketListener = (event) => {\n\t\t\tfail(extractWebSocketCloseError(event));\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tfail(new Error(\"Request was aborted\"), \"aborted\");\n\t\t};\n\n\t\tsocket.addEventListener(\"open\", onOpen);\n\t\tsocket.addEventListener(\"error\", onError);\n\t\tsocket.addEventListener(\"close\", onClose);\n\t\tsignal?.addEventListener(\"abort\", onAbort);\n\n\t\tif (connectTimeoutMs > 0) {\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\tfail(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`), \"connect_timeout\");\n\t\t\t}, connectTimeoutMs);\n\t\t}\n\t\tif (signal?.aborted) {\n\t\t\tonAbort();\n\t\t}\n\t});\n}\n\nasync function acquireWebSocket(\n\turl: string,\n\theaders: Headers,\n\tsessionId: string | undefined,\n\tsignal?: AbortSignal,\n\tconnectTimeoutMs?: number,\n\tenv?: ProviderEnv,\n): Promise<{\n\tsocket: WebSocketLike;\n\tentry?: CachedWebSocketConnection;\n\treused: boolean;\n\trelease: (options?: { keep?: boolean }) => void;\n}> {\n\tif (!sessionId) {\n\t\tconst socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env);\n\t\treturn {\n\t\t\tsocket,\n\t\t\treused: false,\n\t\t\trelease: () => closeWebSocketSilently(socket),\n\t\t};\n\t}\n\n\tconst cached = websocketSessionCache.get(sessionId);\n\tif (cached) {\n\t\tif (cached.idleTimer) {\n\t\t\tclearTimeout(cached.idleTimer);\n\t\t\tcached.idleTimer = undefined;\n\t\t}\n\t\tif (!cached.busy && isWebSocketSessionExpired(cached)) {\n\t\t\tcloseWebSocketSilently(cached.socket, 1000, \"connection_age_limit\");\n\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t} else if (!cached.busy && isWebSocketReusable(cached.socket)) {\n\t\t\tcached.busy = true;\n\t\t\treturn {\n\t\t\t\tsocket: cached.socket,\n\t\t\t\tentry: cached,\n\t\t\t\treused: true,\n\t\t\t\trelease: ({ keep } = {}) => {\n\t\t\t\t\tif (!keep || !isWebSocketReusable(cached.socket)) {\n\t\t\t\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tcached.busy = false;\n\t\t\t\t\tscheduleSessionWebSocketExpiry(sessionId, cached);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (cached.busy) {\n\t\t\tconst socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env);\n\t\t\treturn {\n\t\t\t\tsocket,\n\t\t\t\treused: false,\n\t\t\t\trelease: () => {\n\t\t\t\t\tcloseWebSocketSilently(socket);\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (!isWebSocketReusable(cached.socket)) {\n\t\t\tcloseWebSocketSilently(cached.socket);\n\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t}\n\t}\n\n\tconst socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env);\n\tconst entry: CachedWebSocketConnection = { socket, busy: true, createdAt: Date.now() };\n\twebsocketSessionCache.set(sessionId, entry);\n\treturn {\n\t\tsocket,\n\t\tentry,\n\t\treused: false,\n\t\trelease: ({ keep } = {}) => {\n\t\t\tif (!keep || !isWebSocketReusable(entry.socket)) {\n\t\t\t\tcloseWebSocketSilently(entry.socket);\n\t\t\t\tif (entry.idleTimer) clearTimeout(entry.idleTimer);\n\t\t\t\tif (websocketSessionCache.get(sessionId) === entry) {\n\t\t\t\t\twebsocketSessionCache.delete(sessionId);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tentry.busy = false;\n\t\t\tscheduleSessionWebSocketExpiry(sessionId, entry);\n\t\t},\n\t};\n}\n\nfunction extractWebSocketError(event: unknown): Error {\n\tif (event && typeof event === \"object\") {\n\t\tconst message = \"message\" in event ? (event as { message?: unknown }).message : undefined;\n\t\tif (typeof message === \"string\" && message.length > 0) {\n\t\t\treturn new Error(message);\n\t\t}\n\n\t\tconst nestedError = \"error\" in event ? (event as { error?: unknown }).error : undefined;\n\t\tif (nestedError instanceof Error && nestedError.message.length > 0) {\n\t\t\treturn nestedError;\n\t\t}\n\t\tif (nestedError && typeof nestedError === \"object\" && \"message\" in nestedError) {\n\t\t\tconst nestedMessage = (nestedError as { message?: unknown }).message;\n\t\t\tif (typeof nestedMessage === \"string\" && nestedMessage.length > 0) {\n\t\t\t\treturn new Error(nestedMessage);\n\t\t\t}\n\t\t}\n\t}\n\treturn new Error(\"WebSocket error\");\n}\n\nfunction extractWebSocketCloseError(event: unknown): Error {\n\tif (event && typeof event === \"object\") {\n\t\tconst code = \"code\" in event ? (event as { code?: unknown }).code : undefined;\n\t\tconst reason = \"reason\" in event ? (event as { reason?: unknown }).reason : undefined;\n\t\tconst wasClean = \"wasClean\" in event ? (event as { wasClean?: unknown }).wasClean : undefined;\n\t\tconst codeText = typeof code === \"number\" ? ` ${code}` : \"\";\n\t\tlet reasonText = typeof reason === \"string\" && reason.length > 0 ? ` ${reason}` : \"\";\n\t\tif (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) {\n\t\t\treasonText = \" message too big\";\n\t\t}\n\t\treturn new WebSocketCloseError(`WebSocket closed${codeText}${reasonText}`.trim(), {\n\t\t\tcode: typeof code === \"number\" ? code : undefined,\n\t\t\treason: typeof reason === \"string\" && reason.length > 0 ? reason : undefined,\n\t\t\twasClean: typeof wasClean === \"boolean\" ? wasClean : undefined,\n\t\t});\n\t}\n\treturn new Error(\"WebSocket closed\");\n}\n\nasync function decodeWebSocketData(data: unknown): Promise<string | null> {\n\tif (typeof data === \"string\") return data;\n\tif (data instanceof ArrayBuffer) {\n\t\treturn new TextDecoder().decode(new Uint8Array(data));\n\t}\n\tif (ArrayBuffer.isView(data)) {\n\t\tconst view = data as ArrayBufferView;\n\t\treturn new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));\n\t}\n\tif (data && typeof data === \"object\" && \"arrayBuffer\" in data) {\n\t\tconst blobLike = data as { arrayBuffer: () => Promise<ArrayBuffer> };\n\t\tconst arrayBuffer = await blobLike.arrayBuffer();\n\t\treturn new TextDecoder().decode(new Uint8Array(arrayBuffer));\n\t}\n\treturn null;\n}\n\nasync function* parseWebSocket(\n\tsocket: WebSocketLike,\n\tsignal?: AbortSignal,\n\tidleTimeoutMs?: number,\n): AsyncGenerator<Record<string, unknown>> {\n\tconst queue: Record<string, unknown>[] = [];\n\tlet pending: (() => void) | null = null;\n\tlet done = false;\n\tlet failed: Error | null = null;\n\tlet sawCompletion = false;\n\n\tconst wake = () => {\n\t\tif (!pending) return;\n\t\tconst resolve = pending;\n\t\tpending = null;\n\t\tresolve();\n\t};\n\n\tconst onMessage: WebSocketListener = (event) => {\n\t\tvoid (async () => {\n\t\t\tlet text: string | null = null;\n\t\t\ttry {\n\t\t\t\tif (!event || typeof event !== \"object\" || !(\"data\" in event)) return;\n\t\t\t\ttext = await decodeWebSocketData((event as { data?: unknown }).data);\n\t\t\t\tif (!text) return;\n\t\t\t\tconst parsed = JSON.parse(text) as Record<string, unknown>;\n\t\t\t\tconst type = typeof parsed.type === \"string\" ? parsed.type : \"\";\n\t\t\t\tif (type === \"response.completed\" || type === \"response.done\" || type === \"response.incomplete\") {\n\t\t\t\t\tsawCompletion = true;\n\t\t\t\t\tdone = true;\n\t\t\t\t}\n\t\t\t\tqueue.push(parsed);\n\t\t\t\twake();\n\t\t\t} catch (cause) {\n\t\t\t\tfailed = new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, {\n\t\t\t\t\tcause,\n\t\t\t\t\tpayload: text,\n\t\t\t\t});\n\t\t\t\tdone = true;\n\t\t\t\twake();\n\t\t\t}\n\t\t})();\n\t};\n\n\tconst onError: WebSocketListener = (event) => {\n\t\tfailed = extractWebSocketError(event);\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onClose: WebSocketListener = (event) => {\n\t\tif (sawCompletion) {\n\t\t\tdone = true;\n\t\t\twake();\n\t\t\treturn;\n\t\t}\n\t\tif (!failed) {\n\t\t\tfailed = extractWebSocketCloseError(event);\n\t\t}\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tconst onAbort = () => {\n\t\tfailed = new Error(\"Request was aborted\");\n\t\tdone = true;\n\t\twake();\n\t};\n\n\tsocket.addEventListener(\"message\", onMessage);\n\tsocket.addEventListener(\"error\", onError);\n\tsocket.addEventListener(\"close\", onClose);\n\tsignal?.addEventListener(\"abort\", onAbort);\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\t\t\tif (queue.length > 0) {\n\t\t\t\tyield queue.shift()!;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (done) break;\n\t\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tpending = resolve;\n\t\t\t\tif (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {\n\t\t\t\t\ttimeout = setTimeout(() => {\n\t\t\t\t\t\tconst error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);\n\t\t\t\t\t\tfailed = error;\n\t\t\t\t\t\tdone = true;\n\t\t\t\t\t\tpending = null;\n\t\t\t\t\t\tcloseWebSocketSilently(socket, 1000, \"idle_timeout\");\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}, idleTimeoutMs);\n\t\t\t\t}\n\t\t\t}).finally(() => {\n\t\t\t\tif (timeout) {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (failed) {\n\t\t\tthrow failed;\n\t\t}\n\t\tif (!sawCompletion) {\n\t\t\tthrow new Error(\"WebSocket stream closed before response.completed\");\n\t\t}\n\t} finally {\n\t\tsocket.removeEventListener(\"message\", onMessage);\n\t\tsocket.removeEventListener(\"error\", onError);\n\t\tsocket.removeEventListener(\"close\", onClose);\n\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t}\n}\n\nfunction requestBodyWithoutInput(body: RequestBody): RequestBody {\n\tconst { input: _input, previous_response_id: _previousResponseId, ...rest } = body;\n\treturn rest;\n}\n\nfunction responseInputsEqual(a: ResponseInput | undefined, b: ResponseInput | undefined): boolean {\n\treturn JSON.stringify(a ?? []) === JSON.stringify(b ?? []);\n}\n\nfunction requestBodiesMatchExceptInput(a: RequestBody, b: RequestBody): boolean {\n\treturn JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));\n}\n\nfunction getCachedWebSocketInputDelta(\n\tbody: RequestBody,\n\tcontinuation: CachedWebSocketContinuationState,\n): ResponseInput | undefined {\n\tif (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {\n\t\treturn undefined;\n\t}\n\n\tconst currentInput = body.input ?? [];\n\tconst baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];\n\tif (currentInput.length < baseline.length) {\n\t\treturn undefined;\n\t}\n\n\tconst prefix = currentInput.slice(0, baseline.length);\n\tif (!responseInputsEqual(prefix, baseline)) {\n\t\treturn undefined;\n\t}\n\n\treturn currentInput.slice(baseline.length);\n}\n\nfunction buildCachedWebSocketRequestBody(entry: CachedWebSocketConnection, body: RequestBody): RequestBody {\n\tconst continuation = entry.continuation;\n\tif (!continuation) {\n\t\treturn body;\n\t}\n\n\tconst delta = getCachedWebSocketInputDelta(body, continuation);\n\tif (!delta || !continuation.lastResponseId) {\n\t\tentry.continuation = undefined;\n\t\treturn body;\n\t}\n\n\treturn {\n\t\t...body,\n\t\tprevious_response_id: continuation.lastResponseId,\n\t\tinput: delta,\n\t};\n}\n\nasync function* startWebSocketOutputOnFirstEvent(\n\tevents: AsyncIterable<ResponseStreamEvent>,\n\tonStart: () => void,\n): AsyncGenerator<ResponseStreamEvent> {\n\tlet started = false;\n\tfor await (const event of events) {\n\t\tif (!started) {\n\t\t\tstarted = true;\n\t\t\tonStart();\n\t\t}\n\t\tyield event;\n\t}\n}\n\nasync function processWebSocketStream(\n\turl: string,\n\tbody: RequestBody,\n\theaders: Headers,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n\tonStart: () => void,\n\tidleTimeoutMs: number | undefined,\n\twebsocketConnectTimeoutMs: number | undefined,\n\tcacheSessionId: string | undefined,\n\tgrammarToolInputProperties: ReadonlyMap<string, string>,\n\toptions?: OpenAICodexResponsesOptions,\n): Promise<void> {\n\tconst { socket, entry, reused, release } = await acquireWebSocket(\n\t\turl,\n\t\theaders,\n\t\tcacheSessionId,\n\t\toptions?.signal,\n\t\twebsocketConnectTimeoutMs,\n\t\toptions?.env,\n\t);\n\tlet keepConnection = true;\n\tconst useCachedContext = options?.transport === \"websocket-cached\" || options?.transport === \"auto\";\n\t// ChatGPT Codex Responses rejects `store: true` (\"Store must be set to false\").\n\t// WebSocket continuation still works via connection-scoped previous_response_id state.\n\tconst fullBody = body;\n\tconst requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;\n\tconst stats = cacheSessionId ? getOrCreateWebSocketDebugStats(cacheSessionId) : undefined;\n\tif (stats) {\n\t\tstats.requests++;\n\t\tif (reused) stats.connectionsReused++;\n\t\telse stats.connectionsCreated++;\n\t\tif (useCachedContext) stats.cachedContextRequests++;\n\t\tif (requestBody.store === true) stats.storeTrueRequests++;\n\t\tstats.lastInputItems = requestBody.input?.length ?? 0;\n\t\tif (requestBody.previous_response_id) {\n\t\t\tstats.deltaRequests++;\n\t\t\tstats.lastDeltaInputItems = requestBody.input?.length ?? 0;\n\t\t\tstats.lastPreviousResponseId = requestBody.previous_response_id;\n\t\t} else {\n\t\t\tstats.fullContextRequests++;\n\t\t\tstats.lastDeltaInputItems = undefined;\n\t\t\tstats.lastPreviousResponseId = undefined;\n\t\t}\n\t}\n\ttry {\n\t\tsocket.send(JSON.stringify({ type: \"response.create\", ...requestBody }));\n\t\tawait processResponsesStream(\n\t\t\tstartWebSocketOutputOnFirstEvent(\n\t\t\t\tmapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)),\n\t\t\t\tonStart,\n\t\t\t),\n\t\t\toutput,\n\t\t\tstream,\n\t\t\tmodel,\n\t\t\t{\n\t\t\t\tserviceTier: options?.serviceTier,\n\t\t\t\tgrammarToolInputProperties,\n\t\t\t\tresolveServiceTier: resolveCodexServiceTier,\n\t\t\t\tapplyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),\n\t\t\t},\n\t\t);\n\t\tif (options?.signal?.aborted) {\n\t\t\tkeepConnection = false;\n\t\t} else if (useCachedContext && entry && output.responseId) {\n\t\t\tconst responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {\n\t\t\t\tincludeSystemPrompt: false,\n\t\t\t\tgrammarToolInputProperties,\n\t\t\t}).filter((item) => item.type !== \"function_call_output\" && item.type !== \"custom_tool_call_output\");\n\t\t\tentry.continuation = {\n\t\t\t\tlastRequestBody: fullBody,\n\t\t\t\tlastResponseId: output.responseId,\n\t\t\t\tlastResponseItems: responseItems,\n\t\t\t};\n\t\t}\n\t} catch (error) {\n\t\tif (entry) {\n\t\t\tentry.continuation = undefined;\n\t\t}\n\t\tkeepConnection = false;\n\t\tthrow error;\n\t} finally {\n\t\trelease({ keep: keepConnection });\n\t}\n}\n\n// ============================================================================\n// Error Handling\n// ============================================================================\n\nasync function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {\n\tconst raw = await response.text();\n\tlet message = raw || response.statusText || \"Request failed\";\n\tlet friendlyMessage: string | undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as {\n\t\t\terror?: { code?: string; type?: string; message?: string; plan_type?: string; resets_at?: number };\n\t\t};\n\t\tconst err = parsed?.error;\n\t\tif (err) {\n\t\t\tconst code = err.code || err.type || \"\";\n\t\t\tif (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {\n\t\t\t\tconst plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : \"\";\n\t\t\t\tconst mins = err.resets_at\n\t\t\t\t\t? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000))\n\t\t\t\t\t: undefined;\n\t\t\t\tconst when = mins !== undefined ? ` Try again in ~${mins} min.` : \"\";\n\t\t\t\tfriendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();\n\t\t\t}\n\t\t\tmessage = err.message || friendlyMessage || message;\n\t\t}\n\t} catch {}\n\n\treturn { message, friendlyMessage };\n}\n\n// ============================================================================\n// Auth & Headers\n// ============================================================================\n\nfunction extractAccountId(token: string): string {\n\ttry {\n\t\tconst parts = token.split(\".\");\n\t\tif (parts.length !== 3) throw new Error(\"Invalid token\");\n\t\tconst payload = JSON.parse(atob(parts[1]));\n\t\tconst accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;\n\t\tif (!accountId) throw new Error(\"No account ID in token\");\n\t\treturn accountId;\n\t} catch {\n\t\tthrow new Error(\"Failed to extract accountId from token\");\n\t}\n}\n\nfunction buildBaseCodexHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: ProviderHeaders | undefined,\n\taccountId: string,\n\ttoken: string,\n): Headers {\n\tconst headers = new Headers(initHeaders);\n\tfor (const [key, value] of Object.entries(additionalHeaders || {})) {\n\t\tif (value === null) {\n\t\t\theaders.delete(key);\n\t\t} else {\n\t\t\theaders.set(key, value);\n\t\t}\n\t}\n\theaders.set(\"Authorization\", `Bearer ${token}`);\n\theaders.set(\"chatgpt-account-id\", accountId);\n\theaders.set(\"originator\", \"pi\");\n\tconst userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : \"pi (browser)\";\n\theaders.set(\"User-Agent\", userAgent);\n\treturn headers;\n}\n\nfunction buildSSEHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: ProviderHeaders | undefined,\n\taccountId: string,\n\ttoken: string,\n\tsessionId?: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.set(\"OpenAI-Beta\", \"responses=experimental\");\n\theaders.set(\"accept\", \"text/event-stream\");\n\theaders.set(\"content-type\", \"application/json\");\n\n\tif (sessionId) {\n\t\theaders.set(\"session-id\", sessionId);\n\t\theaders.set(\"x-client-request-id\", sessionId);\n\t}\n\n\treturn headers;\n}\n\nfunction buildWebSocketHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: ProviderHeaders | undefined,\n\taccountId: string,\n\ttoken: string,\n\trequestId: string,\n): Headers {\n\tconst headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);\n\theaders.delete(\"accept\");\n\theaders.delete(\"content-type\");\n\theaders.delete(\"OpenAI-Beta\");\n\theaders.delete(\"openai-beta\");\n\theaders.set(\"OpenAI-Beta\", OPENAI_BETA_RESPONSES_WEBSOCKETS);\n\theaders.set(\"x-client-request-id\", requestId);\n\theaders.set(\"session-id\", requestId);\n\treturn headers;\n}\n"]}
|
|
@@ -16,6 +16,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
|
16
16
|
import { headersToRecord } from "../utils/headers.js";
|
|
17
17
|
import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.js";
|
|
18
18
|
import { uuidv7 } from "../utils/uuid.js";
|
|
19
|
+
import { createGrammarToolInputProperties } from "./constrained-sampling.js";
|
|
19
20
|
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
|
20
21
|
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
|
|
21
22
|
import { buildBaseOptions } from "./simple-options.js";
|
|
@@ -34,6 +35,7 @@ const REQUEST_COMPRESSION_ZSTD_LEVEL = 3;
|
|
|
34
35
|
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
35
36
|
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
|
|
36
37
|
const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached";
|
|
38
|
+
const PREVIOUS_RESPONSE_NOT_FOUND_CODE = "previous_response_not_found";
|
|
37
39
|
const CODEX_RESPONSE_STATUSES = new Set([
|
|
38
40
|
"completed",
|
|
39
41
|
"incomplete",
|
|
@@ -79,9 +81,14 @@ function getRetryAfterDelayMs(headers) {
|
|
|
79
81
|
}
|
|
80
82
|
return undefined;
|
|
81
83
|
}
|
|
82
|
-
|
|
84
|
+
class RetryDelayExceededError extends Error {
|
|
85
|
+
}
|
|
86
|
+
function validateRetryDelayMs(delayMs, options) {
|
|
83
87
|
const maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
84
|
-
|
|
88
|
+
if (maxRetryDelayMs > 0 && delayMs > maxRetryDelayMs) {
|
|
89
|
+
throw new RetryDelayExceededError(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxRetryDelayMs / 1000)}s)`);
|
|
90
|
+
}
|
|
91
|
+
return delayMs;
|
|
85
92
|
}
|
|
86
93
|
function sleep(ms, signal) {
|
|
87
94
|
return new Promise((resolve, reject) => {
|
|
@@ -157,12 +164,14 @@ export const stream = (model, context, options) => {
|
|
|
157
164
|
throw new Error(`No API key for provider: ${model.provider}`);
|
|
158
165
|
}
|
|
159
166
|
const accountId = extractAccountId(apiKey);
|
|
160
|
-
|
|
167
|
+
const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, model.compat?.supportsOpenAIGrammarTools ?? false);
|
|
168
|
+
const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId;
|
|
169
|
+
const codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId);
|
|
170
|
+
let body = buildRequestBody(model, context, options, codexSessionId, grammarToolInputProperties);
|
|
161
171
|
const nextBody = await options?.onPayload?.(body, model);
|
|
162
172
|
if (nextBody !== undefined) {
|
|
163
173
|
body = nextBody;
|
|
164
174
|
}
|
|
165
|
-
const codexSessionId = clampOpenAIPromptCacheKey(options?.sessionId);
|
|
166
175
|
const websocketRequestId = codexSessionId || uuidv7();
|
|
167
176
|
const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, codexSessionId);
|
|
168
177
|
const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
|
|
@@ -170,19 +179,25 @@ export const stream = (model, context, options) => {
|
|
|
170
179
|
const httpTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
|
|
171
180
|
const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
|
|
172
181
|
const transport = options?.transport || "auto";
|
|
173
|
-
|
|
182
|
+
let startEmitted = false;
|
|
183
|
+
const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(cacheSessionId);
|
|
174
184
|
if (websocketDisabledForSession) {
|
|
175
|
-
recordWebSocketSseFallback(
|
|
185
|
+
recordWebSocketSseFallback(cacheSessionId);
|
|
176
186
|
}
|
|
177
187
|
if (transport !== "sse" && !websocketDisabledForSession) {
|
|
178
188
|
let websocketStarted = false;
|
|
179
189
|
let retriedWebSocketConnectionLimit = false;
|
|
190
|
+
let retriedMissingWebSocketContinuation = false;
|
|
180
191
|
while (true) {
|
|
181
192
|
websocketStarted = false;
|
|
182
193
|
try {
|
|
183
194
|
await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
|
|
184
195
|
websocketStarted = true;
|
|
185
|
-
|
|
196
|
+
if (!startEmitted) {
|
|
197
|
+
startEmitted = true;
|
|
198
|
+
stream.push({ type: "start", partial: output });
|
|
199
|
+
}
|
|
200
|
+
}, httpTimeoutMs, websocketConnectTimeoutMs, cacheSessionId, grammarToolInputProperties, options);
|
|
186
201
|
if (options?.signal?.aborted) {
|
|
187
202
|
throw new Error("Request was aborted");
|
|
188
203
|
}
|
|
@@ -197,6 +212,11 @@ export const stream = (model, context, options) => {
|
|
|
197
212
|
catch (error) {
|
|
198
213
|
const aborted = options?.signal?.aborted;
|
|
199
214
|
const connectionLimitBeforeStart = !websocketStarted && isWebSocketConnectionLimitReachedError(error);
|
|
215
|
+
const previousResponseNotFound = isPreviousResponseNotFoundError(error);
|
|
216
|
+
if (!aborted && previousResponseNotFound && !retriedMissingWebSocketContinuation) {
|
|
217
|
+
retriedMissingWebSocketContinuation = true;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
200
220
|
if (!aborted && connectionLimitBeforeStart && !retriedWebSocketConnectionLimit) {
|
|
201
221
|
retriedWebSocketConnectionLimit = true;
|
|
202
222
|
continue;
|
|
@@ -211,11 +231,11 @@ export const stream = (model, context, options) => {
|
|
|
211
231
|
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
|
|
212
232
|
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
213
233
|
}));
|
|
214
|
-
recordWebSocketFailure(
|
|
234
|
+
recordWebSocketFailure(cacheSessionId, error);
|
|
215
235
|
if (websocketStarted) {
|
|
216
236
|
throw error;
|
|
217
237
|
}
|
|
218
|
-
recordWebSocketSseFallback(
|
|
238
|
+
recordWebSocketSseFallback(cacheSessionId);
|
|
219
239
|
break;
|
|
220
240
|
}
|
|
221
241
|
}
|
|
@@ -265,9 +285,7 @@ export const stream = (model, context, options) => {
|
|
|
265
285
|
const retryAfterDelayMs = getRetryAfterDelayMs(response.headers);
|
|
266
286
|
const delayMs = retryAfterDelayMs === undefined
|
|
267
287
|
? BASE_DELAY_MS * 2 ** attempt
|
|
268
|
-
:
|
|
269
|
-
? capRetryDelayMs(retryAfterDelayMs, options)
|
|
270
|
-
: retryAfterDelayMs;
|
|
288
|
+
: validateRetryDelayMs(retryAfterDelayMs, options);
|
|
271
289
|
await sleep(delayMs, options?.signal);
|
|
272
290
|
continue;
|
|
273
291
|
}
|
|
@@ -287,7 +305,9 @@ export const stream = (model, context, options) => {
|
|
|
287
305
|
}
|
|
288
306
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
289
307
|
// Network errors are retryable
|
|
290
|
-
if (attempt < maxRetries &&
|
|
308
|
+
if (attempt < maxRetries &&
|
|
309
|
+
!(lastError instanceof RetryDelayExceededError) &&
|
|
310
|
+
!lastError.message.includes("usage limit")) {
|
|
291
311
|
const delayMs = BASE_DELAY_MS * 2 ** attempt;
|
|
292
312
|
await sleep(delayMs, options?.signal);
|
|
293
313
|
continue;
|
|
@@ -301,8 +321,11 @@ export const stream = (model, context, options) => {
|
|
|
301
321
|
if (!response.body) {
|
|
302
322
|
throw new Error("No response body");
|
|
303
323
|
}
|
|
304
|
-
|
|
305
|
-
|
|
324
|
+
if (!startEmitted) {
|
|
325
|
+
startEmitted = true;
|
|
326
|
+
stream.push({ type: "start", partial: output });
|
|
327
|
+
}
|
|
328
|
+
await processStream(response, output, stream, model, grammarToolInputProperties, options);
|
|
306
329
|
if (options?.signal?.aborted) {
|
|
307
330
|
throw new Error("Request was aborted");
|
|
308
331
|
}
|
|
@@ -311,8 +334,9 @@ export const stream = (model, context, options) => {
|
|
|
311
334
|
}
|
|
312
335
|
catch (error) {
|
|
313
336
|
for (const block of output.content) {
|
|
314
|
-
//
|
|
337
|
+
// Streaming scratch buffers are only used during parsing; never persist them.
|
|
315
338
|
delete block.partialJson;
|
|
339
|
+
delete block.customInput;
|
|
316
340
|
}
|
|
317
341
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
318
342
|
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
|
@@ -338,11 +362,19 @@ export const streamSimple = (model, context, options) => {
|
|
|
338
362
|
// ============================================================================
|
|
339
363
|
// Request Building
|
|
340
364
|
// ============================================================================
|
|
341
|
-
function buildRequestBody(model, context, options) {
|
|
365
|
+
function buildRequestBody(model, context, options, cacheSessionId, grammarToolInputProperties = createGrammarToolInputProperties(context.tools, model.compat?.supportsOpenAIGrammarTools ?? false)) {
|
|
366
|
+
const supportsStrictMode = model.compat?.supportsStrictMode ?? true;
|
|
367
|
+
const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false;
|
|
342
368
|
const toolPlacement = splitDeferredTools(context, model.compat?.supportsToolSearch ?? false);
|
|
343
369
|
const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
|
|
344
370
|
includeSystemPrompt: false,
|
|
371
|
+
grammarToolInputProperties,
|
|
345
372
|
deferredTools: toolPlacement.deferred,
|
|
373
|
+
toolOptions: {
|
|
374
|
+
strict: null,
|
|
375
|
+
supportsStrictMode,
|
|
376
|
+
supportsOpenAIGrammarTools,
|
|
377
|
+
},
|
|
346
378
|
});
|
|
347
379
|
const body = {
|
|
348
380
|
model: model.id,
|
|
@@ -352,7 +384,7 @@ function buildRequestBody(model, context, options) {
|
|
|
352
384
|
input: messages,
|
|
353
385
|
text: { verbosity: options?.textVerbosity || "low" },
|
|
354
386
|
include: ["reasoning.encrypted_content"],
|
|
355
|
-
prompt_cache_key:
|
|
387
|
+
prompt_cache_key: cacheSessionId,
|
|
356
388
|
tool_choice: options?.toolChoice ?? "auto",
|
|
357
389
|
parallel_tool_calls: true,
|
|
358
390
|
};
|
|
@@ -363,7 +395,11 @@ function buildRequestBody(model, context, options) {
|
|
|
363
395
|
body.service_tier = options.serviceTier;
|
|
364
396
|
}
|
|
365
397
|
if (toolPlacement.immediate.length > 0) {
|
|
366
|
-
body.tools = convertResponsesTools(toolPlacement.immediate, {
|
|
398
|
+
body.tools = convertResponsesTools(toolPlacement.immediate, {
|
|
399
|
+
strict: null,
|
|
400
|
+
supportsStrictMode,
|
|
401
|
+
supportsOpenAIGrammarTools,
|
|
402
|
+
});
|
|
367
403
|
}
|
|
368
404
|
if (options?.reasoningEffort !== undefined) {
|
|
369
405
|
const effort = options.reasoningEffort === "none"
|
|
@@ -424,9 +460,10 @@ function resolveCodexWebSocketUrl(baseUrl) {
|
|
|
424
460
|
// ============================================================================
|
|
425
461
|
// Response Processing
|
|
426
462
|
// ============================================================================
|
|
427
|
-
async function processStream(response, output, stream, model, options) {
|
|
463
|
+
async function processStream(response, output, stream, model, grammarToolInputProperties, options) {
|
|
428
464
|
await processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal)), output, stream, model, {
|
|
429
465
|
serviceTier: options?.serviceTier,
|
|
466
|
+
grammarToolInputProperties,
|
|
430
467
|
resolveServiceTier: resolveCodexServiceTier,
|
|
431
468
|
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
|
|
432
469
|
});
|
|
@@ -457,6 +494,9 @@ function isCodexNonTransportError(error) {
|
|
|
457
494
|
function isWebSocketConnectionLimitReachedError(error) {
|
|
458
495
|
return error instanceof CodexApiError && error.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE;
|
|
459
496
|
}
|
|
497
|
+
function isPreviousResponseNotFoundError(error) {
|
|
498
|
+
return error instanceof CodexApiError && error.code === PREVIOUS_RESPONSE_NOT_FOUND_CODE;
|
|
499
|
+
}
|
|
460
500
|
function extractCodexEventError(event) {
|
|
461
501
|
const nested = event.error && typeof event.error === "object" ? event.error : undefined;
|
|
462
502
|
return {
|
|
@@ -1062,26 +1102,25 @@ function buildCachedWebSocketRequestBody(entry, body) {
|
|
|
1062
1102
|
input: delta,
|
|
1063
1103
|
};
|
|
1064
1104
|
}
|
|
1065
|
-
async function* startWebSocketOutputOnFirstEvent(events,
|
|
1105
|
+
async function* startWebSocketOutputOnFirstEvent(events, onStart) {
|
|
1066
1106
|
let started = false;
|
|
1067
1107
|
for await (const event of events) {
|
|
1068
1108
|
if (!started) {
|
|
1069
1109
|
started = true;
|
|
1070
1110
|
onStart();
|
|
1071
|
-
stream.push({ type: "start", partial: output });
|
|
1072
1111
|
}
|
|
1073
1112
|
yield event;
|
|
1074
1113
|
}
|
|
1075
1114
|
}
|
|
1076
|
-
async function processWebSocketStream(url, body, headers, output, stream, model, onStart, idleTimeoutMs, websocketConnectTimeoutMs, options) {
|
|
1077
|
-
const { socket, entry, reused, release } = await acquireWebSocket(url, headers,
|
|
1115
|
+
async function processWebSocketStream(url, body, headers, output, stream, model, onStart, idleTimeoutMs, websocketConnectTimeoutMs, cacheSessionId, grammarToolInputProperties, options) {
|
|
1116
|
+
const { socket, entry, reused, release } = await acquireWebSocket(url, headers, cacheSessionId, options?.signal, websocketConnectTimeoutMs, options?.env);
|
|
1078
1117
|
let keepConnection = true;
|
|
1079
1118
|
const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto";
|
|
1080
1119
|
// ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
|
|
1081
1120
|
// WebSocket continuation still works via connection-scoped previous_response_id state.
|
|
1082
1121
|
const fullBody = body;
|
|
1083
1122
|
const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
|
|
1084
|
-
const stats =
|
|
1123
|
+
const stats = cacheSessionId ? getOrCreateWebSocketDebugStats(cacheSessionId) : undefined;
|
|
1085
1124
|
if (stats) {
|
|
1086
1125
|
stats.requests++;
|
|
1087
1126
|
if (reused)
|
|
@@ -1106,8 +1145,9 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
|
|
|
1106
1145
|
}
|
|
1107
1146
|
try {
|
|
1108
1147
|
socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
|
|
1109
|
-
await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)),
|
|
1148
|
+
await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)), onStart), output, stream, model, {
|
|
1110
1149
|
serviceTier: options?.serviceTier,
|
|
1150
|
+
grammarToolInputProperties,
|
|
1111
1151
|
resolveServiceTier: resolveCodexServiceTier,
|
|
1112
1152
|
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
|
|
1113
1153
|
});
|
|
@@ -1117,7 +1157,8 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
|
|
|
1117
1157
|
else if (useCachedContext && entry && output.responseId) {
|
|
1118
1158
|
const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
|
|
1119
1159
|
includeSystemPrompt: false,
|
|
1120
|
-
|
|
1160
|
+
grammarToolInputProperties,
|
|
1161
|
+
}).filter((item) => item.type !== "function_call_output" && item.type !== "custom_tool_call_output");
|
|
1121
1162
|
entry.continuation = {
|
|
1122
1163
|
lastRequestBody: fullBody,
|
|
1123
1164
|
lastResponseId: output.responseId,
|