Průvodce

Streamování

Jak se streamované odpovědi doručují, co obsahuje každý chunk a jak stream číst.

Zobrazit jako Markdown

Některé endpointy mohou posílat výsledek průběžně, jak vzniká, místo najednou: odpověď chatu, když požadavek nastaví "stream": true, a běh workflow. Všechny streamy používají stejnou obálku, popsanou zde; co obálka nese, závisí na endpointu. Tato stránka popisuje obálku a stream chatu; události běhu workflow jsou popsány u endpointu workflow.

Jak se stream doručuje

Streamovaná odpověď je běžná odpověď HTTP, jejíž tělo přichází po částech:

  • Stav 200, Content-Type: text/event-stream; charset=utf-8, chunked transfer encoding (bez Content-Length).
  • Tělo je posloupnost chunků. Každý chunk je jeden objekt JSON a objekty jsou zapsány přímo jeden za druhým, bez oddělovače: bez nového řádku, bez prefixu data:.
text
{"timestamp": 1759406400810, "data": "Unit tests", "error": null}{"timestamp": 1759406400834, "data": " catch", "error": null}{"timestamp": 1759406400851, "data": " regressions", "error": null}

Navzdory content type tělo není ve formátu Server-Sent Events, takže ho EventSource ani klientské knihovny SSE přečíst nedokážou (a EventSource stejně neumí poslat POST s tělem). Čtěte tělo jako proud bajtů a na objekty JSON si ho rozdělte sami, viz Čtení streamu. Čtení ze sítě může skončit uprostřed objektu nebo obsahovat několik objektů, proto vždy používejte buffer.

Chyby zjištěné před zahájením streamu (autentizace, oprávnění, neplatný požadavek, neznámý model, ...) se vracejí jako běžná chybová odpověď s vlastním stavem HTTP a tělem {"detail": "..."}, viz Chyby. Než začnete tělo číst jako stream, zkontrolujte stavový kód.

Chunky

Pole Typ Popis
timestamp časové razítko Kdy chunk vznikl (milisekundy od epochy).
data řetězec, objekt nebo null Obsah. Jeho tvar závisí na endpointu a u chatu na runner_version. V chybovém chunku null.
error Chyba streamu nebo null Nastaveno, když požadavek selhal po zahájení streamu; jinak null.

Keepalive. Když zhruba sekundu nic nevzniklo (model přemýšlí, běží nástroj), server pošle chunk s data nastaveným na prázdný řetězec "". Tyto chunky ignorujte; jen brání tomu, aby spojení nebo proxy vypršely. Keepalive se posílá v každém formátu streamu, takže data může být "" i ve streamech, jejichž obsahem jsou jinak objekty.

Chyby ve streamu

Jakmile stream začal, stav HTTP už je 200. Selhání po tomto okamžiku se pošle jako poslední chunk s nastaveným error a stream skončí.

json
{"timestamp": 1759406402117, "data": null, "error": {"status": 422, "text": "Validation error", "detail": "not enough user credit"}}
Pole Typ Popis
status celé číslo Stav HTTP, který by chyba měla jako běžná odpověď, například 422 nebo 500.
text řetězec Krátký název třídy chyby: Validation error, Forbidden, Not found, Unauthorized, Too many requests, Lock error, Internal server error; Internal Server Error u neočekávaných selhání.
detail řetězec Chybová zpráva, stejný text, jaký nese v detail běžná chybová odpověď.

Chyby chatu, které mohou přijít tímto způsobem, jsou v tabulce chyb chatu označeny během; nejčastější z nich je vyčerpání kreditů uprostřed odpovědi. Odpověď chatu přerušená chybou se v konverzaci uchová až do místa, kde se zastavila.

Konec streamu

Stream končí, když skončí tělo odpovědi HTTP. Žádný závěrečný chunk „konec“ neexistuje:

  • tělo skončilo po chunku s nastaveným error: požadavek selhal;
  • tělo skončilo jinak: požadavek se dokončil.

Ve streamu událostí chatu označuje událost done okamžik, kdy model dokončil odpověď, ale za konec streamu považujte konec těla.

Pokud váš klient zavře spojení předčasně, server přestane odpověď chatu generovat. Dosud vygenerovaná část se v konverzaci uchová a volání modelu, která už proběhla, se účtují. (Běh workflow po odpojení na serveru pokračuje.)

Formáty streamu chatu

Pole požadavku chatu runner_version určuje, co obsahuje data ve streamu chatu:

runner_version data Použijte, když
"1" (výchozí) Řetězec: další část textu odpovědi. Potřebujete jen text odpovědi.
"2" Objekt události: text, uvažování, činnost nástrojů, stav. Chcete zobrazovat průběh, uvažování nebo činnost nástrojů odděleně od textu.

Oba formáty vznikají ze stejného generování a uložená odpověď je stejná; liší se jen to, co se posílá po síti.

Model bez schopnosti stream (viz Modely a nástroje) lze přesto streamovat: text jeho odpovědi přijde vcelku (jeden textový fragment nebo jedna událost text), až je hotový, a uloží se jako každá jiná odpověď.

Textový stream (verze 1)

Každé data je fragment odpovědi v Markdownu. Pro celou odpověď fragmenty spojte v pořadí; keepalive chunky ("") nic nepřidávají.

Volání nástrojů jsou součástí textu, přesně jako v content nestreamované odpovědi: každé volání nástroje je blok, který začíná značkou "\n\n### calling AI tool ...\n", pokračuje textem průběhu, který nástroj hlásí, a končí značkou "\n***\n". Viz Volání nástrojů v odpovědi. Uvažování, stavové informace a vnitřní kroky sub-asistentů se v tomto formátu neposílají.

text
{"timestamp": 1759406400120, "data": "", "error": null}{"timestamp": 1759406400810, "data": "Let me check", "error": null}{"timestamp": 1759406400833, "data": " that.", "error": null}{"timestamp": 1759406401002, "data": "\n\n### calling AI tool ...\n", "error": null}{"timestamp": 1759406401004, "data": "### Scraping URL: https://example.com/rates\n", "error": null}{"timestamp": 1759406402950, "data": "\n***\n", "error": null}{"timestamp": 1759406403410, "data": "The current rate is 24.35 CZK per euro.", "error": null}

Stream událostí (verze 2)

Každé data (kromě keepalive) je objekt události. Všechna pole jsou vždy přítomna; ta, která se k typu události nevztahují, jsou prázdná ("", null nebo {}).

Pole Typ Popis
type řetězec Typ události, viz Typy událostí.
delta řetězec Text, který nesou události text, reasoning a tool_call_output; jinak "".
phase řetězec nebo null Fáze události status, viz Fáze stavu.
call_id řetězec nebo null Id volání nástroje, ke kterému událost patří (události nástrojů, nested, některé události status).
tool_name řetězec nebo null Technický název nástroje, stejné id, jaké uvádí Modely a nástroje a na jaké odkazují asistenti.
tool_display_name řetězec nebo null Název nástroje čitelný pro člověka.
tool_icon řetězec nebo null Identifikátor ikony, kterou aplikace AYETO pro nástroj používá; může být prázdný.
tool_status řetězec nebo null Výsledek v tool_call_end: ok nebo error.
data objekt Doplňující hodnoty některých událostí (stav model_selected, done). Jinak {}.
chunk událost nebo null Vnitřní událost události nested.

Celý chunk streamu událostí vypadá takto:

json
{"timestamp": 1759406400810, "data": {"type": "text", "delta": "Unit tests", "phase": null, "call_id": null, "tool_name": null, "tool_display_name": null, "tool_icon": null, "tool_status": null, "data": {}, "chunk": null}, "error": null}

Typy událostí

type Význam Použitá pole
text Další část viditelného textu odpovědi (Markdown). delta
reasoning Další část uvažování modelu nebo shrnutí jeho přemýšlení, u modelů, které ho zpřístupňují. Není součástí textu odpovědi. delta
status Požadavek vstoupil do nové fáze. phase a podle fáze call_id, tool_name, tool_display_name, tool_icon, data
tool_call_start Nástroj se spustil. call_id, tool_name, tool_display_name, tool_icon
tool_call_output Text průběhu hlášený běžícím nástrojem (Markdown), například vyhledávací dotazy nebo odkaz na vygenerovaný soubor. call_id, delta
tool_call_end Nástroj skončil. call_id, tool_status
nested Událost sub-asistenta, kterého asistent zavolal jako nástroj. call_id je id tohoto volání nástroje, chunk je vlastní událost sub-asistenta (libovolného typu, včetně dalšího nested pro hlubší úrovně). call_id, chunk
done Model dokončil odpověď. data.stop_reason je důvod od poskytovatele, například stop, end_turn nebo completed, případně null. data

Fáze stavu

phase Význam
context_building Požadavek se připravuje: instrukce, nástroje, historie. Obvykle první událost.
rag_query Prohledává se znalostní báze asistenta.
relevant_history Vybírá se relevantní část historie (relevant_history v požadavku).
model_selected Uvádí model, který odpovídá: data.model (id modelu), data.model_name (zobrazovaný název) a, když asistent používá automatický model, data.auto_model (id automatického modelu, který ho vybral).
model_call Modelu byl odeslán požadavek a čeká se na jeho první tokeny. Posílá se znovu po každém kole volání nástrojů.
context_compacted Historie konverzace byla zkrácena, aby se vešla do kontextového okna modelu.
tool_call_pending Model píše volání nástroje; call_id a tool_name ho identifikují. Nástroj se spustí událostí tool_call_start.
silent_tool Běží nástroj, který nemá viditelný výstup; call_id, tool_name, tool_display_name a tool_icon ho identifikují. Žádné události tool_call_* pro něj nenásledují.

Mohou přibýt nové typy událostí, fáze a klíče data. Hodnoty, které neznáte, ignorujte.

Pořadí událostí

Typická odpověď, která používá jeden nástroj, vytvoří (bez keepalive, jedno data na řádek):

json
{"type": "status", "phase": "context_building", ...}
{"type": "status", "phase": "model_selected", "data": {"model": "gpt-5-mini", "model_name": "GPT-5 mini"}, ...}
{"type": "status", "phase": "model_call", ...}
{"type": "text", "delta": "Let me check", ...}
{"type": "text", "delta": " that.", ...}
{"type": "status", "phase": "tool_call_pending", "call_id": "call_9f2c", "tool_name": "WebScraperFunctionTool", ...}
{"type": "tool_call_start", "call_id": "call_9f2c", "tool_name": "WebScraperFunctionTool", "tool_display_name": "Web Scraper", "tool_icon": "ayeto", ...}
{"type": "tool_call_output", "call_id": "call_9f2c", "delta": "### Scraping URL: https://example.com/rates\n", ...}
{"type": "tool_call_end", "call_id": "call_9f2c", "tool_status": "ok", ...}
{"type": "status", "phase": "model_call", ...}
{"type": "text", "delta": "The current rate is 24.35 CZK per euro.", ...}
{"type": "done", "data": {"stop_reason": "stop"}, ...}

Sestavení textu odpovědi

Chcete-li získat stejný text, jaký obsahuje textový stream nebo uložená zpráva, připojte pro každou událost nejvyšší úrovně:

Událost Připojit
text delta
tool_call_start "\n\n### calling AI tool ...\n"
tool_call_output delta
tool_call_end "\n***\n"
jakákoli jiná nic

Vygenerované soubory (například obrázky) se objevují jako odkazy v Markdownu v tool_call_output. Po skončení streamu si uloženou odpověď včetně jejích příloh můžete přečíst přes API Konverzace.

Čtení streamu

Příklady posílají zprávu chatu s runner_version "2", vypisují text odpovědi, jak přichází, a zastaví se na chybovém chunku. Oba rozdělují tělo na objekty JSON pomocí malého bufferu, takže fungují bez ohledu na to, jak síť data rozdělí.

Python

python
import codecs
import json
import os
import uuid

import requests


def iter_chunks(response):
    """Yield the JSON chunks of a streamed response."""
    decoder = json.JSONDecoder()
    text = codecs.getincrementaldecoder("utf-8")()
    buffer = ""
    for raw in response.iter_content(chunk_size=None):
        buffer += text.decode(raw)
        while True:
            buffer = buffer.lstrip()
            if not buffer:
                break
            try:
                chunk, end = decoder.raw_decode(buffer)
            except json.JSONDecodeError:
                break  # incomplete object, wait for more data
            buffer = buffer[end:]
            yield chunk


response = requests.post(
    "https://ayeto.ai/api/v3/chat",
    headers={"uni-api-key": os.environ["AYETO_API_KEY"]},
    json={
        "conversation_id": str(uuid.uuid4()),
        "model": "gpt-5-mini",
        "message": "Explain recursion with a short example.",
        "stream": True,
        "runner_version": "2",
    },
    stream=True,
    timeout=(10, 600),
)
if response.status_code != 200:  # failed before the stream started
    raise RuntimeError(f"{response.status_code}: {response.json()['detail']}")

for chunk in iter_chunks(response):
    if chunk["error"]:
        error = chunk["error"]
        raise RuntimeError(f"{error['status']}: {error['detail']}")
    event = chunk["data"]
    if not isinstance(event, dict):
        continue  # keepalive
    if event["type"] == "text":
        print(event["delta"], end="", flush=True)
    elif event["type"] == "tool_call_start":
        print(f"\n[{event['tool_display_name']}]", flush=True)
    elif event["type"] == "status" and event["phase"] == "model_selected":
        print(f"[answering: {event['data']['model']}]", flush=True)
print()

S runner_version "1" je chunk["data"] řetězec: vypište nebo připojte ho tak, jak je.

JavaScript

javascript
// Yields the JSON chunks of a streamed response.
async function* readChunks(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let pos = 0;
  let depth = 0;
  let inString = false;
  let escaped = false;
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    for (; pos < buffer.length; pos++) {
      const c = buffer[pos];
      if (inString) {
        if (escaped) escaped = false;
        else if (c === "\\") escaped = true;
        else if (c === '"') inString = false;
      } else if (c === '"') {
        inString = true;
      } else if (c === "{") {
        depth++;
      } else if (c === "}" && --depth === 0) {
        yield JSON.parse(buffer.slice(0, pos + 1));
        buffer = buffer.slice(pos + 1);
        pos = -1; // continue at the start of the remaining buffer
      }
    }
  }
}

const response = await fetch("https://ayeto.ai/api/v3/chat", {
  method: "POST",
  headers: {
    "uni-api-key": process.env.AYETO_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    conversation_id: crypto.randomUUID(),
    model: "gpt-5-mini",
    message: "Explain recursion with a short example.",
    stream: true,
    runner_version: "2",
  }),
});
if (!response.ok) {
  // failed before the stream started
  const body = await response.json();
  throw new Error(`${response.status}: ${JSON.stringify(body.detail)}`);
}

let answer = "";
for await (const chunk of readChunks(response)) {
  if (chunk.error) {
    throw new Error(`${chunk.error.status}: ${chunk.error.detail}`);
  }
  const event = chunk.data;
  if (typeof event !== "object" || event === null) continue; // keepalive
  if (event.type === "text") {
    answer += event.delta;
    process.stdout.write(event.delta);
  }
}

Stejné funkce čtou i stream běhu workflow; liší se jen obsah data, viz Streamování běhu.