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

# Streaming responses

> Token-by-token responses with the OpenAI SDK, over server-sent events.

On a model whose page lists streaming, stream tokens as they are generated so a user sees the answer appear instead of waiting for all of it. Any chat UI wants this. So does anything long enough that silence looks like a hang. This guide uses the OpenAI-compatible stream. See [Anthropic Messages](/docs/api-reference/anthropic-messages#streaming) for the event-named Messages grammar.

## Minimal code

<CodeGroup>
  ```python Python theme={"dark"}
  import os
  from openai import OpenAI

  client = OpenAI(
      base_url="https://api.runinfra.ai/v1",
      api_key=os.environ["RUNINFRA_GATEWAY_KEY"],
  )

  stream = client.chat.completions.create(
      model="nemotron-3-5-lightning-30b",
      messages=[{"role": "user", "content": "Tell me a short story"}],
      max_tokens=16384,
      stream=True,
  )

  for chunk in stream:
      if not chunk.choices:
          continue  # the usage frame has no choices
      delta = chunk.choices[0].delta.content or ""
      print(delta, end="", flush=True)
  ```

  ```typescript TypeScript theme={"dark"}
  import OpenAI from "openai";

  const client = new OpenAI({
    baseURL: "https://api.runinfra.ai/v1",
    apiKey: process.env.RUNINFRA_GATEWAY_KEY,
  });

  const stream = await client.chat.completions.create({
    model: "nemotron-3-5-lightning-30b",
    messages: [{ role: "user", content: "Tell me a short story" }],
    max_tokens: 16384,
    stream: true,
  });

  for await (const chunk of stream) {
    process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
  }
  ```

  ```bash cURL theme={"dark"}
  curl -N https://api.runinfra.ai/v1/chat/completions \
    -H "Authorization: Bearer $RUNINFRA_GATEWAY_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "nemotron-3-5-lightning-30b",
      "messages": [{"role":"user","content":"Tell me a short story"}],
      "max_tokens": 16384,
      "stream": true
    }'
  ```
</CodeGroup>

## What your loop will see

<div className="block dark:hidden">
  <svg viewBox="0 0 720 268" width="100%" role="img" aria-label="The streaming timeline from request to terminal frame: the 180 second first-token budget, the 740 second response budget, the opt-in usage frame with an exception when no final answer was delivered, and the two terminal frames, data DONE or an error frame on the same HTTP 200." fill="none" xmlns="http://www.w3.org/2000/svg"><text x="24" y="16" fill="#9a998e" fontFamily="Consolas, Menlo, monospace" fontSize="9" fontWeight="500" letterSpacing="0.3">Streaming timeline</text><text x="696" y="16" fill="#78786f" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="end">stream: true</text><line x1="24" y1="74" x2="696" y2="74" stroke="#e8e8e3" strokeWidth="1" strokeDasharray="3 3" /><rect x="21.5" y="71.5" width="5" height="5" fill="#bbb9b1" shapeRendering="crispEdges" /><rect x="693.5" y="71.5" width="5" height="5" fill="#bbb9b1" shapeRendering="crispEdges" /><rect x="93.5" y="71.5" width="5" height="5" fill="#bbb9b1" shapeRendering="crispEdges" /><line x1="96" y1="77" x2="96" y2="90" stroke="#e8e8e3" strokeWidth="1" /><text x="96" y="104" fill="#0f0f0e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">request</text><text x="96" y="118" fill="#78786f" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">HTTP 200, event stream opens</text><rect x="261.5" y="71.5" width="5" height="5" fill="#bbb9b1" shapeRendering="crispEdges" /><line x1="264" y1="77" x2="264" y2="90" stroke="#e8e8e3" strokeWidth="1" /><text x="264" y="104" fill="#0f0f0e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">first token</text><text x="264" y="118" fill="#78786f" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">within the 180s TTFT budget</text><rect x="429.5" y="71.5" width="5" height="5" fill="#bbb9b1" shapeRendering="crispEdges" /><line x1="432" y1="77" x2="432" y2="90" stroke="#e8e8e3" strokeWidth="1" /><text x="432" y="104" fill="#0f0f0e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">delta frames</text><text x="432" y="118" fill="#78786f" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">one complete frame at a time</text><rect x="573.5" y="71.5" width="5" height="5" fill="#bbb9b1" shapeRendering="crispEdges" /><line x1="576" y1="77" x2="576" y2="90" stroke="#e8e8e3" strokeWidth="1" /><text x="576" y="104" fill="#0f0f0e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">usage frame</text><text x="576" y="118" fill="#78786f" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">opt-in, or no final answer</text><line x1="96" y1="48" x2="664" y2="48" stroke="#bbb9b1" strokeWidth="1" /><line x1="96" y1="44" x2="96" y2="52" stroke="#bbb9b1" strokeWidth="1" /><line x1="664" y1="44" x2="664" y2="52" stroke="#bbb9b1" strokeWidth="1" /><text x="380" y="42" fill="#78786f" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">740s response budget, then 504</text><text x="24" y="146" fill="#9a998e" fontFamily="Consolas, Menlo, monospace" fontSize="9" fontWeight="500" letterSpacing="0.3">How a stream ends</text><rect x="24" y="159" width="108" height="3" fill="#76b900" shapeRendering="crispEdges" /><rect x="24" y="162" width="118" height="22" fill="#76b900" shapeRendering="crispEdges" /><rect x="34" y="184" width="108" height="3" fill="#76b900" shapeRendering="crispEdges" /><text x="83" y="177" fill="#0f0f0e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="12" fontWeight="500" textAnchor="middle" letterSpacing="-0.12">data: \[DONE]</text><text x="158" y="177" fill="#52524c" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">the normal terminal frame after the last delta</text><rect x="24" y="210" width="5" height="5" fill="#b95f5f" shapeRendering="crispEdges" /><text x="36" y="218" fill="#b95f5f" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0">error frame</text><text x="126" y="218" fill="#52524c" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">a stream that fails mid-generation ends with one final frame carrying the standard</text><text x="126" y="234" fill="#78786f" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0">upstream\_stream\_error</text><text x="272" y="234" fill="#52524c" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">envelope, on the same HTTP 200. Handle it in your frame loop.</text><text x="24" y="254" fill="#78786f" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">Only tokens actually delivered to you are billed.</text></svg>
</div>

<div className="hidden dark:block">
  <svg viewBox="0 0 720 268" width="100%" role="img" aria-label="The streaming timeline from request to terminal frame: the 180 second first-token budget, the 740 second response budget, the opt-in usage frame with an exception when no final answer was delivered, and the two terminal frames, data DONE or an error frame on the same HTTP 200." fill="none" xmlns="http://www.w3.org/2000/svg"><text x="24" y="16" fill="#6e6d64" fontFamily="Consolas, Menlo, monospace" fontSize="9" fontWeight="500" letterSpacing="0.3">Streaming timeline</text><text x="696" y="16" fill="#9a998e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="end">stream: true</text><line x1="24" y1="74" x2="696" y2="74" stroke="#383833" strokeWidth="1" strokeDasharray="3 3" /><rect x="21.5" y="71.5" width="5" height="5" fill="#6e6d64" shapeRendering="crispEdges" /><rect x="693.5" y="71.5" width="5" height="5" fill="#6e6d64" shapeRendering="crispEdges" /><rect x="93.5" y="71.5" width="5" height="5" fill="#6e6d64" shapeRendering="crispEdges" /><line x1="96" y1="77" x2="96" y2="90" stroke="#383833" strokeWidth="1" /><text x="96" y="104" fill="#f0efe2" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">request</text><text x="96" y="118" fill="#9a998e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">HTTP 200, event stream opens</text><rect x="261.5" y="71.5" width="5" height="5" fill="#6e6d64" shapeRendering="crispEdges" /><line x1="264" y1="77" x2="264" y2="90" stroke="#383833" strokeWidth="1" /><text x="264" y="104" fill="#f0efe2" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">first token</text><text x="264" y="118" fill="#9a998e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">within the 180s TTFT budget</text><rect x="429.5" y="71.5" width="5" height="5" fill="#6e6d64" shapeRendering="crispEdges" /><line x1="432" y1="77" x2="432" y2="90" stroke="#383833" strokeWidth="1" /><text x="432" y="104" fill="#f0efe2" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">delta frames</text><text x="432" y="118" fill="#9a998e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">one complete frame at a time</text><rect x="573.5" y="71.5" width="5" height="5" fill="#6e6d64" shapeRendering="crispEdges" /><line x1="576" y1="77" x2="576" y2="90" stroke="#383833" strokeWidth="1" /><text x="576" y="104" fill="#f0efe2" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">usage frame</text><text x="576" y="118" fill="#9a998e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10" textAnchor="middle">opt-in, or no final answer</text><line x1="96" y1="48" x2="664" y2="48" stroke="#6e6d64" strokeWidth="1" /><line x1="96" y1="44" x2="96" y2="52" stroke="#6e6d64" strokeWidth="1" /><line x1="664" y1="44" x2="664" y2="52" stroke="#6e6d64" strokeWidth="1" /><text x="380" y="42" fill="#9a998e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0" textAnchor="middle">740s response budget, then 504</text><text x="24" y="146" fill="#6e6d64" fontFamily="Consolas, Menlo, monospace" fontSize="9" fontWeight="500" letterSpacing="0.3">How a stream ends</text><rect x="24" y="159" width="108" height="3" fill="#76b900" shapeRendering="crispEdges" /><rect x="24" y="162" width="118" height="22" fill="#76b900" shapeRendering="crispEdges" /><rect x="34" y="184" width="108" height="3" fill="#76b900" shapeRendering="crispEdges" /><text x="83" y="177" fill="#0f0f0e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="12" fontWeight="500" textAnchor="middle" letterSpacing="-0.12">data: \[DONE]</text><text x="158" y="177" fill="#c8c7ba" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">the normal terminal frame after the last delta</text><rect x="24" y="210" width="5" height="5" fill="#c76f6f" shapeRendering="crispEdges" /><text x="36" y="218" fill="#c76f6f" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0">error frame</text><text x="126" y="218" fill="#c8c7ba" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">a stream that fails mid-generation ends with one final frame carrying the standard</text><text x="126" y="234" fill="#9a998e" fontFamily="Consolas, Menlo, monospace" fontSize="10.5" letterSpacing="0">upstream\_stream\_error</text><text x="272" y="234" fill="#c8c7ba" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">envelope, on the same HTTP 200. Handle it in your frame loop.</text><text x="24" y="254" fill="#9a998e" fontFamily="'Helvetica Neue', Helvetica, Arial, sans-serif" fontSize="10.5" letterSpacing="0">Only tokens actually delivered to you are billed.</text></svg>
</div>

## What to tune

| Parameter | Effect |
| - | - |
| `stream_options.include_usage` | Adds the final usage frame with token counts. Off by default. |
| `max_tokens` | Hard cap on generated length. Reasoning models spend part of this budget before answering, so keep it generous. |
| `temperature` | Higher is more surprise per token. |
| `stop` | Up to four stop sequences. The stream ends early on a match. |

## Common mistakes

* **Forgetting `flush=True` in Python.** Without it stdout buffers and the tokens arrive in clumps.
* **Treating an error frame as a network failure.** A stream that fails mid-generation ends with one final frame carrying the standard error envelope, on the same HTTP 200. Handle it inside your frame loop.
* **Breaking the loop too early.** The final content frame carries `finish_reason` and an empty `delta.content`. Keep reading until the stream closes.
* **Streaming through a buffering proxy.** Some edges buffer server-sent events. Stream direct, or disable buffering on that layer.
* **Mixing `n > 1` with streaming.** Allowed, but every delta carries its own `choices[].index`. Route by index or the outputs interleave.

## Next steps

<Columns cols={3}>
  <Card title="Stream reference" icon="radio" href="/docs/api-reference/streaming">
    Exact frame shapes, and the usage frame.
  </Card>

  <Card title="Tool calling" icon="wrench" href="/docs/cookbook/tool-calling">
    Let the model act, then answer.
  </Card>

  <Card title="Structured output" icon="braces" href="/docs/cookbook/structured-output">
    Get JSON that matches your schema.
  </Card>
</Columns>
