From ed42f8f298360fdd114bdb0f39870a7e934ae2cc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 9 Apr 2026 23:03:33 +0900 Subject: [PATCH] fix(api): surface provider error in SSE stream frames (companion to ff416ff) Same fix as ff416ff but for the streaming path. Some backends embed an error JSON object in an SSE data: frame: data: {"error":{"message":"context too long","code":400}} parse_sse_frame() was attempting to deserialize this as ChatCompletionChunk and failing with 'missing field' / 'invalid type', hiding the actual backend error message. Fix: check for an 'error' key before full chunk deserialization, same as the non-streaming path in ff416ff. Symmetric pair: - ff416ff: non-streaming path (response body) - this: streaming path (SSE data: frame) 115 api + 159 CLI tests pass. Fmt clean. --- .../crates/api/src/providers/openai_compat.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index e6502ba..e7234b6 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -1084,6 +1084,35 @@ fn parse_sse_frame( if payload == "[DONE]" { return Ok(None); } + // Some backends embed an error object in a data: frame instead of using an + // HTTP error status. Surface the error message directly rather than letting + // ChatCompletionChunk deserialization fail with a cryptic 'missing field' error. + if let Ok(raw) = serde_json::from_str::(&payload) { + if let Some(err_obj) = raw.get("error") { + let msg = err_obj + .get("message") + .and_then(|m| m.as_str()) + .unwrap_or("provider returned an error in stream") + .to_string(); + let code = err_obj + .get("code") + .and_then(|c| c.as_u64()) + .map(|c| c as u16); + let status = reqwest::StatusCode::from_u16(code.unwrap_or(400)) + .unwrap_or(reqwest::StatusCode::BAD_REQUEST); + return Err(ApiError::Api { + status, + error_type: err_obj + .get("type") + .and_then(|t| t.as_str()) + .map(str::to_owned), + message: Some(msg), + request_id: None, + body: payload.to_string(), + retryable: false, + }); + } + } serde_json::from_str::(&payload) .map(Some) .map_err(|error| ApiError::json_deserialize(provider, model, &payload, error))