@@ -714,6 +714,27 @@ for (const workflowPath of workflowPaths) {
714714 }
715715}
716716
717+ // Matches the Codex config.toml heredoc opening followed (possibly with
718+ // previously-injected lines in between) by [shell_environment_policy], so we
719+ // can inject [model_providers.openai] config at the top of the config.toml
720+ // before the shell environment policy section. The non-greedy (?:...)* skips
721+ // any lines previously inserted by earlier versions of this script, making the
722+ // transformation idempotent and upgradable. The hash in the heredoc delimiter
723+ // varies across compiler versions, so we match \w+ instead of a literal hash.
724+ //
725+ // Codex v0.121+ ignores OPENAI_BASE_URL env var when constructing WebSocket URLs
726+ // for the responses API (wss://api.openai.com/v1/responses), connecting directly
727+ // to OpenAI and sending the api-proxy placeholder key → 401 Unauthorized.
728+ // Setting supports_websockets=false disables WebSocket transport, forcing Codex
729+ // to use REST for all API calls. REST calls respect OPENAI_BASE_URL (set by AWF's
730+ // docker-manager to http://172.30.0.30:10000), which routes them through the
731+ // api-proxy sidecar that injects the real OpenAI API key.
732+ //
733+ // See: https://developers.openai.com/codex/config-reference
734+ const codexConfigTomlHeredocRegex =
735+ / ^ ( \s + ) ( c a t > " \/ t m p \/ g h - a w \/ m c p - c o n f i g \/ c o n f i g \. t o m l " < < G H _ A W _ C O D E X _ S H E L L _ P O L I C Y _ \w + _ E O F \n ) (?: \1[ ^ \n ] * \n ) * ?( \1\[ s h e l l _ e n v i r o n m e n t _ p o l i c y \] ) / m;
736+ const CODEX_OPENAI_BASE_URL_SENTINEL = 'supports_websockets = false' ;
737+
717738// Apply Codex-specific transformations to OpenAI/Codex workflow files only.
718739// These transformations must not be applied to Claude, Copilot, or other
719740// non-OpenAI workflows.
@@ -727,6 +748,37 @@ for (const workflowPath of codexWorkflowPaths) {
727748 }
728749 let modified = false ;
729750
751+ // Inject [model_providers.openai] with supports_websockets=false into the Codex
752+ // config.toml heredoc to disable WebSocket transport for the OpenAI provider.
753+ // Codex v0.121+ ignores OPENAI_BASE_URL for WebSocket URL construction and
754+ // connects directly to wss://api.openai.com/v1/responses with the api-proxy
755+ // placeholder key, causing 401 Unauthorized. With WebSocket disabled, Codex
756+ // falls back to REST, which correctly routes through OPENAI_BASE_URL
757+ // (http://172.30.0.30:10000) → api-proxy sidecar → real OpenAI API key.
758+ if ( ! content . includes ( CODEX_OPENAI_BASE_URL_SENTINEL ) ) {
759+ const heredocMatch = content . match ( codexConfigTomlHeredocRegex ) ;
760+ if ( heredocMatch ) {
761+ const indent = heredocMatch [ 1 ] ;
762+ const modelProvidersBlock =
763+ `${ indent } [model_providers.openai]\n` +
764+ `${ indent } ${ CODEX_OPENAI_BASE_URL_SENTINEL } \n` +
765+ `${ indent } \n` ;
766+ content = content . replace (
767+ codexConfigTomlHeredocRegex ,
768+ `$1$2${ modelProvidersBlock } $3`
769+ ) ;
770+ modified = true ;
771+ console . log ( ` Injected [model_providers.openai] supports_websockets=false into Codex config.toml heredoc` ) ;
772+ } else {
773+ console . warn (
774+ ` WARNING: Could not find Codex config.toml heredoc pattern to inject model_providers config. ` +
775+ `The compiled lock file may have changed structure. Manual review required.`
776+ ) ;
777+ }
778+ } else {
779+ console . log ( ` [model_providers.openai] supports_websockets=false already present in Codex config.toml` ) ;
780+ }
781+
730782 // Preserve empty lines as truly empty (no trailing whitespace) to keep the
731783 // YAML block scalar clean and diff-friendly.
732784 function buildXpiaHeredoc ( indent : string , appendSuffix : string ) : string {
0 commit comments