> Keiro Labs API documentation - Streaming
> Page: https://docs.keirolabs.ai/streaming.html
> Markdown: https://docs.keirolabs.ai/streaming.md
> Agent index: https://docs.keirolabs.ai/llms.txt
> API base URL: https://api.keirolabs.ai/v1
> Auth: send "Authorization: Bearer <api-key>" with a key created in the
> Keiro console (https://console.keirolabs.ai/api-keys). Use credentials saved by
> `keiro setup`, or resolve the key from a secret manager or an owner-only
> secret file and pass it to the client explicitly. Never read another
> provider's variable, and never put raw key material in environment
> variables, code, docs, or logs.

# Streaming

Keiro streams completion responses as server-sent events. Set `stream` to
`true`, process events in order, and keep the connection open until the
surface's terminal event or in-band error.

Use curl `-N` when testing from a terminal so client-side buffering does not
hide incremental delivery.

During a long stream, the gateway writes the static SSE comment `: keepalive`
at least every 10 seconds (Messages uses its native `ping` event). SSE comments
carry no model output, consume no event sequence number, and are not replayed;
conforming parsers ignore them while the bytes keep transport-idle timers alive.

## Chat Completions stream

Chat Completions sends `data:` chunks with text in
`choices[0].delta.content`. Tool-call fragments appear in
`choices[0].delta.tool_calls`. Assemble by choice and tool-call index.

The successful stream ends with a terminal choice and `data: [DONE]`.

Runnable streaming samples live on
[Chat Completions](chat-completions.md#streaming).

## Responses stream

Responses emits typed events. Read `type` and ignore event types your client
does not handle.

| Event `type` | Meaning |
|---|---|
| `response.created` | The response exists and streaming started |
| `response.in_progress` | Generation is active |
| `response.output_item.added` | A typed output item started |
| `response.content_part.added` | A content part started |
| `response.output_text.delta` | Incremental text in `delta` |
| `response.output_text.done` | One text segment finished |
| `response.function_call_arguments.delta` | Function arguments arrived for a tool call |
| `response.completed` | Successful terminal event |
| `response.incomplete` | Terminal event for an early stop, such as an output limit |
| `response.error` | In-band stream failure |

Successful Responses streams also finish with `data: [DONE]` after the
terminal event.

Runnable streaming samples live on [Responses](responses.md#streaming).

## Messages stream

Messages emits `message_start`, content-block start/delta/stop events,
`message_delta`, and `message_stop`. The terminal `message_delta` carries the
final stop reason and usage. Tool calls use `tool_use` content blocks and input
JSON deltas. When extended thinking is enabled, unsigned `thinking` summary
blocks stream first, through `thinking_delta` events, and close before the
text block opens.

See [Messages](messages.md) for supported request fields and content shapes.

## Stream lifetime and time entitlements

A stream that is actively working is never ended for taking long. While the
model keeps producing activity, the stream stays open until it finishes or
reaches the wall-clock entitlement for its requested reasoning effort. A live
stream ends early in exactly two documented cases:

- **Idle timeout** — no model activity for the idle window. Keepalive comments
  are transport liveness only; they do not count as model activity.
- **Ceiling timeout** — the run reaches the wall-clock entitlement for its
  effort tier, measured from stream accept.

The requested reasoning effort — `reasoning.effort` on Responses,
`reasoning_effort` on Chat Completions — selects both compute depth and the
run's time entitlement:

| Requested effort | Idle window | Wall-clock ceiling |
|---|---|---|
| `none`, `minimal` | 90 seconds | 5 minutes |
| `low`, `medium` | 90 seconds | 10 minutes |
| `high` | 90 seconds | 20 minutes |
| `xhigh` | 120 seconds | 40 minutes |
| `max`, `ultra` | 120 seconds | 60 minutes |

Requests that do not set a reasoning effort use the model's default
reasoning depth, never below the `medium` row. `eb1-frontier-preview` and
models documented with extended runtimes default to the `max` row: long runs
need no opt-in. Messages requests carry no effort field; an extended-thinking
budget selects the entitlement instead — `thinking.budget_tokens` of 16,384
or more earns the `high` row, 131,072 or more the `xhigh` row, and smaller
budgets keep the `medium` row.

When a bound fires, the stream ends with one honest terminal event, and tokens
consumed before the stop are billed:

- Responses: `response.incomplete` with `incomplete_details.reason` of
  `stream_idle_timeout` or `stream_ceiling_timeout`.
- Chat Completions: a terminal choice with `finish_reason` `length`.
- Messages: a terminal `message_delta` with `stop_reason` `max_tokens`.

Configure client timeouts to fit the entitlement, not the other way around:
use a read or idle timeout of at least 30 seconds (keepalives arrive at least
every 10 seconds, so 30 seconds only trips on a dead connection) and a total
timeout at least two minutes above the ceiling for the effort you request.

## Handle in-band errors

Once response headers and the first body byte are sent, the HTTP status cannot
change. A later failure therefore arrives in the stream.

- Chat Completions places `type`, `code`, `message`, and optional
  `retry_after_seconds` on the error frame.
- Responses and Messages place those fields under `error`.

Treat an error frame as terminal. Do not wait for normal completion after it.
See [Errors](errors.md) for the shared code taxonomy.

## Verify delivery

```bash
printf 'Keiro API key: '
IFS= read -rs KEIRO_BEARER
printf '\n'

curl -sS -N https://api.keirolabs.ai/v1/responses \
  -w '\nTTFB=%{time_starttransfer}s total=%{time_total}s http=%{http_code}\n' \
  -H "Content-Type: application/json" \
  -d '{
    "model": "eb1-preview",
    "stream": true,
    "input": "Count from one to five, one number per line."
  }' \
  -H @- <<<"Authorization: Bearer $KEIRO_BEARER"
```

Time to first byte should normally be lower than total time. Similar values can
mean the response was short or that a client, proxy, or network path buffered
the stream.

## Retry safely

An interrupted stream can contain output that the user already saw. Retry only
when repeating the logical operation is safe. Use an idempotency key for
retry-sensitive requests, honor `Retry-After`, and avoid concatenating a retry
onto partial output as if it were one response.

## Related pages

- [API reference](api-reference.md)
- [Tool calling](tool-calling.md)
- [Errors](errors.md)
- [Idempotency](idempotency.md)
