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 default
  • Authorization: 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_control blocks 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/batch instead
  • Chat Completions scope - Function tools and structured output are supported; multiple choices, audio, legacy function calling, and nonempty reasoning history are not
  • Image file_id and 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