codex: use item done for realtime transcript done

This commit is contained in:
Ahmed Ibrahim
2026-04-13 22:13:58 -07:00
parent b1b26c4b7c
commit b97dea890f
3 changed files with 73 additions and 2 deletions
@@ -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();
@@ -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<RealtimeEvent> {
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<String, Value>) -> Option<RealtimeEvent> {
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::<String>();
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<String, Value>) -> Option<RealtimeEvent> {
let item_type = item.get("type").and_then(Value::as_str);
let item_name = item.get("name").and_then(Value::as_str);