OP

openrouter-streaming-setup

A guide to setting up Server-Sent Events (SSE) streaming for OpenRouter API completions.

Install

mkdir -p .claude/skills/openrouter-streaming-setup && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3520" && unzip -o skill.zip -d .claude/skills/openrouter-streaming-setup && rm skill.zip

Installs to .claude/skills/openrouter-streaming-setup

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Implement streaming responses with OpenRouter for real-time UIs. Use
68 charsno explicit “when” trigger
Intermediate

Key capabilities

  • Implement basic streaming with usage statistics in Python
  • Measure time-to-first-token and total time for streaming requests
  • Stream completions in TypeScript using an async iterator
  • Forward SSE streams from a FastAPI backend to a browser client
  • Handle mid-stream failures like cut-offs or missing usage data

How it works

The skill uses the OpenAI SDK with stream: true to receive tokens incrementally. It provides code to process these tokens, measure performance, and forward them as SSE to a browser.

Inputs & outputs

You give it
A request to the OpenRouter API with stream: true
You get back
Tokens output as the model generates, followed by usage counts from the final chunk

When to use openrouter-streaming-setup

  • Implement real-time chat UI streaming
  • Reduce time-to-first-token in AI apps
  • Stream long completions to frontend clients
  • Configure SSE forwarding via FastAPI

About this skill

OpenRouter Streaming Setup

Overview

OpenRouter supports Server-Sent Events (SSE) streaming via stream: true, compatible with the OpenAI SDK. Streaming returns tokens as they're generated, reducing time-to-first-token (TTFT) from seconds to milliseconds. Usage stats are available via stream_options: {include_usage: true} in the final chunk. This skill covers Python and TypeScript streaming, SSE forwarding to browsers, and error recovery.

Prerequisites

  • An OpenRouter API key (sk-or-v1-...) exported as OPENROUTER_API_KEY — see the openrouter-install-auth skill for setup
  • Python 3.8+ or Node.js 18+ with the OpenAI SDK (the async example uses AsyncOpenAI from the same Python package)
  • FastAPI if you plan to forward the SSE stream to browsers per the SSE Forwarding section
  • A streaming-appropriate client timeout (e.g. 120s) — longer than for non-streaming requests

Instructions

  1. Start with Python: Basic Streaming — pass stream=True plus stream_options={"include_usage": True} so the final chunk carries token counts, and print each chunk.choices[0].delta.content as it arrives.
  2. Wrap that loop in the Python: Streaming with Metrics generator to capture TTFT and total time per request; the metrics dict is available after the generator is exhausted.
  3. For Node services, use the TypeScript: Streaming for await loop over the same stream: true request.
  4. To reach a browser UI, expose the FastAPI endpoint in SSE Forwarding to Browser — it re-emits each token as a data: {"token": ...} SSE line and terminates with data: [DONE].
  5. Consume that endpoint with the Browser Client (JavaScript) reader loop, appending tokens to the DOM as they decode.
  6. In async web frameworks, switch to the Async Streaming pattern built on AsyncOpenAI.
  7. Handle mid-stream failures (cut-offs, missing usage, keep-alive pings, finish_reason: "length") per the Error Handling table.

Python: Basic Streaming

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
    default_headers={"HTTP-Referer": "https://my-app.com", "X-Title": "my-app"},
)

# Stream with usage stats
stream = client.chat.completions.create(
    model="anthropic/claude-3.5-sonnet",
    messages=[{"role": "user", "content": "Explain how HTTP streaming works"}],
    max_tokens=500,
    stream=True,
    stream_options={"include_usage": True},  # Get token counts in final chunk
)

full_content = []
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        token = chunk.choices[0].delta.content
        print(token, end="", flush=True)
        full_content.append(token)

    # Final chunk contains usage stats
    if chunk.usage:
        print(f"\n---\nTokens: {chunk.usage.prompt_tokens} in + {chunk.usage.completion_tokens} out")

result = "".join(full_content)

Python: Streaming with Metrics

import time

def stream_with_metrics(messages, model="anthropic/claude-3.5-sonnet", **kwargs):
    """Stream response and capture performance metrics."""
    start = time.monotonic()
    first_token_time = None
    chunks = []
    usage = None

    stream = client.chat.completions.create(
        model=model, messages=messages, stream=True,
        stream_options={"include_usage": True},
        **kwargs,
    )

    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            token = chunk.choices[0].delta.content
            if first_token_time is None:
                first_token_time = (time.monotonic() - start) * 1000
            chunks.append(token)
            yield token  # Yield each token as it arrives

        if chunk.usage:
            usage = {
                "prompt_tokens": chunk.usage.prompt_tokens,
                "completion_tokens": chunk.usage.completion_tokens,
            }

    total_time = (time.monotonic() - start) * 1000
    # Metrics available after generator exhausted
    stream_with_metrics.last_metrics = {
        "ttft_ms": round(first_token_time or 0),
        "total_ms": round(total_time),
        "usage": usage,
        "model": model,
    }

# Usage
for token in stream_with_metrics(
    [{"role": "user", "content": "Hello"}],
    model="openai/gpt-4o-mini",
    max_tokens=200,
):
    print(token, end="", flush=True)
print(f"\nMetrics: {stream_with_metrics.last_metrics}")

TypeScript: Streaming

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
  defaultHeaders: { "HTTP-Referer": "https://my-app.com", "X-Title": "my-app" },
});

async function streamCompletion(prompt: string, model = "openai/gpt-4o-mini") {
  const stream = await client.chat.completions.create({
    model,
    messages: [{ role: "user", content: prompt }],
    max_tokens: 500,
    stream: true,
  });

  const chunks: string[] = [];
  for await (const chunk of stream) {
    const token = chunk.choices[0]?.delta?.content;
    if (token) {
      process.stdout.write(token);
      chunks.push(token);
    }
  }
  return chunks.join("");
}

SSE Forwarding to Browser (FastAPI)

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

@app.post("/v1/stream")
async def stream_endpoint(prompt: str, model: str = "openai/gpt-4o-mini"):
    """Forward OpenRouter SSE stream to browser."""
    async def generate():
        stream = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            max_tokens=1024,
            stream=True,
        )
        for chunk in stream:
            if chunk.choices and chunk.choices[0].delta.content:
                token = chunk.choices[0].delta.content
                yield f"data: {json.dumps({'token': token})}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

Browser Client (JavaScript)

// Consume SSE stream from your backend
async function streamChat(prompt) {
  const response = await fetch("/v1/stream", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const text = decoder.decode(value);
    for (const line of text.split("\n")) {
      if (line.startsWith("data: ") && line !== "data: [DONE]") {
        const data = JSON.parse(line.slice(6));
        document.getElementById("output").textContent += data.token;
      }
    }
  }
}

Async Streaming (Python)

from openai import AsyncOpenAI

aclient = AsyncOpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
    default_headers={"HTTP-Referer": "https://my-app.com", "X-Title": "my-app"},
)

async def async_stream(messages, model="openai/gpt-4o-mini", **kwargs):
    """Async streaming for use in async web frameworks."""
    stream = await aclient.chat.completions.create(
        model=model, messages=messages, stream=True, **kwargs,
    )
    async for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            yield chunk.choices[0].delta.content

Output

  • Token-by-token console output as the model generates, followed by usage counts from the final chunk (Tokens: 14 in + 132 out)
  • A metrics dict after the generator is exhausted: ttft_ms, total_ms, usage token counts, and the model used
  • A FastAPI SSE endpoint emitting data: {"token": ...} lines and a terminating data: [DONE] for browser consumption
  • Incrementally rendered text in the browser as the JavaScript reader loop decodes each SSE line

Examples

Stream with metrics and inspect TTFT after the tokens finish printing:

for token in stream_with_metrics(
    [{"role": "user", "content": "Write a haiku about programming"}],
    model="openai/gpt-4o-mini", max_tokens=60,
):
    print(token, end="", flush=True)
print(f"\nMetrics: {stream_with_metrics.last_metrics}")
# Code flows like a stream / bugs surface then sink away / green tests light the dawn
# Metrics: {'ttft_ms': 412, 'total_ms': 1875, 'usage': {'prompt_tokens': 14, 'completion_tokens': 21}, 'model': 'openai/gpt-4o-mini'}

More worked examples: references/examples.md.

Error Handling

ErrorCauseFix
Stream cuts off mid-responseNetwork timeout or provider errorSave partial content; implement retry from last position
Missing usage in streamDidn't set stream_optionsAdd stream_options: {"include_usage": True}
Empty delta chunksKeep-alive pingsFilter chunk.choices[0].delta.content is None
finish_reason: "length"Hit max_tokens limitIncrease max_tokens or continue with follow-up request

Enterprise Considerations

  • Always use stream_options: {"include_usage": True} to get token counts for cost tracking
  • Set connection timeouts appropriate for streaming (longer than non-streaming, e.g., 120s)
  • Implement heartbeat detection: if no chunks for >30s, consider the stream dead and retry
  • Buffer partial tokens on the server before forwarding to the client for smoother rendering
  • Log TTFT per model to benchmark streaming performance over time
  • Use streaming for all user-facing requests; use non-streaming for batch/background processing

References

When not to use it

  • When batch processing or background tasks do not require real-time feedback
  • When the client does not support Server-Sent Events (SSE)

Prerequisites

An OpenRouter API key exported as OPENROUTER_API_KEYPython 3.8+ or Node.js 18+ with the OpenAI SDKFastAPI if you plan to forward the SSE stream to browsersA streaming-appropriate client timeout (e.g. 120s)

Limitations

  • The skill requires setting stream_options: {include_usage: true} to get token counts
  • The skill requires filtering empty delta chunks for keep-alive pings
  • The skill requires increasing max_tokens or follow-up requests if finish_reason: "length" occurs

How it compares

This skill provides specific code examples for implementing and managing OpenRouter streaming responses, unlike a generic API call that returns the full response at once.

Compared to similar skills

openrouter-streaming-setup side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
openrouter-streaming-setup (this skill)126dReviewIntermediate
backend-architect104moNo flagsAdvanced
generating-grpc-services126dReviewAdvanced
openrouter-caching-strategy126dReviewIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

More by jeremylongshore

View all by jeremylongshore

analyzing-logs

jeremylongshore

Analyze application logs to detect performance issues, identify error patterns, and improve stability by extracting key insights.

14123

ollama-setup

jeremylongshore

Configure auto-configure Ollama when user needs local LLM deployment, free AI alternatives, or wants to eliminate hosted API costs. Trigger phrases: "install ollama", "local AI", "free LLM", "self-hosted AI", "replace OpenAI", "no API costs". Use when appropriate context detected. Trigger with relevant phrases based on skill purpose.

1167

backtesting-trading-strategies

jeremylongshore

Backtest crypto and traditional trading strategies against historical data. Calculates performance metrics (Sharpe, Sortino, max drawdown), generates equity curves, and optimizes strategy parameters. Use when user wants to test a trading strategy, validate signals, or compare approaches. Trigger with phrases like "backtest strategy", "test trading strategy", "historical performance", "simulate trades", "optimize parameters", or "validate signals".

1071

generating-database-seed-data

jeremylongshore

Process this skill enables AI assistant to generate realistic test data and database seed scripts for development and testing environments. it uses faker libraries to create realistic data, maintains relational integrity, and allows configurable data volumes. u... Use when working with databases or data models. Trigger with phrases like 'database', 'query', or 'schema'.

1033

cursor-codebase-indexing

jeremylongshore

Execute set up and optimize Cursor codebase indexing. Triggers on "cursor index setup", "codebase indexing", "index codebase", "cursor semantic search". Use when working with cursor codebase indexing functionality. Trigger with phrases like "cursor codebase indexing", "cursor indexing", "cursor".

885

testing-mobile-apps

jeremylongshore

Execute mobile app testing on iOS and Android devices/simulators. Use when performing specialized testing. Trigger with phrases like "test mobile app", "run iOS tests", or "validate Android functionality".

810

You might also like

backend-architect

sickn33

Expert backend architect specializing in scalable API design, microservices architecture, and distributed systems. Masters REST/GraphQL/gRPC APIs, event-driven architectures, service mesh patterns, and modern backend frameworks. Handles service boundary definition, inter-service communication, resilience patterns, and observability. Use PROACTIVELY when creating new backend services or APIs.

1014

generating-grpc-services

jeremylongshore

Generate gRPC service definitions, stubs, and implementations from Protocol Buffers. Use when creating high-performance gRPC services. Trigger with phrases like "generate gRPC service", "create gRPC API", or "build gRPC server".

13

openrouter-caching-strategy

jeremylongshore

Implement response caching for OpenRouter efficiency. Use when optimizing costs or reducing latency for repeated queries. Trigger with phrases like 'openrouter cache', 'cache llm responses', 'openrouter redis', 'semantic caching'.

12

api-documenter

ovachiever

Auto-generate API documentation from code and comments. Use when API endpoints change, or user mentions API docs. Creates OpenAPI/Swagger specs from code. Triggers on API file changes, documentation requests, endpoint additions.

00

etag

aalmada

Use this skill for any request involving HTTP ETags, conditional requests, or optimistic concurrency in REST APIs: implementing/explaining ETag headers, preventing lost updates, designing cache validation or conditional GET/PUT/DELETE, explaining If-Match, If-None-Match, 304 Not Modified, or 412 Pre

00

fastapi-templates

wshobson

Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.

5201,086

Search skills

Search the agent skills registry