Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,12 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::ThreadRealtimeAppendTextResponse,
},
#[experimental("thread/realtime/appendSpeech")]
ThreadRealtimeAppendSpeech => "thread/realtime/appendSpeech" {
params: v2::ThreadRealtimeAppendSpeechParams,
serialization: thread_id(params.thread_id),
response: v2::ThreadRealtimeAppendSpeechResponse,
},
#[experimental("thread/realtime/stop")]
ThreadRealtimeStop => "thread/realtime/stop" {
params: v2::ThreadRealtimeStopParams,
Expand Down Expand Up @@ -3026,6 +3032,8 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
architecture: Some(RealtimeConversationArchitecture::Avas),
codex_responses_as_items: None,
codex_response_item_prefix: None,
thread_id: "thr_123".to_string(),
model: Some("realtime-treatment-model".to_string()),
output_modality: RealtimeOutputModality::Audio,
Expand All @@ -3043,6 +3051,8 @@ mod tests {
"params": {
"architecture": "avas",
"threadId": "thr_123",
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"model": "realtime-treatment-model",
"outputModality": "audio",
"prompt": "You are on a call",
Expand All @@ -3063,6 +3073,8 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
architecture: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
Expand All @@ -3080,6 +3092,8 @@ mod tests {
"params": {
"architecture": null,
"threadId": "thr_123",
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"model": null,
"outputModality": "audio",
"realtimeSessionId": null,
Expand All @@ -3095,6 +3109,8 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::ThreadRealtimeStartParams {
architecture: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
Expand All @@ -3112,6 +3128,8 @@ mod tests {
"params": {
"architecture": null,
"threadId": "thr_123",
"codexResponsesAsItems": null,
"codexResponseItemPrefix": null,
"model": null,
"outputModality": "audio",
"prompt": null,
Expand Down Expand Up @@ -3160,6 +3178,29 @@ mod tests {
Ok(())
}

#[test]
fn serialize_thread_realtime_append_speech() -> Result<()> {
let request = ClientRequest::ThreadRealtimeAppendSpeech {
request_id: RequestId::Integer(10),
params: v2::ThreadRealtimeAppendSpeechParams {
thread_id: "thr_123".to_string(),
text: "Short voice update".to_string(),
},
};
assert_eq!(
json!({
"method": "thread/realtime/appendSpeech",
"id": 10,
"params": {
"threadId": "thr_123",
"text": "Short voice update"
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}

#[test]
fn serialize_thread_status_changed_notification() -> Result<()> {
let notification =
Expand Down Expand Up @@ -3270,6 +3311,8 @@ mod tests {
request_id: RequestId::Integer(1),
params: v2::ThreadRealtimeStartParams {
architecture: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
Expand Down
21 changes: 21 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/realtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ pub struct ThreadRealtimeStartParams {
/// Overrides the configured realtime architecture for this session only.
#[ts(optional = nullable)]
pub architecture: Option<RealtimeConversationArchitecture>,
/// Sends automatic Codex responses as realtime conversation items instead of handoff appends.
#[ts(optional = nullable)]
pub codex_responses_as_items: Option<bool>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Model omission-false flag as a bool

This flag is collapsed with unwrap_or(false), so omission and null mean the same thing while clients see a nullable tri-state. The app-server API guidance says omission-means-false booleans should be defaulted bool; please avoid the meaningless null case. guidance

Useful? React with 👍 / 👎.

/// Optional prefix added to automatic Codex response items when `codexResponsesAsItems` is true.
#[ts(optional = nullable)]
pub codex_response_item_prefix: Option<String>,
/// Overrides the configured realtime model for this session only.
#[ts(optional = nullable)]
pub model: Option<String>,
Expand Down Expand Up @@ -146,6 +152,21 @@ pub struct ThreadRealtimeAppendTextParams {
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendTextResponse {}

/// EXPERIMENTAL - append speakable text to thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendSpeechParams {
pub thread_id: String,
pub text: String,
}

/// EXPERIMENTAL - response for appending realtime speech.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadRealtimeAppendSpeechResponse {}

/// EXPERIMENTAL - stop thread realtime.
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
Expand Down
12 changes: 11 additions & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ 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, 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/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. By default, automatic Codex text follows the protocol's speakable output path. Pass `codexResponsesAsItems: true` to send automatic Codex responses as realtime conversation items instead, and optionally pass `codexResponseItemPrefix` to prepend experiment instructions to those items. 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 with a required `role` of `user` or `developer` (experimental); returns `{}`. Older clients that omit `role` default to `user`.
- `thread/realtime/appendSpeech` — append text that the realtime model should speak to the user (experimental); returns `{}`.
- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`.
- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
Expand Down Expand Up @@ -870,6 +871,15 @@ Omit `prompt` to use Codex's default realtime backend prompt. Send `prompt: null
`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.
Pass `codexResponsesAsItems: true` to inject automatic Codex responses with
`conversation.item.create` instead of the protocol's default speakable output
path. When using that mode, `codexResponseItemPrefix` can prepend short
experiment instructions to each automatic Codex response item. Omit
`codexResponsesAsItems`, or pass `false`, to preserve the default speakable
behavior. Call
`thread/realtime/appendText` to append app-provided realtime text items, or
`thread/realtime/appendSpeech` when the app decides a realtime update should be
spoken.

```javascript
await pc.setRemoteDescription({
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,11 @@ impl MessageProcessor {
.thread_realtime_append_text(&request_id, params)
.await
}
ClientRequest::ThreadRealtimeAppendSpeech { params, .. } => {
self.turn_processor
.thread_realtime_append_speech(&request_id, params)
.await
}
ClientRequest::ThreadRealtimeStop { params, .. } => {
self.turn_processor
.thread_realtime_stop(&request_id, params)
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadReadResponse;
use codex_app_server_protocol::ThreadRealtimeAppendAudioParams;
use codex_app_server_protocol::ThreadRealtimeAppendAudioResponse;
use codex_app_server_protocol::ThreadRealtimeAppendSpeechParams;
use codex_app_server_protocol::ThreadRealtimeAppendSpeechResponse;
use codex_app_server_protocol::ThreadRealtimeAppendTextParams;
use codex_app_server_protocol::ThreadRealtimeAppendTextResponse;
use codex_app_server_protocol::ThreadRealtimeListVoicesResponse;
Expand Down Expand Up @@ -390,6 +392,7 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::ConversationAudioParams;
use codex_protocol::protocol::ConversationSpeechParams;
use codex_protocol::protocol::ConversationStartParams;
use codex_protocol::protocol::ConversationStartTransport;
use codex_protocol::protocol::ConversationTextParams;
Expand Down
37 changes: 37 additions & 0 deletions codex-rs/app-server/src/request_processors/turn_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ impl TurnRequestProcessor {
.map(|response| response.map(Into::into))
}

pub(crate) async fn thread_realtime_append_speech(
&self,
request_id: &ConnectionRequestId,
params: ThreadRealtimeAppendSpeechParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_realtime_append_speech_inner(request_id, params)
.await
.map(|response| response.map(Into::into))
}

pub(crate) async fn thread_realtime_stop(
&self,
request_id: &ConnectionRequestId,
Expand Down Expand Up @@ -936,6 +946,8 @@ impl TurnRequestProcessor {
thread.as_ref(),
Op::RealtimeConversationStart(ConversationStartParams {
architecture: params.architecture,
codex_responses_as_items: params.codex_responses_as_items.unwrap_or(false),
codex_response_item_prefix: params.codex_response_item_prefix,
model: params.model,
output_modality: params.output_modality,
prompt: params.prompt,
Expand Down Expand Up @@ -1012,6 +1024,31 @@ impl TurnRequestProcessor {
Ok(Some(ThreadRealtimeAppendTextResponse::default()))
}

async fn thread_realtime_append_speech_inner(
&self,
request_id: &ConnectionRequestId,
params: ThreadRealtimeAppendSpeechParams,
) -> Result<Option<ThreadRealtimeAppendSpeechResponse>, JSONRPCErrorError> {
let Some((_, thread)) = self
.prepare_realtime_conversation_thread(request_id, &params.thread_id)
.await?
else {
return Ok(None);
};
self.submit_core_op(
request_id,
thread.as_ref(),
Op::RealtimeConversationSpeech(ConversationSpeechParams { text: params.text }),
)
.await
.map_err(|err| {
internal_error(format!(
"failed to append realtime conversation speech: {err}"
))
})?;
Ok(Some(ThreadRealtimeAppendSpeechResponse::default()))
}

async fn thread_realtime_stop_inner(
&self,
request_id: &ConnectionRequestId,
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/app-server/tests/common/test_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ use codex_app_server_protocol::ThreadMemoryModeSetParams;
use codex_app_server_protocol::ThreadMetadataUpdateParams;
use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadRealtimeAppendAudioParams;
use codex_app_server_protocol::ThreadRealtimeAppendSpeechParams;
use codex_app_server_protocol::ThreadRealtimeAppendTextParams;
use codex_app_server_protocol::ThreadRealtimeListVoicesParams;
use codex_app_server_protocol::ThreadRealtimeStartParams;
Expand Down Expand Up @@ -1023,6 +1024,16 @@ impl TestAppServer {
.await
}

/// Send a `thread/realtime/appendSpeech` JSON-RPC request (v2).
pub async fn send_thread_realtime_append_speech_request(
&mut self,
params: ThreadRealtimeAppendSpeechParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/realtime/appendSpeech", params)
.await
}

/// Send a `thread/realtime/stop` JSON-RPC request (v2).
pub async fn send_thread_realtime_stop_request(
&mut self,
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server/tests/suite/v2/experimental_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
let request_id = mcp
.send_thread_realtime_start_request(ThreadRealtimeStartParams {
architecture: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
Expand Down Expand Up @@ -189,6 +191,8 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
let request_id = mcp
.send_thread_realtime_start_request(ThreadRealtimeStartParams {
architecture: None,
codex_responses_as_items: None,
codex_response_item_prefix: None,
thread_id: "thr_123".to_string(),
model: None,
output_modality: RealtimeOutputModality::Audio,
Expand Down
Loading
Loading