Proxy server that lets Claude Code (and any Anthropic API client) talk to self-hosted sglang / vLLM backends. Converts Anthropic API format to OpenAI-compatible format on the fly, streams responses back in Anthropic SSE format, and applies KV cache normalizations that push prefix-cache hit rates from ~22% to 95%+.
- Anthropic <-> OpenAI API conversion -- full bidirectional streaming translation including extended thinking, tool use, multi-turn conversations
- KV cache normalization -- stabilizes token prefixes so sglang/vLLM prefix caching actually works (see KV Cache Normalization)
- Vision routing -- optional image agent that intercepts images, routes them to a dedicated vision model, and injects descriptions back into the text conversation (see Vision Routing)
- Auto context overflow recovery -- detects context-too-long errors from the backend and automatically retries with reduced
max_completion_tokens - Backend auto-detection -- probes
/v1/modelsto detect sglang vs vLLM and sets backend-specific parameters automatically - Token counting --
/v1/messages/count_tokensendpoint for Claude Code's/contextcommand
git clone https://github.com/voipmonitor/claude-relay.git
cd claude-relay
pip install -r requirements.txt
# Minimal -- text-only, no vision
python -m claude_relay --backend http://localhost:30000
# With vision routing
python -m claude_relay \
--backend http://localhost:30000 \
--vision-url http://localhost:8000/v1/chat/completions \
--vision-model Qwen2.5-VL-72BPoint Claude Code at the proxy:
export ANTHROPIC_BASE_URL=http://localhost:5021
claudeUsage: python -m claude_relay [OPTIONS]
Server:
--host HOST Listen address (default: 0.0.0.0)
--port PORT Listen port (default: 5021)
--config PATH TOML config file
--backend URL sglang/vLLM backend URL (default: http://localhost:30000)
--model-route SPEC Per-model backend route: MODEL=URL[,UPSTREAM_MODEL]
--ttl SECONDS Backend auto-detection cache TTL (default: 30)
--log-level LEVEL Logging level (default: INFO)
Vision (image agent):
--vision-url URL Vision model endpoint -- enables image agent
--vision-model NAME Vision model name (required with --vision-url)
--no-image-agent Force-disable image agent even if vision URL is set
KV cache normalization (all enabled by default):
--no-sort-tools Don't sort tools alphabetically
--no-strip-billing Keep x-anthropic-billing-header in system prompt
--no-strip-cache-control Keep cache_control fields (unused by sglang/vLLM)
--no-strip-date Keep "Today's date is YYYY-MM-DD." in user messages
Diagnostics:
--dump-requests Write Anthropic and converted OpenAI JSON bodies to claude_relay/debug/
--tool-debug For requests with tools, write raw backend SSE and a tool-call summary
--request-timeout SEC Total backend request timeout
--sock-read-timeout SEC Max silent period between backend chunks
By default every request goes to --backend, and the relay still probes that backend's /v1/models endpoint to find the actual upstream model name. To send specific client-facing model names to different backends, add model routes:
python -m claude_relay \
--backend http://localhost:8000 \
--model-route 'claude-3-5-sonnet-20241022=http://localhost:8000,Qwen3.5' \
--model-route 'claude-opus-*=http://localhost:8001,Kimi-K2.6'Routes are matched against the incoming Anthropic model field. Exact matches win first; route names containing *, ?, or [ are treated as glob patterns. The optional UPSTREAM_MODEL overrides the model name sent to that backend. If omitted, the relay uses that backend's first /v1/models result, preserving the old behavior.
You can also put routes in a TOML config file:
backend_url = "http://localhost:8000"
backend_detect_ttl = 30
tool_debug = true
dump_requests = true
dump_responses = true
[model_routes."claude-3-5-sonnet-20241022"]
backend_url = "http://localhost:8000"
upstream_model = "Qwen3.5"
[model_routes."claude-opus-*"]
backend_url = "http://localhost:8001"
upstream_model = "Kimi-K2.6"Start it with:
python -m claude_relay --config ~/.config/claude-relay/config.tomlFor compatibility with resp2chat naming, [model_profiles] is accepted as an alias for [model_routes] in this relay config.
For GLM/vLLM tool-call issues, run the relay with request and tool debugging enabled:
python -m claude_relay \
--backend http://localhost:8000 \
--dump-requests \
--dump-responses \
--tool-debugWhen installed as claude-relay.service, the same flags belong on ExecStart. Then watch:
journalctl -u claude-relay.service -f
ls -lt claude_relay/debug/Each tool request writes:
*_anthropic.json- the original Claude/Anthropic request*_openai.json- the converted OpenAI-compatible request sent to vLLM/sglang*_anthropic_stream.sse- exact Anthropic SSE bytes sent back to the client*_backend_stream.ndjson- raw backend SSE chunks*_tool_debug.json- parsed tool-call diagnostics, includingfinish_reasons,delta_keys, assembled tool arguments, and JSON validity
You can also run a direct backend-vs-proxy smoke test:
python scripts/diagnose_tool_calls.py \
--backend http://localhost:8000 \
--proxy http://localhost:5021The script sends a forced get_weather tool call to both endpoints and writes a *_tool_diagnosis.json report under claude_relay/debug/.
Claude Code (Anthropic API)
|
v
+--------------+
| claude-relay |
| |
| 1. Normalize (KV cache)
| 2. Strip images -> placeholders (optional)
| 3. Convert Anthropic -> OpenAI
| 4. Route to backend
| 5. Convert OpenAI SSE -> Anthropic SSE
| 6. Intercept vision tool calls (optional)
+--------------+
|
v
sglang / vLLM (OpenAI API)
|
v
(optional) Vision model
- Claude Code sends a standard Anthropic
/v1/messagesrequest - Normalization -- request body is cleaned up for KV cache stability (tool sorting, nonce stripping, etc.)
- Image stripping (if vision enabled) -- images in the last user message are replaced with
[Image #N]placeholders and cached in memory - Conversion -- Anthropic request format is converted to OpenAI
/v1/chat/completionsformat (system prompts, messages, tools, thinking config) - Backend routing -- request is sent to the sglang/vLLM backend with auto-detected model name and backend-specific parameters
- Stream conversion -- OpenAI SSE chunks are converted back to Anthropic SSE format and streamed to the client
- Vision interception (if images were present) -- if the model calls the injected
analyzeImagetool, the proxy intercepts it, calls the vision model, and continues the conversation with the description
| Anthropic | OpenAI |
|---|---|
system (string or text blocks) |
messages[0].role = "system" |
messages[].content (text/tool_use/tool_result/thinking blocks) |
Equivalent OpenAI message structure |
tools[].input_schema |
tools[].function.parameters |
thinking.budget_tokens |
reasoning.effort (low/medium/high) + reasoning.max_tokens |
max_tokens |
max_completion_tokens |
stop_sequences |
stop |
Anthropic SSE (message_start, content_block_delta, ...) |
Converted from OpenAI SSE (choices[].delta) |
Self-hosted sglang and vLLM use prefix KV caching -- if any token in the prefix changes, the entire cached computation for that prefix is invalidated. Claude Code injects several elements that change between requests, destroying cache hits.
Without normalization, typical cache hit rates are around 22%. With all normalizations enabled, hit rates reach 95%+.
Based on the analysis from buster-ripper.
| Normalization | Flag | Problem | Fix |
|---|---|---|---|
| Tool sorting | --no-sort-tools |
MCP servers reconnect in arbitrary order, shuffling tool definitions at position 0 of the prompt prefix. Any reorder = entire KV cache miss. | Sort tools[] alphabetically by name. Tools are a declarative set, order has no effect on model behavior. |
| Billing nonce | --no-strip-billing |
Claude Code injects a unique x-anthropic-billing-header:... string into system prompt blocks on every request. |
Regex-strip from system blocks. |
| Cache control | --no-strip-cache-control |
cache_control markers migrate between messages each turn, changing content hashes. sglang/vLLM don't use Anthropic's explicit cache_control anyway. |
Remove cache_control fields from system blocks, message content blocks, and message-level. |
| Date injection | --no-strip-date |
Claude Code injects Today's date is YYYY-MM-DD. into user messages. Changes daily at midnight = cache bust. |
Regex-strip from user messages only. |
All normalizations are enabled by default. Disable individually with --no-* flags if needed.
The image agent solves a fundamental problem: text-only LLMs (most large models served via sglang/vLLM) can't process images, but Claude Code sends screenshots, diagrams, and other images as part of the conversation.
The image agent is disabled by default and activates only when you provide --vision-url and --vision-model. You can force-disable it with --no-image-agent even if a vision URL is configured.
When the image agent is enabled and the proxy detects images in the last user message, it performs a two-phase streaming interception:
Phase 1 -- Image stripping and tool injection
- All image blocks in the last user message (including images inside
tool_resultblocks) are extracted and stored in a per-session in-memory LRU cache (keyed by session ID frommetadata.user_id) - Each image is replaced with a text placeholder:
[Image #N](call analyzeImage with imageId "N" to view this image) - An
analyzeImagetool is injected into the request's tool list with parameters:imageId-- array of image IDs from[Image #N]placeholderstask-- what to analyze (the model formulates this based on the user's question)context-- conversation context for the vision model
- A system prompt is injected instructing the model to always call
analyzeImagebefore responding about images - The modified (image-free) request is sent to the text backend
Phase 2 -- Vision model call and follow-up
- The proxy streams the backend's response while watching for
analyzeImagetool calls - When detected, the proxy:
- Retrieves the original images from cache
- Converts them to data URLs
- Sends them to the vision model (
--vision-url) with the task and context from the tool call - Gets back a detailed text description
- The proxy builds a follow-up request appending:
- The assistant's
analyzeImagetool call - A tool result containing the vision model's description
- The assistant's
- This follow-up is sent to the text backend, and the response is streamed back to the client
- The two response streams are stitched together with proper block index numbering so the client sees a single coherent response
Image cache:
- Per-session LRU cache, max 100 images (configurable via
image_cache_max_size) - TTL of 300 seconds (configurable via
image_cache_ttl) - Session is identified by
metadata.user_idsent by Claude Code
Supported image formats: Any format supported by the vision model. Images are passed as base64 data URLs or direct URLs, matching the Anthropic API's image content block format.
- The text model (which handles reasoning, code generation, tool use) can be a large, powerful model that doesn't support vision
- The vision model can be a smaller, specialized multimodal model
- This decouples the two concerns and lets you independently scale/upgrade each model
- The text model "sees" images through detailed text descriptions, which is sufficient for most Claude Code use cases (screenshots of errors, UI mockups, diagrams)
# Clone the repository
git clone https://github.com/voipmonitor/claude-relay.git /opt/claude-relay
cd /opt/claude-relay
# Create a virtual environment
python3 -m venv /opt/claude-relay/.venv
source /opt/claude-relay/.venv/bin/activate
pip install -r requirements.txtcat > /etc/systemd/system/claude-relay.service << 'EOF'
[Unit]
Description=Claude Relay - Anthropic API to sglang/vLLM proxy
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/claude-relay
ExecStart=/opt/claude-relay/.venv/bin/python -m claude_relay \
--backend http://localhost:30000 \
--port 5021
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Optional: uncomment and adjust for vision routing
# ExecStart=/opt/claude-relay/.venv/bin/python -m claude_relay \
# --backend http://localhost:30000 \
# --vision-url http://localhost:8000/v1/chat/completions \
# --vision-model Qwen2.5-VL-72B \
# --port 5021
[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable claude-relay
systemctl start claude-relay
# Check status
systemctl status claude-relay.service
# View logs
journalctl -u claude-relay.service -fAdd to your shell profile (~/.bashrc, ~/.zshrc, etc.):
export ANTHROPIC_BASE_URL=http://localhost:5021Or for a remote server:
export ANTHROPIC_BASE_URL=http://your-server:5021| Method | Path | Description |
|---|---|---|
| POST | /v1/messages |
Main Anthropic-compatible messages endpoint |
| POST | /v1/messages/count_tokens |
Token counting (for Claude Code /context) |
| GET | /health |
Health check with backend status |
Request bodies are dumped to claude_relay/debug/ as JSON files:
{req_id}_anthropic.json-- original Anthropic request{req_id}_openai.json-- converted OpenAI request
Set --log-level DEBUG for verbose logging of all conversions and streaming events.
MIT