Anthropic Adapter is a small Bun service that accepts Anthropic Messages API requests on POST /v1/messages and forwards them to an OpenAI-compatible backend.
It is useful when you already have clients built against the Anthropic message format but want to route requests to another provider or gateway that speaks the OpenAI chat completions interface.
It is not a full compatibility layer for every Anthropic feature, and it is not a multi-tenant gateway. The current implementation is best suited to single-service deployments where a narrow HTTP surface is a feature, not a limitation.
- Anthropic-compatible
POST /v1/messages - Streaming and non-streaming responses
- Tool calls
- Basic request normalization and response serialization
- API key auth for clients
- CORS
- In-memory rate limiting
- In-memory metrics at
GET /metrics - Health checks at
GET /health - Model name mapping between client-facing and provider-facing names
Use this project if:
- your application already speaks the Anthropic Messages API
- you want a narrow adapter in front of an OpenAI-compatible backend
- you want a codebase that is easy to audit and extend
Do not use it if:
- you need full multimodal parity with Anthropic
- you need distributed rate limiting or shared metrics storage
- you need a general-purpose API gateway with tenancy, billing, or persistence
bun install
cp .env.example .env
bun run devThe server listens on http://localhost:3000 by default.
# Create .env from example and configure your settings
cp .env.example .env
# Edit .env with your provider details
# Build and start the container
docker compose up -d
# View logs
docker compose logs -f
# Stop the container
docker compose downThe Docker container exposes the service on 127.0.0.1:3000 (localhost only).
All configuration is provided through environment variables.
| Variable | Default | Required | Notes |
|---|---|---|---|
PORT |
3000 |
No | HTTP listen port |
PROVIDER |
openai |
No | openai or mock |
OPENAI_API_KEY |
none | Yes when PROVIDER=openai |
Upstream API key |
OPENAI_BASE_URL |
none | Yes when PROVIDER=openai |
OpenAI-compatible base URL |
OPENAI_DEFAULT_MODEL |
none | No | Optional fallback model |
OPENAI_TIMEOUT |
120000 |
No | Upstream timeout in milliseconds |
AUTH_DISABLED |
false |
No | Disables client auth entirely |
API_KEYS |
none | No | Comma-separated client API keys |
LOG_LEVEL |
info |
No | debug, info, warn, or error |
LOG_FORMAT |
pretty |
No | pretty or json |
MODEL_MAP |
none | No | from:to pairs separated by commas |
RETRY_COUNT |
2 |
No | Retries on retryable upstream failures |
RETRY_DELAY_MS |
1000 |
No | Base backoff delay in milliseconds |
CORS_ORIGINS |
* |
No | * or a comma-separated allowlist |
RATE_LIMIT_RPM |
none | No | Requests per minute per client key or IP |
SHUTDOWN_TIMEOUT_MS |
5000 |
No | Drain period before forced shutdown |
See .env.example for a documented operator-facing template.
Minimum request:
{
"model": "claude-3-5-sonnet-latest",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "Hello"
}
]
}Example non-streaming request:
curl -X POST http://localhost:3000/v1/messages
-H "Content-Type: application/json"
-H "x-api-key: local-dev-key"
-d '{
"model": "claude-3-5-sonnet-latest",
"max_tokens": 256,
"messages": [
{ "role": "user", "content": "Say hello in one sentence." }
]
}'Example response:
{
"id": "msg_abc123",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "Hello."
}
],
"model": "claude-3-5-sonnet-latest",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 10,
"output_tokens": 3
}
}Example streaming request:
curl -N -X POST http://localhost:3000/v1/messages
-H "Content-Type: application/json"
-d '{
"model": "claude-3-5-sonnet-latest",
"max_tokens": 256,
"stream": true,
"messages": [
{ "role": "user", "content": "Stream a short answer." }
]
}'The streaming response uses server-sent events and emits Anthropic-style event names such as message_start, content_block_delta, message_delta, and message_stop.
Returns a small JSON document with status, uptime, version, and provider selection.
Returns in-memory counters for requests, errors, latency, tokens, and estimated cost.
If AUTH_DISABLED=true, all requests are accepted.
If AUTH_DISABLED=false and API_KEYS is empty, the service still accepts all requests. That behavior is convenient for local development but is not a safe production default. Set API_KEYS explicitly before exposing the service.
CORS_ORIGINS=* is fine for local testing. In production, use an explicit origin allowlist when the service is called from browsers.
Rate limiting and metrics are in-memory. They reset on restart and do not coordinate across multiple instances. If you need shared state, treat the current implementation as a starting point rather than a finished distributed design.
MODEL_MAP lets the service accept one model name from clients and send another upstream. The response still reports the client-facing model name.
Use LOG_FORMAT=json for production log collection. The service includes a request ID in logs and returns x-request-id on responses.
- Image and document inputs are degraded to placeholder text during normalization.
- Anthropic thinking support is translated into a provider-facing reasoning hint and depends on upstream compatibility.
- Streaming request accounting records request volume and latency, but token accounting is less complete than the non-streaming path.
- The service only exposes the Anthropic-style messages endpoint plus health and metrics.
bun run dev
bun run start
bun test- See CONTRIBUTING.md for development and review expectations.
- See SECURITY.md for responsible disclosure guidance.
MIT. See LICENSE.