# anyllm-proxy An API translation proxy that lets Anthropic-based tools (Claude Code, Cursor, Windsurf, Cline) talk to any OpenAI-compatible backend, local LLM, or alternative provider. **[Releases](https://github.com/whit3rabbit/anyllm-proxy/releases)** | **[Library Usage](#using-as-a-library)** --- ## Quick Start Download a binary from the [releases page](https://github.com/whit3rabbit/anyllm-proxy/releases), or install from source: ```bash cargo install anyllm_proxy ``` Create a `.anyllm.env` config file: ```env OPENAI_API_KEY=unused OPENAI_BASE_URL=http://localhost:11434/v1 BIG_MODEL=qwen2.5-coder:32b SMALL_MODEL=qwen2.5-coder:32b ``` Run the proxy (auto-loads `.anyllm.env` from the current directory): ```bash anyllm_proxy # or: anyllm_proxy --env-file ~/configs/ollama.env ``` ### Simple mode vs. advanced mode | | Simple mode | Advanced mode | |---|---|---| | **Config** | 3 env vars or `.anyllm.env` | `config.toml` / `config.yaml` | | **Routing** | Single backend | Multi-backend with path prefixes | | **Admin UI** | Not started | `--webui` flag | | **Translation warnings** | Silent (never exposed to clients) | `x-anyllm-degradation` header active | | **How to enable** | Default | Pass `--webui`, set `PROXY_CONFIG`, or `ANYLLM_DEGRADATION_WARNINGS=true` | Most users never leave simple mode. Start there. Point Claude Code at the proxy: ```bash ANTHROPIC_BASE_URL=http://localhost:3000 claude ``` ### Admin Web Interface (optional) Pass `--webui` (or `--admin`) to also start the admin dashboard on `127.0.0.1:3001`. The dashboard has the following tabs: - **Dashboard:** Live RPM, error rate, P50/P95 latency, per-backend cards, and a filterable live request feed. - **Request Log:** Historical request log with filters (backend, status, key, date range), paginated, with per-request cost and token detail. - **Settings:** Mutable config (log level, log_bodies, per-backend model mappings), read-only env vars (secrets masked), and **Export .env** to generate a `.anyllm.env` template. - **Backends:** Configured backends and their settings. - **Access Control:** Virtual key CRUD — create, edit (RPM/TPM limits, budget, expiry, model allowlist), and revoke keys without restarting. - **Models:** Add/remove model routing deployments (LiteLLM config mode only). - **Audit:** Log of all admin config mutations and key lifecycle events. ```bash anyllm_proxy --webui # Proxy API: http://localhost:3000 # Admin UI: http://127.0.0.1:3001/admin/?token=$(cat .admin_token) ``` The dashboard's Settings tab shows all active environment variables (API keys masked) and has an **Export .env** button that generates a `.anyllm.env` template you can edit and reuse. To use a custom port or a fixed token: ```bash ADMIN_PORT=4000 ADMIN_TOKEN=mysecret anyllm_proxy --webui ``` To force-disable the admin even when the flag is present (useful in automated environments): ```bash DISABLE_ADMIN=1 anyllm_proxy --webui # admin will NOT start ``` Additional admin env vars: `ADMIN_DB_PATH` (SQLite file, default: `admin.db`), `ADMIN_TOKEN_PATH` (where the generated token is written, default: `.admin_token`), `ADMIN_LOG_RETENTION_DAYS` (request log retention, default: `7`). ## Advanced Mode ### Multiple backends on one proxy (recommended) A single proxy instance can serve all your backends simultaneously. Each backend gets its own URL path. Use a `config.toml` (see [section 2](#2-multi-routing-and-the-web-interface)): ```toml # config.toml listen_port = 3000 default_backend = "local" [backends.local] kind = "openai" api_key = "unused" base_url = "http://localhost:11434/v1" big_model = "qwen2.5-coder:32b" small_model = "qwen2.5-coder:7b" [backends.openai] kind = "openai" api_key = "sk-..." base_url = "https://api.openai.com/v1" big_model = "gpt-4o" small_model = "gpt-4o-mini" [backends.deepseek] kind = "openai" api_key = "sk-deepseek-..." base_url = "https://api.deepseek.com/v1" big_model = "deepseek-coder" small_model = "deepseek-chat" ``` ```bash PROXY_CONFIG=config.toml anyllm_proxy --webui ``` All three backends are live at once: | Path | Backend | |------|---------| | `http://localhost:3000/v1/messages` | local (default) | | `http://localhost:3000/openai/v1/messages` | OpenAI | | `http://localhost:3000/deepseek/v1/messages` | DeepSeek | Point different tools at different paths, or switch in Claude Code by changing `ANTHROPIC_BASE_URL`. ### Coming from LiteLLM? Drop in your config.yaml anyllm-proxy accepts LiteLLM `config.yaml` files directly. If you already have a LiteLLM deployment, point the proxy at your existing config: ```bash PROXY_CONFIG=config.yaml anyllm_proxy --webui ``` A standard LiteLLM config works as-is: ```yaml # config.yaml (LiteLLM format) model_list: - model_name: gpt-4o litellm_params: model: azure/gpt-4o-eu api_base: https://my-resource.openai.azure.com/ api_key: os.environ/AZURE_API_KEY rpm: 6000 - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_key: os.environ/OPENAI_API_KEY rpm: 10000 - model_name: claude-3-opus litellm_params: model: anthropic/claude-3-opus-20240229 api_key: os.environ/ANTHROPIC_API_KEY general_settings: master_key: os.environ/LITELLM_MASTER_KEY ``` Multiple deployments of the same model name are load-balanced with round-robin routing. Deployments at their RPM limit are automatically skipped. **Env var compatibility:** LiteLLM env var names are accepted as aliases, so you do not need to rename anything: | LiteLLM env var | anyllm-proxy equivalent | Notes | |---|---|---| | `LITELLM_MASTER_KEY` | `PROXY_API_KEYS` | Admin/auth key | | `LITELLM_CONFIG` | `PROXY_CONFIG` | Config file path | | `AZURE_API_KEY` | `AZURE_OPENAI_API_KEY` | Azure auth | | `AZURE_API_BASE` | `AZURE_OPENAI_ENDPOINT` | Azure endpoint | | `AZURE_API_VERSION` | `AZURE_OPENAI_API_VERSION` | Azure API version | | `AWS_REGION_NAME` | `AWS_REGION` | Bedrock region | | `OPENAI_API_KEY` | `OPENAI_API_KEY` | Same name | | `ANTHROPIC_API_KEY` | `ANTHROPIC_API_KEY` | Same name | The `os.environ/VAR_NAME` syntax in YAML values is supported alongside anyllm's native `env:VAR_NAME`. See [docs/COMPARISON_LITELLM.md](docs/COMPARISON_LITELLM.md) for a full feature comparison. ### Multiple separate instances (for isolated deployments) For cases where you want completely separate proxy processes (different ports, different machines, different Docker containers), keep one `.env` file per deployment: ``` ~/proxies/ ollama.env # local Ollama openai-prod.env # production OpenAI deepseek.env # DeepSeek API ``` Run any one: ```bash anyllm_proxy --env-file ~/proxies/deepseek.env ``` Docker-compatible — same file works with `--env-file`: ```bash docker run --env-file ~/proxies/openai-prod.env -p 3000:3000 anyllm-proxy ``` The admin UI's **Export .env** button (Settings tab) generates a ready-to-edit template from the current configuration. --- ## What, Why, and How? ### What is it? A lightweight, fast Rust-based proxy that accepts Anthropic Messages API requests, translates them to OpenAI Chat Completions format, forwards them to any compliant backend, and translates responses back in real-time. Supports streaming SSE, tool calling, and image/document blocks. ### Why use it? - **Local AI Coding:** Run Claude Code against local models (Llama 3, DeepSeek, Qwen) without API credits. - **Broad Compatibility:** Works with open-weights and alternative models including Qwen and DeepSeek. - **Multi-Backend Routing:** Route `haiku` requests to a fast local model and `opus` requests to external providers, transparently. - **Observability:** Built-in admin dashboard for request logs, latency, and live config changes. ### How to Build from Source ```bash cargo build # Proxy only (default) cargo run -p anyllm_proxy # Proxy + admin web UI cargo run -p anyllm_proxy -- --webui ``` The proxy listens on `0.0.0.0:3000`. The admin dashboard (opt-in via `--webui`) binds to `127.0.0.1:3001`. --- ## 1. Primary Use Case: Claude Code + Local LLMs ### Example: Running with Ollama (DeepSeek / Qwen) ```bash # 1. Start your local LLM ollama run qwen2.5-coder:32b & # 2. Start the translation proxy OPENAI_API_KEY=unused \ OPENAI_BASE_URL=http://localhost:11434/v1 \ BIG_MODEL=qwen2.5-coder:32b \ SMALL_MODEL=qwen2.5-coder:32b \ cargo run -p anyllm_proxy & # 3. Use Claude Code targeting the local proxy ANTHROPIC_BASE_URL=http://localhost:3000 claude ``` Use the same pattern for **LM Studio** (default port `1234`) or **vLLM** (default port `8000`) by substituting `OPENAI_BASE_URL`. --- ## 2. Multi-Routing and the Web Interface Create a `config.toml` to map different routes to different backends: ```toml listen_port = 3000 default_backend = "local_qwen" [backends.local_qwen] kind = "openai" api_key = "unused" base_url = "http://localhost:11434/v1" big_model = "qwen2.5-coder:32b" small_model = "qwen2.5-coder:7b" [backends.deepseek_api] kind = "openai" api_key = "sk-deepseek-..." base_url = "https://api.deepseek.com/v1" big_model = "deepseek-coder" small_model = "deepseek-chat" [backends.openrouter] kind = "openai" api_key = "sk-or-..." base_url = "https://openrouter.ai/api/v1" big_model = "anthropic/claude-3.5-sonnet" small_model = "google/gemini-2.5-flash" ``` ```bash PROXY_CONFIG=config.toml anyllm_proxy --webui ``` Additional per-backend fields: `api_format = "chat"` (OpenAI only; `chat` or `responses`), `omit_stream_options = true` (strip `stream_options` for backends that reject it). Top-level `log_bodies = true` enables request/response body logging. Any config value can use `env:VAR_NAME` to read from the environment at startup (e.g., `api_key = "env:OPENAI_API_KEY"`). Key environment variables for advanced mode: - `LOG_BODIES`: Enable request/response body logging at debug level (`true` or `1`, default: disabled). - `ANYLLM_DEGRADATION_WARNINGS`: Set to `true` or `1` to expose `x-anyllm-degradation` response header when translation silently drops features (default: disabled; auto-enabled when `PROXY_CONFIG` is set). All backends are live at once on a single port. The path prefix matches the backend name in the config: | Path | Backend | Notes | |------|---------|-------| | `/v1/messages` | `local_qwen` | default | | `/deepseek_api/v1/messages` | `deepseek_api` | | | `/openrouter/v1/messages` | `openrouter` | | Point Claude Code at a specific backend: ```bash ANTHROPIC_BASE_URL=http://localhost:3000/deepseek_api claude ``` ### The Admin Dashboard Start the proxy with `--webui`, then open: ```bash open http://127.0.0.1:3001/admin/?token=$(cat .admin_token) ``` The dashboard tabs are described under [Admin Web Interface](#admin-web-interface-optional) above. When using a LiteLLM config, the **Models** tab lets you add/remove deployments without editing the config file. All config mutations (model changes, key creation/revocation) are recorded in the **Audit** tab. --- ## 3. Commercial APIs (OpenAI, Gemini, OpenRouter) **OpenRouter:** ```bash OPENAI_API_KEY=sk-or-... \ OPENAI_BASE_URL=https://openrouter.ai/api/v1 \ BIG_MODEL=anthropic/claude-3.5-sonnet \ SMALL_MODEL=anthropic/claude-3-haiku \ cargo run -p anyllm_proxy ``` **OpenAI:** ```bash OPENAI_API_KEY=sk-... \ BIG_MODEL=gpt-4o \ SMALL_MODEL=gpt-4o-mini \ cargo run -p anyllm_proxy ``` **Google Gemini:** ```bash BACKEND=gemini \ GEMINI_API_KEY=AIza... \ BIG_MODEL=gemini-2.5-pro \ SMALL_MODEL=gemini-2.5-flash \ cargo run -p anyllm_proxy ``` --- ## 4. Additional Backends and Features ### Azure OpenAI ```bash BACKEND=azure \ AZURE_OPENAI_ENDPOINT=https://myresource.openai.azure.com \ AZURE_OPENAI_DEPLOYMENT=my-gpt4o \ AZURE_OPENAI_API_KEY=... \ cargo run -p anyllm_proxy ``` ### AWS Bedrock ```bash BACKEND=bedrock \ AWS_REGION=us-east-1 \ AWS_ACCESS_KEY_ID=AKIA... \ AWS_SECRET_ACCESS_KEY=... \ BIG_MODEL=anthropic.claude-3-5-sonnet-20241022-v2:0 \ SMALL_MODEL=anthropic.claude-3-5-haiku-20241022-v1:0 \ cargo run -p anyllm_proxy ``` ### Anthropic Passthrough Forwards requests to the Anthropic API with no format translation. Use this when the upstream is already Anthropic and you only need auth, routing, or rate limiting from the proxy. ```bash BACKEND=anthropic \ ANTHROPIC_API_KEY=sk-ant-... \ cargo run -p anyllm_proxy ``` `ANTHROPIC_BASE_URL` overrides the upstream URL (default: `https://api.anthropic.com`). Note: `POST /v1/embeddings` is not available on this backend. ### OpenAI Chat Completions Input The proxy accepts `POST /v1/chat/completions` in OpenAI format and returns OpenAI format. This means any OpenAI-native client (LiteLLM, LangChain, etc.) can route through the proxy unchanged: ```bash curl http://localhost:3000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "x-api-key: your-key" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 100 }' ``` ### Virtual Key Management Create short-lived, rate-limited, or budget-capped API keys without restarting the proxy. Start with `--webui` to enable the admin server, then: ```bash # Create a key with RPM/TPM limits, a monthly budget, and a model allowlist curl -X POST http://localhost:3001/admin/api/keys \ -H "Authorization: Bearer $(cat .admin_token)" \ -H "Content-Type: application/json" \ -d '{ "description": "dev key", "rpm_limit": 60, "tpm_limit": 100000, "max_budget_usd": 10.00, "budget_duration": "monthly", "expires_at": "2026-12-31T00:00:00Z", "allowed_models": ["claude-*", "gpt-4o"] }' # Response: {"id": 1, "key": "sk-vk...", ...} # Use the key like any other proxy key curl http://localhost:3000/v1/messages \ -H "x-api-key: sk-vk..." \ -d '{"model": "claude-sonnet-4-20250514", "max_tokens": 100, "messages": [...]}' # Update limits on an existing key (no restart needed) curl -X PUT http://localhost:3001/admin/api/keys/1 \ -H "Authorization: Bearer $(cat .admin_token)" \ -H "Content-Type: application/json" \ -d '{"rpm_limit": 120, "max_budget_usd": 20.00}' # Check spend for a key curl http://localhost:3001/admin/api/keys/1/spend \ -H "Authorization: Bearer $(cat .admin_token)" # Revoke immediately (no restart needed) curl -X DELETE http://localhost:3001/admin/api/keys/1 \ -H "Authorization: Bearer $(cat .admin_token)" ``` `budget_duration` accepts `daily`, `monthly`, or `lifetime`. `allowed_models` supports exact names and `prefix/*` wildcards. A key at 100% of its budget returns 429 with period reset information. Webhook notifications fire at 80%, 95%, and 100% of the budget via `WEBHOOK_URLS`. Requests from unauthenticated clients are rejected by default. For local development, set `PROXY_OPEN_RELAY=true` to accept any non-empty key (insecure, never use in production). **Distributed rate limiting (optional):** Build with `--features redis` and set `REDIS_URL=redis://localhost:6379` to use Redis-backed rate limiting across multiple proxy instances. In-process rate limits are per-instance only. `RATE_LIMIT_FAIL_POLICY=open` (default) allows requests when Redis is unavailable; `closed` rejects them with 503. ### OpenTelemetry Export ```bash cargo build -p anyllm_proxy --features otel OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ OTEL_SERVICE_NAME=anyllm-proxy \ OPENAI_API_KEY=sk-... \ ./target/debug/anyllm_proxy ``` Spans are exported via OTLP HTTP (protobuf). Standard OpenTelemetry SDK environment variables control endpoint, service name, and sampling. The feature adds zero runtime overhead when not compiled in. --- ## Using as a Library The translation engine is available as standalone Rust crates. ``` crates/translator (lib, IO-free pure translation) | crates/client (lib, async HTTP client wrapping translator) | crates/proxy (bin, full proxy server) ``` | Level | Crate | Use Case | |---|---|---| | **Pure translation** | `anyllm_translate` | Stateless type conversion between Anthropic and OpenAI formats. No IO, no HTTP. Bring your own transport. | | **HTTP client** | `anyllm_client` | `client.messages(req).await` -- send Anthropic requests, get Anthropic responses. Handles translation, HTTP, retry, and streaming internally. | | **Embedded middleware** | `anyllm_translate` with `middleware` feature | Drop-in axum Router that adds `/v1/messages` to an existing server. | | **Full proxy** | `anyllm_proxy` | Multi-backend routing, admin UI, metrics, auth. Everything in this README. | ### Adding as a dependency ```toml [dependencies] # HTTP client (includes translation) anyllm_client = { git = "https://github.com/whit3rabbit/anyllm-proxy" } # Translation only (no HTTP, no async) anyllm_translate = { git = "https://github.com/whit3rabbit/anyllm-proxy" } # With axum middleware support anyllm_translate = { git = "https://github.com/whit3rabbit/anyllm-proxy", features = ["middleware"] } ``` ### HTTP Client (translation + transport) The simplest path. Send Anthropic requests, get Anthropic responses. Translation, retry, and SSE streaming are handled internally. ```rust use anyllm_client::{Client, ClientError}; use anyllm_translate::anthropic::MessageCreateRequest; let client = Client::builder() .base_url("https://api.openai.com/v1/chat/completions") .api_key("sk-...") .build()?; let req: MessageCreateRequest = serde_json::from_str(r#"{ "model": "claude-sonnet-4-6", "max_tokens": 256, "messages": [{"role": "user", "content": "Hello"}] }"#)?; let response = client.messages(&req).await?; ``` For custom TLS, SSRF protection, or per-model mapping, use `ClientConfig::builder()`: ```rust use anyllm_client::{Client, ClientConfig, Auth}; use anyllm_translate::TranslationConfig; let client = Client::new( ClientConfig::builder() .backend_url("https://api.openai.com/v1/chat/completions") .auth(Auth::Bearer("sk-...".into())) .translation( TranslationConfig::builder() .model_map("claude-sonnet-4-6", "gpt-4o") .model_map("claude-haiku-4-5", "gpt-4o-mini") .build() ) .build() ); ``` **Error handling:** ```rust match client.messages(&req).await { Ok(resp) => { /* ... */ } Err(ClientError::ApiError { status, body, .. }) => eprintln!("HTTP {status}: {body}"), Err(ClientError::Transport(e)) => eprintln!("network: {e}"), Err(ClientError::Translation(e)) => eprintln!("translation: {e}"), Err(e) => eprintln!("{e}"), } ``` **Streaming:** ```rust use anyllm_translate::anthropic::{Delta, StreamEvent}; use futures::StreamExt; let (mut stream, _rate_limits) = client.messages_stream(&req).await?; while let Some(event) = stream.next().await { if let StreamEvent::ContentBlockDelta { delta: Delta::TextDelta { text }, .. } = event? { print!("{text}"); } } ``` **Tool calling:** ```rust use anyllm_client::{ToolBuilder, ToolChoiceBuilder}; use serde_json::json; let tool = ToolBuilder::new("get_weather") .description("Get the current weather for a location") .input_schema(json!({ "type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"] })) .build(); // Attach tool to MessageCreateRequest via serde_json, then call client.messages(). ``` Runnable examples: `cargo run --example basic -p anyllm_client`, `streaming`, `tools`. ### Pure Translation (no IO) Use when you want to bring your own HTTP client or embed translation in a non-async context. ```rust use anyllm_translate::{TranslationConfig, translate_request, translate_response}; use anyllm_translate::anthropic::MessageCreateRequest; let config = TranslationConfig::builder() .model_map("claude-sonnet-4-6", "gpt-4o") .build(); let anthropic_req: MessageCreateRequest = serde_json::from_str(&body)?; let openai_req = translate_request(&anthropic_req, &config)?; // ... send openai_req with your HTTP client ... let anthropic_resp = translate_response(&openai_resp, &anthropic_req.model); ``` **Streaming (OpenAI chunks → Anthropic SSE events):** ```rust use anyllm_translate::new_stream_translator; let mut translator = new_stream_translator(model); // Feed each OpenAI chunk as it arrives: let events = translator.process_chunk(&chunk); // After the stream ends: let final_events = translator.finish(); ``` **Reverse direction (OpenAI ← Anthropic), for serving OpenAI-native clients:** ```rust use anyllm_translate::{ translate_openai_to_anthropic_request, translate_anthropic_to_openai_response, new_reverse_stream_translator, TranslationWarnings, }; let mut warnings = TranslationWarnings::default(); let anthropic_req = translate_openai_to_anthropic_request(&openai_req, &mut warnings)?; // ... forward to Anthropic API ... let openai_resp = translate_anthropic_to_openai_response(&anthropic_resp, "gpt-4o"); ``` Runnable examples: `cargo run --example translate_request -p anyllm_translate`, `reverse_translation`. ### Embedded Middleware (for existing axum apps) ```rust use anyllm_translate::middleware::{anthropic_compat_router, AnthropicCompatConfig}; let config = AnthropicCompatConfig::builder() .backend_url("https://api.openai.com") .api_key("sk-...") .build(); let app = Router::new() .merge(anthropic_compat_router(config)) .route("/my-other-endpoint", get(handler)); ``` For cross-language bindings (FFI, WASM, PyO3), see [docs/library-integration.md](docs/library-integration.md). --- ## Advanced Features - **Streaming SSE:** Real-time translation of chunked responses. - **Tool Calling:** Transparent tool definition and `tool_use`/`tool_result` translation. - **Image & Document Blocks:** Base64/URL and document block support. - **Embeddings passthrough:** `POST /v1/embeddings` forwarded as-is to the backend (no translation). Works with OpenAI, Azure, Vertex, Gemini, and vLLM. Not available when `BACKEND=anthropic`. - **Degradation header:** `x-anyllm-degradation` is set on responses when features are silently dropped during translation (e.g., `top_k`, `cache_control`, `document_blocks`, `thinking_config`). - **Model allowlist:** Per-virtual-key restriction by exact model name or `prefix/*` wildcard, enforced pre-request. - **Budget tracking and spend alerts:** Per-key `max_budget_usd` with daily/monthly/lifetime periods. Webhook notifications (via `WEBHOOK_URLS`) fire at 80%, 95%, and 100% of the budget. - **Audit log:** All admin config mutations and key lifecycle events stored in SQLite, queryable via `GET /admin/api/audit`. - **OIDC/JWT authentication:** Set `OIDC_ISSUER_URL` (and optionally `OIDC_AUDIENCE`) to accept JWT bearer tokens for proxy authentication. - **Observability:** SQLite request logging, metrics endpoint, WebSocket live dashboard. - **Safety:** SSRF protection (including IPv6 ULA/link-local), concurrency limits, exponential backoff retry, CSRF protection on admin endpoints. ## License MIT