Originally published at heycc.cn. This is a mirrored copy — the canonical version is kept up to date at the source.
How Streaming Actually Works Across the Claude, OpenAI, and Gemini APIs
Every provider's streaming quickstart shows you the happy path: open a connection, print tokens as they arrive, done. What the quickstart doesn't show is the part that actually breaks production code — reassembling a tool call from a dozen partial-JSON fragments, telling a genuine mid-stream error apart from a dropped TCP connection, and deciding what to do with the half-written paragraph a user is staring at when the stream dies. Those three problems have different, provider-specific answers, and getting them wrong is what turns "we added streaming" into "customer support is fielding tickets about garbled function calls."
This piece works through the actual wire format each provider sends — the SSE event names, the exact JSON shapes, sourced from the primary docs and checked against them a second time before publish — then builds one accumulation pattern that works across all three, then covers the failure mode nobody's quickstart mentions: a stream that dies mid-response, and what you can and can't do about the tokens you already committed to the user. This is the streaming-specific companion to our broader guides on structured outputs from LLM APIs and LLM API error handling, retries, and backoff — this one is scoped narrowly to what changes when the response arrives token-by-token instead of all at once.
The three event models, side by side
All three providers stream over server-sent events (SSE), but they picked structurally different event vocabularies. Anthropic's is the most explicit: named events with a content-block index. OpenAI's Chat Completions API collapses everything into one repeating chunk type and leans on null-vs-populated fields to signal state. Gemini's stable generateContent/streamGenerateContent surface streams text incrementally, and — this needs a real caveat, worked out below — Google's own docs no longer clearly specify how (or whether) that surface fragments function-call arguments, because Google has since shipped a second, newer streaming surface for exactly that purpose.
Claude: named events, one per content-block lifecycle stage
Anthropic's Messages API streams a strict event sequence per the official streaming docs:
-
message_start— aMessageobject withcontent: [] - For each content block:
content_block_start→ one or morecontent_block_delta→content_block_stop - One or more
message_deltaevents (top-level changes, cumulative usage) message_stop
ping events can appear anywhere in the stream and carry no payload beyond {"type": "ping"}. A minimal text response looks like this, straight from the docs:
event: message_start
data: {"type": "message_start", "message": {"id": "msg_01...", "role": "assistant", "content": [], "model": "claude-opus-4-8", "stop_reason": null, "usage": {"input_tokens": 25, "output_tokens": 1}}}
event: content_block_start
data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}
event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}}
event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "!"}}
event: content_block_stop
data: {"type": "content_block_stop", "index": 0}
event: message_delta
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}, "usage": {"output_tokens": 15}}
event: message_stop
data: {"type": "message_stop"}
Two details in that payload matter for anyone writing token-counting code: the usage field on message_delta is explicitly documented as cumulative, not incremental — sum it wrong and you'll double-count. And content_block_start for a tool_use block always ships with input: {} already present (an empty object, not null), which is a convenient sentinel for "block opened but no arguments yet."
For a tool-use turn, Claude opens a second content block at index: 1 with content_block: {"type": "tool_use", "id": "toolu_01...", "name": "get_weather", "input": {}}, then streams the arguments as input_json_delta events carrying a partial_json string fragment — not a partial object:
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"location\":"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"San"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" Francisc"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"o,"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" CA\"}"}}
event: content_block_stop
data: {"type":"content_block_stop","index":1}
The docs are explicit about the chunking granularity: "Current models only support emitting one complete key and value property from input at a time" — meaning the fragmentation you see is at roughly the JSON-property level today, not arbitrary byte splits, though the format is deliberately designed to support finer-grained splitting in future models. Anthropic's own recommended reassembly method: buffer the partial_json strings and parse once, on content_block_stop, using a partial-JSON-capable parser (they name Pydantic's partial JSON support) or the SDK's built-in accumulation helpers.
Extended thinking adds a third delta type. thinking_delta events stream the reasoning text incrementally, and — this is the part people miss — a signature_delta event arrives just before content_block_stop, carrying a cryptographic signature used to verify the thinking block's integrity when you pass it back on a later turn. If you configure display: "omitted" on the thinking block, no thinking_delta events are sent at all — you get the block open, a single signature_delta, and close, with no visible reasoning text.
OpenAI Chat Completions: one chunk shape, delta-keyed by array index
OpenAI's Chat Completions streaming is structurally flatter. Every chunk is a chat.completion.chunk object — same id across the whole stream, a choices array, and (only on the final chunk, if you asked for it) a usage object. Per the API reference, each choices[i] entry carries delta, finish_reason, and index. The delta itself can carry content (text fragment), role (only on the first chunk), tool_calls (array), or refusal.
Tool calls are where the indexing scheme matters. Per OpenAI's function-calling guide, only the first delta for a given tool call carries id and function.name; every subsequent chunk for that same index carries id: null, function.name: null, and only an incremental function.arguments string fragment:
[{"index": 0, "id": "call_DdmO9pD3xa9XTPNJ32zg2hcA", "type": "function", "function": {"name": "get_weather", "arguments": ""}}]
[{"index": 0, "function": {"arguments": "{\""}}]
[{"index": 0, "function": {"arguments": "location"}}]
[{"index": 0, "function": {"arguments": "\":\"Paris, France\"}"}}]
The stream terminates when choices[0].finish_reason becomes one of "stop", "length", "tool_calls", "content_filter", or the deprecated "function_call". The very last content delta is typically an empty object. If you set stream_options: {"include_usage": true} on the request, one extra chunk arrives after the final content chunk, with choices: [] and a populated usage object (completion_tokens, prompt_tokens, total_tokens, plus completion_tokens_details/prompt_tokens_details breakdowns) — every prior chunk has usage: null. The reference documentation notes explicitly that if the stream is interrupted or cancelled before it finishes, you may never receive that final usage chunk at all — a detail worth designing around if you bill on token counts. The whole SSE stream is closed by a literal data: [DONE] line, which is not JSON and must be special-cased in your parser.
Gemini: text streams incrementally; function-call streaming depends on which API you're calling
This is the detail most comparison posts get wrong, and it is also the detail that changed under this article while it was being written — which is itself the point. Gemini's generateContent/streamGenerateContent surface streams GenerateContentResponse chunks whose candidates[].content.parts[] array can contain text fragments incrementally, per the GenerateContentResponse reference. Historically, and in every code sample Google has published for this endpoint, a functionCall part shows up as a complete {"name": "...", "args": {...}} object within a single chunk rather than as a stream of argument fragments — but as of this check, neither the GenerateContentResponse reference page nor the generateContent-toggled version of the function-calling guide actually states this as an explicit contract. It's a pattern visible in every published example, not a documented guarantee. Treat "Gemini's stable REST surface delivers functionCall.args as a single parsed object, no reassembly required" as the current empirical pattern, not a spec you can hold Google to.
That distinction matters more than usual right now because Google has restructured its own documentation around a second, newer surface. As of this writing, ai.google.dev/api/generate-content — the reference page for generateContent/streamGenerateContent — carries a banner stating "The Interactions API is now generally available. We recommend using this API for access to all the latest features and models." The generateContent-toggle version of the function-calling guide goes further, opening with: "Note: This version of the page covers the previous generateContent API. We recommend using the new Interactions API for access to all the latest features and models." generateContent is not deprecated or shut off, and it is almost certainly still what client.models.generate_content(..., stream=True) calls in the current SDKs — but it is no longer the API Google's own docs lead with, and "the one every SDK's streaming call hits by default" is a claim that needs an explicit as-of date rather than being stated as a durable fact.
The Interactions API's own guide is explicit about incremental function-argument streaming, in sharp contrast to generateContent's silence on the topic: "Tool arguments can be streamed as partial arguments using arguments. You must aggregate these deltas to reconstruct the complete tool calls before executing them," with the accumulation pattern shown as current_calls[event.index]["arguments"] += event.delta.partial_arguments. That is structurally the same fragmented-string-concatenation problem as OpenAI's function.arguments, and the opposite of generateContent's apparent single-chunk delivery. So the real, checkable state of Gemini streaming as of this writing is: two APIs, two different function-call delivery models, and Google steering new integrations toward the one that fragments (Interactions), away from the one this article otherwise documents (generateContent). If you're building against Gemini today, confirm in your own SDK version and endpoint which of the two you're actually calling before you write accumulation code — don't assume based on this article or any other secondary source.
One accumulator, three input formats
Despite the wire-format differences, the reassembly problem is identical in shape across Claude, OpenAI, and Gemini's Interactions API: buffer partial strings keyed by position, don't parse until you have a completion signal, then parse once. Gemini's generateContent surface, per the caveat above, appears not to need this step at all for function calls — but confirm that against your own traffic rather than trusting it blindly. Below is the pattern normalized across the three that do fragment. This is not SDK code — it is the manual accumulation logic you need if you're hitting the raw SSE endpoint directly (which you'll do if you're proxying, logging raw events, or writing an SDK-agnostic gateway).
# Claude: keyed by content_block index, buffer partial_json strings
class ClaudeToolCallBuffer:
def __init__(self):
self.blocks = {} # index -> {"id":..., "name":..., "json": ""}
def handle_event(self, event):
if event["type"] == "content_block_start" and event["content_block"]["type"] == "tool_use":
self.blocks[event["index"]] = {
"id": event["content_block"]["id"],
"name": event["content_block"]["name"],
"json": "",
}
elif event["type"] == "content_block_delta" and event["delta"]["type"] == "input_json_delta":
self.blocks[event["index"]]["json"] += event["delta"]["partial_json"]
elif event["type"] == "content_block_stop" and event["index"] in self.blocks:
block = self.blocks[event["index"]]
block["input"] = json.loads(block["json"] or "{}") # parse exactly once
# OpenAI Chat Completions: keyed by tool_calls[].index, concat arguments strings
final_tool_calls = {}
for chunk in stream:
for tc in (chunk.choices[0].delta.tool_calls or []):
if tc.index not in final_tool_calls:
final_tool_calls[tc.index] = tc # first sighting: keep id/name
else:
final_tool_calls[tc.index].function.arguments += tc.function.arguments
# after stream ends: json.loads(final_tool_calls[i].function.arguments) per index
# Gemini Interactions API: keyed by event.index, concat arguments_delta strings
# (mirrors OpenAI's shape almost exactly -- per ai.google.dev/gemini-api/docs/function-calling)
current_calls = {}
for event in stream:
if event.type == "response.function_call_arguments.delta":
current_calls.setdefault(event.index, {"arguments": ""})
current_calls[event.index]["arguments"] += event.delta.partial_arguments
# after stream ends: json.loads(current_calls[i]["arguments"]) per index
If you're designing the tool schemas these buffers eventually get parsed into, our guide to structured outputs from LLM APIs covers the schema-shaping side of this same problem. The one universal rule underneath all of these: never call JSON.parse/json.loads on a buffer that hasn't received its explicit completion signal (content_block_stop for Claude, the last chunk where tool_call.index stops appearing for OpenAI — in practice, finish_reason: "tool_calls"). Parsing eagerly on every delta is the single most common bug in hand-rolled streaming code, and it's an easy one to not notice in testing because short arguments sometimes complete within one or two chunks and happen to parse validly at intermediate points by accident.
Reproducible check: partial JSON does not parse
You can verify the failure mode yourself without hitting any API — this is exactly what a naive "parse on every delta" implementation does to itself:
$ node -e 'JSON.parse("{\"location\": \"San Fra")'
undefined:2
{"location": "San Fra
^
SyntaxError: Unterminated string in JSON at position 21 (line 1 column 22)
at JSON.parse (<anonymous>)
$ python3 -c 'import json; json.loads("{\"location\": \"San Fra")'
Traceback (most recent call last):
...
json.decoder.JSONDecodeError: Unterminated string starting at: line 1 column 13 (char 12)
Both interpreters fail exactly as expected — a truncated JSON fragment is not valid JSON, full stop. If you need to display partial arguments to a user while streaming (e.g., a "calling search(query: 'weat...')" progress indicator), you need an actual partial-JSON parser like Pydantic's, not a try/except around the real one — a bare try: json.loads(buf) except: pass will intermittently succeed on accidentally-well-formed prefixes (e.g., right after a fragment that happens to close a string) and hand you a half-built object that looks complete but is missing keys the model hasn't emitted yet.
Comparison table: streaming event models
| Claude (Messages API) | OpenAI (Chat Completions) | Gemini (generateContent, current default) |
Gemini (Interactions API, now GA-recommended) | |
|---|---|---|---|---|
| Transport | SSE, named event: types |
SSE, single chat.completion.chunk type |
SSE with alt=sse, one GenerateContentResponse type |
SSE, response.* typed events |
| Text delta shape |
content_block_delta / text_delta.text
|
choices[0].delta.content |
candidates[0].content.parts[].text |
response.output_text.delta (per Google's event-naming pattern) |
| Tool-call streaming granularity | Fragmented (input_json_delta.partial_json) |
Fragmented (function.arguments string concat) |
Not documented as fragmented; every published example shows one complete functionCall part |
Explicitly fragmented (arguments_delta / partial_arguments, must be concatenated) |
| Tool-call identity signal |
id/name on content_block_start, index-keyed |
id/name only on first delta, index-keyed |
name/args both present in the single part |
event.index-keyed, per Google's docs |
| Completion signal per block |
content_block_stop (per index) |
Absence of further deltas at that index + finish_reason
|
Chunk containing that part, finishReason on candidate |
Documented completion event per Interactions API guide |
| Cumulative vs incremental usage |
message_delta.usage is cumulative |
Final usage-only chunk (opt-in via stream_options); documented as possibly absent if stream is interrupted |
usageMetadata cumulative, on final chunk |
Not verified in this piece — check current docs |
| Stream-end sentinel |
message_stop event |
Literal data: [DONE] line |
HTTP stream close (no explicit sentinel event documented) | Not verified in this piece — check current docs |
| Documented mid-stream error frame (inside the open SSE stream, after a 200) | Yes — event: error with typed error.type (see below) |
Not documented — reference shows only success-path chunks | Not documented — troubleshooting guide covers HTTP-level codes (400/429/500/499) only | Not verified in this piece — check current docs |
| Extended reasoning deltas |
thinking_delta + signature_delta
|
N/A (Chat Completions has no equivalent stream event) | N/A in generateContent | Not verified in this piece |
| Google's current recommendation | N/A | N/A | Labeled "previous API" in the function-calling guide as of this check | Labeled GA and recommended as of this check |
When the stream dies: detection, resumption, and what you've already committed
This is the part that separates a demo from production code, and it is also where the three providers' documentation diverges most sharply — not just in wire format, but in how much they tell you at all. A stream can die three structurally different ways, and each needs different handling:
- A clean mid-stream error event — the connection stays open, but the provider sends you an explicit error payload instead of the next expected delta.
- A silent connection drop — TCP reset, proxy timeout, client network change, server-side cancellation. No error payload, just no more bytes.
-
A stream that completes normally but with a
stop_reason/finish_reasonyou didn't want (e.g.,max_tokens/lengthtruncation) — not a failure exactly, but the same "what do I do with the partial content" problem.
Claude: the only one of the three with a documented in-stream error frame
Claude sends typed errors inside the SSE stream itself, which is a distinct failure mode from a raw network drop, and — per the research for this piece — it's the only one of the three providers whose public docs describe this mechanism explicitly. Per Anthropic's error docs, the documented error types map to specific HTTP status codes when they occur outside streaming — overloaded_error → 529, rate_limit_error → 429, api_error → 500, authentication_error → 401, not_found_error → 404, invalid_request_error → 400, request_too_large → 413, timeout_error → 504. Inside a stream, that same taxonomy arrives as an event: error frame after the initial 200:
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}
The docs flag this explicitly: "it's possible that an error can occur after returning a 200 response, in which case error handling wouldn't follow these standard [HTTP status] mechanisms." That single sentence is the whole reason hand-rolled stream error handling is easy to get subtly wrong: your HTTP client sees a 200 and a clean SSE stream; only application-level parsing of the event body tells you the request actually failed partway through — see our error handling and retries guide for the classify-before-retry framework this feeds into once you've correctly detected the failure.
This exact gap produced a real, documented bug: anthropic-sdk-python issue #1258, filed against SDK v0.52.0. The reported root cause: the SDK's _streaming.py built the resulting APIStatusError by dispatching on the original HTTP response object's status_code — which was 200, since the connection itself succeeded — rather than mapping the SSE error body's own type field (e.g. overloaded_error) to its documented status code (529). The practical effect: application code checking status_code >= 500 (a completely reasonable check) never saw the error as retryable, because it was looking at a 200. This specifically broke pydantic-ai's FallbackModel, whose failover logic gates on that same status_code >= 500 condition — so an overloaded provider degraded to a silently-swallowed request failure instead of transparent failover to a backup model.
The issue is closed, but not by an explicit "Closes #1258" link — the closing event was fired by stainless-app[bot] one second after PR #1264 merged, which reads as a release-automation side effect of timing/reference rather than a semantic GitHub link. Three separate community-submitted PRs that explicitly claimed to fix the described status-code remapping (#1262, #1263, #1269) were each closed without merging. PR #1264 itself is worth being precise about rather than pointing to it as a clean, self-contained fix: it is Stainless's automated release: 0.87.0 PR, a bundle of 19 commits (dependency bumps, CI changes, a TOCTOU symlink-race fix, file-permission hardening, and an unrelated async-transform bugfix among them) rather than a single targeted patch. The one change in that bundle that is clearly on-topic is a new error_type field added to APIStatusError for better error classification. Whether that field alone constitutes the full fix described in the issue, or whether the actual status-code remapping landed in a commit this research could not cleanly isolate inside that bundle, is not something this piece can state with confidence — bundled release PRs are exactly the kind of artifact where a bot-driven closure does not mean "here is the one diff that fixes it." The generalizable lesson doesn't depend on resolving that ambiguity: if you write your own SSE parsing, don't trust the wrapping HTTP response's status code once you're inside the stream body — branch on the payload's own type field, and verify the fix landed in whatever SDK version you actually pin, not just "some version after the issue closed."
OpenAI and Gemini: no documented in-stream error frame, just a possible silent drop
This is a real, checkable asymmetry between the providers, not an oversight in this piece. OpenAI's Chat Completions streaming reference lists error as a value that can theoretically appear as an event type, but the reference documentation itself shows only success-path chunk sequences and does not specify a payload shape for a mid-stream error frame the way Claude's docs do. What the docs do say explicitly: "If the stream is interrupted or cancelled, you may not receive the final usage chunk which contains the token usage statistics for the entire request" — which tells you that OpenAI's documented failure mode is an incomplete stream, not a typed error event you can branch on. In practice, this means your OpenAI streaming client needs a timeout-based drop detector, not just an event-type switch statement: if you haven't received a delta or a finish_reason within your expected interval, treat the connection as dead and fall back to whatever partial content you've accumulated so far.
Gemini's documentation follows the same pattern. Its troubleshooting guide documents backend error codes (400, 429, 500, and a 499 CANCELLED specifically described as occurring when "the client closed the connection before the API could finish responding") but does not document an in-stream error event/frame comparable to Claude's event: error. Read together with the 499 guidance to "check if your client or network infrastructure is prematurely closing the connection," the implication is the same as OpenAI's: Gemini's documented failure surface is connection-level, and a caller has to detect a stalled or closed stream by absence of expected activity, not by parsing a typed error payload out of the open stream.
The practical asymmetry, stated plainly: Claude is the only one of the three where you should build an explicit event: error branch into your parser. For OpenAI and Gemini, your failure-detection code has to be a watchdog timer around the whole stream (no bytes/no expected event within N seconds → treat as dead), not a case in your event-type switch. Building an OpenAI or Gemini client that waits for a typed error event that the docs never promise will exist is a bug waiting to ship — you'll hang on drops instead of failing fast.
What you're actually allowed to do with the tokens you already committed
Say you're at token 400 of an expected ~800-token response, streaming to a chat UI, and the connection dies (any of the three failure modes above). The half-written paragraph is already rendered on the user's screen. Your options, in order of how much state you're allowed to assume:
Discard and restart from scratch. Always correct, always available, and the only safe option if you cannot cheaply tell whether the partial output ended on a semantically complete unit (a finished sentence, a finished JSON key) or mid-token. This is the right default for anything structured — a partially-streamed tool call is not resumable in any of the three APIs discussed here; none of them offer a "resume this specific tool call from byte offset N" primitive. Re-issue the full request.
Keep the partial text, append an explicit marker, and let the user decide. For free-text chat responses specifically (not tool calls, not JSON you're about to parse), the partial content the user already read is not wrong — it's just incomplete. Appending something like "⚠ response interrupted" and offering a "continue" affordance that re-sends the original prompt plus the partial output as assistant-turn context is a common, workable pattern, but it is a pattern you build yourself — none of the three APIs in this piece document a stream-resumption primitive that continues an interrupted generation from where it left off. You are re-prompting with context, not resuming a paused stream. Treat any implementation of this as a new request that happens to be seeded with prior partial output, with all the same cost and prompt-injection considerations as a fresh call.
Never execute a tool call assembled from a stream that didn't reach its completion signal. This is the one hard rule, not a judgment call. If your Claude buffer never saw
content_block_stopfor that index, if your OpenAI buffer never sawfinish_reason: "tool_calls", or if your Gemini Interactions buffer never saw its documented completion event, thearguments/partial_json/inputyou've accumulated is provably incomplete —json.loadswill throw (per the reproducible check above) or, worse, silently succeed on a truncated-but-coincidentally-valid prefix and hand your tool-execution code a call with missing or wrong arguments. Fail the tool call explicitly and let your orchestration layer decide whether to retry the whole turn, rather than attempting to execute against a buffer you can't prove is complete.Treat
max_tokens/lengthtruncation as a distinct case from a hard failure. This isn't a dropped connection — the stream completed normally, the provider is telling you it stopped because it hit your configured limit. Claude reports this asstop_reason: "max_tokens"on themessage_delta; OpenAI reportsfinish_reason: "length". Both are successful, parseable streams with valid (if incomplete) content — the correct response is usually to either raise your token limit and continue the conversation with the partial output as prior context, or surface the truncation to the user explicitly rather than silently presenting a cut-off answer as if it were the model's complete, intended response.
None of the three providers documented in this piece offer a true mid-generation resume primitive — the "reconnect and continue exactly where the byte stream left off" feature that, say, resumable file uploads have. Every recovery strategy above is a variant of "restart the request, optionally seeded with what you already have," not "resume the same generation." If your production system needs guaranteed exactly-once delivery of a complete response, the honest design constraint is: buffer nothing as final until you've seen the provider's own completion signal, and budget for full re-generation cost on any interrupted stream, because that's what every documented recovery path here actually costs you.
How the wire formats in this piece were checked (2026-07-03)
Checked against primary sources on the date above:
- Anthropic streaming event sequence, cumulative
message_delta.usage,input_json_delta/partial_jsonchunking granularity note,thinking_delta/signature_deltabehavior, and theevent: errorSSE frame format — verified against platform.claude.com/docs/en/docs/build-with-claude/streaming and platform.claude.com/docs/en/api/errors. -
anthropic-sdk-pythonissue #1258 — verified as closed, with thestatus_code=200-vs-529 root cause and thepydantic-aiFallbackModelbreakage matching the issue text exactly (confirmed against GitHub's REST API directly, not a summarized view), filed against SDK v0.52.0. The closure itself was fired bystainless-app[bot]one second after PR #1264 merged — a timing-driven release-automation closure, not an explicit "Closes #1258" semantic link; three community PRs (#1262, #1263, #1269) that explicitly claimed the fix were each closed unmerged. PR #1264 was independently checked and is a bundled Stainlessrelease: 0.87.0PR containing 19 commits. This piece no longer asserts that #1264 "maps SSE error types to status codes" as a clean, isolated fix — it names the one clearly on-topic change in that bundle (a newerror_typefield onAPIStatusError) and states plainly that the exact commit implementing the full remapping described in the issue could not be cleanly isolated from the bundle. Readers pinning a fix should verify against their installed SDK version directly, not against a PR number. - OpenAI Chat Completions streaming chunk shape, tool-call delta indexing,
stream_options.include_usage, thedata: [DONE]sentinel, and the "may not receive the final usage chunk" interruption note — verified against developers.openai.com/api/reference/resources/chat/subresources/completions/streaming-events and developers.openai.com/api/docs/guides/function-calling. No documented mid-stream typed-error frame was found in this reference as of this check. - Gemini: confirmed that, as of this check, ai.google.dev/api/generate-content displays a banner stating the Interactions API "is now generally available" and is Google's recommendation, and that the generateContent-toggled version of ai.google.dev/gemini-api/docs/generate-content/function-calling opens with a note labeling generateContent "the previous API." Neither page states in explicit terms whether
streamGenerateContentdeliversfunctionCallparts atomically or fragmented — this piece now describes atomic delivery as the pattern shown in every published example, not a documented guarantee, correcting the earlier draft's overstated claim. The Interactions API's incrementalarguments_delta/partial_argumentsstreaming, quoted directly from ai.google.dev/gemini-api/docs/function-calling, was confirmed as explicitly documented. Gemini's troubleshooting guide was checked for a mid-stream error frame; none was found, only HTTP-level status codes including a 499CANCELLEDfor client-side connection closure. - Not independently verified and flagged as such in the table above: the exact event names and completion/usage-signaling shape of the Interactions API beyond the
arguments_deltamechanism quoted above. Readers building against the Interactions API specifically should treat this piece as a wire-format comparison for generateContent/Chat Completions/Messages API, with the Interactions API included only where directly documented, and confirm remaining details against Google's current reference before shipping.
Sources
- Anthropic: Streaming Messages — Claude SSE event sequence, delta types, usage semantics
-
Anthropic: Errors — error type-to-status-code mapping and the mid-stream
event: errorbehavior note - anthropic-sdk-python issue #1258 — the status_code=200 mid-stream error misclassification bug and its FallbackModel impact
- anthropic-sdk-python PR #1264 — the bundled release PR GitHub links as closing #1258 (release: 0.87.0, 19 commits)
- OpenAI: Chat Completions streaming events reference — chunk shape, usage-chunk opt-in, interrupted-stream usage note
- OpenAI: Function calling guide — tool-call delta indexing and argument-fragment reassembly
- Google: GenerateContentResponse reference — Gemini streaming response shape and the Interactions API GA banner
- Google: Function calling guide (generateContent version) — the "previous API" deprecation banner
-
Google: Function calling guide (Interactions API version) — documented
arguments_delta/partial_argumentsincremental streaming - Google: Gemini API troubleshooting — HTTP-level error codes including 499 CANCELLED for dropped connections
- Pydantic: Partial JSON parsing — the partial-JSON parser Anthropic's docs recommend for mid-stream display use cases

Top comments (0)