A Cloudflare Worker that lets Claude Code talk to OpenAI-compatible backends (OpenRouter, OpenAI, Groq, and others). It translates the Anthropic Messages API into the OpenAI chat format and back, so you can point Claude Code at any model without installing anything locally.
Set two environment variables and you're done:
export ANTHROPIC_BASE_URL="https://proxycodeclaude.mellot-jules.workers.dev"
export ANTHROPIC_API_KEY="sk-or-v1-..." # your own OpenRouter key
claude --model "z-ai/glm-4.5"The base URL is the bare host — no /v1/messages, no query string. Your key is sent through the usual x-api-key / anthropic-api-key header and used as-is against the upstream provider (BYOK). Nothing is stored.
Claude Code only speaks the Anthropic Messages API. Most other model providers speak the OpenAI chat-completions API. The worker sits in the middle and translates both directions, per request:
Claude Code ──POST /v1/messages──▶ Worker ──POST /chat/completions──▶ Provider
(Anthropic format) │ rewrite request (OpenAI format)
│
Claude Code ◀──── SSE / JSON ────────┘ rewrite response ◀──── SSE / JSON ────
(Anthropic format) (OpenAI format)
On the way in, the worker:
- Flattens Anthropic
system/messagesblocks into OpenAI messages, turningtool_useblocks intotool_callsandtool_resultblocks intotoolmessages — and re-orders them so every tool reply sits directly after the assistant call that produced it (some providers reject anything else). - Converts image blocks to OpenAI
image_urlparts. - Maps Anthropic tool definitions (
input_schema) to OpenAI function schemas.
On the way out, it does the reverse. The interesting part is streaming: the worker reads the provider's OpenAI-style SSE and re-emits the exact Anthropic event sequence Claude Code expects — message_start, content_block_start/delta/stop for each text or tool_use block, then message_delta (with stop_reason and token usage) and message_stop. Tool-call arguments are streamed back as input_json_delta chunks. Without this, tool calling — and therefore Claude Code's whole agent loop — silently breaks.
Auth is pass-through: whatever key you give Claude Code is forwarded to the provider. The worker keeps no state and stores nothing.
There is no central "model list" the proxy controls. The model is just a string Claude Code puts in the model field of every request, and the worker forwards (or remaps) it to the provider. You pick it in any of these ways:
claude --model "z-ai/glm-4.5" # for one session
/model deepseek/deepseek-chat # switch mid-session (slash command)
export ANTHROPIC_MODEL="..." # default main modelImportant — Claude Code actually uses two models. Besides the main model, it fires a smaller "fast" model for background chores (conversation titles, simple classifications). That one is sent as a Claude haiku name by default, which your provider won't recognize. Point it somewhere real too, or those background calls fail:
export ANTHROPIC_MODEL="z-ai/glm-4.5"
export ANTHROPIC_SMALL_FAST_MODEL="z-ai/glm-4.5-air"(Recent Claude Code versions also expose ANTHROPIC_DEFAULT_HAIKU_MODEL / ANTHROPIC_DEFAULT_SONNET_MODEL / ANTHROPIC_DEFAULT_OPUS_MODEL to remap each tier individually.) Alternatively, handle the remap server-side with MODEL_MAP_EXT, e.g. {"claude-3-5-haiku-20241022":"z-ai/glm-4.5-air"}.
How the worker resolves the final model name, in order:
FORCE_MODELenv — overrides everything (one model for all traffic)x-or-modelrequest header orPRIMARY_MODELenvMODEL_MAP_EXTentry for the incoming name- Otherwise the name passes through unchanged
The built-in model picker in Claude Code only lists Anthropic models and ignores a custom base URL, so with the proxy you choose models by name (--model / /model / env) rather than from that menu. GET /v1/models is exposed for tooling that wants the provider's catalog, but Claude Code itself doesn't need it.
For each request the worker decides which backend to hit, in this order:
x-base-urlheader orUPSTREAM_BASE_URLenv — any OpenAI-compatible endpointx-providerheader —openrouter,openai,groq,deepseek,together,mistral,xai,cerebrasPROVIDERenv var- The API key prefix —
sk-or-→ OpenRouter,gsk_→ Groq,xai-→ xAI,sk-→ OpenAI - Default: OpenRouter
In BYOK mode the simplest switch is just using that provider's key — the prefix gives it away:
# OpenRouter (default)
export ANTHROPIC_API_KEY="sk-or-v1-..."
claude --model "z-ai/glm-4.5"
# OpenAI
export ANTHROPIC_API_KEY="sk-proj-..."
claude --model "gpt-4o"
# Groq
export ANTHROPIC_API_KEY="gsk_..."
claude --model "llama-3.3-70b-versatile"When the key prefix is ambiguous (DeepSeek, Together and Mistral all issue sk-… keys, like OpenAI), name the provider explicitly:
export PROVIDER="deepseek"
export ANTHROPIC_API_KEY="sk-..." # a DeepSeek key
claude --model "deepseek-chat"Anything that speaks the OpenAI chat API works through UPSTREAM_BASE_URL (or the per-request x-base-url header) — a local Ollama/LM Studio, vLLM, a gateway, etc.:
export UPSTREAM_BASE_URL="http://localhost:11434/v1" # set on the workerx-provider, x-base-url and x-or-model are per-request headers, so a single deployment can serve several backends at once if your client sets them.
- Streaming responses, including tool calls — the worker rebuilds the full Anthropic SSE sequence (
content_block_start/input_json_delta/content_block_stop…), which Claude Code needs for its agentic loop. - Tool / function calling in both streaming and non-streaming mode.
- Images in user messages (converted to OpenAI
image_url). - Token usage and an optional USD cost estimate, returned in
X-OR-*response headers. - Automatic retry on
429/5xxagainst a fallback model when one is configured. - Upstream errors rewritten into Anthropic's
{ "type": "error", ... }shape so the CLI shows something sensible.
| Method | Path | |
|---|---|---|
| POST | /v1/messages |
main endpoint, streaming or not |
| POST | /v1/messages/count_tokens |
token estimate |
| GET | /v1/models |
model list from the active provider |
| GET | /health |
health check |
| GET | / |
service info |
Deploy to Cloudflare Workers with npm run deploy. Configuration lives in wrangler.toml:
REQUIRE_PROXY_TOKEN = "1"to lock the proxy behind aPROXY_TOKENsecret (otherwise it's open for BYOK).- Server-key mode: set the provider's key as a secret (
OPENROUTER_API_KEY,OPENAI_API_KEY,GROQ_API_KEY, …) and callers don't need their own. FORCE_MODEL,PRIMARY_MODEL,FALLBACK_MODEL,MODEL_MAP_EXTto remap or pin models.REASONING_EFFORT(low/medium/high) andMAP_REASONING = "1"to surface reasoning asthinkingblocks.PRICING_JSONto get cost estimates in the response headers.TIMEOUT_MSfor the upstream request timeout (default 4 min).
By default model names pass through unchanged, so claude --model "anything/the-provider-knows" just works.
worker.js entrypoint — routing, auth, fallback, streaming
src/util.js CORS, JSON, error envelopes
src/providers.js provider resolution
src/mappers.js Anthropic <-> OpenAI message/tool/usage mapping
src/streaming.js OpenAI SSE -> Anthropic SSE
test/ vitest unit tests
npm install
npm test # run the suite
npm run dev # local worker on http://localhost:8787