diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index b360d4bdd..2701dfb34 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1993,6 +1993,13 @@ ], "type": "object" }, + "RealtimeConversationVersion": { + "enum": [ + "v1", + "v2" + ], + "type": "string" + }, "RealtimeOutputModality": { "enum": [ "text", diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d3c8eae22..d3245091a 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2942,10 +2942,12 @@ mod tests { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), + model: Some("realtime-treatment-model".to_string()), output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("You are on a call".to_string())), realtime_session_id: Some("sess_456".to_string()), transport: None, + version: Some(RealtimeConversationVersion::V1), voice: Some(RealtimeVoice::Marin), }, }; @@ -2955,10 +2957,12 @@ mod tests { "id": 9, "params": { "threadId": "thr_123", + "model": "realtime-treatment-model", "outputModality": "audio", "prompt": "You are on a call", "realtimeSessionId": "sess_456", "transport": null, + "version": "v1", "voice": "marin" } }), @@ -2973,10 +2977,12 @@ mod tests { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: None, realtime_session_id: None, transport: None, + version: None, voice: None, }, }; @@ -2986,9 +2992,11 @@ mod tests { "id": 9, "params": { "threadId": "thr_123", + "model": null, "outputModality": "audio", "realtimeSessionId": null, "transport": null, + "version": null, "voice": null } }), @@ -2999,10 +3007,12 @@ mod tests { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(None), realtime_session_id: None, transport: None, + version: None, voice: None, }, }; @@ -3012,10 +3022,12 @@ mod tests { "id": 9, "params": { "threadId": "thr_123", + "model": null, "outputModality": "audio", "prompt": null, "realtimeSessionId": null, "transport": null, + "version": null, "voice": null } }), @@ -3168,10 +3180,12 @@ mod tests { request_id: RequestId::Integer(1), params: v2::ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("You are on a call".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, }, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs index c6ea0744d..a86cd8067 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs @@ -65,6 +65,9 @@ impl From for CoreRealtimeAudioFrame { #[ts(export_to = "v2/")] pub struct ThreadRealtimeStartParams { pub thread_id: String, + /// Overrides the configured realtime model for this session only. + #[ts(optional = nullable)] + pub model: Option, /// Selects text or audio output for the realtime session. Transport and voice stay /// independent so clients can choose how they connect separately from what the model emits. pub output_modality: RealtimeOutputModality, @@ -80,6 +83,9 @@ pub struct ThreadRealtimeStartParams { pub realtime_session_id: Option, #[ts(optional = nullable)] pub transport: Option, + /// Overrides the configured realtime protocol version for this session only. + #[ts(optional = nullable)] + pub version: Option, #[ts(optional = nullable)] pub voice: Option, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index e09866e19..999cc8fa3 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -162,7 +162,7 @@ Example with notification opt-out: - `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. - `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. -- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. +- `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, and optionally pass `model` and `version` to override configured realtime selection for this session only. Returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. - `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. - `thread/realtime/appendText` — append text input to the active realtime session (experimental); returns `{}`. - `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. @@ -837,6 +837,8 @@ Then send `offer.sdp` to app-server. Core uses `experimental_realtime_ws_backend Omit `prompt` to use Codex's default realtime backend prompt. Send `prompt: null` or `prompt: ""` when the session should start without that default backend prompt. +Clients may also pass `model` and `version` on `thread/realtime/start` to select a +different realtime session configuration without changing thread or user config. ```javascript await pc.setRemoteDescription({ diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 3d4478194..b2e6ddc0b 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -934,6 +934,7 @@ impl TurnRequestProcessor { request_id, thread.as_ref(), Op::RealtimeConversationStart(ConversationStartParams { + model: params.model, output_modality: params.output_modality, prompt: params.prompt, realtime_session_id: params.realtime_session_id, @@ -945,6 +946,7 @@ impl TurnRequestProcessor { ConversationStartTransport::Webrtc { sdp } } }), + version: params.version, voice: params.voice, }), ) diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index fc1100d17..03c9d455f 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -80,10 +80,12 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R let request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("hello".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, }) .await?; @@ -186,12 +188,14 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< let request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("hello".to_string())), realtime_session_id: None, transport: Some(ThreadRealtimeStartTransport::Webrtc { sdp: "v=offer\r\n".to_string(), }), + version: None, voice: None, }) .await?; 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 f88fb5492..bd03af8e0 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -311,12 +311,14 @@ impl RealtimeE2eHarness { .mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: self.thread_id.clone(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: Some(ThreadRealtimeStartTransport::Webrtc { sdp: offer_sdp.to_string(), }), + version: None, voice: None, }) .await?; @@ -556,10 +558,12 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: thread_start.thread.id.clone(), + model: Some("realtime-treatment-model".to_string()), output_modality: RealtimeOutputModality::Audio, prompt: None, realtime_session_id: None, transport: None, + version: None, voice: Some(RealtimeVoice::Cedar), }) .await?; @@ -588,6 +592,10 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { startup_context_request.body_json()["session"]["audio"]["output"]["voice"], "cedar" ); + assert_eq!( + realtime_server.single_handshake().uri(), + "/v1/realtime?model=realtime-treatment-model" + ); assert_eq!( startup_context_request.body_json()["session"]["output_modalities"], json!(["audio"]) @@ -805,10 +813,12 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: thread_start.thread.id.clone(), + model: None, output_modality: RealtimeOutputModality::Text, prompt: None, realtime_session_id: None, transport: None, + version: None, voice: None, }) .await?; @@ -979,10 +989,12 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: thread_start.thread.id.clone(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, }) .await?; @@ -1076,12 +1088,14 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: thread_id.clone(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: Some(ThreadRealtimeStartTransport::Webrtc { sdp: "v=offer\r\n".to_string(), }), + version: None, voice: None, }) .await?; @@ -1991,12 +2005,14 @@ async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: thread_start.thread.id, + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: Some(ThreadRealtimeStartTransport::Webrtc { sdp: "v=offer\r\n".to_string(), }), + version: None, voice: None, }) .await?; @@ -2050,10 +2066,12 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> { let start_request_id = mcp .send_thread_realtime_start_request(ThreadRealtimeStartParams { thread_id: thread_start.thread.id.clone(), + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, }) .await?; diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 47bdc8c0c..66dc653e5 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -625,12 +625,14 @@ async fn prepare_realtime_start( if let Some(realtime_ws_base_url) = &config.experimental_realtime_ws_base_url { api_provider.base_url = realtime_ws_base_url.clone(); } - let version = config.realtime.version; + let version = params.version.unwrap_or(config.realtime.version); let session_config = build_realtime_session_config( sess, + params.model, params.prompt, params.realtime_session_id, params.output_modality, + version, params.voice, ) .await?; @@ -664,9 +666,11 @@ async fn prepare_realtime_start( pub(crate) async fn build_realtime_session_config( sess: &Arc, + model: Option, prompt: Option>, realtime_session_id: Option, output_modality: RealtimeOutputModality, + version: RealtimeWsVersion, voice: Option, ) -> CodexResult { let config = sess.get_config().await; @@ -689,18 +693,15 @@ pub(crate) async fn build_realtime_session_config( (false, false) => format!("{prompt}\n\n{startup_context}"), }; let model = Some( - config - .experimental_realtime_ws_model - .clone() + model + .or_else(|| config.experimental_realtime_ws_model.clone()) .unwrap_or_else(|| DEFAULT_REALTIME_MODEL.to_string()), ); - let event_parser = match config.realtime.version { + let event_parser = match version { RealtimeWsVersion::V1 => RealtimeEventParser::V1, RealtimeWsVersion::V2 => RealtimeEventParser::RealtimeV2, }; - if config.realtime.version == RealtimeWsVersion::V1 - && matches!(output_modality, RealtimeOutputModality::Text) - { + if version == RealtimeWsVersion::V1 && matches!(output_modality, RealtimeOutputModality::Text) { return Err(CodexErr::InvalidRequest( "text realtime output modality requires realtime v2".to_string(), )); @@ -711,8 +712,8 @@ pub(crate) async fn build_realtime_session_config( }; let voice = voice .or(config.realtime.voice) - .unwrap_or_else(|| default_realtime_voice(config.realtime.version)); - validate_realtime_voice(config.realtime.version, voice)?; + .unwrap_or_else(|| default_realtime_voice(version)); + validate_realtime_voice(version, voice)?; Ok(RealtimeSessionConfig { instructions: prompt, model, diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 73a500f04..eee445ea9 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -194,10 +194,12 @@ async fn start_remote_realtime_server() -> responses::WebSocketTestServer { async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result<()> { codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 095b3f59d..25f0516b0 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -282,10 +282,12 @@ async fn conversation_start_audio_text_close_round_trip() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -420,10 +422,12 @@ async fn conversation_start_defaults_to_v2_and_gpt_realtime_1_5() -> Result<()> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -502,18 +506,20 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> { config.experimental_realtime_ws_model = Some("realtime-test-model".to_string()); config.experimental_realtime_ws_startup_context = Some("startup context".to_string()); config.experimental_realtime_ws_base_url = Some(realtime_ws_base_url); - config.realtime.version = RealtimeWsVersion::V1; + config.realtime.version = RealtimeWsVersion::V2; }); let test = builder.build(&server).await?; test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: Some("session-override-model".to_string()), output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: Some(ConversationStartTransport::Webrtc { sdp: "v=offer\r\n".to_string(), }), + version: Some(RealtimeConversationVersion::V1), voice: None, })) .await?; @@ -572,7 +578,7 @@ async fn conversation_webrtc_start_posts_generated_session() -> Result<()> { Some("multipart/form-data; boundary=codex-realtime-call-boundary") ); let body = String::from_utf8(request.body).context("multipart body should be utf-8")?; - let session = r#"{"audio":{"input":{"format":{"type":"audio/pcm","rate":24000}},"output":{"voice":"cove"}},"type":"quicksilver","model":"realtime-test-model","instructions":"backend prompt\n\nstartup context"}"#; + let session = r#"{"audio":{"input":{"format":{"type":"audio/pcm","rate":24000}},"output":{"voice":"cove"}},"type":"quicksilver","model":"session-override-model","instructions":"backend prompt\n\nstartup context"}"#; let session = normalized_json_string(session)?; assert_eq!( body, @@ -678,12 +684,14 @@ async fn conversation_webrtc_close_while_sideband_connecting_drops_pending_join( test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: Some(ConversationStartTransport::Webrtc { sdp: "v=offer\r\n".to_string(), }), + version: None, voice: None, })) .await?; @@ -762,12 +770,14 @@ async fn conversation_webrtc_sideband_connect_failure_closes_with_error() -> Res test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: Some(ConversationStartTransport::Webrtc { sdp: "v=offer\r\n".to_string(), }), + version: None, voice: None, })) .await?; @@ -847,10 +857,12 @@ async fn conversation_start_uses_openai_env_key_fallback_with_chatgpt_auth() -> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -913,10 +925,12 @@ async fn conversation_transport_close_emits_closed_event() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1003,10 +1017,12 @@ async fn conversation_start_preflight_failure_emits_realtime_error_only() -> Res test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1047,10 +1063,12 @@ async fn conversation_start_connect_failure_emits_realtime_error_only() -> Resul test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1138,10 +1156,12 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("old".to_string())), realtime_session_id: Some("conv_old".to_string()), transport: None, + version: None, voice: None, })) .await?; @@ -1161,10 +1181,12 @@ async fn conversation_second_start_replaces_runtime() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("new".to_string())), realtime_session_id: Some("conv_new".to_string()), transport: None, + version: None, voice: None, })) .await?; @@ -1255,10 +1277,12 @@ async fn conversation_uses_experimental_realtime_ws_base_url_override() -> Resul test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1317,10 +1341,12 @@ async fn conversation_uses_default_realtime_backend_prompt() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: None, realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1387,10 +1413,12 @@ async fn conversation_uses_empty_instructions_for_null_or_empty_prompt() -> Resu ] { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt, realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1450,10 +1478,12 @@ async fn conversation_uses_explicit_start_voice() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: Some(RealtimeVoice::Breeze), })) .await?; @@ -1505,10 +1535,12 @@ async fn conversation_uses_configured_realtime_voice() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1548,10 +1580,12 @@ async fn conversation_rejects_voice_for_wrong_realtime_version() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: Some(RealtimeVoice::Cove), })) .await?; @@ -1592,10 +1626,12 @@ async fn conversation_uses_experimental_realtime_ws_backend_prompt_override() -> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("prompt from op".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1662,10 +1698,12 @@ async fn conversation_uses_experimental_realtime_ws_startup_context_override() - test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("prompt from op".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1726,10 +1764,12 @@ async fn conversation_disables_realtime_startup_context_with_empty_override() -> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("prompt from op".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1783,10 +1823,12 @@ async fn conversation_start_injects_startup_context_from_thread_history() -> Res test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1892,10 +1934,12 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -1997,10 +2041,12 @@ async fn conversation_startup_context_falls_back_to_workspace_map() -> Result<() test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2054,10 +2100,12 @@ async fn conversation_startup_context_is_truncated_and_sent_once_per_start() -> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2131,10 +2179,12 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2225,10 +2275,12 @@ async fn realtime_v2_noop_tool_call_returns_empty_function_output_without_respon test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2321,10 +2373,12 @@ async fn conversation_mirrors_assistant_message_text_to_realtime_handoff() -> Re test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2455,10 +2509,12 @@ async fn conversation_handoff_persists_across_item_done_until_turn_complete() -> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2604,10 +2660,12 @@ async fn inbound_handoff_request_starts_turn() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2703,10 +2761,12 @@ async fn inbound_handoff_request_uses_active_transcript() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2803,10 +2863,12 @@ async fn inbound_handoff_request_sends_transcript_delta_after_each_handoff() -> test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -2901,10 +2963,12 @@ async fn inbound_conversation_item_does_not_start_turn_and_still_forwards_audio( test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -3021,10 +3085,12 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -3171,10 +3237,12 @@ async fn inbound_handoff_request_does_not_block_realtime_event_forwarding() -> R test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -3305,10 +3373,12 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> { test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; @@ -3455,10 +3525,12 @@ async fn inbound_handoff_request_starts_turn_and_does_not_block_realtime_audio() test.codex .submit(Op::RealtimeConversationStart(ConversationStartParams { + model: None, output_modality: RealtimeOutputModality::Audio, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, + version: None, voice: None, })) .await?; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 5abc9c639..e93801d29 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -183,11 +183,15 @@ pub struct McpServerRefreshConfig { #[derive(Debug, Clone, PartialEq)] pub struct ConversationStartParams { + /// Overrides the configured realtime model for this session only. + pub model: Option, /// Selects whether the realtime session should produce text or audio output. pub output_modality: RealtimeOutputModality, pub prompt: Option>, pub realtime_session_id: Option, pub transport: Option, + /// Overrides the configured realtime protocol version for this session only. + pub version: Option, pub voice: Option, }