# Claude Proxy Architecture ## 1. System Overview The Claude proxy is a local HTTP server that sits between Claude Code and the Anthropic API. It provides multi-account rotation, automatic token refresh, rate-limit handling with exponential backoff, and optional model translation to non-Anthropic providers. ### Two operational modes | Mode | When | What happens | | --------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Passthrough** | Target provider is `anthropic` (or `null`) | The request body is forwarded byte-for-byte to `api.anthropic.com` via plain `fetch()` with client headers forwarded. No parsing, no tool injection, no SDK involvement. | | **Translation** | Target provider is anything else (e.g. `vertex`, `openai`) | The Claude-format request is parsed by `parseClaudeRequest()`, routed through `ctx.neurolink.stream()` / `ctx.neurolink.generate()`, and the NeuroLink response is serialized back to Claude SSE format via `ClaudeStreamSerializer`. | Passthrough exists because Claude Code sends complex bodies (multi-turn conversations, tool definitions, thinking blocks, context management betas) that would be lossy to parse and re-serialize. The proxy's job for Claude-to-Claude is purely auth and account management. ### How it fits into NeuroLink The proxy is started via the CLI (`neurolink proxy start`) and creates a Hono HTTP server. It registers routes from `createClaudeProxyRoutes()` and injects a live `NeuroLink` SDK instance into the request context for translation-mode and fallback paths. MCP initialization is explicitly skipped (`NEUROLINK_SKIP_MCP=true`) because tools come from Claude Code, not from MCP servers. --- ## 2. Request Lifecycle A complete request through the passthrough path: ``` Claude Code │ │ HTTP POST /v1/messages │ (body: JSON with model, messages, stream, tools, thinking, etc.) │ ▼ Hono app (proxy.ts) │ Logs: method, path, model, stream/non-stream, tool count │ ├─ Peer-sharing gate (shareGate.ts) — see /docs/features/proxy-peer-sharing │ │ Runs after the body is parsed, because the model allowlist is a gate. │ │ │ ├─ No share token: │ │ NEUROLINK_PROXY_REQUIRE_GRANT unset → `local`, request continues untouched │ │ NEUROLINK_PROXY_REQUIRE_GRANT=1 → 401 missing_token │ │ │ ├─ Share token present → resolve grant, apply refill, evaluate admission │ │ (state, notAfter, schedule, models, rate, concurrency, coins) │ │ Refused → 401/403/429 carrying x-neurolink-grant-status / -reason │ │ │ └─ Admitted → open a coin hold, claim a concurrency slot, and run the │ handler inside an AsyncLocalStorage share context │ │ Builds ServerContext with NeuroLink instance │ ▼ claudeProxyRoutes.ts POST /v1/messages handler │ ├─ Validate: body must contain model + messages │ ├─ ModelRouter.resolve(body.model) │ → { provider: "anthropic", model: "claude-sonnet-4-..." } │ (or null provider for unknown non-Claude models) │ ├─ isClaudeTarget? YES → passthrough path │ ├─ Load accounts (see §3 Account Management) │ TokenStore compound keys → legacy credentials file → env var │ ├─ Withhold leased accounts whose lease has lapsed or whose leased gates │ exclude this request (residentGrants.ts — borrower side of a complete share) │ ├─ Narrow to what the borrowed request's grant may use (sharePolicy.ts): │ account allowlist → pool-wide slice ceiling → reserve floor → │ spillover window → per-account ceiling. │ Nothing survives → share-shaped refusal, not the generic 401 │ ├─ Order accounts by configured strategy (`fill-first` by default) │ ├─ FOR EACH account (skip if cooling): │ │ │ ├─ Token refresh check (1 hour buffer before expiry) │ │ │ ├─ Build headers: │ │ - Start with all client headers (forwarded as-is) │ │ - Override: authorization: Bearer (OAuth) │ │ or x-api-key: (API key) │ │ - Fill defaults only when absent: user-agent, anthropic-version │ │ - Ensure oauth-2025-04-20 is in anthropic-beta │ │ │ ├─ Plain fetch("https://api.anthropic.com/v1/messages?beta=true", { body: bodyStr }) │ │ │ ├─ Response handling: │ │ 429 → exponential backoff (1s base, 10min cap), continue to next account │ │ 401 → refresh token, retry up to 5 times, then cooldown 5min │ │ 400/422 + invalid_request_error → return immediately (no retry) │ │ 404 → return immediately (no cooldown) │ │ 5xx/52x → no cooldown, rotate immediately to next account │ │ 200 + stream → bootstrap retry (read first chunk), pipe back │ │ 200 + JSON → return response.json() │ │ │ └─ Reset backoffLevel on success │ ├─ All accounts exhausted → │ ├─ Try peers (peerTransport.ts) — only here, never as a ranking tweak │ ├─ Try fallback chain (modelRouter.getFallbackChain()) │ ├─ Try auto-provider fallback (no explicit chain) │ └─ Return 429 with Retry-After header │ ├─ On completion (borrowed traffic only): │ settle the coin hold from real usage, record the window delta against │ the grant, and strip x-neurolink-account* and pool counters from the │ response headers │ ▼ Claude Code receives Response ``` --- ## 3. Account Management ### Account loading priority Accounts are loaded in the `POST /v1/messages` handler on every request (not cached across requests), in this order: 1. **TokenStore compound keys** (`anthropic: