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.zipInstalls 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. UseKey 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
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 asOPENROUTER_API_KEY— see theopenrouter-install-authskill for setup - Python 3.8+ or Node.js 18+ with the OpenAI SDK (the async example uses
AsyncOpenAIfrom 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
- Start with Python: Basic Streaming — pass
stream=Trueplusstream_options={"include_usage": True}so the final chunk carries token counts, and print eachchunk.choices[0].delta.contentas it arrives. - 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.
- For Node services, use the TypeScript: Streaming
for awaitloop over the samestream: truerequest. - 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 withdata: [DONE]. - Consume that endpoint with the Browser Client (JavaScript) reader loop, appending tokens to the DOM as they decode.
- In async web frameworks, switch to the Async Streaming pattern built on
AsyncOpenAI. - 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,usagetoken counts, and themodelused - A FastAPI SSE endpoint emitting
data: {"token": ...}lines and a terminatingdata: [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
| Error | Cause | Fix |
|---|---|---|
| Stream cuts off mid-response | Network timeout or provider error | Save partial content; implement retry from last position |
Missing usage in stream | Didn't set stream_options | Add stream_options: {"include_usage": True} |
| Empty delta chunks | Keep-alive pings | Filter chunk.choices[0].delta.content is None |
finish_reason: "length" | Hit max_tokens limit | Increase 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
- Examples | Errors
- Streaming | API Reference
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
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.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| openrouter-streaming-setup (this skill) | 1 | 26d | Review | Intermediate |
| backend-architect | 10 | 4mo | No flags | Advanced |
| generating-grpc-services | 1 | 26d | Review | Advanced |
| openrouter-caching-strategy | 1 | 26d | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by jeremylongshore
View all by jeremylongshore →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.
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".
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'.
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.
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
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.