mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-22 00:00:50 +00:00
9.5 KiB
9.5 KiB
anyllm_proxy
The axum HTTP server: request routing, backend dispatch, auth, virtual keys, admin UI, cost tracking, caching, fallback. Ties the other crates together.
See root ../../CLAUDE.md for workspace-wide commands, env vars, and the full gotchas list. This file covers proxy-internal specifics.
Run / Test
OPENAI_API_KEY=sk-... cargo run -p anyllm_proxy # proxy only, :3000
OPENAI_API_KEY=sk-... cargo run -p anyllm_proxy -- --webui # + admin UI :3001
cargo test -p anyllm_proxy
cargo test --test virtual_keys # virtual key + rate-limit integration
cargo test --test live_api -- --ignored --test-threads=1 # needs real key
- Built binary is
anyllm-proxy(hyphen); Cargo package id isanyllm_proxy(underscore). Run the built artifact astarget/debug/anyllm-proxy;cargo ... -p anyllm_proxyuses the underscore. Bin-target unit tests needcargo test -p anyllm_proxy --bins <name>(default-pruns lib tests only).
Layout
server/routes.rs— main request routing; highest-churn file in the repo, change carefully.main.rs— startup, config wiring, env-alias resolution; also high-churn.backend/— one client per backend:openai_client,gemini_client,anthropic_client,bedrock_client.mod.rshasresolve_backend()+send_with_retry.config/— config loading (simple YAML, LiteLLM YAML, TOML), env aliases, model router, TLS, URL validation.admin/— admin API, auth, keys, spend, websocket feed.state.rsholdsRuntimeConfig.admin-ui/— React SPA (Vite). Built separately, embedded at compile time.batch/— HTTP surface overanyllm_batch_engine.cache/— memory / redis / semantic (qdrant,--features qdrant).cost/— pricing + spend; embedsassets/model_pricing.jsonviainclude_str!.
Gotchas (proxy-specific)
- Admin UI defaults on for bare invocation. Bare
anyllm-proxy(no args) starts proxy + admin UI and auto-opens the browser (zero-arg default).--webui/--adminorWEBUI=1/ADMIN=1force it on with other args (no browser). Any other arg keeps it CLI-only.DISABLE_ADMIN=1force-disables. Single gate:main_helpers::bootstrap::admin_enabled(used bymain.rsandinit_admin); browser open only onis_default_launchviamain_helpers::browser::open. - Runtime smoke-test in isolation. Use a fresh
ANYLLM_HOME=$(mktemp -d)+ non-defaultLISTEN_PORT/ADMIN_PORT: the real~/.anyllmDB's persisted admin config overrides can hang--webuibefore the servers bind (stalls right after "applied config overrides from database"), and port 3000 is often held by other dev servers (giving false200s from something that isn't the proxy).--redact-secrets/REDACT_SECRETSalso adds multi-second startup — allow more time or omit for quick checks. main_helpers(bin-only) tests can't useENV_TEST_LOCK. It'spub(crate)in the lib crate, unreachable from the bin crate. For bin-only code that reads env, split the pure logic (e.g.admin_requested(args)) from the env read (admin_enabled) and unit-test the pure part with no env mutation, instead of trying to serialize on the lock.- Auth defaults to reject-all. Without
PROXY_API_KEYS,PROXY_OPEN_RELAY=true, virtual keys, or OIDC, every request gets 401, including from localhost.effective_auth_mode()feedsGET /admin/api/status(auth_mode) and the admin UI banner. - CSRF tokens are one-time-use. Fetch a fresh one from
GET /admin/csrf-tokenbefore each admin POST/PUT/DELETE. Scripts must too. - Live admin-endpoint smoke: run with
ADMIN_TOKEN=<32+ chars> ... --webui(admin on :3001). GET needsAuthorization: Bearer $ADMIN_TOKEN. POST/PUT/DELETE ALSO need CSRF:GET /admin/csrf-tokenwith a cookie jar (curl -c jar), then resend with-b jar+X-CSRF-Token: <token>(header must equal the cookie). Missing/mismatched CSRF returns 403 before your handler runs. main_helpersis bin-only (declared inmain.rs, NOTlib.rs). Library code (anything reached viacrate::at runtime, e.g.optimizer.rs) cannot usecrate::main_helpers::bootstrap::*— it won't compile. The data-dir/home helpers live incrate::config::helpers::{resolve_data_dir, home_dir}; use those from lib code.- Docker admin needs
ADMIN_BIND=0.0.0.0— default 127.0.0.1 is unreachable from outside the container. OPENAI_API_KEYtakes precedence over provider keys for stub backends. WithBACKEND=groqbutOPENAI_API_KEYset globally, the OpenAI key gets sent to Groq. Unset it when switching to a stub provider.BACKEND=sagemakerpanics at startup (ProviderProtocol::Custom->resolve_backend()None). UseBACKEND=bedrockfor AWS-hosted Anthropic.- Adding a
RuntimeConfigfield touches 6 sites; 2 are not compiler-caught. The struct +RuntimeConfigDefaults(admin/state.rs) and 3 constructors fail to compile if missed, but the SQLite override-applymatch(main_helpers/async_main/admin.rs) anddelete_config_overridereset (admin/routes/config.rs) do NOT — add there too or the field won't persist/reset across restart. - Managed backend fields cannot be cleared to NULL.
ManagedBackendPatchhas no omitted-vs-null sentinel. Edit forms must resend the current value, not omit it. bedrock_client.rshas its own retry loop — keep it in sync with the canonicalanyllm_client::retry(two loops total;anthropic_client.rsdelegates toretry.rsand tracks automatically).- CPU-bound work (token counting) must use
tokio::task::spawn_blocking.count_request_tokens_syncinserver/token_counting.rsispub(crate)for reuse. - Env-var tests must share
crate::config::ENV_TEST_LOCK(a per-moduleMutexdoes NOT serialize across modules in one test binary -> flakes). - Admin rate limiter resets on restart (10 RPM/IP, in-memory).
set_admin_rpm()overrides for tests. - Virtual keys use a global
OnceLock<DashMap>— integration tests share oneOnceLockto avoid conflicts. - Passthrough routes: reuse
passthrough_to_backend(...)inroutes.rs(Translate mode) oranthropic_generic_passthroughinpassthrough.rs(Anthropic mode). - Admin UI npm: with vite 8,
npm ci --legacy-peer-deps(plugin-react peer dep caps at vite 7). - Cache key is a DENYLIST.
should_include_cache_field(cache/mod.rs) hashes every request-body field EXCEPTstream/stream_options/_scope_auth/_scope_backend/user/parallel_tool_calls/metadata. Add response-irrelevant fields here or they fragment the cache per-request. The oldCACHE_FIELDSallowlist is gone. - Azure deployment URL is built once in
config/litellm/resolve_base_url;build_backend_configpasses it through.azure_deployment_from_modelstrips route-group markers (o_series/,gpt5_series/); that list is the complete authoritative set. - Anthropic thinking-block repair (
thinking_repair/,ANTHROPIC_THINKING_REPAIR=true) only wires up inanthropic_passthrough(server/passthrough.rs,BACKEND=anthropic's/v1/messages). It never touches messages before the last assistant one (prompt-cache prefix safety). In-memorymokastore only, keyed on message id / thinking-block signature / tool_use id — restart loses it, feature fails open until a fresh response is recorded. All store keys are scoped by anamespace(backend name + virtual-key id) computed once per request inpassthrough.rs— never callThinkingRepairStore/repair_request/record_responsewithout it, or one shared store across backends/tenants can cross-contaminate. Also toggleable live from the admin UI /RuntimeConfig.anthropic_thinking_repair(no restart) —ThinkingRepairStoreis now always constructed for Anthropic backends regardless of the flag; the flag only gates therepair_request/record_response/store.commitcall sites inpassthrough.rsviaAppState::thinking_repair_enabled(). server_advertised_tool_namesis hardcoded to an emptyHashSetat every production call site (chat_completions/handler.rs,routes/messages.rs,chat_completions/stream/generic/tool_loop.rs).partition_tool_callstherefore always routes every tool call topass_through;auto_exec/deniedare never populated in production today (tracked separately,.eatahorse-integrate-forge-guardrails-opt-in-tool-c/tasks/ready/EH-0001-*). Any new tool-loop mechanism (guardrails, audit, rate-limiting) must evaluate against the post-partitionauto_execset, never the rawtool_callslist — otherwise it silently answers on behalf of pass-through (client-owned) tool calls instead of returning them unresolved. Seetools::execution::partition_and_nudgefor the pattern.ThinkingRepairStore(thinking_repair/store.rs) is 3 independentmoka::future::Cacheinstances (by_msg/by_sig/by_tool_use) backing one logical record. They evict independently even at the same capacity, so one index can miss while another still resolves the same message. Any new multi-index cache in this crate should either share one eviction clock or defensively re-verify via a second index before trusting a miss (seerepair.rs'sverified_via_owner_recordfallback).patch_repaired_body(thinking_repair/mod.rs) fails open (forwards unrepaired bytes) if the last assistant message has acache_controlfield or a block"type"outsideKNOWN_BLOCK_TYPES. Adding a new Anthropic content-block type toContentBlockwithout adding it toKNOWN_BLOCK_TYPESdoesn't break anything loudly — repair just silently stops firing for messages containing that block type.