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) and content strings
  • Tool call detection: Identifies tool_calls arrays and constructs internal ToolUse representations
  • Usage normalization: Converts token counts to internal numeric types
  • Finish reason capture: Maps stop, length, content_filter, tool_calls to 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 ContentBlock variants (Text, Image, ToolUse, ToolResult)
  • Role translation: Maps OpenAI roles to Anthropic's stricter user/assistant dichotomy, with tool messages rendered as ToolResult blocks
  • Stop reason priority: Implements the rule that max_tokens stop reasons take precedence over generic stop — as verified in test anthropic_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:

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‑translation as the core crate
  • The Engine struct in engine.rs dispatches decode/encode operations based on WireFormat enums
  • OpenAI responses are parsed by codecs/openai_chat/buffered.rs into an internal Response model
  • Anthropic Messages are generated by codecs/anthropic/buffered.rs from 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →