Skip [DONE] stream terminator in anthropic and google_ai SSE parsers (#61627)
## Problem
When `api_url` points to a proxy or gateway that appends `data: [DONE]`
to the SSE stream, every streaming completion fails with:
```
error deserializing Anthropic API response: expected value at line 1 column 2
```
In the UI this surfaces as **"error deserializing Anthropic API response
— Retrying (Attempt 3 of 3)"** followed by **"Connection Interrupted"**,
even though the proxy returned a well-formed stream and the model's full
response was already delivered before the terminator. The failure is
100% reproducible on multi-turn tool-use conversations (Zed reads the
stream to completion after each turn, so it always reaches the trailing
`[DONE]`) and intermittent on single-turn conversations (depending on
whether the connection closes before the line is read). The same class
of error appears in the `google_ai` crate as `Error parsing JSON: ...
"[DONE]"`.
Neither side is at fault here. Zed's current parser is a correct
implementation of the Anthropic and Gemini specs, which terminate
streams by connection close and never emit `[DONE]`; at the same time,
appending a `[DONE]` terminator is a common, legitimate convention among
SSE gateways. This PR is a compatibility improvement that makes Zed
robust to that widely-used stream shape, so streaming works whether or
not the upstream emits the terminator.
## Why Zed crashes but the Anthropic SDK does not
The Anthropic TypeScript and Python SDKs handle this correctly
**without** any explicit `[DONE]` check. Their SSE parser uses a
two-layer architecture:
1. **SSE framing layer** — parses `event:` / `data:` fields into
`{event, data}` objects without inspecting the data payload.
2. **Event dispatch layer** — only calls `JSON.parse` on events whose
`event` name is in a known whitelist (`message_start`,
`content_block_delta`, etc.). A bare `data:` line with no preceding
`event:` line yields `event: null`, which is not in the whitelist and is
silently ignored.
So when a proxy sends `data: [DONE]`, the SDK produces `{event: null,
data: "[DONE]"}`, the dispatch layer drops it, and `JSON.parse` is never
run on `[DONE]`. Claude Code and Cline both use the Anthropic SDK under
the hood, so they inherit this behavior and never crash on `[DONE]`.
Zed's `anthropic` and `google_ai` crates use a simpler single-layer
parser: strip the `data:` prefix, then unconditionally
`serde_json::from_str` the remainder. There is no event-name dispatch,
so `[DONE]` goes straight into the JSON parser — `[` looks like an array
start, `D` is not a valid value, serde reports `expected value at line 1
column 2`, and the stream dies.
## Fix
Add a one-line guard after prefix stripping: if the trimmed payload is
`[DONE]`, return `None` from the `filter_map` closure. This is a
lightweight equivalent of the SDK's whitelist-ignore behavior — same
result (silently skip the terminator), without restructuring the parser
into a full event-name dispatch architecture.
Two commits, one per crate:
1. `anthropic` — guard in `stream_completion_with_rate_limit_info`
2. `google_ai` — identical guard in `stream_generate_content`
## Scope
The other ten SSE-based providers in this repo (`open_ai`, `deepseek`,
`mistral`, `open_router`, `lmstudio`, `llama_cpp`, `copilot_chat`)
already handle `[DONE]` because their upstream specs (OpenAI-compatible)
define it as a required stream terminator. `ollama` (NDJSON) and
`bedrock` (AWS SDK event stream) do not use SSE. After this PR, every
SSE parser in Zed handles `[DONE]` without error.
## Safety
- Official Anthropic and Gemini APIs never send `[DONE]`; the guard is a
no-op on direct connections.
- `line.trim() == "[DONE]"` handles both `data: [DONE]` and
`data:[DONE]`.
- Returning `None` drops the line silently; the stream concludes on the
next EOF as usual.
Release Notes:
- Fixed streaming completions failing with "error deserializing
Anthropic API response" when the SSE endpoint appends a `[DONE]` stream
terminator (affects custom `api_url` proxies for Anthropic and Google
Gemini)
This commit is contained in:
1 parent
9ec4826944
commit
db7d5ddd19
2 files changed
+18
No files matched your search
@@ -560,6 +560,15 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
.strip_prefix("data: ")
|
||||
.or_else(|| line.strip_prefix("data:"))?;
|
||||
|
||||
// Some proxies and gateways append `data: [DONE]` as a
|
||||
// stream-termination sentinel (an OpenAI convention).
|
||||
// It is not part of the Anthropic streaming spec and is
|
||||
// not valid JSON, so skip it to avoid a spurious
|
||||
// deserialization error.
|
||||
if line.trim() == "[DONE]" {
|
||||
return None;
|
||||
}
|
||||
|
||||
match serde_json::from_str(line) {
|
||||
Ok(response) => Some(Ok(response)),
|
||||
Err(error) => Some(Err(AnthropicError::DeserializeResponse(error))),
|
||||
|
||||
@@ -42,6 +42,15 @@ pub async fn stream_generate_content(
|
||||
match line {
|
||||
Ok(line) => {
|
||||
if let Some(line) = line.strip_prefix("data: ") {
|
||||
// Some proxies and gateways append `data: [DONE]` as a
|
||||
// stream-termination sentinel (an OpenAI convention).
|
||||
// Google's streaming spec does not include this sentinel,
|
||||
// but we tolerate it here for the same reason as the
|
||||
// anthropic crate: proxies that inject it would otherwise
|
||||
// cause a spurious JSON deserialization error.
|
||||
if line.trim() == "[DONE]" {
|
||||
return None;
|
||||
}
|
||||
match serde_json::from_str(line) {
|
||||
Ok(response) => Some(Ok(response)),
|
||||
Err(error) => Some(Err(anyhow!(format!(
|
||||
|
||||
Reference in new issue
Block a user