From d017295f3243de8d109b037374ec844ac8174567 Mon Sep 17 00:00:00 2001 From: Angie Jones Date: Wed, 27 May 2026 13:05:00 -0500 Subject: [PATCH] fix: tolerate missing responses output (#9449) Signed-off-by: Angie Jones --- .../src/providers/formats/openai_responses.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/goose/src/providers/formats/openai_responses.rs b/crates/goose/src/providers/formats/openai_responses.rs index ef39fb8b..d5c2e15a 100644 --- a/crates/goose/src/providers/formats/openai_responses.rs +++ b/crates/goose/src/providers/formats/openai_responses.rs @@ -273,6 +273,7 @@ pub struct ResponseMetadata { pub created_at: i64, pub status: String, pub model: String, + #[serde(default)] pub output: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub usage: Option, @@ -981,6 +982,47 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_responses_stream_completed_allows_missing_output() -> anyhow::Result<()> { + let lines = vec![ + r#"data: {"type":"response.created","sequence_number":1,"response":{"id":"resp_1","object":"response","created_at":1737368310,"status":"in_progress","model":"gpt-5.2-pro","output":[]}}"#.to_string(), + r#"data: {"type":"response.output_text.delta","sequence_number":2,"item_id":"msg_1","output_index":0,"content_index":0,"delta":"Hello"}"#.to_string(), + r#"data: {"type":"response.output_text.delta","sequence_number":3,"item_id":"msg_1","output_index":0,"content_index":0,"delta":" world"}"#.to_string(), + r#"data: {"type":"response.completed","sequence_number":4,"response":{"id":"resp_1","object":"response","created_at":1737368310,"status":"completed","model":"gpt-5.2-pro","usage":{"input_tokens":10,"output_tokens":4,"total_tokens":14}}}"#.to_string(), + "data: [DONE]".to_string(), + ]; + + let response_stream = tokio_stream::iter(lines.into_iter().map(Ok)); + let messages = responses_api_to_streaming_message(response_stream); + futures::pin_mut!(messages); + + let mut text_parts = Vec::new(); + let mut usage: Option = None; + + while let Some(item) = messages.next().await { + let (message, maybe_usage) = item?; + if let Some(msg) = message { + for content in msg.content { + if let MessageContent::Text(text) = content { + text_parts.push(text.text.clone()); + } + } + } + if let Some(final_usage) = maybe_usage { + usage = Some(final_usage); + } + } + + assert_eq!(text_parts.concat(), "Hello world"); + let usage = usage.expect("usage should be present at completion"); + assert_eq!(usage.model, "gpt-5.2-pro"); + assert_eq!(usage.usage.input_tokens, Some(10)); + assert_eq!(usage.usage.output_tokens, Some(4)); + assert_eq!(usage.usage.total_tokens, Some(14)); + + Ok(()) + } + #[test] fn test_responses_api_to_message_captures_reasoning_summary() -> anyhow::Result<()> { let response: ResponsesApiResponse = serde_json::from_value(serde_json::json!({