Link.Assistant.Router
A Rust-based API gateway that proxies Anthropic (Claude) APIs through a Claude MAX OAuth session, providing multi-tenant access via custom-issued tokens.
Overview
Link.Assistant.Router is a transparent proxy that sits between API clients (such as Claude Code) and the Anthropic API. It is the OpenRouter-equivalent for Claude MAX accounts: every feature found in the community Claude proxies is available behind a single configurable surface.
- Proxies all Anthropic API requests transparently, including SSE/streaming responses
- Supports Claude MAX (OAuth) by reading Claude Code session credentials
- OpenAI-compatible endpoints —
/v1/chat/completions,/v1/responses,/v1/modelstranslate to Anthropic or forward to a configured OpenAI-compatible provider - Optional Gonka upstream —
UPSTREAM_PROVIDER=gonkaforwards OpenAI-compatible routes to Gonka instead of translating them to Anthropic - Optional Crater ForgeFed upstream —
UPSTREAM_PROVIDER=craterturns OpenAI chat requests into ForgeFedOffer{Ticket}tasks and waits for resolved task results - Optional LiteLLM/OpenAI-compatible upstream —
UPSTREAM_PROVIDER=openai-compatibleroutes OpenAI SDK traffic to a stored provider such as LiteLLM - Multi-account routing — pool any number of Claude MAX accounts; round-robin / priority / least-used; automatic cooldowns on 429
- Issues custom
la_sk_...JWT tokens with expiration and revocation for multi-tenant access - Persistent token store — text (Lino) and binary backends, both on by default; tokens survive restarts
- Live observability — Prometheus
/metrics, JSON/v1/usage, per-account health at/v1/accounts lino-arguments+.lenv— every flag has an env-var alias and an optional.lenvfile fallback- First-class CLI —
serve,tokens issue|list|revoke|expire|show,providers add|list|show|remove|import,accounts list,doctorsubcommands - Replaces custom tokens with real OAuth credentials internally, so the OAuth token is never exposed to clients
- Runs as a single Docker container for easy deployment
Every feature is configurable — conflicting design choices in upstream community proxies become toggles (--routing-mode, --storage-policy, --disable-openai-api, --disable-anthropic-api, --disable-metrics, --experimental-compatibility).
Architecture
Client (Claude Code / API user)
|
| Authorization: Bearer la_sk_...
v
Link.Assistant.Router (Rust / axum)
|
| Authorization: Bearer <real OAuth token>
v
Anthropic API (api.anthropic.com)
When UPSTREAM_PROVIDER=gonka, clients still authenticate to the router with
Authorization: Bearer la_sk_..., but upstream OpenAI-compatible requests are
sent to Gonka with Gonka signing headers instead of the client token. This
project remains Link.Assistant.Router; Gonka is an optional backend.
When UPSTREAM_PROVIDER=openai-compatible, clients still authenticate to the
router with Authorization: Bearer la_sk_... or x-api-key: la_sk_.... The
router forwards OpenAI-compatible requests to the configured provider, such as
a LiteLLM proxy, and substitutes only the upstream provider key inside the
router.
When UPSTREAM_PROVIDER=crater, /v1/chat/completions accepts normal OpenAI
chat requests, delivers a ForgeFed Offer containing a Ticket to
CRATER_FORGEFED_INBOX, reads Accept.result, polls that task URI until
isResolved:true, and maps the resolved content back to OpenAI JSON or SSE.
Quick Start
Prerequisites
- Rust 1.70+ (for building from source)
- Docker (for containerized deployment)
- A Claude MAX subscription with an active Claude Code OAuth session
1. Build from source
The binary will be at target/release/link-assistant-router.
2. Set up Claude Code credentials
The router reads OAuth credentials from the Claude Code home directory. By default, it looks in ~/.claude for credential files. Make sure you have an active Claude Code session:
# Log in with Claude Code (this creates the session files)
The router searches these files in order:
credentials.json.credentials.jsonauth.jsonoauth.jsonconfig.json
It reads the accessToken (or access_token, oauthToken, oauth_token) field from the first file found.
3. Start the router
# Required: set the JWT signing secret
# Optional: customize port (default: 8080)
# Optional: set Claude Code home directory (default: ~/.claude)
# Optional: override upstream URL (default: https://api.anthropic.com)
# Start the router
You should see:
INFO Link.Assistant.Router v0.2.0
INFO Upstream: https://api.anthropic.com
INFO Claude Code home: /home/user/.claude
INFO Listening on 0.0.0.0:8080
4. Issue a custom token
|
Response:
Save the token value for use in API requests.
5. Use the router as an Anthropic API proxy
# Use the custom token to make requests through the router
|
The router will:
- Validate the
la_sk_...token - Replace it with the real OAuth token from the Claude Code session
- Forward the request to
https://api.anthropic.com/v1/messages - Stream the response back to the client
Using with Claude Code
The primary use case is routing Claude Code through the proxy so multiple users can share a single Claude MAX subscription.
Step 1: Start the router (on the server/host machine)
Step 2: Issue a token for each user
# Issue a token for user Alice
|
# Issue a token for user Bob
|
Step 3: Configure Claude Code to use the router (on each user's machine)
# Set the base URL to point to the router
# Set the custom token as the API key
# Run Claude Code normally — all requests go through the router
Claude Code will work exactly as normal, with all requests transparently proxied through the router.
API Endpoints
Always available
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check, returns ok |
/api/tokens |
POST | Issue a new custom token |
/api/tokens/list |
GET | (admin) List every persisted token |
/api/tokens/revoke |
POST | (admin) Revoke a token by id |
/api/providers |
GET/POST | (admin) List or upsert OpenAI-compatible upstream providers |
/api/providers/{name} |
GET/DELETE | (admin) Show or delete one provider |
Anthropic surface (--disable-anthropic-api to opt out)
| Endpoint | Method | Description |
|---|---|---|
/v1/messages |
POST | Anthropic Messages — preserves SSE streaming |
/v1/messages/count_tokens |
POST | Token-count helper |
/invoke |
POST | Bedrock-format invoke |
/invoke-with-response-stream |
POST | Bedrock streaming invoke |
/api/latest/anthropic/* |
ANY | Legacy prefix; stripped and forwarded |
/*:rawPredict, /*:streamRawPredict |
POST | Vertex rawPredict pass-through |
OpenAI surface (--disable-openai-api to opt out)
| Endpoint | Method | Description |
|---|---|---|
/v1/chat/completions |
POST | Chat Completions, translated to Anthropic Messages, forwarded to the selected OpenAI-compatible provider, or delivered as a Crater ForgeFed task |
/v1/responses |
POST | Responses API, translated to Anthropic Messages or forwarded to the selected OpenAI-compatible provider |
/v1/models |
GET | OpenAI-shaped model list |
gpt-4o, gpt-4o-mini, gpt-4, and the o* reasoning families auto-map to the Claude Sonnet / Haiku / Opus tiers respectively. Native claude-* IDs pass through unchanged.
With UPSTREAM_PROVIDER=gonka, /v1/chat/completions and /v1/responses
forward OpenAI-compatible JSON to Gonka without Anthropic translation. If a
request omits model, the router uses GONKA_MODEL.
With UPSTREAM_PROVIDER=openai-compatible, the same routes forward JSON to the
configured provider. This supports LiteLLM proxy deployments by setting the
provider base URL to the LiteLLM /v1 API base. Streaming OpenAI requests are
passed through for OpenAI-compatible providers, and Anthropic-backed streaming
requests are translated to OpenAI SSE chunks.
With UPSTREAM_PROVIDER=crater, /v1/chat/completions supports normal JSON
responses and SSE with either request-body "stream": true or ?stream=true.
The SSE stream emits OpenAI chat-completion chunks once the ForgeFed task
resolves.
MPP charges for OpenAI endpoints
The OpenAI-compatible endpoints can advertise Machine Payments Protocol (MPP)
charges with HTTP 402 Payment Required. Enable this only after configuring
the amount, currency, and recipient for your payment method:
MPP_ENABLE=true
MPP_AMOUNT=0.05
MPP_CURRENCY=USD
MPP_RECIPIENT=acct_or_wallet
MPP_METHOD=stripe
When enabled, unpaid calls to /v1/chat/completions and /v1/responses
return WWW-Authenticate: Payment ... with protocol="mpp" and
intent="charge". This is separate from the ForgeFed/ActivityPub discovery
surface. Payment credential settlement is intentionally not accepted until a
method-specific verifier is configured.
Observability (--disable-metrics to opt out)
| Endpoint | Method | Description |
|---|---|---|
/metrics |
GET | Prometheus text-exposition counters |
/v1/usage |
GET | JSON snapshot of all counters |
/v1/accounts |
GET | Multi-account health: cooldowns, last error, used-count |
POST /api/tokens
Issue a new custom JWT token.
Request body:
| Field | Type | Default | Description |
|---|---|---|---|
ttl_hours |
integer | 24 | Token lifetime in hours |
label |
string | "" |
Optional human-readable label |
Response:
Proxy Routes
Any request to /api/latest/anthropic/* is forwarded to the upstream Anthropic API. The proxy:
- Validates the
Authorization: Bearer la_sk_...orx-api-key: la_sk_...token - Replaces it with the real OAuth token
- Forwards all headers (except
host,authorization,x-api-key,connection,transfer-encoding) - Passes through the request body unmodified
- Streams back the response (SSE-compatible)
- Preserves the upstream status code and response headers
Error responses follow the Anthropic API error format:
| Status | Condition |
|---|---|
| 401 | Missing or invalid/expired token |
| 403 | Token has been revoked |
| 502 | OAuth token unavailable or upstream request failed |
Configuration
Configuration is read by lino-arguments in this order: CLI flags,
environment variables, .lenv, then .env. The default file format is
Lino-style key/value notation:
TOKEN_SECRET: your-router-token-secret
UPSTREAM_PROVIDER: openai-compatible
OPENAI_COMPATIBLE_PROVIDER_NAME: litellm
OPENAI_COMPATIBLE_BASE_URL: http://litellm:4000/v1
OPENAI_COMPATIBLE_MODEL: claude-sonnet
OPENAI_COMPATIBLE_MODELS: claude-sonnet,gpt-4o
Every flag listed in --help has an env-var alias and can be configured from
.lenv with the same env-var key.
Core
| Flag / env | Default | Required | Description |
|---|---|---|---|
--token-secret / TOKEN_SECRET |
— | Yes | Secret key for signing/validating JWT tokens |
--port / ROUTER_PORT |
8080 |
No | Port to listen on |
--host / ROUTER_HOST |
0.0.0.0 |
No | Host/IP to bind to |
--claude-code-home / CLAUDE_CODE_HOME |
~/.claude |
No | Primary Claude Code credentials directory |
--upstream-provider / UPSTREAM_PROVIDER |
anthropic |
No | Upstream provider: anthropic, gonka, crater, or openai-compatible |
--upstream-base-url / UPSTREAM_BASE_URL |
https://api.anthropic.com |
No | Upstream Anthropic API URL |
--api-format / UPSTREAM_API_FORMAT |
(auto) | No | Restrict the proxy to anthropic / bedrock / vertex |
--verbose / VERBOSE |
false |
No | Verbose tracing |
Gonka provider
Gonka support is optional. Anthropic remains the default provider, and existing
Claude MAX OAuth behavior is unchanged unless UPSTREAM_PROVIDER=gonka is set.
TOKEN_SECRET=your-router-token-secret
UPSTREAM_PROVIDER=gonka
GONKA_PRIVATE_KEY=your_gonka_private_key
GONKA_SOURCE_URL=https://node4.gonka.ai
GONKA_MODEL=Qwen/Qwen3-235B-A22B-Instruct-2507-FP8
| Flag / env | Default | Required | Description |
|---|---|---|---|
--gonka-private-key / GONKA_PRIVATE_KEY |
— | Yes, for Gonka | Private key used to sign Gonka upstream requests |
--gonka-source-url / GONKA_SOURCE_URL |
https://node4.gonka.ai |
No | Gonka source node URL |
--gonka-model / GONKA_MODEL |
Qwen/Qwen3-235B-A22B-Instruct-2507-FP8 |
No | Default model for Gonka OpenAI-compatible requests |
Your Gonka account must be activated for inference, funded, and have a published on-chain public key. Participant registration is only needed for hosting.
Crater ForgeFed provider
Crater support is optional. It keeps router-issued la_sk_... tokens at the
edge, then uses ForgeFed to submit work to a remote ticket tracker or exchange.
TOKEN_SECRET=your-router-token-secret
UPSTREAM_PROVIDER=crater
CRATER_FORGEFED_INBOX=https://tracker.example/inbox
CRATER_FORGEFED_TARGET=https://tracker.example/projects/demo
# Optional; defaults to ACTIVITYPUB_ACTOR_BASE_URL/actor/code
CRATER_FORGEFED_ACTOR=https://router.example/actor/code
| Flag / env | Default | Required | Description |
|---|---|---|---|
--crater-forgefed-inbox / CRATER_FORGEFED_INBOX |
— | Yes, for Crater | Remote ForgeFed inbox that receives Offer{Ticket} activities |
--crater-forgefed-actor / CRATER_FORGEFED_ACTOR |
${ACTIVITYPUB_ACTOR_BASE_URL}/actor/code |
No | Local actor URI used in outbound activities |
--crater-forgefed-target / CRATER_FORGEFED_TARGET |
inbox URI | No | Ticket tracker or project URI used as Offer.target |
--crater-poll-interval-ms / CRATER_POLL_INTERVAL_MS |
1000 |
No | Delay between task URI polls |
--crater-poll-timeout-secs / CRATER_POLL_TIMEOUT_SECS |
120 |
No | Maximum wait for isResolved:true |
OpenAI-compatible / LiteLLM provider
Generic OpenAI-compatible providers are used when
UPSTREAM_PROVIDER=openai-compatible. The boot-time config can come from
.lenv, env vars, or CLI flags:
TOKEN_SECRET: your-router-token-secret
UPSTREAM_PROVIDER: openai-compatible
OPENAI_COMPATIBLE_PROVIDER_NAME: litellm
OPENAI_COMPATIBLE_BASE_URL: http://litellm:4000/v1
OPENAI_COMPATIBLE_API_KEY_ENV: LITELLM_MASTER_KEY
OPENAI_COMPATIBLE_MODEL: claude-sonnet
OPENAI_COMPATIBLE_MODELS: claude-sonnet,gpt-4o
| Flag / env | Default | Required | Description |
|---|---|---|---|
--openai-compatible-provider-name / OPENAI_COMPATIBLE_PROVIDER_NAME |
litellm |
No | Stored provider name to resolve |
--openai-compatible-base-url / OPENAI_COMPATIBLE_BASE_URL |
http://localhost:4000/v1 |
No | Upstream OpenAI-compatible /v1 API base |
--openai-compatible-api-key / OPENAI_COMPATIBLE_API_KEY |
— | No | Inline upstream key; prefer persisted provider storage for long-lived secrets |
--openai-compatible-api-key-env / OPENAI_COMPATIBLE_API_KEY_ENV |
— | No | Environment variable containing the upstream key |
--openai-compatible-model / OPENAI_COMPATIBLE_MODEL |
— | No | Default model injected when requests omit model |
--openai-compatible-models / OPENAI_COMPATIBLE_MODELS |
— | No | Comma-separated models exposed from /v1/models |
Persistent provider records live in <DATA_DIR>/providers.lenv. Inline
provider API keys are encrypted with AES-GCM using a key derived from
TOKEN_SECRET; API responses and CLI output only show whether a stored key is
present.
Provider records can also be imported from JSON, provider-store .lenv, or an
indented Links-style config:
litellm
kind "openai-compatible"
base-url "http://litellm:4000/v1"
model "claude-sonnet"
models "claude-sonnet,gpt-4o"
api-key-env "LITELLM_MASTER_KEY"
The HTTP API accepts the same shape at POST /api/providers:
Routing & storage
| Flag / env | Default | Description |
|---|---|---|
--routing-mode / ROUTING_MODE |
direct |
direct (OAuth substitution), cli (Claude CLI subprocess), or hybrid |
--storage-policy / STORAGE_POLICY |
both |
Persistent token store: memory, text (Lino), binary, or both |
--data-dir / DATA_DIR |
platform-specific | Where tokens.lino / tokens.bin live |
--claude-cli-bin / CLAUDE_CLI_BIN |
claude |
Local Claude CLI binary used by the cli backend |
--additional-account-dirs / ADDITIONAL_ACCOUNT_DIRS |
(empty) | Comma-separated extra credential dirs for multi-account routing |
Feature toggles
| Flag / env | Default | Description |
|---|---|---|
--disable-openai-api / DISABLE_OPENAI_API |
off | Hide /v1/chat/completions, /v1/responses, /v1/models |
--disable-anthropic-api / DISABLE_ANTHROPIC_API |
off | Hide /v1/messages* and Bedrock paths |
--disable-metrics / DISABLE_METRICS |
off | Hide /metrics, /v1/usage, /v1/accounts |
--experimental-compatibility / EXPERIMENTAL_COMPATIBILITY |
off | XML history, model spoofing and other community-proxy behaviours |
--admin-key / TOKEN_ADMIN_KEY |
(open) | Bearer key required for /api/tokens* admin endpoints |
--mpp-enable / MPP_ENABLE |
off | Return MPP 402 Payment Required challenges on OpenAI endpoints |
--mpp-amount / MPP_AMOUNT |
0.00 |
Per-request MPP charge amount |
--mpp-currency / MPP_CURRENCY |
USD |
Currency or asset for MPP charges |
--mpp-recipient / MPP_RECIPIENT |
— | Recipient wallet, merchant account, or payment address |
--mpp-method / MPP_METHOD |
— | Optional MPP payment method identifier |
CLI subcommands
# Default: starts the HTTP server (same as `serve`).
# Issue / list / revoke / show tokens locally (no HTTP needed):
# Inspect configured accounts:
# Manage OpenAI-compatible upstream providers:
# Print resolved configuration + credential / store probes:
Logging
The router uses tracing with the RUST_LOG environment variable:
# Default: info level
RUST_LOG=info
# Debug level for detailed request tracing
RUST_LOG=debug
# Trace level for maximum verbosity
RUST_LOG=trace
Docker Deployment
Build the image
Run the container
The Dockerfile sets CLAUDE_CODE_HOME=/data/claude by default, so mount your Claude Code session directory to /data/claude.
Docker Compose example
version: "3.8"
services:
router:
build: .
ports:
- "8080:8080"
environment:
TOKEN_SECRET: ${TOKEN_SECRET}
ROUTER_PORT: "8080"
volumes:
- ${HOME}/.claude:/data/claude:ro
restart: unless-stopped
VPS Deployment
To deploy on a VPS (e.g., Ubuntu):
# 1. Install Rust
|
# 2. Clone and build
# 3. Set up Claude Code credentials on the VPS
# (log in with Claude Code to create session files)
# 4. Create a systemd service (optional, for auto-start)
# 5. Check status
Akash and Kubernetes
Ready-to-edit deployment templates are included for hosted environments:
Replace placeholder secrets, set ACTIVITYPUB_ACTOR_BASE_URL to the public
router URL, and mount or provision Claude Code credentials at
CLAUDE_CODE_HOME before exposing the service.
ForgeFed Integration
The router exposes ActivityPub/ForgeFed endpoints for service discovery and problem-source federation. See docs/forgefed.md for the actor document, inbox, follow activity, and deployment verification steps.
Token System
The router uses JWT-based custom tokens with the la_sk_ prefix.
Token lifecycle
- Issue:
POST /api/tokenscreates a signed JWT with a UUID subject, expiration, and optional label - Validate: Each proxy request extracts the
Authorization: Bearer la_sk_...header, strips the prefix, and verifies the JWT signature and expiration - Revoke: Tokens can be revoked by their subject ID (stored in-memory; revocations are lost on restart)
Token format
Tokens are standard HS256 JWTs with the la_sk_ prefix. The JWT payload contains:
Security notes
- The
TOKEN_SECRETmust be kept secure — anyone with the secret can forge tokens - OAuth tokens from the Claude Code session are never exposed to clients
- Tokens are validated on every request
- Use a strong, random secret (e.g.,
openssl rand -hex 32)
Testing
Run all tests
This runs:
- Unit tests in every module under
src/(44 tests covering config, oauth, token, storage, accounts, openai, metrics, cli) - Integration tests in
tests/integration_test.rscover API path routing, OpenAI translation, metrics rendering, and CLI parsing
Run specific test suites
# Unit tests only
# Integration tests only
# A specific test
# With verbose output
Code quality checks
# Check formatting
# Run Clippy lints
# All checks together
&& &&
Manual end-to-end testing
Use the provided script to test the router locally:
# Make the script executable
# Run manual tests (starts the router, issues a token, tests endpoints)
Or test manually step by step:
# Terminal 1: Start the router with a test credential file
# Terminal 2: Test the endpoints
# 1. Health check
# Expected: ok
# 2. Issue a token
TOKEN=
# 3. Test proxy with token (will get auth error from Anthropic since test-oauth-token is not real)
|
# 4. Test without token (should get 401)
|
# 5. Test with invalid token (should get 401)
|
Run the example
This demonstrates token issuance, validation, and revocation programmatically.
Project Structure
.
├── .github/workflows/
│ └── release.yml # CI/CD pipeline (lint, test, build, release)
├── changelog.d/ # Changelog fragments (per-PR documentation)
├── docs/ # Documentation
├── examples/
│ └── basic_usage.rs # Token management example
├── scripts/
│ ├── test-manual.sh # Manual end-to-end testing script
│ ├── bump-version.rs # Version bumping utility
│ ├── check-file-size.rs # File size validation
│ └── ... # Other CI/CD scripts
├── src/
│ ├── lib.rs # Library root — re-exports modules
│ ├── main.rs # Binary entry point — Cli dispatch + server setup
│ ├── cli.rs # `lino-arguments`-based CLI parser + subcommands
│ ├── config.rs # CLI/env/.lenv configuration
│ ├── crater.rs # Crater ForgeFed task provider
│ ├── oauth.rs # Claude Code OAuth credential reader
│ ├── accounts.rs # Multi-account router (round-robin/priority/least-used + cooldowns)
│ ├── storage.rs # Persistent token store (text Lino + binary backends)
│ ├── providers.rs # OpenAI-compatible provider store + encrypted secrets
│ ├── proxy.rs # Transparent API proxy with token swap, OpenAI shim, ops endpoints
│ ├── openai.rs # OpenAI <-> Anthropic translation helpers
│ ├── metrics.rs # Atomic counters, Prometheus rendering, JSON snapshots
│ └── token.rs # Custom JWT token management (la_sk_...)
├── tests/
│ └── integration_test.rs # Integration tests
├── Cargo.toml # Project configuration and dependencies
├── Dockerfile # Multi-stage Docker build
├── CHANGELOG.md # Project changelog
├── CONTRIBUTING.md # Contribution guidelines
├── LICENSE # Unlicense (public domain)
└── README.md # This file
Contributing
See CONTRIBUTING.md for development setup, coding guidelines, and the pull request process.