11 KiB
AGENTS.md
What This Is
anyllm-proxy is an API translation proxy in Rust. Accepts Anthropic Messages API and OpenAI Chat Completions requests, translates between formats, forwards to any supported backend (OpenAI, Azure, Vertex, Gemini, Bedrock, Anthropic passthrough), and translates back. Supports streaming SSE, tool calling, file/document blocks, virtual key management, batch API, and optional OpenTelemetry export.
Build and Test
cargo build # build everything
cargo build --features otel # with OpenTelemetry support
cargo test # ~1100+ tests, 10 ignored (live API)
cargo test -p anyllm_client # client crate only
cargo test -p anyllm_translate # translator crate only
cargo test -p anyllm_proxy # proxy crate only
cargo test -p anyllm_providers # provider/model catalog tests
cargo test health_endpoint # single test by name
cargo test --test virtual_keys # virtual key + rate limit integration tests
cargo clippy -- -D warnings # lint
cargo fmt --check # format check
Run the proxy:
OPENAI_API_KEY=sk-... cargo run -p anyllm_proxy
# Listens on 0.0.0.0:3000, health at GET /health
Admin UI (separate port 3001):
OPENAI_API_KEY=sk-... cargo run -p anyllm_proxy -- --webui
Essential Env Vars
| Var | Purpose |
|---|---|
OPENAI_API_KEY |
Required for default backend |
BACKEND |
openai (default), azure, vertex, gemini, anthropic, bedrock, or any LiteLLM provider id from the generated provider snapshot (e.g. groq, mistral, together_ai, gmi, publicai, zai) |
PROXY_CONFIG |
Path to config file (simple YAML, LiteLLM YAML, or TOML) |
PROXY_API_KEYS |
Comma-separated allowed keys (if unset and no PROXY_OPEN_RELAY, all requests rejected) |
PROXY_OPEN_RELAY |
true to accept any key (local dev only) |
RUST_LOG |
Tracing filter (e.g., info, anyllm_proxy=debug) |
Full env var reference: crates/proxy/src/config/mod.rs or docs/ENV.md.
LiteLLM env var aliases: search for litellm_env_aliases in main.rs.
Not Fully Validated
- OpenAI Responses API backend (
OPENAI_API_FORMAT=responses): wired up, not live-tested - AWS Bedrock backend (
BACKEND=bedrock): SigV4 signing + Event Stream decoding, not live-tested - Azure OpenAI backend (
BACKEND=azure): not live-tested - Live integration tests:
cargo test --test live_api -- --ignored --test-threads=1(needs real API key)
Docker
Published as followthewhit3rabbit/anyllm-proxy. See Docker section commands:
docker compose up # uses .env file
# Smoke tests (no real key needed):
docker compose -f docker-compose.test.yml up -d --build
bash scripts/docker-smoke-test.sh
docker compose -f docker-compose.test.yml down -v
Debian Package
cargo build --release -p anyllm_proxy
cargo deb -p anyllm_proxy --no-build --no-strip
After install: sudo systemctl enable --now anyllm-proxy, edit /etc/default/anyllm-proxy.
Config Directory
Data lives in ~/.anyllm/ by default. Override with ANYLLM_HOME.
See docs/CONFIG.md for lookup order, file layout, and config format docs.
Architecture
Five-crate Cargo workspace: providers (metadata catalog), client (Anthropic HTTP client), translator (pure format mapping, no IO), batch_engine (job queue + webhook), proxy (axum HTTP server + admin UI). See docs/proxy-architecture.md for crate details and data flow.
Key Design Decisions
- Translator crate is IO-free: pure
fn(A) -> Bmapping, testable without mocks. - Tool call IDs pass through directly (Anthropic
tool_use.id= OpenAItool_call.id). - OpenAI
argumentsis a JSON string; Anthropicinputis a JSON object. Mapping layer handles serialization. - Streaming uses a state machine (
streaming_map.rs) with bounded channel (32) for backpressure. ChatCompletionRequestuses#[serde(flatten)] pub extra: serde_json::Mapfor unknown OpenAI fields. Only fields needing translation logic get explicit struct fields.reasoning_contentmaps bidirectionally to Anthropic thinking blocks (DeepSeek/Qwen support).- Backoff jitter is deterministic (upper bound, not random) to keep tests predictable.
- Golden-file testing with JSON fixtures in
fixtures/anthropic/andfixtures/openai/.
Gotchas
- Managed backend fields cannot be cleared to NULL.
ManagedBackendPatchhas no sentinel to distinguish "omitted" from "set to null". Once a field likeapi_baseis set, it cannot be cleared via PATCH. UI should always send the current value in edit forms, not omit fields. OPENAI_API_KEYtakes precedence over provider-specific keys for stub backends.config/mod.rstriesOPENAI_API_KEYfirst, then falls back toGROQ_API_KEY/MISTRAL_API_KEY/ etc. IfOPENAI_API_KEYis set globally, it gets sent to Groq/Mistral/etc. even whenBACKEND=groq. Unset it or clear it from.anyllm.envbefore switching to a stub provider.- LiteLLM provider IDs are canonical. Use
gmi,publicai,zai,aiml,github_copilot,jina_ai,exa_ai, andstabilityinstead of older local ids likegmi_cloud,public_ai,zhipuai,ai_ml_api,github,jina,exa, andstability_ai. Legacy aliases are accepted in lookup paths only for migration. - Known providers with no global base URL must set one. OpenAI-compatible providers such as
azure_ai,cloudflare,databricks,snowflake,vercel_ai_gateway, and similar workspace/account-scoped providers must setOPENAI_BASE_URLor LiteLLMapi_base; never fall back to OpenAI's base URL for a known provider with an empty catalog default. BACKEND=sagemakerpanics at startup. ItsProviderProtocol::Custommakesresolve_backend()returnNone, triggering the "unknown backend" panic. UseBACKEND=bedrockfor AWS-hosted Anthropic models instead.- Adding a passthrough route (Translate mode): Reuse
passthrough_to_backend(&state, &headers, body, "/v2/path")inroutes.rs— it handles content-type forwarding and error mapping. The Anthropic mode equivalent isanthropic_generic_passthroughinpassthrough.rsviaAnthropicClient::forward_generic. - Header
&strslices lifetime: When building&[(&str, &str)]fromHeaderMap, collect values into ownedStringlocals first, then create references — the borrow checker rejects inline.to_str()in the slice. - CPU-bound work in handlers: Token counting and similar CPU work must use
tokio::task::spawn_blocking.count_request_tokens_sync(intoken_counting.rs) ispub(crate)for reuse. - Gemini input actions:
parse_model_actioningemini_input.rsreturns aGeminiActionenum. Extend it (not a bool) when adding new:actionsuffixes. - CSRF tokens are one-time-use. Fetch a fresh token from
GET /admin/csrf-tokenbefore each admin POST/PUT/DELETE. The SPA does this automatically; scripts must too. - Admin UI requires a flag. Pass
--webuior--admin(orWEBUI=1/ADMIN=1env). Without it, only the proxy starts. - Virtual key OnceLock in tests.
set_virtual_keysuses a globalOnceLock<DashMap>. Integration tests incrates/proxy/tests/virtual_keys.rsuse a sharedOnceLockto avoid conflicts. - Auth defaults to reject-all. Without
PROXY_API_KEYSorPROXY_OPEN_RELAY=true, every request gets 401. - Admin rate limiter resets on restart. 10 RPM per source IP, in-memory sliding window.
set_admin_rpm()overrides for tests. - Docker admin needs
ADMIN_BIND=0.0.0.0. Default binds to 127.0.0.1 which is unreachable from outside the container. - PLAN.md references in source comments are stale. Some files reference line ranges in a removed PLAN.md.
Conventions
- Provider catalog is generated for LiteLLM compatibility.
crates/providers/src/providers/litellm_snapshot.rsis generated from LiteLLM'smodel_prices_and_context_window.json; do not edit its provider/model rows by hand. Refresh withpython3 scripts/check_litellm_providers.py --all --write-rust-snapshot crates/providers/src/providers/litellm_snapshot.rs, then verify withpython3 scripts/check_litellm_providers.py --all --check. - Provider metadata fixes live in the generator or source aliases. For new provider IDs, update
scripts/check_litellm_providers.pysource alias/default metadata logic, regenerate the snapshot, and keepregistry.rsfocused on lookup/alias behavior. OpenAI-compat providers usually need no HTTP code. - Test files live alongside source (
#[cfg(test)]) and incrates/proxy/tests/for integration tests. - Error types use
thiserrorderive macros. - Fixture-based golden tests for translation correctness.
- Model pricing is auto-updated.
scripts/update_pricing.pypulls from LiteLLM'smodel_prices_and_context_window.jsonand writesassets/model_pricing.jsonplus the packaged proxy copy atcrates/proxy/assets/model_pricing.json. Run manually or via.github/workflows/update-pricing.yml(weekly, Monday 06:00 UTC). The proxy copy is embedded at compile time (include_str!incrates/proxy/src/cost/mod.rs); editing it requires recompile. Override at runtime withMODEL_PRICING_FILE.
Active Technologies
- Rust stable (1.83+, workspace edition 2021)
- SQLite, Redis (optional rate-limit/cache), Qdrant (optional semantic cache,
--features qdrant)
CI / Workflow Validation
- Validate workflows before pushing:
brew install actionlint && actionlint .github/workflows/*.ymlRuby YAML parser validates syntax only; actionlint catches GitHub Actions semantic errors. - secrets context in
ifconditions: Not allowed at job or step level. Pass viaenv:and check in shell:if [ -z "${SECRET}" ]; then echo "skipping"; exit 0; fi - Heredocs in
run:blocks: Content must be indented to match the block level. Unindented heredoc content (col 0) breaks YAML parsing. Useprintf '%s\n' ...or{ echo ...; } > file. gh release uploadrequires the release to exist. Add acreate-releasejob before upload jobs:gh release create "$TAG" --generate-notes || echo "already exists".- cargo publish exit 101 = version already exists on crates.io (not an error for re-runs).
Pattern:
cargo publish -p FOO || { ec=$?; [ "$ec" -eq 101 ] && echo "already published" || exit "$ec"; }
npm / Frontend
@vitejs/plugin-react@4.7.0declares peer deps only up to vite 7. With vite 8, usenpm ci --legacy-peer-deps(in ci.yml AND Dockerfile stage that runs npm ci).
crates.io Publish Order
anyllm_translate → anyllm_providers → anyllm_client → anyllm_batch_engine → anyllm_proxy (sleep 30 between each for index propagation)
Version Bumping
crates/client/Cargo.tomlpinsversiondirectly (notversion.workspace = true). When bumping the workspace version, also update it there and all inter-crateversion = "X.Y.Z"path deps. Quick check:grep -r 'version.*0\.' crates/*/Cargo.toml Cargo.toml | grep -v "workspace"- Deb package version = Cargo workspace version, NOT the release tag. Keep them in sync.
References
- OpenAI API spec: https://github.com/openai/openai-openapi/blob/manual_spec/openapi.yaml (very large, ~70k+ lines). Reference specific sections, do not load full spec.
- Endpoint inventory: docs/ENDPOINTS.md