# Streaming

Set streaming and the reply becomes server-sent events. Text and reasoning arrive as separate bracketed runs so you can render them apart; tool calls arrive whole. The frames are the same whichever provider answers.

## The shape of a stream

Each SSE event's name is the frame kind; its `data` is an envelope carrying either the frame or a failure. The HTTP
status is `200` as soon as the stream opens, so everything after that point — including failure — is reported in the
stream rather than in the status line.

```json
{
  "model": "claude-sonnet-5",
  "streaming": true,
  "systemPrompt": "Answer in one sentence.",
  "messages": [{ "type": "user", "value": "What is the capital of France?" }],
  "parameters": { "maxTokens": 1024 }
}
```

## Event names and discriminators

The SSE `event:` name and the `type` discriminator inside `data.value` use the same lower-camel spelling.
`event: streamStart` carries `"type":"streamStart"`. The `error` event carries no `value` object.

## The envelope

Every `data` payload is a result envelope — `success`, `code`, and either `value` or `message`. One parse handles
both outcomes, so a failure never needs a different code path than a success.

```json
{ "success": true,  "code": 200, "value": { "type": "content", "value": "Paris." } }
{ "success": false, "code": 502, "message": "The provider closed the connection mid-answer." }
```

```http
event: streamStart
data: {"success":true,"code":200,"value":{"type":"streamStart","model":{"name":"claude-sonnet-5","tier":"high","publisher":"Anthropic","provider":"Anthropic"}}}

event: thinkingStart
data: {"success":true,"code":200,"value":{"type":"thinkingStart"}}

event: thinking
data: {"success":true,"code":200,"value":{"type":"thinking","value":"The user wants a capital city."}}

event: thinkingEnd
data: {"success":true,"code":200,"value":{"type":"thinkingEnd","data":{"anthropic.signature":"…"}}}

event: contentStart
data: {"success":true,"code":200,"value":{"type":"contentStart"}}

event: content
data: {"success":true,"code":200,"value":{"type":"content","value":"Paris."}}

event: contentEnd
data: {"success":true,"code":200,"value":{"type":"contentEnd"}}

event: stop
data: {"success":true,"code":200,"value":{"type":"stop","usage":{"provider":"anthropic","inputTokens":24,"outputTokens":3,"finishReason":"stop"}}}
```

## Frame kinds

| `event` | `value.type` | Payload |
|---|---|---|
| `streamStart` | `streamStart` | `model` — canonical name, tier, publisher, and provider. The only place the resolved model appears. |
| `thinkingStart` | `thinkingStart` | None. Opens a reasoning run. |
| `thinking` | `thinking` | `value` — a text delta to append to the open reasoning run. |
| `thinkingEnd` | `thinkingEnd` | `data` — the provider state needed to replay this reasoning natively later. |
| `contentStart` | `contentStart` | None. Opens an answer run. |
| `content` | `content` | `value` — a text delta to append to the open answer run. |
| `contentEnd` | `contentEnd` | None. Closes an answer run. |
| `tool` | `tool` | `value` — the whole call at once: `id`, `name`, `arguments`. Never partial. |
| `stop` | `stop` | `usage`. Always last on a successful stream. |
| `streamError` | `streamError` | `model` — the provider that broke after producing output. |
| `error` | — | Terminal. `success:false`, an HTTP `code`, a `message`. No `value`. |

A run can open and close more than once. A model that thinks, answers, thinks again and then calls a tool emits
`thinkingStart … thinkingEnd contentStart … contentEnd thinkingStart … thinkingEnd tool stop`. Render each bracketed
run as its own element rather than appending everything to one buffer.

## Tool calls arrive whole

There are no argument deltas to reassemble.

```http
event: tool
data: {"success":true,"code":200,"value":{"type":"tool","value":{"id":"toolu_01H","name":"lookupIncident","arguments":"{\"id\":\"INC-4412\"}"}}}
```

## One parse, both outcomes

```csharp
var envelope = JsonSerializer.Deserialize<Result<ModelResponse>>(sse.Data, JsonHelper.SerializerOptions);
if (!envelope.Success)
{
    throw new GatewayException(envelope.Code, envelope.Message);
}

switch (envelope.Value)
{
    case ModelResponse.StreamStart start: view.OpenSegment(start.Model); break;
    case ModelResponse.Thinking thinking:  view.Thinking.Append(thinking.Value); break;
    case ModelResponse.Content content:    view.Answer.Append(content.Value); break;
    case ModelResponse.Tool tool:          await Dispatch(tool.Value); break;
    case ModelResponse.StreamError failed: view.MarkHandover(failed.Model); break;
    case ModelResponse.Stop stop:          view.Finish(stop.Usage); break;
}
```

## Assembling a turn from frames

The frames assemble back into exactly the `messages` the unary route would have returned: thinking frames between
`thinkingStart` and `thinkingEnd` become one `thinking` message carrying that `data`, content frames become one
`content` message, and each `tool` frame becomes a `toolCall`. Markers are not messages and are discarded.

If you persist conversations, store the assembled messages rather than the raw frames — that is the shape you send
back, and it is what carries the provider state that lets reasoning replay natively.

## How a stream ends

Exactly one terminal outcome closes a completed stream. An EOF without either means the stream was cut short and is
a failure, not a finished answer.

| Ending | Meaning |
|---|---|
| `stop` | The model finished. `usage` is attached. |
| `error` | The request failed. `code` and `message` say how. |
| `error` with `502` | The provider opened a stream and produced nothing, or ended without a terminal event. |
| EOF with neither | Incomplete. Do not treat the text so far as a finished answer. |

Disconnecting cancels the provider call rather than letting it run on.
