FromZero2AI
CoursesPlaygroundBlog
Support on Ko-fi
Structured Outputs & AI API Design • Module B: API Design & ProductionLesson 5: Building OpenAI-Compatible APIs
PreviousNext

Lesson 5: Building OpenAI-Compatible APIs

Design a drop-in OpenAI API endpoint that routes to Anthropic or any open-source model, adding semantic caching, model routing, and usage tracking — enabling client code to switch providers transparently.

Built with AI for beginners. Free forever.

Support on Ko-fi•About•Blog•Privacy Policy•Terms of Service

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.

Why Build an OpenAI-Compatible Proxy?

Use any LLM tooling

LangChain, LlamaIndex, CrewAI, most evals frameworks all speak OpenAI. Route them to Anthropic with zero code changes.

A/B test providers

Route 10% of traffic to a new provider to compare quality and cost without changing client code.

Add cross-cutting concerns

Inject rate limiting, cost tracking, PII redaction, and audit logging into all LLM calls through one proxy.

Prevent vendor lock-in

If Anthropic raises prices or has an outage, switch providers by changing one config variable, not rewriting the codebase.

OpenAI → Anthropic Format Mapping

# 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,
        },
    }

FastAPI OpenAI-Compatible Proxy

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"])

Using the Proxy with Any OpenAI Client

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.