The OpenAI Chat Completions API is the de facto standard that most LLM tooling supports. Building an OpenAI-compatible proxy in front of Anthropic lets your team use any OpenAI-compatible library while routing to the model of your choice — and enables zero-cost provider switching.
LangChain, LlamaIndex, CrewAI, most evals frameworks all speak OpenAI. Route them to Anthropic with zero code changes.
Route 10% of traffic to a new provider to compare quality and cost without changing client code.
Inject rate limiting, cost tracking, PII redaction, and audit logging into all LLM calls through one proxy.
If Anthropic raises prices or has an outage, switch providers by changing one config variable, not rewriting the codebase.
# OpenAI format (what the client sends)
openai_request = {
"model": "gpt-4o", # Mapped to Anthropic model
"messages": [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "What is Python?"},
],
"max_tokens": 500,
"temperature": 0.7,
"stream": False,
}
# Anthropic format (what we need to send)
def openai_to_anthropic(req: dict) -> dict:
messages = req.get("messages", [])
# Extract system message (Anthropic uses separate system field)
system = None
filtered_messages = []
for msg in messages:
if msg["role"] == "system":
system = msg["content"]
else:
filtered_messages.append(msg)
# Model name mapping
MODEL_MAP = {
"gpt-4o": "claude-sonnet-4-6",
"gpt-4o-mini": "claude-haiku-4-5-20251001",
"gpt-4-turbo": "claude-opus-4-8",
"gpt-3.5-turbo": "claude-haiku-4-5-20251001",
}
anthropic_model = MODEL_MAP.get(req["model"], req["model"]) # Pass through if already Anthropic name
anthropic_params = {
"model": anthropic_model,
"messages": filtered_messages,
"max_tokens": req.get("max_tokens", 1024),
}
if system:
anthropic_params["system"] = system
return anthropic_params
# Anthropic response → OpenAI format
def anthropic_to_openai(response, model: str) -> dict:
return {
"id": f"chatcmpl-{response.id}",
"object": "chat.completion",
"model": model,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": response.content[0].text,
},
"finish_reason": "stop" if response.stop_reason == "end_turn" else response.stop_reason,
}],
"usage": {
"prompt_tokens": response.usage.input_tokens,
"completion_tokens": response.usage.output_tokens,
"total_tokens": response.usage.input_tokens + response.usage.output_tokens,
},
}from fastapi import FastAPI, HTTPException, Header
from fastapi.responses import StreamingResponse
import anthropic
import json
app = FastAPI(title="Anthropic OpenAI Proxy")
client = anthropic.Anthropic()
@app.post("/v1/chat/completions")
async def chat_completions(
request: dict,
authorization: str = Header(...),
):
# Validate API key (your app's key, not Anthropic's)
api_key = authorization.replace("Bearer ", "")
if not validate_api_key(api_key):
raise HTTPException(status_code=401, detail="Invalid API key")
try:
anthropic_params = openai_to_anthropic(request)
except (KeyError, ValueError) as e:
raise HTTPException(status_code=400, detail=f"Invalid request format: {e}")
if request.get("stream"):
# Streaming response in OpenAI SSE format
def generate():
with client.messages.stream(**anthropic_params) as stream:
for chunk in stream.text_stream:
data = {
"choices": [{"delta": {"content": chunk}, "index": 0}],
"object": "chat.completion.chunk",
}
yield f"data: {json.dumps(data)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
# Non-streaming
response = client.messages.create(**anthropic_params)
return anthropic_to_openai(response, model=request["model"])from openai import OpenAI
# Point any OpenAI client at your proxy
client = OpenAI(
api_key="your-proxy-api-key",
base_url="http://localhost:8000/v1", # Your proxy URL
)
# Works exactly like the OpenAI API
response = client.chat.completions.create(
model="gpt-4o-mini", # Mapped to claude-haiku-4-5-20251001 by proxy
messages=[{"role": "user", "content": "What is RAG?"}],
)
print(response.choices[0].message.content)
# LangChain also works with no code changes
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
api_key="your-proxy-api-key",
base_url="http://localhost:8000/v1",
)
result = llm.invoke("Explain transformers")Not all features map 1:1.Anthropic doesn't support every OpenAI parameter (e.g., logprobs, n for multiple completions). Return a clear error message when unsupported parameters are passed, rather than silently ignoring them — silent drops cause subtle quality bugs that are hard to trace.
Run LiteLLM instead of building your own proxy.If you just need OpenAI compatibility, litellmis a production-grade library that translates between 100+ providers with one line. Build your own proxy only when you need custom middleware (rate limiting, tenant routing, audit logging) that LiteLLM doesn't support.
The final lesson is the capstone: build a production-grade structured output API that demonstrates all the patterns from this course — schema validation, streaming, retry, rate limiting, and OpenAI compatibility — in one system.