From b97dea890fd2fd4aa6798abf54af5d60a43a8073 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 13 Apr 2026 22:13:58 -0700 Subject: [PATCH] codex: use item done for realtime transcript done --- .../tests/suite/v2/realtime_conversation.rs | 9 ++++- .../endpoint/realtime_websocket/methods.rs | 29 +++++++++++++++ .../realtime_websocket/protocol_v2.rs | 37 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) 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 e3e3c8ac1a..a08b041286 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -732,8 +732,13 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript "delta": "world" }), json!({ - "type": "response.output_text.done", - "text": "hello world" + "type": "conversation.item.done", + "item": { + "id": "item_output_1", + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "hello world"}] + } }), ]]]) .await; diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index 3b21a72820..4f2ec9c54c 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -1136,6 +1136,35 @@ mod tests { ); } + #[test] + fn parse_realtime_v2_item_done_output_text_event() { + let payload = json!({ + "type": "conversation.item.done", + "item": { + "id": "item_output_1", + "type": "message", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "hello"}, + {"type": "output_text", "text": " world"} + ] + } + }) + .to_string(); + + assert_eq!( + parse_realtime_event(payload.as_str(), RealtimeEventParser::RealtimeV2), + Some(RealtimeEvent::OutputTranscriptDone( + RealtimeTranscriptDone { + text: "hello world".to_string(), + item_id: Some("item_output_1".to_string()), + output_index: None, + content_index: None, + } + )) + ); + } + #[test] fn complete_transcript_entry_replaces_current_part_only() { let mut state = ActiveTranscriptState::default(); diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs index e09a60cb21..4cea1535d6 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_v2.rs @@ -10,6 +10,7 @@ use codex_protocol::protocol::RealtimeInputAudioSpeechStarted; use codex_protocol::protocol::RealtimeResponseCancelled; use codex_protocol::protocol::RealtimeResponseCreated; use codex_protocol::protocol::RealtimeResponseDone; +use codex_protocol::protocol::RealtimeTranscriptDone; use serde_json::Map as JsonMap; use serde_json::Value; use tracing::debug; @@ -128,12 +129,48 @@ fn parse_conversation_item_done_event(parsed: &Value) -> Option { return Some(handoff); } + if let Some(transcript_done) = parse_item_done_transcript(item) { + return Some(transcript_done); + } + item.get("id") .and_then(Value::as_str) .map(str::to_string) .map(|item_id| RealtimeEvent::ConversationItemDone { item_id }) } +fn parse_item_done_transcript(item: &JsonMap) -> Option { + let role = item.get("role").and_then(Value::as_str)?; + let text = item + .get("content") + .and_then(Value::as_array)? + .iter() + .filter_map(item_content_text) + .collect::(); + if text.is_empty() { + return None; + } + + let done = RealtimeTranscriptDone { + text, + item_id: item.get("id").and_then(Value::as_str).map(str::to_string), + output_index: None, + content_index: None, + }; + match role { + "user" => Some(RealtimeEvent::InputTranscriptDone(done)), + "assistant" => Some(RealtimeEvent::OutputTranscriptDone(done)), + _ => None, + } +} + +fn item_content_text(content: &Value) -> Option<&str> { + content + .get("text") + .or_else(|| content.get("transcript")) + .and_then(Value::as_str) +} + fn parse_handoff_requested_event(item: &JsonMap) -> Option { let item_type = item.get("type").and_then(Value::as_str); let item_name = item.get("name").and_then(Value::as_str);