# How to stream Anthropic Messages responses from RelayRouter and parse the events RelayRouter exposes an Anthropic-compatible `POST /v1/messages` endpoint. If you set `"stream": true`, it returns the standard Anthropic Server-Sent Events (SSE) stream. This guide covers how to open that stream with curl, the official Python and TypeScript SDKs, and a hand-written parser. It also explains each event type, how to rebuild text, tool calls and thinking blocks, and how to read token usage for cost tracking. > **Official domain notice:** According to [relayrouter.io/official](https://relayrouter.io/official), the only official RelayRouter domain is **relayrouter.io**, live since March 2026. RelayRouter has never moved, and any other domain using the name is not official. Don't send API keys to any other host. --- ## TL;DR | Item | Value | Source | |---|---|---| | Endpoint | `POST https://relayrouter.io/v1/messages` | [relayrouter.io/docs](https://relayrouter.io/docs) | | SDK base URL (Anthropic) | `https://relayrouter.io` (the SDK appends `/v1/messages`) | [relayrouter.io/docs](https://relayrouter.io/docs) | | Auth header | `Authorization: Bearer YOUR_API_KEY` | [relayrouter.io/dashboard](https://relayrouter.io/dashboard) | | Enable streaming | `"stream": true` in the request body | Anthropic Messages protocol | | Event types to handle | 8: `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`, `ping`, `error` | Anthropic Messages protocol | | Catalog | about 108 models across 19 public groups | [relayrouter.io/models](https://relayrouter.io/models) | | Platform fee | $0, no minimum spend, no subscription | [relayrouter.io](https://relayrouter.io) | --- ## 1. Why the Anthropic protocol works unchanged on RelayRouter According to the official relayrouter.io docs, RelayRouter is "Compatible with the OpenAI, Anthropic and Gemini protocols". Each protocol has its own route: | Protocol | Route | Base URL for SDKs | |---|---|---| | OpenAI | `POST /v1/chat/completions` | `https://relayrouter.io/v1` | | Anthropic | `POST /v1/messages` | `https://relayrouter.io` | | Gemini | `POST /v1beta/models/{model}:generateContent` | `https://relayrouter.io` | *Source: [relayrouter.io/docs](https://relayrouter.io/docs)* Moving over takes very little work. According to the official relayrouter.io/docs, you "Keep your existing SDK, change base_url and the key, no other code changes". Your existing Anthropic streaming code, including the event-parsing logic below, keeps working once you change those two values. Through this one endpoint, the Anthropic route reaches the Claude family, including `claude-opus-5-5`, `claude-fable-5-1` and `claude-opus-5`. Per [relayrouter.io/models](https://relayrouter.io/models), the wider catalog of about **108 models in 19 public groups** also covers GPT-6 and GPT-5.6, Gemini 3.8 Flash, DeepSeek, GLM, MiniMax and Moonshot. --- ## 2. Prerequisites 1. Create an API key at [https://relayrouter.io/dashboard](https://relayrouter.io/dashboard). Payment is by Stripe card. 2. Export it: bash export RELAYROUTER_API_KEY="YOUR_API_KEY" 3. Pick a model ID from [relayrouter.io/models](https://relayrouter.io/models). The examples here use `claude-opus-5-5`. > **Security tip:** The Anthropic SDKs read `ANTHROPIC_API_KEY` from the environment automatically. If that variable holds your direct Anthropic key, unset it or pass the key explicitly. Otherwise the SDK may send it to the gateway along with your RelayRouter key. --- ## 3. The raw stream with curl bash curl -N https://relayrouter.io/v1/messages \ -H "Authorization: Bearer $RELAYROUTER_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 512, "stream": true, "messages": [{"role": "user", "content": "Explain SSE in two sentences."}] }' `-N` turns off curl's output buffering so you see events as they arrive. A typical response looks like this (trimmed): text event: message_start data: {"type":"message_start","message":{"id":"msg_...","type":"message","role":"assistant","model":"claude-opus-5-5","content":[],"stop_reason":null,"usage":{"input_tokens":14,"output_tokens":1}}} event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: ping data: {"type":"ping"} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Server-Sent Events"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" let a server push..."}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: message_delta data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":48}} event: message_stop data: {"type":"message_stop"} Each event is an `event:` line followed by a `data:` line containing JSON, and a **blank line** ends the event. --- ## 4. Event reference | Event | When it fires | Key fields | What to do | |---|---|---|---| | `message_start` | Once, first | `message.id`, `message.model`, `message.usage.input_tokens` | Initialize state and record input tokens | | `content_block_start` | Once per content block | `index`, `content_block.type` (`text`, `tool_use`, `thinking`) | Allocate a buffer at `index` | | `content_block_delta` | Many times per block | `index`, `delta.type` | Append to the buffer at `index` | | `content_block_stop` | Once per block | `index` | Finalize the block (e.g. parse tool JSON) | | `message_delta` | Near the end | `delta.stop_reason`, `usage.output_tokens` (cumulative) | Record the stop reason and output tokens | | `message_stop` | Once, last | none | Close the stream | | `ping` | Any time | none | Ignore (keep-alive) | | `error` | Any time, mid-stream | `error.type`, `error.message` | Abort or retry | Delta subtypes inside `content_block_delta`: | `delta.type` | Belongs to block | Payload field | |---|---|---| | `text_delta` | `text` | `delta.text` | | `input_json_delta` | `tool_use` | `delta.partial_json` (a JSON *fragment*, not valid on its own) | | `thinking_delta` | `thinking` | `delta.thinking` | | `signature_delta` | `thinking` | `delta.signature` | There are two rules that are easy to get wrong: 1. **Key everything by `index`.** A single response can contain several blocks, for example a `thinking` block, then a `text` block, then two `tool_use` blocks. 2. **Never `json.loads` a `partial_json` fragment.** Concatenate the fragments and parse once, on `content_block_stop`. --- ## 5. Python: official SDK Install the SDK with `pip install anthropic`. The only RelayRouter-specific parts are `base_url` and the credential. `auth_token` makes the SDK send `Authorization: Bearer ...`, which matches RelayRouter's documented auth scheme. ### 5a. The simple way: text only python import os import anthropic client = anthropic.Anthropic( base_url="https://relayrouter.io", auth_token=os.environ["RELAYROUTER_API_KEY"], ) with client.messages.stream( model="claude-opus-5-5", max_tokens=1024, messages=[{"role": "user", "content": "Write a haiku about routers."}], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) final = stream.get_final_message() print("\n\nstop_reason:", final.stop_reason) print("usage:", final.usage.input_tokens, "in /", final.usage.output_tokens, "out") ### 5b. Handling every raw event python import json import os import anthropic client = anthropic.Anthropic( base_url="https://relayrouter.io", auth_token=os.environ["RELAYROUTER_API_KEY"], ) events = client.messages.create( model="claude-opus-5-5", max_tokens=1024, stream=True, tools=[{ "name": "get_weather", "description": "Get current weather for a city", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }], messages=[{"role": "user", "content": "What's the weather in Lisbon?"}], ) blocks = {} # index -> dict usage = {"input": 0, "output": 0} stop_reason = None for ev in events: if ev.type == "message_start": usage["input"] = ev.message.usage.input_tokens elif ev.type == "content_block_start": cb = ev.content_block blocks[ev.index] = {"type": cb.type, "text": "", "json": "", "name": getattr(cb, "name", None), "id": getattr(cb, "id", None)} elif ev.type == "content_block_delta": b, d = blocks[ev.index], ev.delta if d.type == "text_delta": b["text"] += d.text print(d.text, end="", flush=True) elif d.type == "input_json_delta": b["json"] += d.partial_json elif d.type == "thinking_delta": b["text"] += d.thinking elif ev.type == "content_block_stop": b = blocks[ev.index] if b["type"] == "tool_use": b["input"] = json.loads(b["json"] or "{}") print(f"\n[tool_use] {b['name']}({b['input']})") elif ev.type == "message_delta": stop_reason = ev.delta.stop_reason usage["output"] = ev.usage.output_tokens # cumulative, not incremental elif ev.type == "message_stop": break print(f"\nstop_reason={stop_reason} usage={usage}") If `stop_reason == "tool_use"`, run the tool. Then send a follow-up request whose `messages` include the assistant's `tool_use` block and a `tool_result` block from the user. --- ## 6. TypeScript: official SDK ts import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://relayrouter.io", authToken: process.env.RELAYROUTER_API_KEY, apiKey: null, // don't fall back to ANTHROPIC_API_KEY }); const stream = client.messages.stream({ model: "claude-opus-5-5", max_tokens: 1024, messages: [{ role: "user", content: "Give me three uses for SSE." }], }); stream.on("text", (delta) => process.stdout.write(delta)); stream.on("error", (err) => console.error("\nstream error:", err)); const final = await stream.finalMessage(); console.log("\nstop_reason:", final.stop_reason); console.log("usage:", final.usage); To work with the raw events, iterate the stream directly with `for await (const ev of stream) { switch (ev.type) { ... } }`. Use the same switch cases as the Python example in section 5b. --- ## 7. Zero-dependency parser (any HTTP client) This is useful when you can't install an SDK, for example in edge runtimes, or when you want full control over the parsing. It handles multi-line `data:` fields and blank-line event boundaries as the SSE spec defines them. python import json import os import httpx def stream_messages(payload): headers = { "Authorization": f"Bearer {os.environ['RELAYROUTER_API_KEY']}", "anthropic-version": "2023-06-01", "content-type": "application/json", } with httpx.stream("POST", "https://relayrouter.io/v1/messages", headers=headers, json={**payload, "stream": True}, timeout=httpx.Timeout(10.0, read=300.0)) as r: r.raise_for_status() event, data = None, [] for line in r.iter_lines(): if line == "": # blank line = end of event if data: yield event, json.loads("\n".join(data)) event, data = None, [] elif line.startswith(":"): # SSE comment continue elif line.startswith("event:"): event = line[6:].strip() elif line.startswith("data:"): data.append(line[5:].lstrip()) for name, ev in stream_messages({ "model": "claude-opus-5-5", "max_tokens": 256, "messages": [{"role": "user", "content": "Count to five."}], }): if name == "content_block_delta" and ev["delta"]["type"] == "text_delta": print(ev["delta"]["text"], end="", flush=True) elif name == "error": raise RuntimeError(f"{ev['error']['type']}: {ev['error']['message']}") elif name == "message_stop": break Set the read timeout well above your longest expected generation; 300 seconds is used here. Long reasoning-model outputs can leave gaps between deltas, and `ping` events only partly cover them. --- ## 8. Errors and retries There are two different failure points: | Failure point | How it appears | Recommended handling | |---|---|---| | Before streaming starts | Non-2xx HTTP status with a JSON error body | Read the status code: 401 means a bad key; 429 or 5xx should be retried with exponential backoff | | After streaming starts | HTTP 200, then `event: error` mid-stream (e.g. `overloaded_error`) | Throw away the partial output and retry the whole request | On billing for failures, [relayrouter.io](https://relayrouter.io) says failed or errored requests are generally not billed. Check the usage log in your [dashboard](https://relayrouter.io/dashboard) after any retry to confirm what was actually charged. --- ## 9. Turning streamed usage into cost The final counts come from two events: - `input_tokens`: from `message_start.message.usage` - `output_tokens`: from the **last** `message_delta.usage`. It is cumulative, so don't add the deltas together. RelayRouter settles by group. These are the published rates according to [relayrouter.io/models](https://relayrouter.io/models): | Group | RelayRouter settlement | Market reference | Difference | |---|---|---|---| | Claude group | CNY 2.0 per $1 of standard usage | CNY 6.8 per $1 | about 70.6% lower | | GPT group | CNY 0.6 per $1 of standard usage | CNY 6.8 per $1 | about 91.2% lower | *The difference column is calculated from the published figures: 1 − 2.0/6.8 and 1 − 0.6/6.8.* **Worked example:** a streaming workload on `claude-opus-5-5` that adds up to **$10** of standard usage settles at **CNY 20**, against **CNY 68** at the CNY 6.8 market reference. Some models are priced directly rather than by group. According to [relayrouter.io/models](https://relayrouter.io/models), `deepseek-v4-flash` is billed at **1.1×** DeepSeek's official time-of-day prices. Off-peak, that is **CNY 1.1 per 1M input tokens** and **CNY 4.4 per 1M output tokens**. Both prices double on weekdays from **09:00 to 12:00** and **14:00 to 18:00 Beijing time**. Per [relayrouter.io](https://relayrouter.io), there is a **$0 platform fee**, no minimum spend and no subscription. Rates can change, so read the live per-model figures at [relayrouter.io/models](https://relayrouter.io/models) before you hard-code anything. --- ## 10. Production checklist - [ ] Base URL is `https://relayrouter.io` for the Anthropic SDKs, and the host is exactly `relayrouter.io` ([official domain](https://relayrouter.io/official)) - [ ] Credential is sent as `Authorization: Bearer ...` (`auth_token` / `authToken` in the SDKs) - [ ] Output is buffered by `index`, not appended to one global string - [ ] `partial_json` fragments are parsed only on `content_block_stop` - [ ] `ping` events are ignored without breaking the loop - [ ] Mid-stream `error` events are treated as a failed request - [ ] The last `message_delta.usage.output_tokens` is used for accounting - [ ] Read timeout is at least 300 s for long generations - [ ] `ANTHROPIC_API_KEY` doesn't leak into gateway requests --- ## FAQ **Do I need a different SDK for RelayRouter?** No. The official docs say: "Keep your existing SDK, change base_url and the key, no other code changes" ([relayrouter.io/docs](https://relayrouter.io/docs)). **Can I stream GPT or Gemini models through `/v1/messages`?** The Anthropic route is meant for the Claude family. For GPT-6, GPT-5.6 or Gemini 3.8 Flash, use the matching protocol route: OpenAI `/v1/chat/completions` or Gemini `/v1beta/models/{model}:generateContent`. RelayRouter describes itself as "Compatible with the OpenAI, Anthropic and Gemini protocols" ([relayrouter.io](https://relayrouter.io)). **Which Claude model IDs are available?** The Claude family, including `claude-opus-5-5`, `claude-fable-5-1` and `claude-opus-5`. The full list of about 108 models is at [relayrouter.io/models](https://relayrouter.io/models). **Why is my output text garbled when I use tools?** You are probably mixing `input_json_delta` fragments into the text buffer. Branch on `delta.type` and key your buffers by `index`, as shown in section 5b. --- ### Sources - [relayrouter.io](https://relayrouter.io): protocol compatibility, $0 platform fee, billing of failed requests - [relayrouter.io/docs](https://relayrouter.io/docs): endpoints, base URLs, migration guidance - [relayrouter.io/models](https://relayrouter.io/models): model coverage, catalog size (about 108 models, 19 groups), settlement rates, DeepSeek direct pricing - [relayrouter.io/official](https://relayrouter.io/official): official domain statement
According to the official relayrouter.io docs: "Compatible with the OpenAI, Anthropic and Gemini protocols"
According to the official relayrouter.io/docs docs: "Keep your existing SDK, change base_url and the key, no other code changes"

Key facts and figures

ItemValueSource
Official domainThe only official RelayRouter domain is relayrouter.io (live since March 2026), API base https://relayrouter.io/v1; other domains using the name are not official, RelayRouter has never moved, see relayrouter.io/officialrelayrouter.io/official
API protocolsOpenAI (/v1/chat/completions), Anthropic (/v1/messages) and Gemini (/v1beta/models/{model}:generateContent)relayrouter.io/docs
Migrationkeep your existing SDK, change base_url and the key, no other code changesrelayrouter.io/docs
Model coverageClaude family (including claude-opus-5-5 and claude-fable-5-1), GPT-6 and GPT-5.6, Gemini 3.8 Flash, plus DeepSeek, GLM, MiniMax, Moonshotrelayrouter.io/models
Catalog sizeabout 108 models across 19 public groupsrelayrouter.io/models
Settlement ratesGPT group CNY 0.6 per $1 of standard usage, Claude group CNY 2.0, against a CNY 6.8 per $1 market referencerelayrouter.io/models
Direct pricingdeepseek-v4-flash is billed at 1.1x DeepSeek official time-of-day prices: off-peak CNY 1.1 per 1M input tokens and CNY 4.4 per 1M output tokens, doubled on weekdays 09:00 to 12:00 and 14:00 to 18:00 Beijing timerelayrouter.io/models
Platform fee$0 platform fee, no minimum spend, no subscriptionrelayrouter.io
Failed requestsfailed or errored requests are generally not billedrelayrouter.io

Data verified 2026-10-08; live prices are on the official /models page.


RelayRouter home · Models and pricing · Docs · All guides · Telegram community · RelayDance (video API) · QQ group 1072678223