SDK Compatibility
ModelRelay provides drop-in compatibility endpoints for the OpenAI and Anthropic APIs. If you have existing code using these providers’ SDKs, you can point them at ModelRelay with a base URL change to get unified billing, observability, and access to any model.
Why Use Compatibility Endpoints?
- Minimal code changes - Point existing code at ModelRelay with a base URL change
- Unified billing - Track usage across all providers through ModelRelay
- Provider flexibility - Use any model through the same API interface
- Gradual migration - Move to ModelRelay without rewriting your application
OpenAI Responses API
Endpoint: POST /v1/responses
Point the OpenAI SDK at ModelRelay by changing the base URL and using your ModelRelay API key.
Configuration
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.MODELRELAY_API_KEY,
baseURL: "https://api.modelrelay.ai/v1",
});
const response = await client.responses.create({
model: "claude-sonnet-5", // Use any ModelRelay model
input: "What is the capital of France?",
});
console.log(response.output_text);
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["MODELRELAY_API_KEY"],
base_url="https://api.modelrelay.ai/v1",
)
response = client.responses.create(
model="claude-sonnet-5", # Use any ModelRelay model
input="What is the capital of France?",
)
print(response.output_text)
curl -X POST https://api.modelrelay.ai/v1/responses \
-H "Authorization: Bearer $MODELRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"input": "What is the capital of France?"
}'
Streaming
Enable streaming by setting stream: true:
const stream = await client.responses.create({
model: "claude-sonnet-5",
input: "Write a haiku about programming.",
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") {
process.stdout.write(event.delta);
}
}
stream = client.responses.create(
model="claude-sonnet-5",
input="Write a haiku about programming.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
Authentication
Use Authorization: Bearer <mr_sk_...> with your ModelRelay secret key. The compatibility layer maps this to X-ModelRelay-Api-Key internally.
Field Mapping
| OpenAI Field | ModelRelay Behavior |
|---|---|
model |
Passed through to ModelRelay |
input |
String or array of messages. input_image parts map to native file content. |
instructions |
Mapped to system message |
stream |
Enables SSE streaming |
tools |
Passed through |
tool_choice |
Passed through |
input_image.image_url must be a data:image/...;base64,... URL. HTTP(S) image URLs and OpenAI file_id values are rejected rather than fetched or resolved. detail is accepted and ignored. Image input still requires a model that advertises vision and a rate card that prices image tokens.
Some routes accept only certain image encodings, and that is separate from whether they accept images at all. Where the upstream publishes a constraint, ModelRelay declares it and converts your image into a format the route takes — WebP and GIF are re-encoded as PNG, losslessly — rather than forwarding bytes the host will reject. Cerebras (qwen-3.8-27b) publishes PNG and JPEG only, so a WebP screenshot works there and arrives as PNG. Two consequences worth knowing: PNG is larger than WebP, which counts against a host’s request size limit, and an image in a format with no decoder (or a corrupt one) is refused before dispatch with an error naming the format you sent, the ones the route accepts, and why the conversion failed. Animated WebP is not decodable and is refused; an animated GIF converts to its first frame.
function_call_output.output accepts either a string or the same content-part array a message carries, so a tool that returns an image hands it back directly:
{
"type": "function_call_output",
"call_id": "call_1",
"output": [
{ "type": "input_text", "text": "Read image file [image/png]" },
{ "type": "input_image", "image_url": "data:image/png;base64,..." }
]
}
An image in a tool result counts as image input for routing and pricing exactly as one in a user message does, so it needs the same vision route and image rate card. Where the upstream transport carries images inside its own tool result — Anthropic, Gemini, the OpenAI Responses API — the image stays there. Where it does not, notably Chat Completions hosts such as Cerebras and Together, ModelRelay moves the image to a user turn immediately after the tool results and labels it with the call_id it came from. What you send back on the next turn is unaffected: the conversation ModelRelay returns is the shape you sent.
OpenAI Chat Completions API
Endpoint: POST /v1/chat/completions
Existing client.chat.completions.create(...) calls can use ModelRelay for streaming and non-streaming completions, function calling, and JSON schema output. Set the base URL to https://api.modelrelay.ai/v1 and use a ModelRelay key. Model IDs must exist in ModelRelay or match an explicitly supported OpenRouter alias. User message content may include image_url data URLs.
Use stream: true for SSE and stream_options: { "include_usage": true } for an OpenAI-style terminal usage chunk with empty choices. Tool calls and results use standard tool_calls and tool_call_id fields, so SDK-assembled assistant messages can be replayed for function-calling conversations. Structured output uses response_format.type: "json_schema"; json_object mode is also supported by OpenAI, OpenAI Chat Completions transports, Google, and xAI, and rejected by Anthropic.
OpenRouter extensions include reasoning.effort, reasoning.exclude, and provider selection through only, order, ignore, and allow_fallbacks. This is partial OpenRouter compatibility: plugins, advanced routing/privacy constraints, opaque reasoning history, and unconditional OpenRouter-style streaming usage are unsupported. Nonempty reasoning history, audio/refusal metadata, HTTP(S) image URLs, unrecognized generation controls, and ambiguous message shapes return errors. temperature is limited to resolved models without mapped reasoning controls and cannot be combined with reasoning_effort. See the Chat Completions API reference for supported fields and exact behavior.
Codex
Codex uses the OpenAI Responses protocol. Configure a custom provider in your Codex configuration and set MODELRELAY_API_KEY in the environment:
model_provider = "modelrelay"
model = "glm-5.3-flash"
model_reasoning_effort = "low"
web_search = "disabled"
[features]
apps = false
multi_agent = false
tool_suggest = false
[model_providers.modelrelay]
name = "ModelRelay"
base_url = "https://api.modelrelay.ai/v1"
env_key = "MODELRELAY_API_KEY"
wire_api = "responses"
The repository’s scripts/smoke-codex.sh runs a small Codex file-edit task in an isolated temporary directory through ModelRelay, defaulting to glm-5.3-flash with apps, multiple agents, and tool suggestions disabled. It requires an installed Codex CLI and a funded ModelRelay API key with access to the selected model. This config limits the smoke task to standard function tools: newer Codex protocol features such as additional_tools and tool namespaces are not supported. This exercises the Responses endpoint; the Chat Completions endpoint has separate SDK streaming and tool-cycle tests.
Anthropic Messages API
Endpoint: POST /v1/messages
Point the Anthropic SDK at ModelRelay by changing the base URL and using your ModelRelay API key.
Configuration
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.MODELRELAY_API_KEY,
baseURL: "https://api.modelrelay.ai/v1",
});
const message = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [
{ role: "user", content: "What is the capital of France?" }
],
});
console.log(message.content[0].text);
import os
import anthropic
client = anthropic.Anthropic(
api_key=os.environ["MODELRELAY_API_KEY"],
base_url="https://api.modelrelay.ai/v1",
)
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "What is the capital of France?"}
],
)
print(message.content[0].text)
curl -X POST https://api.modelrelay.ai/v1/messages \
-H "x-api-key: $MODELRELAY_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "What is the capital of France?"}
]
}'
Streaming
Enable streaming by setting stream: true:
const stream = await client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [
{ role: "user", content: "Write a haiku about programming." }
],
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
process.stdout.write(event.delta.text);
}
}
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Write a haiku about programming."}
],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
Authentication
The Anthropic compatibility endpoint supports two authentication styles:
x-api-key: <mr_sk_...>- Anthropic SDK defaultAuthorization: Bearer <mr_sk_...>- Alternative style
Both are mapped to X-ModelRelay-Api-Key internally.
Every executed Messages request returns X-ModelRelay-Execution-Id. The UUID
identifies the immutable content-free pricing and settlement evidence available
from GET /api/v1/responses/executions/{execution_id}/evidence.
Field Mapping
| Anthropic Field | ModelRelay Behavior |
|---|---|
model |
Passed through to ModelRelay |
messages |
Converts user and assistant roles; also accepts beta-gated mid-conversation system roles |
system |
Mapped to system message |
max_tokens |
Required, passed through |
temperature |
Passed through |
stop_sequences |
Passed through |
tools |
Converted to ModelRelay tool format |
tool_choice |
Passed through |
stream |
Enables SSE streaming |
Mid-conversation system messages
The Messages compatibility endpoint supports system role items inside the ordered messages array. This feature requires the exact beta token below in the anthropic-beta request header:
anthropic-beta: mid-conversation-system-2026-04-07
Without that token, a system role inside messages returns 400. The role accepts string content or an array of text blocks. Its content position is preserved for both streaming and non-streaming requests. The top-level system field continues to provide the leading system prompt and does not require this beta.
System-level priority is preserved when the selected model and message placement support native mid-conversation system messages. The current native contract is limited to the claude-opus-5 family, including dated/versioned IDs: in the messages actually sent to Anthropic, the system item must follow a user turn and must either end the message array or be followed by an assistant turn. For other opted-in request shapes, including claude-sonnet-5 and a user / system / user sequence, ModelRelay sends the system item’s text at the same position as a user-role message. This compatibility representation avoids moving the instruction ahead of earlier conversation content, but it has user-level rather than system-level priority; consecutive user-role messages may also be combined by the provider.
This representation is scoped to beta-opted Anthropic Messages requests. Chat Completions, Responses, RLM, workflows, and CLI requests keep their existing system-message behavior.
Content Block Types
The Anthropic adapter supports all standard content block types:
| Type | Description |
|---|---|
text |
Plain text content |
image |
Base64-encoded images |
tool_use |
Tool call from assistant |
tool_result |
Tool execution result |
Model Flexibility
The compatibility endpoints allow you to use any model available in ModelRelay, not just models from the corresponding provider:
// Use OpenAI SDK to call an Anthropic model
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.MODELRELAY_API_KEY,
baseURL: "https://api.modelrelay.ai/v1",
});
await client.responses.create({
model: "claude-opus-5", // Anthropic model via OpenAI API format
input: "Hello!",
});
// Use Anthropic SDK to call an OpenAI model
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.MODELRELAY_API_KEY,
baseURL: "https://api.modelrelay.ai/v1",
});
await client.messages.create({
model: "gpt-5.2", // OpenAI model via Anthropic API format
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }],
});
Error Handling
Errors are returned in the format expected by each API:
OpenAI Format
{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"code": "invalid_api_key"
}
}
Anthropic Format
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "Invalid API key"
}
}
Limitations
The compatibility endpoints provide high-fidelity API translation, but some provider-specific features may not be available:
- Anthropic caching -
cache_controlblocks are not supported - Provider-specific extensions - Check the endpoint reference; unsupported Chat Completions generation and routing fields are rejected
- Batch endpoints - Use ModelRelay’s native
/responses/batchinstead - Chat Completions scope - Function tools and structured output are supported; multiple choices, audio, legacy function calling, and nonempty reasoning history are not
- Image
file_idand remote URLs - Compatibility vision input is data URLs only; OpenAI file IDs and HTTP(S) image URLs are rejected
For full access to ModelRelay features like state handles and built-in tools,
use the ModelRelay SDK or native API.
Next Steps
- First Request - Quick start with ModelRelay SDK
- Responses API - Full API reference
- Streaming - Real-time response streaming
- Tool Use - Function calling with tools