Convert any OpenAI Chat Completions compatible endpoint to the Anthropic Messages API, enabling Claude Code CLI to use it directly.
This project follows the Unix philosophy: one process, one upstream endpoint. If you need multiple upstreams (e.g. different models or providers), run multiple processes on different ports and let the client or a load balancer handle routing. Benefits:
- Each process is simple, predictable, and easy to debug
- No built-in routing state — processes are stateless, start and stop at will
- Independent scaling and rolling upgrades without cross-contamination
Inspired by free-claude-code, this project was created to provide a lighter-weight alternative for deployment:
- Smaller footprint — Pure Bun runtime with zero npm dependencies, consuming less disk space and memory than a Python + FastAPI stack
- Simplified routing — Multi-upstream forwarding is removed entirely; only single-upstream OpenAI-to-Anthropic protocol translation remains
- Passthrough-friendly — Auth token passthrough allows deploying on minimal servers (e.g. 1 vCPU / 1 GB RAM) without hardcoding upstream keys
- Multi-process scaling — When multiple upstreams are needed, simply run one process per upstream on different ports, and let a reverse proxy or DNS route traffic
# Process 1: NVIDIA NIM on port 8082
bun run src/server/index.ts \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-xxxx \
--port 8082
# Process 2: OpenRouter on port 8083
bun run src/server/index.ts \
--upstream-base-url https://openrouter.ai/api/v1 \
--upstream-api-key sk-or-xxxx \
--port 8083Then point Claude Code to the desired upstream:
# Use NVIDIA NIM
ANTHROPIC_BASE_URL=http://localhost:8082 claude
# Use OpenRouter
ANTHROPIC_BASE_URL=http://localhost:8083 claudeOr place both behind a reverse proxy (nginx, Caddy, etc.) and route by domain or path.
Claude Code CLI
│ Anthropic API (SSE)
▼
chat-to-claude-code ────► OpenAI /chat/completions (SSE)
│ (NVIDIA NIM / OpenAI / Ollama / LM Studio / ...)
│ Anthropic SSE
▼
Claude Code CLI receives standard Anthropic response
Key features:
- Protocol conversion — Anthropic Messages API ↔ OpenAI Chat Completions bidirectional translation
- Streaming SSE — OpenAI streaming chunks converted to Anthropic SSE events in real time
- Thinking support — Both
reasoning_contentand thinking tag formats are converted to Anthropic thinking blocks - Tool calls — Both native
tool_callsand heuristic text-based● <function=...>parsing are supported - Downstream auth — Optional
--auth-tokenfor x-api-key verification of connecting clients - Request dumping — Optional
--dump <dir>records full request/response for debugging - Zero dependencies — Pure Bun runtime, no external npm packages
curl -fsSL https://bun.sh/install | bashAll configuration is passed via CLI arguments:
bun run src/server/index.ts \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-xxxxOutput:
chat-to-claude-code listening on http://localhost:8082
Upstream: https://integrate.api.nvidia.com/v1
Upstream API key: configured
Auth token: not set
Passthrough mode: false
Thinking: true
Dump: disabled
export ANTHROPIC_BASE_URL="http://localhost:8082"
export ANTHROPIC_AUTH_TOKEN="your-token-here"
export ANTHROPIC_DEFAULT_OPUS_MODEL="deepseek-ai/deepseek-v4-pro"
export ANTHROPIC_DEFAULT_SONNET_MODEL="qwen/qwen3.5-397b-a17b"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="minimaxai/minimax-m2.7"
claudeOr as a one-liner:
ANTHROPIC_BASE_URL=http://localhost:8082 ANTHROPIC_AUTH_TOKEN=freecc claude| Argument | Default | Description |
|---|---|---|
--upstream-base-url |
https://api.openai.com/v1 |
Upstream OpenAI Chat Completions compatible endpoint |
--upstream-api-key |
"" |
Upstream API key for authenticating with the upstream endpoint |
--auth-token |
"" |
Downstream auth token; clients must provide a matching x-api-key header when set |
--port |
8082 |
HTTP listen port |
--enable-thinking |
true |
Convert upstream reasoning content to Anthropic thinking blocks |
--no-enable-thinking |
— | Disable thinking conversion |
--upstream-extra-params |
— | Model-specific extra parameters for upstream requests (repeatable); see below |
--dump |
"" |
Request dump directory; when set, each request is written to a unique subdirectory |
--enable-web-search |
false |
Enable proxy-side web search |
--web-search-engine |
brave |
Search engine type: brave (Brave Search API) or searxng (SearXNG) |
--enable-web-fetch |
false |
Enable proxy-side web fetch (HTTP GET with domain filtering) |
--web-search-api-key |
"" |
Search API key (required for Brave, optional for SearXNG) |
--web-search-base-url |
https://api.search.brave.com |
Search API base URL |
--web-fetch-allowed-domain |
— | Allowed domain for web fetch (repeatable, e.g., --web-fetch-allowed-domain example.com) |
--web-fetch-blocked-domain |
— | Blocked domain for web fetch (repeatable) |
--web-fetch-max-content-tokens |
5000 |
Max content tokens for web fetch results |
This proxy can execute Anthropic-style web_search and web_fetch server tools on behalf of upstream models that don't natively support them. When a model emits a tool call matching WebSearch / WebFetch in its text output, the proxy intercepts it, executes the request, and returns the result as an Anthropic server_tool_use / web_search_tool_result / web_fetch_tool_result content block.
bun run src/server/index.ts \
--upstream-base-url https://api.openai.com/v1 \
--upstream-api-key sk-xxx \
--enable-web-search \
--web-search-api-key BST-xxxxbun run src/server/index.ts \
--upstream-base-url https://api.openai.com/v1 \
--upstream-api-key sk-xxx \
--enable-web-search \
--web-search-engine searxng \
--web-search-base-url https://sea.mayeve.cnNo --web-search-api-key is required for SearXNG unless your instance requires authentication.
bun run src/server/index.ts \
--upstream-base-url https://api.openai.com/v1 \
--upstream-api-key sk-xxx \
--enable-web-search \
--web-search-engine searxng \
--web-search-base-url https://sea.mayeve.cn \
--enable-web-fetch \
--web-fetch-allowed-domain docs.example.com \
--web-fetch-allowed-domain api.example.comHow it works:
- The client sends a request with
server_tools: [{type: "web_search_20250305"}, {type: "web_fetch_20250305"}] - The proxy detects the server tool types, strips them before forwarding to the upstream
- If the upstream model calls
WebSearch/WebFetch(as heuristic text-based tool calls), the proxy intercepts them - The proxy executes the call (Brave Search API or SearXNG for search, direct HTTP for fetch) and emits the result back to the client as Anthropic SSE events
Use --upstream-extra-params to inject additional JSON fields into the upstream request body based on the model name. The format is glob=JSON:
--upstream-extra-params 'claude-*={"thinking":{"type":"enabled","budget_tokens":10000}}'- Glob pattern —
*matches any characters,?matches a single character. The first matching pattern wins. - JSON value — Must be a JSON object. Deep-merged into the upstream request body (nested objects are merged, arrays are replaced).
- Repeatable — Specify multiple times for different model patterns:
bun run src/server/index.ts \
--upstream-base-url https://api.openai.com/v1 \
--upstream-api-key sk-xxx \
--upstream-extra-params 'claude-sonnet-*={"thinking":{"type":"enabled","budget_tokens":10000}}' \
--upstream-extra-params 'deepseek*={"reasoning_effort":"high"}' \
--upstream-extra-params '*={"stream":true}'When a request arrives with model: "claude-sonnet-4-20250514", the matching claude-sonnet-* pattern is selected and its JSON is merged into the upstream request body. The catch-all * pattern acts as a default for any unmatched model.
When both --upstream-api-key and --auth-token are unset, passthrough mode is automatically enabled: the key provided by the client via x-api-key or Authorization header is forwarded as-is to the upstream endpoint.
When --auth-token is set, client requests must include a matching x-api-key or Authorization: Bearer xxx header, otherwise a 401 is returned. This protects the proxy from unauthorized access.
Enable --dump <dir> to log each downstream request into a sequentially numbered directory containing downstream-request.log, downstream-response.log, upstream-request.log, and upstream-response.log. On completion, the directory is renamed to {seq}-{startTime}-{endTime} for chronological sorting. Full SSE event streams are also captured.
bun run src/server/index.ts \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-xxxx \
--dump /var/log/chat-to-claude-codeExample dump directory structure:
/var/log/chat-to-claude-code/
└── 1-2026-05-20T08-30-00-000Z-2026-05-20T08-30-05-123Z/
├── downstream-request.log
├── downstream-response.log
├── upstream-request.log
└── upstream-response.log
bun run buildProduces chat-to-claude-code (chat-to-claude-code.exe on Windows), which can be run directly:
./chat-to-claude-code \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-xxxx \
--port 8082docker build -t chat-to-claude-code .docker run -p 8082:8082 chat-to-claude-code \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-xxxxWith downstream auth:
docker run -p 8082:8082 chat-to-claude-code \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-xxxx \
--auth-token my-secret-tokenPassthrough mode (no keys needed):
docker run -p 8082:8082 chat-to-claude-code \
--upstream-base-url http://localhost:11434/v1| Path | Method | Description |
|---|---|---|
/v1/messages |
POST | Anthropic Messages API proxy (core endpoint) |
/health |
GET | Health check |
Unmatched paths return 404 with an Anthropic-format error body.
Note: The model field is required in the request body; omitting it returns an error.
| Anthropic | OpenAI | Description |
|---|---|---|
system (string / content blocks) |
{"role": "system", "content": "..."} |
System prompt extracted as system message |
tool_use block |
tool_calls[i] |
Tool call arguments JSON-serialized |
tool_result block |
{"role": "tool", "tool_call_id": "..."} |
Tool result serialized as tool message |
thinking block |
thinking tag embedded in content | Controlled by ReasoningReplayMode |
redacted_thinking block |
Dropped | Not forwarded upstream |
| OpenAI chunk | Anthropic SSE event |
|---|---|
delta.reasoning_content |
content_block_start(thinking) + content_block_delta(thinking_delta) |
delta.content (contains thinking tag) |
Parsed and dispatched as thinking / text delta |
delta.content (plain text) |
content_block_delta(text_delta) |
delta.tool_calls |
content_block_start(tool_use) + content_block_delta(input_json_delta) |
delta.content (WebSearch/WebFetch text) |
content_block_start(server_tool_use) + content_block_start(web_search_tool_result) |
finish_reason: "stop" |
message_delta(stop_reason: "end_turn") |
finish_reason: "tool_calls" |
message_delta(stop_reason: "tool_use") |
| OpenAI | Anthropic |
|---|---|
stop |
end_turn |
length |
max_tokens |
tool_calls |
tool_use |
content_filter |
end_turn |
| other | end_turn |
Some models don't return native tool_calls but emit tool invocations as text:
● <function=read_file><parameter=path>/etc/hosts</parameter>
The parser converts these to structured tool_use blocks. It also supports WebFetch / WebSearch JSON text format:
Use WebFetch {"url": "https://example.com"}
Use WebSearch {"query": "test query"}
bun run src/server/index.ts \
--upstream-base-url https://integrate.api.nvidia.com/v1 \
--upstream-api-key nvapi-your-keybun run src/server/index.ts \
--upstream-base-url https://api.openai.com/v1 \
--upstream-api-key sk-your-keybun run src/server/index.ts \
--upstream-base-url http://localhost:11434/v1bun run src/server/index.ts \
--upstream-base-url http://localhost:1234/v1bun run src/server/index.ts \
--upstream-base-url https://openrouter.ai/api/v1 \
--upstream-api-key sk-or-your-keysrc/
├── conversion/
│ └── converter.ts # Anthropic → OpenAI message/tool/system-prompt conversion
├── core/
│ ├── dump.ts # Request/response dump logger
│ ├── errors.ts # Anthropic-format error responses
│ └── tokens.ts # Token estimation (char/4 heuristic)
├── parsers/
│ ├── think_tag_parser.ts # Think tag streaming parser
│ └── heuristic_tool_parser.ts # ● <function=...> heuristic tool call parser
├── server/
│ ├── config.ts # CLI argument configuration loader
│ ├── index.ts # Bun.serve() entry point + CORS
│ ├── routes.ts # HTTP route handling + auth
│ └── server_tools.ts # Proxy-side web_search / web_fetch execution
├── sse/
│ └── builder.ts # Anthropic SSE event builder
├── transport/
│ └── stream.ts # OpenAI stream → Anthropic SSE stream converter
bun testbun run dev -- --upstream-base-url https://integrate.api.nvidia.com/v1 --upstream-api-key nvapi-xxxxbunx tsc --noEmitbun run buildThis project is a streamlined TypeScript/Bun port of free-claude-code, focused on core protocol conversion:
| Feature | Python version | This project (TS/Bun) |
|---|---|---|
| Runtime | Python 3.14 + FastAPI | Bun |
| External deps | FastAPI, Pydantic, httpx, tiktoken, etc. | Zero |
| Provider count | 11 (NIM, OpenRouter, DeepSeek, Kimi, Wafer, LM Studio, llama.cpp, Ollama, OpenCode, Z.ai, OpenAI) | 1 (generic OpenAI-compatible endpoint) |
| Model Router | Opus/Sonnet/Haiku multi-provider routing | None (single upstream) |
| Configuration | Environment variables | CLI startup arguments |
| Downstream auth | None | AUTH_TOKEN verification |
| Executable | None | bun build --compile single file |
| Containerization | None | Dockerfile (distroless) |
| Request dump | None | --dump sequential directories |
| Admin UI | Local web configuration UI | None |
| Request optimization | quota mock / title skip / prefix detection / filepath mock | None |
| Discord/Telegram Bot | Full bot integration | None |
| Web Server Tools | Proxy-side web_search / web_fetch | Proxy-side web_search (Brave / SearXNG) / web_fetch |
| Rate Limiting | Token bucket rate limiting | None |
| Token counting | tiktoken (cl100k_base) | char/4 estimation |
| Logging/tracing | loguru + structured trace | console + optional dump |
MIT