ブログに戻る

APIとSDKガイド

Python でのストリーミング応答

公式の openai パッケージを使用して、モデルの回答を生成されると同時に出力します — 同期と非同期の例、推論出力、トークン使用量、エラー処理。

投稿者: CheapTokens Team公開日 読了時間 4 分

ストリームチャンクを反復処理する for ループとトークンごとに表示されるテキストを含む Python コード
このページの内容
  1. セットアップ
  2. 最小限の例
  3. 推論モデル
  4. トークン使用量
  5. asyncio
  6. 早期停止とタイムアウト
  7. エラー
  8. ワイヤー上で何が起きているか

ストリーミングを使うと、モデルがまだ生成している間に回答が少しずつ届くため、ユーザーは完全な応答を待たずにすぐテキストを見ることができます。API は標準の OpenAI ストリーミング形式(サーバー送信イベント)を使用しており、公式の openai Python パッケージがそれを処理します。

以下のすべての例は完全です。ファイルにコピーして実行してください。例は Python 3.10.12 と openai 3.31.0 で、API のストリーミング形式を再現するローカルテストサーバーに対して確認されています(構文と実行)。

セットアップ#

bash
pip install openai
export CHEAPTOKENS_API_KEY="paste-your-key-here"

最小限の例#

python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

stream = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V4-Flash-0731",
    messages=[{"role": "user", "content": "Write a haiku about latency"}],
    stream=True,
)
for chunk in stream:
    if not chunk.choices:  # service chunk without text, e.g. the final usage chunk
        continue
    text = chunk.choices[0].delta.content
    if text:
        print(text, end="", flush=True)
print()

2 つの詳細が重要です:

  • choices のないチャンクはスキップする。 一部のチャンクにはテキストが含まれません — 例えば、トークン使用量を含む最後のチャンクです。
  • delta.content は空になることがあります。 最初のチャンクはしばしばロールのみを含み、最後のチャンクは finish_reason のみを含みます。テキストがある場合にのみ出力してください。

推論モデル#

zai-org/GLM-5.3-Flash は回答する前に推論します。推論はデルタの別のフィールド reasoning_content に含まれ、回答自体は content に含まれます。SDK は未知のフィールドを保持するため、getattr で読み取ることができます。これは独自のクライアントと独自のストリームを持つ完全な例です:

python
import os
import sys
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

stream = client.chat.completions.create(
    model="zai-org/GLM-5.3-Flash",
    messages=[{"role": "user", "content": "Is 1013 a prime number?"}],
    stream=True,
)
for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    thinking = getattr(delta, "reasoning_content", None)
    if thinking:
        print(thinking, end="", flush=True, file=sys.stderr)  # reasoning → stderr
    if delta.content:
        print(delta.content, end="", flush=True)              # answer → stdout
print()

推論は回答の最初の単語が現れるまでに時間がかかることがあります — これもストリーミングする理由の一つです。

トークン使用量#

stream_options で使用量を要求します。それは最後のチャンクに含まれ、choices はありません:

python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

stream = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V4-Flash-0731",
    messages=[{"role": "user", "content": "Explain idempotency in one paragraph"}],
    stream=True,
    stream_options={"include_usage": True},
)
usage = None
for chunk in stream:
    if chunk.usage:
        usage = chunk.usage
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

if usage:
    print(f"\ninput: {usage.prompt_tokens}, output: {usage.completion_tokens}")

これらの数値にモデルの料金を掛けると、リクエストのコストが得られます — AI エージェントのコストを見積もる方法 を参照してください。

asyncio#

非同期コードでは AsyncOpenAI と async for を使用します。クライアントには独自の名前 async_client があるため、他の例の同期 client と衝突しません:

python
import asyncio
import os
from openai import AsyncOpenAI

async_client = AsyncOpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

async def main() -> None:
    stream = await async_client.chat.completions.create(
        model="deepseek-ai/DeepSeek-V4-Flash-0731",
        messages=[{"role": "user", "content": "Name three uses of a message queue"}],
        stream=True,
    )
    async for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            print(chunk.choices[0].delta.content, end="", flush=True)
    print()

asyncio.run(main())

早期停止とタイムアウト#

ループを抜けると生成が停止します。with でストリームを開くと、ブロックが終了したときに接続が閉じられます。クライアントのタイムアウトは明示的に設定されています。SDK は一部の失敗したリクエストを独自に再試行します(max_retries、デフォルトは 2)。

python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
    timeout=120,  # seconds; long answers take time
)

received = ""
with client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V4-Flash-0731",
    messages=[{"role": "user", "content": "Write a long story about a lighthouse"}],
    stream=True,
) as stream:
    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            received += chunk.choices[0].delta.content
        if len(received) > 300:  # stop early: enough text received
            break
print(received)

エラー#

最初のチャンクの前に発生するエラー — 間違ったキー、残高不足、不明なモデル — は、ストリームを作成するときに例外として発生します:

python
import os
import openai
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

try:
    stream = client.chat.completions.create(
        model="deepseek-ai/DeepSeek-V4-Flash-0731",
        messages=[{"role": "user", "content": "Hello"}],
        stream=True,
    )
    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            print(chunk.choices[0].delta.content, end="", flush=True)
    print()
except openai.AuthenticationError:          # 401: wrong or rotated key
    print("Check the API key")
except openai.BadRequestError as e:         # 400: e.g. unknown model ID
    print("Bad request:", e.message)
except openai.APIStatusError as e:          # other statuses, e.g. 402 when the balance is used up
    print(e.status_code, e.message)

ステータスコードと修正方法の完全なリストは 一般的な接続エラーとその修正方法 にあります。

ワイヤー上で何が起きているか#

デバッグするには、curl で生のストリームを確認します(-N はバッファリングをオフにします):

bash
curl -N https://api.cheaptokens.dev/v1/chat/completions \
  -H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek-ai/DeepSeek-V4-Flash-0731", "stream": true,
       "messages": [{"role": "user", "content": "Hi"}]}'

各イベントは JSON チャンクを含む data: {…} 行で、ストリームは data: [DONE] で終了します。コロンで始まる行は、長い一時停止中に送信されるキープアライブコメントです — クライアントはそれらを無視します。