From 2f717d33459ff1840f2eb47276b29f97279184f3 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 02:31:31 -0700 Subject: [PATCH] fix(tui): accept compact SSE data fields Accept SSE data frames with either `data: {...}` or `data:{...}` in Chat Completions, Responses, and Anthropic stream readers. Harvested from PR #3152. Co-authored-by: wgeeker <169752135+wgeeker@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ crates/tui/CHANGELOG.md | 4 ++++ crates/tui/src/client.rs | 29 +++++++++++++++++++++++++++++ crates/tui/src/client/anthropic.rs | 2 +- crates/tui/src/client/chat.rs | 2 +- crates/tui/src/client/responses.rs | 2 +- 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347c1ae6..de6db201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **SSE data lines without spaces (#3152).** Chat Completions, Responses, and + Anthropic stream readers now accept both `data: {...}` and `data:{...}` SSE + frames, matching the spec and preventing providers that omit the optional + space from streaming empty output. Thanks @wgeeker for the PR. - **SiliconFlow China provider config (#2893/#2895).** `siliconflow-CN` now reads its own `[providers.siliconflow_cn]` / `[providers.siliconflow-CN]` table and falls back to `[providers.siliconflow]` only for unset diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index d4a98da3..702f8feb 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -49,6 +49,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **SSE data lines without spaces (#3152).** Chat Completions, Responses, and + Anthropic stream readers now accept both `data: {...}` and `data:{...}` SSE + frames, matching the spec and preventing providers that omit the optional + space from streaming empty output. Thanks @wgeeker for the PR. - **SiliconFlow China provider config (#2893/#2895).** `siliconflow-CN` now reads its own `[providers.siliconflow_cn]` / `[providers.siliconflow-CN]` table and falls back to `[providers.siliconflow]` only for unset diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 9f7c44a6..878d328b 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -1543,6 +1543,11 @@ mod anthropic; mod chat; mod responses; +fn extract_sse_data_value(line: &str) -> Option<&str> { + line.strip_prefix("data:") + .map(|value| value.strip_prefix(' ').unwrap_or(value)) +} + pub(crate) use chat::{CacheWarmupKey, PromptInspection}; pub(crate) fn inspect_prompt_for_request(request: &MessageRequest) -> PromptInspection { @@ -3942,4 +3947,28 @@ mod tests { "https://api.deepseek.com/v1/chat/completions" ); } + + #[test] + fn extract_sse_data_value_accepts_optional_space() { + assert_eq!( + extract_sse_data_value("data: {\"ok\":true}"), + Some("{\"ok\":true}") + ); + assert_eq!( + extract_sse_data_value("data:{\"ok\":true}"), + Some("{\"ok\":true}") + ); + } + + #[test] + fn extract_sse_data_value_handles_done_marker() { + assert_eq!(extract_sse_data_value("data: [DONE]"), Some("[DONE]")); + assert_eq!(extract_sse_data_value("data:[DONE]"), Some("[DONE]")); + } + + #[test] + fn extract_sse_data_value_rejects_non_data_lines() { + assert_eq!(extract_sse_data_value("event: message"), None); + assert_eq!(extract_sse_data_value(": heartbeat"), None); + } } diff --git a/crates/tui/src/client/anthropic.rs b/crates/tui/src/client/anthropic.rs index 0511791d..4a21517b 100644 --- a/crates/tui/src/client/anthropic.rs +++ b/crates/tui/src/client/anthropic.rs @@ -205,7 +205,7 @@ impl DeepSeekClient { // `event:` lines are redundant (the data payload carries // `type`) and comment/heartbeat lines are ignorable. - let Some(data) = line.strip_prefix("data: ") else { + let Some(data) = super::extract_sse_data_value(&line) else { continue; }; diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 15a2c67d..53666567 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -437,7 +437,7 @@ impl DeepSeekClient { continue; } - if let Some(data) = line.strip_prefix("data: ") { + if let Some(data) = super::extract_sse_data_value(&line) { line_buf.push_str(data); } // Ignore other SSE fields (event:, id:, retry:) diff --git a/crates/tui/src/client/responses.rs b/crates/tui/src/client/responses.rs index 5133268a..7b7460d4 100644 --- a/crates/tui/src/client/responses.rs +++ b/crates/tui/src/client/responses.rs @@ -165,7 +165,7 @@ impl DeepSeekClient { continue; } - if let Some(data) = line.strip_prefix("data: ") { + if let Some(data) = super::extract_sse_data_value(&line) { if data == "[DONE]" { done = true; break;