diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index d920dc5e..acbf4292 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -573,11 +573,12 @@ where let line = line_result?; // Skip empty lines and non-data lines - if line.trim().is_empty() || !line.starts_with("data: ") { + // Note: SSE spec allows both "data: value" and "data:value" (space is optional) + if line.trim().is_empty() || !line.starts_with("data:") { continue; } - let data_part = line.strip_prefix("data: ").unwrap_or(&line); + let data_part = line.strip_prefix("data: ").or_else(|| line.strip_prefix("data:")).unwrap_or(&line); // Handle end of stream if data_part.trim() == "[DONE]" { diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 5ca05fa5..bc946b55 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -520,7 +520,10 @@ fn ensure_valid_json_schema(schema: &mut Value) { } fn strip_data_prefix(line: &str) -> Option<&str> { - line.strip_prefix("data: ").map(|s| s.trim()) + // SSE spec allows both "data: value" and "data:value" (space after colon is optional) + line.strip_prefix("data: ") + .or_else(|| line.strip_prefix("data:")) + .map(|s| s.trim()) } pub fn response_to_streaming_message( @@ -536,12 +539,13 @@ where let mut accumulated_reasoning_content = String::new(); 'outer: while let Some(response) = stream.next().await { - if response.as_ref().is_ok_and(|s| s == "data: [DONE]") { - break 'outer; - } let response_str = response?; let line = strip_data_prefix(&response_str); + if line.is_some_and(|l| l == "[DONE]") { + break 'outer; + } + if line.is_none() || line.is_some_and(|l| l.is_empty()) { continue } @@ -580,11 +584,11 @@ where let mut done = false; while !done { if let Some(response_chunk) = stream.next().await { - if response_chunk.as_ref().is_ok_and(|s| s == "data: [DONE]") { - break 'outer; - } let response_str = response_chunk?; if let Some(line) = strip_data_prefix(&response_str) { + if line == "[DONE]" { + break 'outer; + } let tool_chunk: StreamingChunk = serde_json::from_str(line) .map_err(|e| anyhow!("Failed to parse streaming chunk: {}: {:?}", e, &line))?; diff --git a/crates/goose/src/providers/formats/openai_responses.rs b/crates/goose/src/providers/formats/openai_responses.rs index c5cce0f1..68e0f785 100644 --- a/crates/goose/src/providers/formats/openai_responses.rs +++ b/crates/goose/src/providers/formats/openai_responses.rs @@ -684,9 +684,12 @@ where // Parse SSE format: "event: \ndata: " // For now, we only care about the data line + // SSE spec allows both "data: value" and "data:value" (space after colon is optional) let data_line = if response_str.starts_with("data: ") { response_str.strip_prefix("data: ").unwrap() - } else if response_str.starts_with("event: ") { + } else if response_str.starts_with("data:") { + response_str.strip_prefix("data:").unwrap() + } else if response_str.starts_with("event: ") || response_str.starts_with("event:") { // Skip event type lines continue; } else { diff --git a/crates/goose/src/providers/formats/snowflake.rs b/crates/goose/src/providers/formats/snowflake.rs index cd301e50..e3835b18 100644 --- a/crates/goose/src/providers/formats/snowflake.rs +++ b/crates/goose/src/providers/formats/snowflake.rs @@ -128,13 +128,15 @@ pub fn parse_streaming_response(sse_data: &str) -> Result { // Parse each SSE event for line in sse_data.lines() { - if !line.starts_with("data: ") { + // SSE spec allows both "data: value" and "data:value" (space after colon is optional) + if !line.starts_with("data:") { continue; } - let Some(json_str) = line.get(6..) else { - continue; - }; // Remove "data: " prefix + let json_str = line + .strip_prefix("data: ") + .or_else(|| line.strip_prefix("data:")) + .unwrap(); // Remove "data:" prefix if json_str.trim().is_empty() || json_str.trim() == "[DONE]" { continue; }