Add sanitize_api_key() to strip curly/smart quotes silently injected by copy-paste from rich-text sources (Slack, docs). Add strip_v1_suffix() to prevent doubled /v1/v1 paths when provider URLs already include /v1. Applied across all config paths (env, simple YAML, LiteLLM YAML, TOML). Also adds crate structure section to proxy-architecture.md and rebuilds admin UI dist after vite upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.1 KiB
Proxy Architecture
Crate Structure
Cargo workspace with five crates:
crates/providers (lib: anyllm_providers)
Metadata-only catalog: no HTTP, no IO. ProviderDef (protocol, auth, env vars, LiteLLM prefix) and ModelDef (context window, capabilities). Registry functions in registry.rs. Add a new provider: create providers/src/providers/<name>.rs, register in providers/mod.rs and registry.rs. OpenAI-compatible providers route through the existing OpenAIClient automatically.
crates/client (lib: anyllm_client)
Async HTTP client (Anthropic-in, Anthropic-out). ClientBuilder, ToolBuilder, messages_stream() returning impl Stream.
crates/translator (lib: anyllm_translate)
Pure translation logic, no IO. Stateless fn(A) -> B mapping between Anthropic and OpenAI types.
anthropic/: Anthropic Messages API typesopenai/: OpenAI types (Chat Completions + Responses API)mapping/: Conversion functions (message_map, tools_map, streaming_map, reverse_streaming_map, responses_*, warnings)middleware/: Request/response handler orchestrating translation
crates/batch_engine (lib: anyllm_batch_engine)
HTTP-agnostic batch orchestration: job queue, file storage, webhook delivery.
crates/proxy (bin: anyllm_proxy)
HTTP proxy on axum + reqwest:
server/: Routes, middleware (auth, rate limit, request ID, size/concurrency limits), SSE streaming, passthrough handlers.bedrock_native.rs: Bedrock Converse/InvokeModel native passthrough (SigV4 handled by proxy).generic_passthrough.rs: catch-all/v1/{*path}for Translate mode (registered last).backend/:BackendClientenum dispatching to OpenAI/Azure/Vertex/Gemini/Anthropic/Bedrock with retryadmin/: Admin server (localhost:3001), virtual key CRUD, managed backend CRUD (routes/managed_backends.rs), model management, audit log, WebSocket live updatesadmin-ui/: React 19 + TypeScript SPA (Vite). Build:cd crates/proxy/admin-ui && npm run build
Data Flow
Client (Anthropic SDK) -> POST /v1/messages
-> Auth middleware (validate x-api-key or Bearer)
-> Request ID middleware (generate/echo x-request-id)
-> Body size limit (32MB via DefaultBodyLimit)
-> Concurrency limit (100 via tower ConcurrencyLimitLayer)
-> Route handler
-> Translate: Anthropic request -> OpenAI request
-> OpenAI client (reqwest with retry/backoff)
-> Translate: OpenAI response -> Anthropic response
-> Client receives Anthropic-format response
Header Rules
Inbound (from client)
| Header | Required | Action |
|---|---|---|
| x-api-key | One of these | Validated for presence only |
| Authorization: Bearer ... | One of these | Validated for presence only |
| anthropic-version | No | Accepted but not forwarded |
| content-type | Yes | Must be application/json |
| x-request-id | No | Echoed; generated if absent |
Outbound (to OpenAI)
| Header | Value | Notes |
|---|---|---|
| Authorization | Bearer {OPENAI_API_KEY} | From config, never from client |
| Content-Type | application/json | Set by reqwest |
Response (to client)
| Header | Value |
|---|---|
| x-request-id | Request correlation ID |
| content-type | application/json or text/event-stream |
Error Shape Translation
All errors returned to clients use Anthropic format:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "..."
}
}
OpenAI error status codes are mapped:
- 400 -> invalid_request_error
- 401 -> authentication_error
- 403 -> permission_error
- 404 -> not_found_error
- 429 -> rate_limit_error
- 500-502 -> api_error
- 503, 529 -> overloaded_error
Retry Policy
- Retries on 429 and 5xx status codes
- Maximum 3 retries
- Exponential backoff: 500ms * 2^attempt + 25% jitter
- Respects retry-after header when present
- Each retry logged at WARN level
Security
- Auth boundary: proxy never forwards client credentials to OpenAI
- SSRF prevention: only connects to configured OPENAI_BASE_URL
- Secret redaction utility for logging (shows first/last 4 chars)
- 32MB body size limit enforced at proxy edge
- 100 concurrent request limit