diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index d65caad86..50b3f40c5 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -113,7 +113,9 @@ impl ResponsesWebsocketClient { extra_headers: HeaderMap, turn_state: Option>>, ) -> Result { - let ws_url = Url::parse(&self.provider.url_for_path("responses")) + let ws_url = self + .provider + .websocket_url_for_path("responses") .map_err(|err| ApiError::Stream(format!("failed to build websocket URL: {err}")))?; let mut headers = self.provider.headers.clone(); diff --git a/codex-rs/codex-api/src/provider.rs b/codex-rs/codex-api/src/provider.rs index 846a25bf5..8fba2905b 100644 --- a/codex-rs/codex-api/src/provider.rs +++ b/codex-rs/codex-api/src/provider.rs @@ -6,6 +6,7 @@ use http::Method; use http::header::HeaderMap; use std::collections::HashMap; use std::time::Duration; +use url::Url; /// Wire-level APIs supported by a `Provider`. #[derive(Debug, Clone, PartialEq, Eq)] @@ -105,6 +106,19 @@ impl Provider { self.base_url.to_ascii_lowercase().contains("openai.azure.") || matches_azure_responses_base_url(&self.base_url) } + + pub fn websocket_url_for_path(&self, path: &str) -> Result { + let mut url = Url::parse(&self.url_for_path(path))?; + + let scheme = match url.scheme() { + "http" => "ws", + "https" => "wss", + "ws" | "wss" => return Ok(url), + _ => return Ok(url), + }; + let _ = url.set_scheme(scheme); + Ok(url) + } } fn matches_azure_responses_base_url(base_url: &str) -> bool { diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 596fe6d2f..aea80a91d 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -109,6 +109,9 @@ "remote_models": { "type": "boolean" }, + "responses_websockets": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, @@ -601,6 +604,9 @@ "remote_models": { "type": "boolean" }, + "responses_websockets": { + "type": "boolean" + }, "shell_snapshot": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 934c54263..b65a20b79 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1363,6 +1363,12 @@ impl Config { || cfg.sandbox_mode.is_some(); let mut model_providers = built_in_model_providers(); + if features.enabled(Feature::ResponsesWebsockets) + && let Some(provider) = model_providers.get_mut("openai") + && provider.is_openai() + { + provider.wire_api = crate::model_provider_info::WireApi::ResponsesWebsocket; + } // Merge user-defined providers into the built-in list. for (key, provider) in cfg.model_providers.into_iter() { model_providers.entry(key).or_insert(provider); @@ -2511,6 +2517,30 @@ profile = "project" Ok(()) } + #[test] + fn responses_websockets_feature_updates_openai_provider() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let mut entries = BTreeMap::new(); + entries.insert("responses_websockets".to_string(), true); + let cfg = ConfigToml { + features: Some(crate::features::FeaturesToml { entries }), + ..Default::default() + }; + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.path().to_path_buf(), + )?; + + assert_eq!( + config.model_provider.wire_api, + crate::model_provider_info::WireApi::ResponsesWebsocket + ); + + Ok(()) + } + #[test] fn config_honors_explicit_file_oauth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 94610f0b7..6d1c40769 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -107,6 +107,8 @@ pub enum Feature { Steer, /// Enable collaboration modes (Plan, Pair Programming, Execute). CollaborationModes, + /// Use the Responses API WebSocket transport for OpenAI by default. + ResponsesWebsockets, } impl Feature { @@ -461,4 +463,10 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Beta, default_enabled: false, }, + FeatureSpec { + id: Feature::ResponsesWebsockets, + key: "responses_websockets", + stage: Stage::Beta, + default_enabled: false, + }, ];