> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chattler.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Стриминг

> Получайте ответ токен за токеном через server-sent events.

`POST /v1/agents/{agent_id}/chat/stream` принимает ровно тот же запрос, что и синхронный эндпоинт чата: то же тело, те же заголовки `Authorization` и `Idempotency-Key`, тот же доступ `full`, те же правила диалогов, биллинг и коды ошибок. Отличается только транспорт: ответ приходит как `text/event-stream`.

## События

У каждого события есть имя и одна строка `data` с JSON.

| Событие                      | Данные                                                                  | Когда                                                                   |
| ---------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `response.created`           | `{"request_id", "conversation_id"}`                                     | Как только диалог определён                                             |
| `response.output_text.delta` | `{"delta": "..."}`                                                      | На каждый фрагмент текста ответа по мере генерации моделью; повторяется |
| `response.completed`         | Полный ответ чата (`request_id`, `conversation_id`, `message`, `usage`) | Всегда последнее событие успешного стрима                               |
| `error`                      | `{"code", "message", "request_id"}`                                     | Всегда последнее событие неуспешного стрима                             |

```text theme={null}
event: response.created
data: {"request_id":"7f20a5f0-...","conversation_id":"1c2e..."}

event: response.output_text.delta
data: {"delta":"The basic"}

event: response.output_text.delta
data: {"delta":" plan is $19 per month."}

event: response.completed
data: {"request_id":"7f20a5f0-...","conversation_id":"1c2e...","message":{"id":"66e0...","role":"assistant","content":"The basic plan is $19 per month.","created_at":"2026-09-13T12:00:00Z"},"usage":{"prompt_tokens":120,"completion_tokens":45,"total_tokens":165,"charged_usd":"0.00123400"}}
```

<Note>
  Текст рассуждений и активность инструментов никогда не попадают в стрим. `response.completed` всегда несёт полный итоговый текст, поэтому клиент, пропустивший часть дельт, может восстановить ответ из него, а не из склеенных дельт.
</Note>

## Ошибки в стриме

Сбои **до** установления стрима (неверный ключ, валидация, `idempotency_conflict`, `conversation_busy`, отключённый агент, лимит запросов) — это обычные JSON-[конверты ошибок](/ru/guides/errors) со своим HTTP-статусом; проверяйте `response.ok`, прежде чем читать события.

После отправки заголовков статус уже `200`. Сбой после этого момента (например, `insufficient_balance` или `internal_error`) приходит как событие `error` с теми же полями, что и в конверте, и закрывает стрим.

## Обрывы соединения и повторы

Клиент, разорвавший соединение, не останавливает ответ: он всё равно генерируется и сохраняется, а запись идемпотентности завершается. Повтор с тем же `Idempotency-Key` стримит `response.created`, а сразу за ним `response.completed` с сохранённым результатом; дельты не воспроизводятся. См. [Идемпотентность](/ru/guides/idempotency).

## Примеры

<CodeGroup>
  ```bash curl theme={null}
  curl -N -X POST "$CHATTLER_BASE_URL/v1/agents/$CHATTLER_AGENT_ID/chat/stream" \
    -H "Authorization: Bearer $CHATTLER_API_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{"external_user_id": "customer-123", "message": "Tell me more"}'
  ```

  ```python Python theme={null}
  import json
  import os
  import uuid

  import httpx


  def stream_chat(external_user_id: str, message: str) -> dict:
      url = f"{os.environ['CHATTLER_BASE_URL']}/v1/agents/{os.environ['CHATTLER_AGENT_ID']}/chat/stream"
      headers = {
          "Authorization": f"Bearer {os.environ['CHATTLER_API_KEY']}",
          "Idempotency-Key": str(uuid.uuid4()),
      }
      with httpx.stream("POST", url, headers=headers, timeout=120,
                        json={"external_user_id": external_user_id, "message": message}) as response:
          if response.status_code >= 400:
              response.read()
              raise RuntimeError(response.json()["error"])
          event, completed = None, None
          for line in response.iter_lines():
              if line.startswith("event: "):
                  event = line[len("event: "):]
              elif line.startswith("data: "):
                  data = json.loads(line[len("data: "):])
                  if event == "response.output_text.delta":
                      print(data["delta"], end="", flush=True)
                  elif event == "response.completed":
                      completed = data
                  elif event == "error":
                      raise RuntimeError(f"{data['code']}: {data['message']}")
              elif line == "":
                  event = None
          return completed


  stream_chat("customer-123", "Tell me more")
  ```

  ```javascript Node theme={null}
  const baseUrl = process.env.CHATTLER_BASE_URL;
  const agentId = process.env.CHATTLER_AGENT_ID;
  const apiKey = process.env.CHATTLER_API_KEY;

  async function streamChat(externalUserId, message, onDelta) {
    const response = await fetch(`${baseUrl}/v1/agents/${agentId}/chat/stream`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ external_user_id: externalUserId, message }),
    });
    if (!response.ok) {
      const body = await response.json();
      throw new Error(`${body.error.code}: ${body.error.message}`);
    }
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let completed = null;
    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      let boundary;
      while ((boundary = buffer.indexOf("\n\n")) >= 0) {
        const frame = buffer.slice(0, boundary);
        buffer = buffer.slice(boundary + 2);
        const event = frame.match(/^event: (.+)$/m)?.[1];
        const data = JSON.parse(frame.match(/^data: (.+)$/m)?.[1] ?? "{}");
        if (event === "response.output_text.delta") onDelta(data.delta);
        else if (event === "response.completed") completed = data;
        else if (event === "error") throw new Error(`${data.code}: ${data.message}`);
      }
    }
    return completed;
  }

  const result = await streamChat("customer-123", "Tell me more", (t) => process.stdout.write(t));
  console.log("\n", result.usage);
  ```
</CodeGroup>

<Tip>
  Используйте `curl -N` (без буферизации), чтобы видеть события по мере поступления. В браузерах `EventSource` не умеет отправлять тела `POST` и собственные заголовки, а ключ в любом случае не должен попадать в браузер; стримьте со своего сервера и передавайте текст клиенту.
</Tip>
