How Switchyard Handles Streaming Responses Between OpenAI and Anthropic Formats
Switchyard abstracts streaming API differences by converting every provider chunk into a neutral LlmResponseChunk representation, then re-encoding it for the target format through a decode-state-encode pipeline.
Unlike other translation layers that require manual mapping, NVIDIA's Switchyard solves the OpenAI-Anthropic streaming incompatibility with a unified intermediate representation. The system lets you stream from Claude and consume as OpenAI-format chunks (or vice versa) without client-side changes. This article examines the three-stage pipeline, key data mappings, and the Rust implementation powering this translation.
The Three-Stage Translation Pipeline
Switchyard's streaming architecture in crates/switchyard-translation/src/engine.rs follows a consistent pattern regardless of source or destination provider:
- Decode — Parse provider-specific JSON events into neutral chunks
- Maintain State — Track stream lifecycle via
StreamTranslationState - Encode — Emit target-format chunks from neutral representation
Stage 1: Decode with Provider-Specific Codecs
Each supported provider implements a stream codec with two core methods: decode_event for inbound translation and encode_* for outbound generation.
The OpenAiChatStreamCodec processes OpenAI's chat.completion.chunk events. Its decode_event implementation in crates/switchyard-translation/src/codecs/openai_chat/stream.rs (lines 24-48) parses JSON deltas and extracts:
- Text content from
choices[0].delta.content - Tool call fragments from
choices[0].delta.tool_calls - Finish reasons from
choices[0].finish_reason - Usage statistics from top-level
usageobjects
Similarly, AnthropicMessagesStreamCodec handles Anthropic's Server-Sent Events format with message_start, content_block_start, content_block_delta, and message_stop event types.
Both codecs output the same neutral type: LlmResponseChunk.
Stage 2: State Management Per Stream
The StreamTranslationState struct in engine.rs (lines 1-30) persists across all chunks in a single request-response cycle. It tracks:
- Whether
MessageStarthas been emitted - Accumulated token usage (prompt and completion)
- Cached model identifier
- Stop reason and error flags
- Pending tool call state
This state enables correct encoding decisions. For example, Anthropic does not emit usage in its streaming format—Switchyard preserves usage data in state so it can be attached to final OpenAI-format chunks when that is the target.
Stage 3: Encode for Target Format
The target codec transforms neutral chunks into provider-compliant JSON. Two encoding functions demonstrate the symmetry:
OpenAI encoding (encode_openai_chat_stream, lines 60-89 in openai_chat/stream.rs) builds objects with:
id,object,created,modelmetadatachoices[0].deltacontaining content/tool_callsusageandfinish_reasonon terminal chunks
Anthropic encoding (AnthropicMessagesStreamCodec, lines 30-78 in anthropic/stream.rs) produces:
message_startwith model and rolecontent_block_deltawiththinkingortexttypesmessage_deltawith stop reasonmessage_stopto signal completion
Key Data Mappings: Neutral to Provider Formats
The LlmResponseChunk enum provides six variants that map differently to OpenAI and Anthropic outputs:
| Neutral Chunk | OpenAI Output | Anthropic Output |
|---|---|---|
TextDelta { text } |
"content": "<text>" |
"content": "<text>" |
ReasoningDelta { text } |
"reasoning_content": "<text>" |
"reasoning_content": "<text>" |
ToolCallDelta { index, id, name, arguments_delta } |
"tool_calls[].function.{name,arguments}" with index, id |
"tool_calls[].{name,input,id,type,index}" |
Usage |
Full usage object |
State-only (not emitted in stream) |
MessageStop { reason } |
"finish_reason": "stop|length|tool_calls" |
"stop_reason": "end_turn|max_tokens|tool_use" |
StreamError / DecodeError |
{"error":{"message":"..."}} (terminal) |
Same error frame |
Finish-Reason Normalization
Bidirectional mapping requires vocabulary translation. The helper openai_finish_reason in openai_chat/stream.rs (lines 94-101) converts:
- Anthropic
end_turn→ OpenAIstop - Anthropic
max_tokens→ OpenAIlength - Anthropic
tool_use→ OpenAItool_calls
The inverse mapping in the Anthropic codec ensures round-trip fidelity.
Error Handling: Terminal Stream Failure
Both codecs treat decode errors as stream-terminating events. From openai_chat/stream.rs (lines 30-36), when DecodeError occurs:
- Emit an error chunk with
{"error":{"message":"..."}}structure - Set
state.errored = true - Drop all subsequent chunks from the source stream
This prevents partial or corrupted translations from propagating downstream.
Streaming Flow in Practice
When using Switchyard's Python interface in switchyard_rust/libsy.py, the translation engine operates transparently:
import asyncio
from switchyard import launch
async def stream_comparison():
# Same consumption code works for both providers
for provider in ["openai", "claude"]:
client = launch(provider, model="switchyard", stream=True)
async for chunk in client.run_stream({"prompt": "Explain quantum computing"}):
# chunk is always LlmResponseChunk regardless of source
match chunk.type:
case "TextDelta":
print(chunk.text, end="", flush=True)
case "ToolCallDelta":
print(f"\n[Tool: {chunk.name}]")
case "MessageStop":
print(f"\n[Finished: {chunk.reason}]")
asyncio.run(stream_comparison())
The underlying Rust translation guarantees format-agnostic consumption. The run_stream method in libsy.py drives the engine from crates/switchyard-translation/src/stream.rs, which re-exports the public streaming API.
Implementation Reference
| File | Purpose | Key Components |
|---|---|---|
crates/switchyard-translation/src/engine.rs |
Orchestration layer | StreamTranslationState, translation engine |
crates/switchyard-translation/src/stream.rs |
Public API surface | pub use crate::codecs::stream::* |
crates/switchyard-translation/src/codecs/openai_chat/stream.rs |
OpenAI codec | OpenAiChatStreamCodec, encode_openai_chat_stream, openai_finish_reason |
crates/switchyard-translation/src/codecs/anthropic/stream.rs |
Anthropic codec | AnthropicMessagesStreamCodec, reasoning_content handling |
switchyard_rust/libsy.py |
Python bindings | Async run_stream method |
examples/libsy.py |
Usage demonstration | End-to-end streaming examples |
Summary
Switchyard handles streaming response translation between OpenAI and Anthropic formats through three architectural decisions:
- Neutral intermediate representation —
LlmResponseChunkdecouples source and target formats - Stateful stream tracking —
StreamTranslationStatepreserves cross-chunk information like usage and stop reasons - Bidirectional codec symmetry — Each provider implements decode and encode, enabling any-to-any translation
The system preserves reasoning content, tool calls, usage statistics, and finish reasons across format boundaries, with terminal error handling that prevents partial translation failures.
Frequently Asked Questions
What is the LlmResponseChunk type in Switchyard?
LlmResponseChunk is a Rust enum defined in the translation engine that represents a streaming response in provider-neutral form. It has variants for text deltas, reasoning content, tool call fragments, usage data, stream completion, and errors. All provider codecs convert to and from this type, making it the central abstraction for cross-format streaming.
Does Switchyard support reasoning content from Claude in OpenAI format?
Yes. The ReasoningDelta variant carries reasoning text from Claude's thinking content blocks. When encoding to OpenAI format, Switchyard emits this as "reasoning_content": "<text>" in the delta object—a non-standard but widely-supported extension. The neutral representation ensures reasoning content is preserved regardless of translation direction.
How does Switchyard handle usage statistics when streaming from Anthropic to OpenAI?
Anthropic's streaming API does not include usage data in its SSE events. Switchyard accumulates token counts in StreamTranslationState during the decode phase. When encoding to OpenAI format, the final chunk includes a synthesized usage object with prompt_tokens and completion_tokens populated from state. This allows OpenAI-format consumers to receive complete usage information even from Anthropic sources.
What happens when a stream decode error occurs mid-response?
The codec sets state.errored = true and emits a terminal error chunk with JSON structure {"error":{"message":"..."}}. All subsequent chunks from the upstream provider are dropped. This design prevents partial or malformed translations from reaching downstream consumers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →