diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs index d12578448..a771fb874 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -51,7 +51,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { vec![], vec![ json!({ - "type": "conversation.output_audio.delta", + "type": "response.output_audio.delta", "delta": "AQID", "sample_rate": 24_000, "channels": 1, @@ -403,6 +403,10 @@ sandbox_mode = "read-only" model_provider = "mock_provider" experimental_realtime_ws_base_url = "{realtime_server_uri}" +[realtime] +version = "v2" +type = "conversational" + [features] {realtime_feature_key} = {realtime_enabled} diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b2164f723..0f1768937 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -426,9 +426,6 @@ "realtime_conversation": { "type": "boolean" }, - "realtime_conversation_v2": { - "type": "boolean" - }, "remote_models": { "type": "boolean" }, @@ -1342,6 +1339,18 @@ }, "type": "object" }, + "RealtimeToml": { + "additionalProperties": false, + "properties": { + "type": { + "$ref": "#/definitions/RealtimeWsMode" + }, + "version": { + "$ref": "#/definitions/RealtimeWsVersion" + } + }, + "type": "object" + }, "RealtimeWsMode": { "enum": [ "conversational", @@ -1349,6 +1358,13 @@ ], "type": "string" }, + "RealtimeWsVersion": { + "enum": [ + "v1", + "v2" + ], + "type": "string" + }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -1827,14 +1843,6 @@ "description": "Experimental / do not use. Overrides only the realtime conversation websocket transport base URL (the `Op::RealtimeConversation` `/v1/realtime` connection) without changing normal provider HTTP requests.", "type": "string" }, - "experimental_realtime_ws_mode": { - "allOf": [ - { - "$ref": "#/definitions/RealtimeWsMode" - } - ], - "description": "Experimental / do not use. Selects the realtime websocket intent mode. `conversational` is speech-to-speech while `transcription` is transcript-only." - }, "experimental_realtime_ws_model": { "description": "Experimental / do not use. Selects the realtime websocket model/snapshot used for the `Op::RealtimeConversation` connection.", "type": "string" @@ -1959,9 +1967,6 @@ "realtime_conversation": { "type": "boolean" }, - "realtime_conversation_v2": { - "type": "boolean" - }, "remote_models": { "type": "boolean" }, @@ -2309,6 +2314,15 @@ }, "type": "object" }, + "realtime": { + "allOf": [ + { + "$ref": "#/definitions/RealtimeToml" + } + ], + "default": null, + "description": "Experimental / do not use. Realtime websocket session selection. `version` controls v1/v2 and `type` controls conversational/transcription." + }, "review_model": { "description": "Review model override used by the `/review` feature.", "type": "string" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c95d45a47..2eb183b71 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4130,7 +4130,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, experimental_realtime_ws_model: None, - experimental_realtime_ws_mode: RealtimeWsMode::Conversational, + realtime: RealtimeConfig::default(), experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_startup_context: None, base_instructions: None, @@ -4268,7 +4268,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, experimental_realtime_ws_model: None, - experimental_realtime_ws_mode: RealtimeWsMode::Conversational, + realtime: RealtimeConfig::default(), experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_startup_context: None, base_instructions: None, @@ -4404,7 +4404,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, experimental_realtime_ws_model: None, - experimental_realtime_ws_mode: RealtimeWsMode::Conversational, + realtime: RealtimeConfig::default(), experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_startup_context: None, base_instructions: None, @@ -4526,7 +4526,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, experimental_realtime_ws_model: None, - experimental_realtime_ws_mode: RealtimeWsMode::Conversational, + realtime: RealtimeConfig::default(), experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_startup_context: None, base_instructions: None, @@ -5575,17 +5575,22 @@ experimental_realtime_ws_model = "realtime-test-model" } #[test] -fn experimental_realtime_ws_mode_loads_from_config_toml() -> std::io::Result<()> { +fn realtime_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( r#" -experimental_realtime_ws_mode = "transcription" +[realtime] +version = "v2" +type = "transcription" "#, ) .expect("TOML deserialization should succeed"); assert_eq!( - cfg.experimental_realtime_ws_mode, - Some(RealtimeWsMode::Transcription) + cfg.realtime, + Some(RealtimeToml { + version: Some(RealtimeWsVersion::V2), + session_type: Some(RealtimeWsMode::Transcription), + }) ); let codex_home = TempDir::new()?; @@ -5596,8 +5601,11 @@ experimental_realtime_ws_mode = "transcription" )?; assert_eq!( - config.experimental_realtime_ws_mode, - RealtimeWsMode::Transcription + config.realtime, + RealtimeConfig { + version: RealtimeWsVersion::V2, + session_type: RealtimeWsMode::Transcription, + } ); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d73f7cdc6..e8301f4ef 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -466,9 +466,9 @@ pub struct Config { /// Experimental / do not use. Selects the realtime websocket model/snapshot /// used for the `Op::RealtimeConversation` connection. pub experimental_realtime_ws_model: Option, - /// Experimental / do not use. Selects the realtime websocket intent mode. - /// `conversational` is speech-to-speech while `transcription` is transcript-only. - pub experimental_realtime_ws_mode: RealtimeWsMode, + /// Experimental / do not use. Realtime websocket session selection. + /// `version` controls v1/v2 and `type` controls conversational/transcription. + pub realtime: RealtimeConfig, /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport instructions (the `Op::RealtimeConversation` /// `/ws` session.update instructions) without changing normal prompts. @@ -1244,9 +1244,10 @@ pub struct ConfigToml { /// Experimental / do not use. Selects the realtime websocket model/snapshot /// used for the `Op::RealtimeConversation` connection. pub experimental_realtime_ws_model: Option, - /// Experimental / do not use. Selects the realtime websocket intent mode. - /// `conversational` is speech-to-speech while `transcription` is transcript-only. - pub experimental_realtime_ws_mode: Option, + /// Experimental / do not use. Realtime websocket session selection. + /// `version` controls v1/v2 and `type` controls conversational/transcription. + #[serde(default)] + pub realtime: Option, /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport instructions (the `Op::RealtimeConversation` /// `/ws` session.update instructions) without changing normal prompts. @@ -1400,6 +1401,30 @@ pub enum RealtimeWsMode { Transcription, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RealtimeWsVersion { + #[default] + V1, + V2, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct RealtimeConfig { + pub version: RealtimeWsVersion, + #[serde(rename = "type")] + pub session_type: RealtimeWsMode, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct RealtimeToml { + pub version: Option, + #[serde(rename = "type")] + pub session_type: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct RealtimeAudioToml { @@ -2482,7 +2507,12 @@ impl Config { }), experimental_realtime_ws_base_url: cfg.experimental_realtime_ws_base_url, experimental_realtime_ws_model: cfg.experimental_realtime_ws_model, - experimental_realtime_ws_mode: cfg.experimental_realtime_ws_mode.unwrap_or_default(), + realtime: cfg + .realtime + .map_or_else(RealtimeConfig::default, |realtime| RealtimeConfig { + version: realtime.version.unwrap_or_default(), + session_type: realtime.session_type.unwrap_or_default(), + }), experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt, experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context, experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions, diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 8072add4a..d0e4e290b 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -180,8 +180,6 @@ pub enum Feature { VoiceTranscription, /// Enable experimental realtime voice conversation mode in the TUI. RealtimeConversation, - /// Route realtime conversations through the v2 event parser. - RealtimeConversationV2, /// Prevent idle system sleep while a turn is actively running. PreventIdleSleep, /// Use the Responses API WebSocket transport for OpenAI by default. @@ -825,12 +823,6 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, - FeatureSpec { - id: Feature::RealtimeConversationV2, - key: "realtime_conversation_v2", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, FeatureSpec { id: Feature::PreventIdleSleep, key: "prevent_idle_sleep", diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 03407a427..243f4d8f2 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -2,10 +2,11 @@ use crate::CodexAuth; use crate::api_bridge::map_api_error; use crate::auth::read_openai_api_key_from_env; use crate::codex::Session; +use crate::config::RealtimeWsMode; +use crate::config::RealtimeWsVersion; use crate::default_client::default_headers; use crate::error::CodexErr; use crate::error::Result as CodexResult; -use crate::features::Feature; use crate::realtime_context::build_realtime_startup_context; use async_channel::Receiver; use async_channel::Sender; @@ -294,14 +295,13 @@ pub(crate) async fn handle_start( format!("{prompt}\n\n{startup_context}") }; let model = config.experimental_realtime_ws_model.clone(); - let event_parser = if config.features.enabled(Feature::RealtimeConversationV2) { - RealtimeEventParser::RealtimeV2 - } else { - RealtimeEventParser::V1 + let event_parser = match config.realtime.version { + RealtimeWsVersion::V1 => RealtimeEventParser::V1, + RealtimeWsVersion::V2 => RealtimeEventParser::RealtimeV2, }; - let session_mode = match config.experimental_realtime_ws_mode { - crate::config::RealtimeWsMode::Conversational => RealtimeSessionMode::Conversational, - crate::config::RealtimeWsMode::Transcription => RealtimeSessionMode::Transcription, + let session_mode = match config.realtime.session_type { + RealtimeWsMode::Conversational => RealtimeSessionMode::Conversational, + RealtimeWsMode::Transcription => RealtimeSessionMode::Transcription, }; let requested_session_id = params .session_id