How Switchyard Translates OpenAI Responses to Anthropic Messages Format
Switchyard converts OpenAI‑style chat completion responses into Anthropic Messages format through a codec‑based translation engine that decodes OpenAI wire format into an internal Response model, then re‑encodes it using the Anthropic codec.
The NVIDIA NeMo Switchyard project provides a unified translation layer for large language model (LLM) wire protocols. When downstream systems need to consume OpenAI API responses through an Anthropic-compatible interface, Switchyard's switchyard‑translation crate handles the protocol conversion automatically.
The Translation Pipeline Architecture
Switchyard's translation follows a decode‑transform‑encode pattern. The engine (Engine::translate_response) orchestrates the process by selecting codec pairs based on the source and target WireFormat variants.
Step‑by‑Step Conversion Flow
The translation proceeds through six distinct mapping operations:
| Step | OpenAI Response Element | Anthropic Messages Target | Implementation Location |
|---|---|---|---|
| 1 | id, model, created metadata |
Preserved as top‑level fields | Direct field copy in decoders |
| 2 | choices[].message (role + content) |
content array of Message objects with type, text, or image blocks |
codecs/openai_chat/buffered.rs → codecs/anthropic/buffered.rs |
| 3 | usage (prompt/completion/total tokens) |
usage (input_tokens, output_tokens) |
Field remapping with numeric transformation |
| 4 | tool_calls arrays |
tool_result objects (tool_use_id, content, cache_control) |
Special‑case handling in both codecs |
| 5 | finish_reason (stop, length, tool_calls) |
stop_reason (stop_sequence, max_tokens, tool_use) |
Enum mapping with priority rules |
| 6 | logprobs, OpenAI extensions |
Dropped — no Anthropic equivalent | Silent filtering during encode |
Core Source Files and Their Roles
crates/switchyard‑translation/src/engine.rs
The central dispatcher implements Engine::translate_response(). This function receives raw bytes, selects the appropriate decoder based on WireFormat::OpenAIChat, and routes the intermediate Response struct to the encoder for WireFormat::AnthropicMessages.
use switchyard_translation::engine::Engine;
use switchyard_translation::protocol::WireFormat;
let engine = Engine::new();
// Decode OpenAI chat completion response
let openai_resp = engine.decode(&openai_body, WireFormat::OpenAIChat)?;
// Re‑encode as Anthropic Messages format
let anth_msg = engine.encode(openai_resp, WireFormat::AnthropicMessages)?;
println!("{}", serde_json::to_string_pretty(&anth_msg)?);
crates/switchyard‑translation/src/codecs/openai_chat/buffered.rs
The OpenAI decoder extracts structured data from JSON responses. It handles:
- Message parsing: Extracts
role(system,user,assistant,tool) andcontentstrings - Tool call detection: Identifies
tool_callsarrays and constructs internalToolUserepresentations - Usage normalization: Converts token counts to internal numeric types
- Finish reason capture: Maps
stop,length,content_filter,tool_callsto internal enums
Key structures include the OpenAIChatDecoder and BufferedOpenAIChatCodec implementations.
crates/switchyard‑translation/src/codecs/anthropic/buffered.rs
The Anthropic encoder rebuilds the response according to the Messages API specification:
- Content block assembly: Creates
ContentBlockvariants (Text,Image,ToolUse,ToolResult) - Role translation: Maps OpenAI roles to Anthropic's stricter
user/assistantdichotomy, with tool messages rendered asToolResultblocks - Stop reason priority: Implements the rule that
max_tokensstop reasons take precedence over genericstop— as verified in testanthropic_message_stop_does_not_overwrite_max_tokens_stop_reason
Field‑Level Mapping Details
Messages and Content Blocks
OpenAI's flat message.content string becomes Anthropic's structured content array:
// OpenAI input
{
"choices": [{
"message": {
"role": "assistant",
"content": "The answer is 42."
}
}]
}
// Anthropic output
{
"content": [{
"type": "text",
"text": "The answer is 42."
}]
}
For multi‑part content including images, the decoder splits on content type and the encoder builds appropriate image blocks with source objects.
Tool Call Handling
When OpenAI returns tool invocations:
// OpenAI tool_calls
{
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"Paris\"}"
}
}]
}
The translation produces Anthropic tool_use and tool_result blocks:
// Anthropic equivalent
{
"content": [{
"type": "tool_use",
"id": "call_abc123",
"name": "get_weather",
"input": {"location": "Paris"}
}]
}
Usage Token Accounting
The usage translation handles field renaming and aggregation:
| OpenAI Field | Anthropic Field | Transformation |
|---|---|---|
prompt_tokens |
input_tokens |
Direct rename |
completion_tokens |
output_tokens |
Direct rename |
total_tokens |
implicit sum | Validated against input + output |
Finish Reason Priority Logic
The encoder implements careful precedence handling. Per the test suite in stream_translation.rs, an Anthropic stop reason will not overwrite a max_tokens stop reason already set. This ensures accurate reporting of truncation conditions.
Practical Usage Example
import switchyard
from switchyard.protocol import WireFormat
# Raw OpenAI chat completion response body
openai_json = b'''
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help?"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21
}
}
'''
engine = switchyard.translation.Engine()
# Decode from OpenAI format
openai_resp = engine.decode(openai_json, WireFormat.OPENAI_CHAT)
# Translate to Anthropic Messages format
anthropic_msg = engine.encode(openai_resp, WireFormat.ANTHROPIC_MESSAGES)
# Result ready for Anthropic /v1/messages endpoint
print(anthropic_msg)
Verification Through Test Suite
The translation logic is validated across multiple test files:
response_translation.rs: Round‑trip correctness between OpenAI and Anthropic formatsstream_translation.rs: Streaming response handling, including the finish reason priority rulestool_call_translation.rs: Tool use and tool result block accuracy
These tests confirm that edge cases — such as empty content, multiple choices, and mixed tool/text responses — all translate correctly.
Summary
- Switchyard uses a codec‑based architecture with
switchyard‑translationas the core crate - The
Enginestruct inengine.rsdispatches decode/encode operations based onWireFormatenums - OpenAI responses are parsed by
codecs/openai_chat/buffered.rsinto an internalResponsemodel - Anthropic Messages are generated by
codecs/anthropic/buffered.rsfrom that same model - Key transformations include: content block restructuring, tool call reformatting, usage field renaming, and finish reason priority handling
- The implementation is verified by comprehensive tests covering streaming, tools, and edge cases
Frequently Asked Questions
What is the internal representation between OpenAI and Anthropic formats?
Switchyard uses a protocol‑agnostic Response struct defined in the translation crate. This intermediate representation captures messages, usage statistics, tool calls, and metadata in a normalized form. Both codecs operate on this struct, enabling N‑to‑M format translation rather than hardcoded 1‑to‑1 mappings.
Does Switchyard support streaming translation between these formats?
Yes. The buffered.rs implementations in both codecs handle incremental SSE (Server‑Sent Events) streams. The OpenAI decoder parses data: lines into partial responses, and the Anthropic encoder emits compatible incremental deltas. The stream_translation.rs test file specifically validates this behavior, including finish reason preservation across chunk boundaries.
Are tool calls fully compatible between OpenAI and Anthropic formats?
Full compatibility requires semantic transformation. OpenAI's tool_calls with function objects become Anthropic's tool_use blocks with input fields. The translation preserves id values for correlation and handles the round‑trip when tool results return from the client. Complex argument schemas pass through JSON parsing without modification.
What happens to OpenAI‑specific fields that Anthropic doesn't support?
Fields like logprobs, content_filter_results, and system_fingerprint are silently discarded during encoding. The translation prioritizes schema compliance over data preservation — if Anthropic's Messages API has no equivalent field, the information is not included in the output.
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 →