Przejdź do treści
Streaming

Streaming

Ustaw stream na true, a odpowiedź przychodzi jako server-sent events w trakcie pisania przez model: najpierw wnioskowanie, potem odpowiedź, na końcu chunk z przyczyną zakończenia i użyciem (usage). Używaj tego, gdy człowiek czeka na tekst, oraz przy długich odpowiedziach.

POST https://api.shannon-ai.com/v1/chat/completions

SDK OpenAI dają Ci chunki jeden po drugim. Czytaj każdy chunk według tego, co niesie: kawałek wnioskowania, kawałek odpowiedzi, wywołanie narzędzia albo koniec odpowiedzi.

from openai import OpenAI, APIError

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://api.shannon-ai.com/v1",
)

stream = client.chat.completions.create(
    model="shannon-3",
    messages=[{"role": "user", "content": "Explain server-sent events in three sentences."}],
    stream=True,
)

answer, reasoning, tool_calls = [], [], []
finish_reason = usage = None

try:
    for chunk in stream:
        if chunk.usage:
            usage = chunk.usage
        if not chunk.choices:
            continue
        choice = chunk.choices[0]
        delta = choice.delta

        thought = getattr(delta, "reasoning_content", None)
        if thought:
            reasoning.append(thought)
        if delta.content:
            answer.append(delta.content)
            print(delta.content, end="", flush=True)
        for call in delta.tool_calls or []:
            tool_calls.append((call.id, call.function.name, call.function.arguments))
        if choice.finish_reason:
            finish_reason = choice.finish_reason
except APIError as error:
    # A failure after the stream started arrives as an error chunk.
    print(f"\nstream failed: {error}")

print(f"\n{finish_reason} {usage}")

Jak stream jest ramkowany

  • Odpowiedź ma status 200 oraz nagłówki content-type: text/event-stream i cache-control: no-cache.
  • Każde zdarzenie to jeden wiersz zaczynający się od data: , po którym następuje pusty wiersz. Tekst po data: to jeden chunk JSON. Nie ma wierszy event: ani id:.
  • Wiersz zaczynający się od dwukropka, taki jak : ping, to komentarz, który utrzymuje połączenie otwarte. Pomiń go.
  • Stream kończy się wierszem data: [DONE]. Stream, który urywa się bez niego, jest niekompletny.

Chunki po kolei

Każdy chunk to obiekt chat.completion.chunk:

JSON
{
  "id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
  "object": "chat.completion.chunk",
  "created": 1791625200,
  "model": "shannon-3",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "Server-sent"
      },
      "finish_reason": null
    }
  ]
}

Wszystkie chunki jednego streamu niosą to samo id, created i model. choices zawsze zawiera jeden wpis. Jego delta zawiera to, co nowe, a finish_reason ma wartość null do ostatniego chunku.

Klucz w delta Typ Opis Wysyłane przez
role string Zawsze assistant. Przychodzi raz, na początku streamu. Hostowane modele open-weight, shannon-2-lite, shannon-2-pro
reasoning_content string Kawałek wnioskowania modelu. Łącz kawałki po kolei. Hostowane modele open-weight, rodzina Shannon 3, shannon-1.6-pro, shannon-coder-1
content string Kawałek odpowiedzi. Łącz kawałki po kolei. Wszystkie modele
tool_calls array Jedno kompletne wywołanie narzędzia. Zobacz Delty wywołań narzędzi. Wszystkie modele

Najpierw idzie wnioskowanie, potem odpowiedź, potem ewentualne wywołania narzędzi. Czytaj chunk według kluczy w jego delta, a nie według miejsca w streamie: delta może zawierać role razem z pierwszym kawałkiem tekstu, a ostatnia jest pusta.

Cały stream tak, jak podróżuje. Aby wiersze były krótkie, id, object, created i model są tu pominięte w każdym chunku.

200 text/event-stream
data: {"choices":[{"index":0,"delta":{"reasoning_content":"Three sentences:"},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{"reasoning_content":" what it is, how it is framed, why it is used."},"finish_reason":null}]}

: ping

data: {"choices":[{"index":0,"delta":{"content":"Server-sent"},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{"content":" events let a server push text to a client over one HTTP reply."},"finish_reason":null}]}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":84,"total_tokens":1274}}

data: [DONE]

Delty wnioskowania

Model, który wnioskuje, wysyła swoje wnioskowanie w delta.reasoning_content przed odpowiedzią. Trzymaj te dwa teksty osobno: pokaż wnioskowanie jako oddzielną, zwijaną część albo je pomiń.

Hostowane modele open-weight nie wysyłają wnioskowania, gdy reasoning_effort ma wartość off, ani gdy zapytanie ma response_format.

shannon-2-lite i shannon-2-pro też wnioskują, zanim odpowiedzą. W tym endpoincie wysyłają wtedy wiersze komentarza : thinking, a potem odpowiedź.

reasoning_content nie ma w typowanych modelach SDK OpenAI. W Pythonie odczytaj je przez getattr(delta, "reasoning_content", None); w JavaScript delta.reasoning_content działa bez zmian.

Delty wywołań narzędzi

Wywołanie narzędzia przychodzi w całości: jeden chunk na wywołanie, którego delta zawiera wywołanie z kompletnym ciągiem arguments. index numeruje wywołania odpowiedzi od 0. Kod, który skleja arguments kawałek po kawałku, działa bez zmian, bo skleja jeden kawałek.

delta
{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_3d9a7c1e5b2f4a60c8e1d7f09b24a6c5",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Paris\"}"
      }
    }
  ]
}

Po wywołaniach ostatni chunk ma finish_reason tool_calls.

Gdy zapytanie ma tools, shannon-1.6-lite, shannon-1.6-pro i shannon-coder-1 wysyłają tekst odpowiedzi jako jedną deltę na końcu odpowiedzi.

Ostatni chunk

Ostatni chunk przed data: [DONE] ma pustą delta, finish_reason i usage zapytania, wszystko w tym samym chunku. Ma go każdy stream; nie potrzebujesz stream_options.

JSON
{
  "id": "chatcmpl-5f0c1e7a9b3d4c62a8e1f07d2b46c9a3",
  "object": "chat.completion.chunk",
  "created": 1791625200,
  "model": "shannon-3",
  "choices": [
    {
      "index": 0,
      "delta": {},
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 1190,
    "completion_tokens": 84,
    "total_tokens": 1274
  }
}

W hostowanych modelach open-weight usage ma też prompt_tokens_details.cached_tokens i completion_tokens_details.reasoning_tokens.

Po zapytaniu z web_search: true, którego wyszukiwanie coś znalazło, ostatni chunk niesie też sources, a chunk przed nim niesie znaczniki cytowań jako delta.annotations. Wbudowane wyszukiwanie w sieci

finish_reason Opis Wysyłane przez
stop Model skończył odpowiedź albo pojawił się ciąg stop. Wszystkie modele
tool_calls Odpowiedź zawiera jedno lub więcej wywołań narzędzi. Wszystkie modele
length Odpowiedź została ucięta na limicie wyjścia. Rodzina Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1

Pola usage są opisane przy endpoincie. Chat Completions

Wiersze keep-alive

Gdy model pracuje i nie ma jeszcze nic do wysłania, stream niesie wiersze komentarza. Nie zawierają danych. Parser SSE sam je pomija; kod czytający surowe wiersze musi pomijać każdy wiersz zaczynający się od dwukropka.

Wiersz Wysyłane przez Kiedy
: ping Rodzina Shannon 3, shannon-1.6-lite, shannon-1.6-pro, shannon-coder-1 Co 15 sekund.
: keepalive Hostowane modele open-weight Co 15 sekund, gdy model pracuje.
: thinking shannon-2-lite, shannon-2-pro Gdy model wnioskuje.

Błędy wewnątrz streamu

Gdy stream się rozpocznie, jego status to 200, więc awaria przychodzi wewnątrz streamu: chunk z członem error w miejscu, gdzie byłoby choices.

200 text/event-stream
data: {"error":{"message":"The model backend failed to answer. Please retry.","type":"api_error","code":null,"param":null}}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1190,"completion_tokens":12,"total_tokens":1202}}

data: [DONE]

Po chunku błędu nadal następują ostatni chunk, z finish_reason i usage, oraz data: [DONE]. Sprawdzaj każdy chunk pod kątem error. Bez tej kontroli odpowiedź, która zawiodła w połowie, wygląda na kompletną.

Typ Kod Komunikat Kiedy
api_error The model backend failed to answer. Please retry. Model zawiódł albo nic nie napisał. Wyślij zapytanie ponownie.
rate_limit_error Shannon routes are temporarily busy. Please retry. Model jest w tej chwili zajęty. Poczekaj kilka sekund i wyślij zapytanie ponownie.
invalid_request_error context_length_exceeded Rozmowa jest dłuższa niż okno kontekstu modelu. Skróć ją, zanim wyślesz ponownie. Tekst komunikatu bywa różny.

Hostowane modele open-weight wysyłają człon error tylko z type i message.

SDK OpenAI dla Pythona i JavaScript zgłaszają chunk błędu jako błąd API podczas iteracji, więc opakuj pętlę w swoją zwykłą obsługę błędów.

Kody statusu, treść błędu i to, co ponawiać, mają własną stronę. Obsługa błędów

Zamykanie połączenia

Zamknięcie połączenia zatrzymuje dostarczanie, a nie zapytanie. Model dopisuje odpowiedź do końca, a zapytanie jest rozliczane tak, jakbyś przeczytał ją całą. Aby zapłacić mniej, proś o mniej: ustaw niższe max_tokens w modelu, który je stosuje.

Stream może też zakończyć się wcześniej po drodze do Ciebie, na przykład przy przerwie w sieci lub podczas aktualizacji serwera. Kończy się wtedy bez ostatniego chunku i bez data: [DONE]. Traktuj taką odpowiedź jako niekompletną i wyślij zapytanie ponownie.

Czytanie streamu bez SDK

Poprawny czytnik robi pięć rzeczy: kieruje błędy, które przychodzą przed streamem, do swojej obsługi błędów, dzieli treść na wiersze, pomija wiersze komentarza, zatrzymuje się na data: [DONE] i sprawdza każdy chunk pod kątem error.

import json
import requests

response = requests.post(
    "https://api.shannon-ai.com/v1/chat/completions",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "model": "shannon-3",
        "messages": [{"role": "user", "content": "Explain server-sent events in three sentences."}],
        "stream": True,
    },
    stream=True,
)

# 1. Errors before the stream are plain JSON with their own status.
if response.status_code != 200:
    raise RuntimeError(f"{response.status_code}: {response.text}")

answer, done = [], False
for raw in response.iter_lines():          # 2. one line at a time
    line = raw.decode("utf-8")
    if not line.startswith("data: "):      # 3. empty lines and ": ping" comments
        continue
    data = line[len("data: "):]
    if data == "[DONE]":                   # 4. the end of the stream
        done = True
        break
    chunk = json.loads(data)
    if "error" in chunk:                   # 5. a failure inside the stream
        raise RuntimeError(chunk["error"]["message"])
    delta = chunk["choices"][0]["delta"]
    if delta.get("content"):
        answer.append(delta["content"])
    if chunk.get("usage"):
        print(chunk["choices"][0]["finish_reason"], chunk["usage"])

if not done:
    raise RuntimeError("the stream ended before [DONE]")
print("".join(answer))

Przykład w Pythonie używa pakietu requests. Przykład w JavaScript działa w Node.js 18 lub nowszym.

Streamy w innych formatach

Endpointy Responses i Messages też streamują. Ich ramki niosą wiersz event: z nazwą zdarzenia i wiersz data: z jego JSON. Każda strona wymienia swoje zdarzenia po kolei.

W streamie Chat CompletionsResponsesMessages
Ramki Wiersze data:Wiersze event: i data:Wiersze event: i data:
Wnioskowanie przychodzi w delta.reasoning_contentresponse.reasoning_summary_text.deltathinking_delta
Tekst odpowiedzi przychodzi w delta.contentresponse.output_text.deltatext_delta
Użycie (usage) przychodzi w Ostatni chunkresponse.completedmessage_delta
Stream kończy się Ostatni chunk, potem data: [DONE]response.completedmessage_stop