Add per-session realtime model and version overrides (#24999)

## Why

Clients need to select a realtime session configuration for an
individual start without rewriting persisted configuration or restarting
the app-server process.

## What Changed

- Add optional `model` and `version` fields to `thread/realtime/start`
- Forward those optional values through the realtime start operation and
apply them only for that session
- Preserve existing configured/default behavior when the new fields are
omitted
- Update generated protocol schema and app-server documentation

## Validation

- Added/updated protocol serialization coverage for the new optional
request fields
- Added focused core coverage for a session override taking precedence
over configured realtime selection
- Added focused app-server coverage that a request override reaches the
realtime WebSocket handshake
This commit is contained in:
guinness-oai
2026-06-09 17:54:32 -07:00
committed by GitHub
parent 5ac640ac49
commit 4a3eac2144
11 changed files with 145 additions and 13 deletions
+3 -1
View File
@@ -162,7 +162,7 @@ Example with notification opt-out:
- `thread/inject_items` — append raw Responses API items to a loaded threads 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({
@@ -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,
}),
)
@@ -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?;
@@ -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?;