A lightweight Express + TypeScript proxy that speaks the Anthropic Messages API on the front, and talks to Gemini (public or internal gateway) on the back.
Drop it in front of any Claude-compatible client — Claude Code, IDE plugins, your own scripts — and they will transparently run on Gemini.
- API surface —
POST /v1/messages,POST /v1/messages/count_tokens,GET /v1/models - Streaming — pseudo-SSE: upstream Gemini is always called in non-stream mode, and the full response is replayed as Anthropic SSE events (see Streaming behavior)
- Tool calling — bidirectional conversion between Claude
tool_use/tool_resultand GeminifunctionCall/functionResponse - Multimodal — text and base64 images
- Thought-signature cache — LRU (100k entries, 1h TTL) so multi-turn tool use stays coherent
- Model routing — hard-coded mapping in
src/config.ts - Local auth — optional shared token for trusted clients
npm install
cp .env.example .env # then edit it
npm run dev # tsx watch on src/server.tsMinimal .env:
GEMINI_API_KEY=your_key
PROXY_AUTH_TOKEN=local-dev-keyGet Gemini Api Keys: https://aistudio.google.com/app/api-keys
Optional knobs:
# Point at a custom Gemini endpoint (e.g. an internal gateway)
GEMINI_API_BASE=http://api.gateway.com/v1beta
GEMINI_AUTH_TYPE=bearer
# Fallback model when an incoming model name doesn't match the table below
DEFAULT_GEMINI_MODEL=gemini-3.5-flash
# Verbose logs
DEBUG=falseexport ANTHROPIC_BASE_URL="http://127.0.0.1:8318"
export ANTHROPIC_AUTH_TOKEN="local-dev-key"
claudeSome clients read ANTHROPIC_API_KEY instead of ANTHROPIC_AUTH_TOKEN:
export ANTHROPIC_API_KEY="local-dev-key"Defined in src/config.ts. Anything not listed falls back to DEFAULT_GEMINI_MODEL.
| Incoming model | Routed to |
|---|---|
Kimi-K2.5 |
DEFAULT_GEMINI_MODEL |
Claude Sonnet 4.6 |
DEFAULT_GEMINI_MODEL |
Claude Opus 4.6 |
DEFAULT_GEMINI_MODEL |
gemini-3.5-flash |
gemini-3.5-flash |
gemini-3.1-pro-preview |
gemini-3.1-pro-preview |
DEFAULT_GEMINI_MODEL defaults to gemini-3.5-flash.
max_tokens defaults to 32 768 and is clamped to 60 000 before being forwarded to Gemini as maxOutputTokens. Larger requests are silently capped, not rejected.
True token-by-token streaming is not supported. When a client sends "stream": true:
- The proxy calls Gemini with a non-streaming
generateContentrequest and waits for the full response. - It then replays that response to the client as a sequence of Anthropic SSE events (
message_start→content_block_*→message_delta→message_stop), with periodicpingframes every 15 s to keep the connection alive.
In other words, stream mode is just a wire-format adapter — there is no incremental output. Clients see the answer arrive in one chunk after the upstream call finishes. This trade-off keeps tool-call handling and thoughtSignature bookkeeping simple, at the cost of perceived latency on long generations.
Streaming, bearer-token auth:
curl -N http://127.0.0.1:8318/v1/messages \
-H 'content-type: application/json' \
-H 'authorization: Bearer local-dev-key' \
-d '{
"model": "gemini-3.5-flash",
"max_tokens": 1024,
"stream": true,
"messages": [{ "role": "user", "content": "Say hello in one sentence." }]
}'x-api-key style (the header set Claude Code uses):
curl http://127.0.0.1:8318/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "gemini-3.5-flash",
"max_tokens": 20000,
"stream": true,
"messages": [
{ "role": "user", "content": "中国的特大城市有哪些?" }
]
}'npm run build # tsc -> dist/
node dist/server.js # run
# or, for production
pm2 start pm2.config.cjs./build.sh packages everything you need to ship:
./build.sh
# produces output/ and output.tar.gz
# (dist/, package.json, pm2.config.cjs, README.md, .env)Verbose logs:
DEBUG=true node dist/server.jsThis is a compatibility gateway, not a full Anthropic re-implementation. The two parts most worth treating with care are:
- Tool-call conversion — Claude Code workflows lean heavily on
tool_use/tool_result. Breaking either side breaks the agent loop. thoughtSignaturecache — Gemini requires the original signature to be echoed back on follow-up turns. Don't disable the LRU.