LLM SSE Stream Decoder

Paste a raw SSE capture from the Anthropic or OpenAI API. Get the reassembled message, tool arguments rebuilt from their partial_json fragments, and a straight answer on whether the response actually finished.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Decode. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

An answer that was cut off

The text reads as finished and the stop reason says otherwise, which is the only place that fact appears

event: message_start
data: {"type":"message_start","message":{"model":"claude-opus-5"}}

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":"The three main causes are"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"max_tokens"}}

event: message_stop
data: {"type":"message_stop"}

A tool call cut off mid-argument

Tool input arrives as partial_json fragments, and an incomplete concatenation is what a dropped stream looks like

event: message_start
data: {"type":"message_start","message":{"model":"claude-opus-5"}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"t1","name":"get_weather","input":{}}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"loc"}}

The cached usage split

input_tokens is the uncached remainder, so reading it alone under-reports the prompt by however well the cache is working

event: message_start
data: {"type":"message_start","message":{"model":"claude-opus-5","usage":{"input_tokens":120,"cache_read_input_tokens":18400,"cache_creation_input_tokens":0}}}

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":"Done."}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"}}

event: message_stop
data: {"type":"message_stop"}

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Reading the text and ignoring the stop reason

    `max_tokens`, `refusal` and `pause_turn` all produce readable text that is not a finished answer, and nothing in the text says so. Only `end_turn` means the model was done.

    Instead:Branch on the stop reason before you use the content. It arrives on `message_delta`, not on `message_stop`.

  2. Parsing each `input_json_delta` as it arrives

    The fragments are slices of one JSON document and are individually meaningless. This fails on the first tool call with more than a few characters of argument, and looks like the model emitting invalid JSON.

    Instead:Accumulate every `partial_json` for the block and parse once, after `content_block_stop`.

  3. Treating `input_tokens` as the prompt size

    With caching on it is the uncached remainder only. The full prompt is that plus `cache_creation_input_tokens` plus `cache_read_input_tokens`, so the better your caching, the more wrong the number.

    Instead:Sum all three.

A truncated answer looks exactly like a finished one

The text of a response says nothing about whether the model stopped because it was done. That fact lives in a separate field, on a different event, and code that reads only the text ships fragments to users.

The stop reason is on message_delta, not message_stop

end_turn means the model finished. max_tokens means the output cap was reached mid-sentence. refusal means classifiers declined. pause_turn means a server tool needs another round trip. Only the first is a complete answer, and all four produce readable text. Branch on the stop reason before you read the content.

Tool arguments arrive as fragments that mean nothing alone

A tool call's input is streamed as a series of input_json_delta values, each holding a slice of the JSON. Concatenate them in order and parse only after content_block_stop. Parsing a fragment on arrival fails on the first call with more than a few characters of argument, and the failure looks like the model producing invalid JSON.

Thinking tokens spend the same budget as the answer

Reasoning is output. It is billed as output and it counts against max_tokens, so a budget sized for the visible answer can be consumed entirely by thinking, leaving a response that hit the cap having produced nothing readable. This decoder reports the stop reason so that case is distinguishable from the model simply being brief.

input_tokens is the uncached remainder, not the prompt

When caching is on, the full prompt is input_tokens plus cache_creation_input_tokens plus cache_read_input_tokens. Reading the first field alone under-reports by however well the cache is working, which is why an agent that ran for hours can show a four-thousand-token prompt.

A stream that stops is not a stream that finished

A dropped connection produces a partial response with no stop reason at all. That is a different failure from max_tokens and needs a different fix: usually a client read timeout firing during a long thinking phase, when no bytes are on the wire for a while.

What this cannot see

Whether the content is correct, and whether anything was lost before the capture began. It reads the frames you paste, in your browser, and contacts nothing. It handles the Anthropic event-typed dialect and the OpenAI chunk dialect; a stream from anything else may decode partially or not at all. Byte-level transport problems below SSE, such as a proxy re-chunking the body, are invisible here because by the time you have text they have already happened.