A Rust-based API gateway that proxies Anthropic (Claude) APIs through a Claude MAX OAuth session, providing multi-tenant access via custom-issued tokens.
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).
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.
- Rust 1.70+ (for building from source)
- Docker (for containerized deployment)
- A Claude MAX subscription with an active Claude Code OAuth session
git clone https://github.com/link-assistant/router.git
cd router
cargo build --releaseThe binary will be at target/release/link-assistant-router.
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)
claudeThe router searches these files in order:
credentials.json.credentials.jsonauth.jsonoauth.jsonconfig.json
Two on-disk layouts are supported automatically:
-
Nested (the format real Claude Code writes to
~/.claude/.credentials.json):{ "claudeAiOauth": { "accessToken": "sk-ant-oat01-...", "refreshToken": "sk-ant-ort01-...", "expiresAt": 1781050618000, "scopes": ["user:inference", "user:profile"], "subscriptionType": "max" } } -
Flat (convenient for tests and minimal setups):
{ "accessToken": "sk-ant-oat01-..." }
For the nested layout the router reads accessToken and expiresAt from inside
claudeAiOauth. For the flat layout it reads accessToken (or access_token,
oauthToken, oauth_token) from the top level of the first file found. The
file is only ever read — the router never writes back to or deletes your
credential files.
# Required: set the JWT signing secret
export TOKEN_SECRET=your-secure-secret-here
# Optional: customize port (default: 8080)
export ROUTER_PORT=8080
# Optional: set Claude Code home directory (default: ~/.claude)
export CLAUDE_CODE_HOME=~/.claude
# Optional: override upstream URL (default: https://api.anthropic.com)
export UPSTREAM_BASE_URL=https://api.anthropic.com
# Start the router
./target/release/link-assistant-routerYou 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
curl -s -X POST http://localhost:8080/api/tokens \
-H "Content-Type: application/json" \
-d '{"ttl_hours": 24, "label": "my-dev-token"}' | jq .Response:
{
"token": "la_sk_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"ttl_hours": 24,
"label": "my-dev-token"
}Save the token value for use in API requests.
# Use the custom token to make requests through the router
curl -s http://localhost:8080/api/latest/anthropic/v1/messages \
-H "Authorization: Bearer la_sk_eyJ0eXAi..." \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-4-20250514",
"max_tokens": 100,
"messages": [{"role": "user", "content": "Hello!"}]
}' | jq .The router will:
- Validate the
la_sk_...token - Replace it with the real OAuth token from the Claude Code session
- Inject the upstream headers Claude MAX OAuth requires —
anthropic-version(default2023-06-01when the client omits it) and theanthropic-beta: oauth-2025-04-20flag (merged with any betas the client already sent) - Forward the request to
https://api.anthropic.com/v1/messages - Stream the response back to the client
Because the router injects these headers itself, a client only needs to send the
la_sk_... token — it never needs the real OAuth token, the OAuth beta flag, or
even an anthropic-version header.
The primary use case is routing Claude Code through the proxy so multiple users can share a single Claude MAX subscription.
export TOKEN_SECRET=your-secure-secret
./target/release/link-assistant-router# Issue a token for user Alice
curl -s -X POST http://localhost:8080/api/tokens \
-H "Content-Type: application/json" \
-d '{"ttl_hours": 168, "label": "alice"}' | jq -r '.token'
# Issue a token for user Bob
curl -s -X POST http://localhost:8080/api/tokens \
-H "Content-Type: application/json" \
-d '{"ttl_hours": 168, "label": "bob"}' | jq -r '.token'# Set the base URL to point to the router
export ANTHROPIC_BASE_URL=http://your-server:8080/api/latest/anthropic
# Set the custom token as the API key
export ANTHROPIC_API_KEY=la_sk_eyJ0eXAi...
# Run Claude Code normally — all requests go through the router
claudeClaude Code will work exactly as normal, with all requests transparently proxied through the router.
| 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 |
| 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 |
| 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.
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=stripeWhen 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.
| 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 |
Issue a new custom JWT token.
Request body:
{
"ttl_hours": 24,
"label": "my-token"
}| Field | Type | Default | Description |
|---|---|---|---|
ttl_hours |
integer | 24 | Token lifetime in hours |
label |
string | "" |
Optional human-readable label |
Response:
{
"token": "la_sk_eyJ0eXAi...",
"ttl_hours": 24,
"label": "my-token"
}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:
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "Token has expired"
}
}| Status | Condition |
|---|---|
| 401 | Missing or invalid/expired token |
| 403 | Token has been revoked |
| 502 | OAuth token unavailable or upstream request failed |
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.
| 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 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 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 |
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.
link-assistant-router providers add \
--name litellm \
--base-url http://litellm:4000/v1 \
--model claude-sonnet \
--models claude-sonnet,gpt-4o \
--api-key "$LITELLM_MASTER_KEY"
link-assistant-router providers list
link-assistant-router providers show litellm
link-assistant-router providers remove litellmProvider 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:
{
"name": "litellm",
"kind": "openai-compatible",
"base_url": "http://litellm:4000/v1",
"default_model": "claude-sonnet",
"models": ["claude-sonnet", "gpt-4o"],
"api_key_env": "LITELLM_MASTER_KEY"
}| 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 |
| 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 |
# Default: starts the HTTP server (same as `serve`).
link-assistant-router
# Issue / list / revoke / show tokens locally (no HTTP needed):
link-assistant-router tokens issue --ttl-hours 168 --label alice
# ...optionally cap how many upstream requests the token may make:
link-assistant-router tokens issue --ttl-hours 168 --label alice --max-requests 500
link-assistant-router tokens list
link-assistant-router tokens revoke <id>
link-assistant-router tokens show <id>
# Inspect configured accounts:
link-assistant-router accounts list
# Manage OpenAI-compatible upstream providers:
link-assistant-router providers add --name litellm --base-url http://litellm:4000/v1 --model claude-sonnet
link-assistant-router providers import providers.lenv
link-assistant-router providers list
# Print resolved configuration + credential / store probes:
link-assistant-router doctorThe router uses tracing with the RUST_LOG environment variable:
# Default: info level
RUST_LOG=info ./target/release/link-assistant-router
# Debug level for detailed request tracing
RUST_LOG=debug ./target/release/link-assistant-router
# Trace level for maximum verbosity
RUST_LOG=trace ./target/release/link-assistant-routerdocker build -t link-assistant/router .docker run -d \
-p 8080:8080 \
-e TOKEN_SECRET=your-secure-secret \
-v /path/to/claude-code-home:/data/claude:ro \
link-assistant/routerThe Dockerfile sets CLAUDE_CODE_HOME=/data/claude by default, so mount your Claude Code session directory to /data/claude.
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-stoppedTo deploy on a VPS (e.g., Ubuntu):
# 1. Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# 2. Clone and build
git clone https://github.com/link-assistant/router.git
cd router
cargo build --release
# 3. Set up Claude Code credentials on the VPS
# (log in with Claude Code to create session files)
claude
# 4. Create a systemd service (optional, for auto-start)
sudo tee /etc/systemd/system/link-assistant-router.service > /dev/null <<EOF
[Unit]
Description=Link.Assistant.Router
After=network.target
[Service]
Type=simple
User=$USER
Environment=TOKEN_SECRET=your-secure-secret
Environment=ROUTER_PORT=8080
Environment=CLAUDE_CODE_HOME=/home/$USER/.claude
ExecStart=/home/$USER/router/target/release/link-assistant-router
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable link-assistant-router
sudo systemctl start link-assistant-router
# 5. Check status
sudo systemctl status link-assistant-router
journalctl -u link-assistant-router -fReady-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.
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.
The router uses JWT-based custom tokens with the la_sk_ prefix.
- Issue:
POST /api/tokenscreates a signed JWT with a UUID subject, expiration, optional label, and an optional per-token request budget - Validate: Each proxy request extracts the
Authorization: Bearer la_sk_...header, strips the prefix, and verifies the JWT signature and expiration - Meter: When the token carries a request budget, each forwarded request increments a persisted
used_requestscounter; once it reachesmax_requeststhe router returns429 Too Many Requestsinstead of forwarding upstream - Revoke: Tokens can be revoked by their subject ID. Records (including the revoked flag and usage counter) are written to the persistent token store, so revocations and usage survive restarts
Tokens are standard HS256 JWTs with the la_sk_ prefix. The JWT payload contains:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"iat": 1710806400,
"exp": 1710892800,
"label": "my-token"
}Each token can carry an optional cap on the number of upstream requests it may make. This lets you hand a scoped token to a separate task or agent and bound how much of your Claude MAX subscription that task can consume, without ever exposing the real OAuth credential.
# CLI: issue a token limited to 100 upstream requests
link-assistant-router tokens issue --ttl-hours 24 --label scoped-agent --max-requests 100
# HTTP: same, via the admin endpoint
curl -s -X POST http://localhost:8080/api/tokens \
-H "Content-Type: application/json" \
-d '{"ttl_hours": 24, "label": "scoped-agent", "max_requests": 100}' | jq .- Omitting
--max-requests/max_requestsleaves the token unlimited. - Usage is counted per forwarded request and persisted in the token store, so the budget is enforced across restarts.
- When the budget is exhausted the router responds with
429 Too Many Requestsand arate_limit_errorbody ({"error":{"message":"Token has reached its request limit",...}}) instead of forwarding upstream. tokens listshows arequestscolumn asused/max(e.g.42/100), orused/-for unlimited tokens.
- 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) - Pair short TTLs with
max_requeststo give each task a tightly scoped, self-expiring credential
cargo testThis 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
# Unit tests only
cargo test --lib
# Integration tests only
cargo test --test integration_test
# A specific test
cargo test test_token_roundtrip
# With verbose output
cargo test -- --nocapture# Check formatting
cargo fmt --check
# Run Clippy lints
cargo clippy --all-targets --all-features
# All checks together
cargo fmt --check && cargo clippy --all-targets --all-features && cargo testUse the provided script to test the router locally:
# Make the script executable
chmod +x scripts/test-manual.sh
# Run manual tests (starts the router, issues a token, tests endpoints)
./scripts/test-manual.shOr test manually step by step:
# Terminal 1: Start the router with a test credential file
mkdir -p /tmp/test-claude
echo '{"accessToken": "test-oauth-token"}' > /tmp/test-claude/credentials.json
export TOKEN_SECRET=test-secret
export CLAUDE_CODE_HOME=/tmp/test-claude
export UPSTREAM_BASE_URL=https://api.anthropic.com
cargo run
# Terminal 2: Test the endpoints
# 1. Health check
curl -s http://localhost:8080/health
# Expected: ok
# 2. Issue a token
TOKEN=$(curl -s -X POST http://localhost:8080/api/tokens \
-H "Content-Type: application/json" \
-d '{"ttl_hours": 1, "label": "test"}' | jq -r '.token')
echo "Token: $TOKEN"
# 3. Test proxy with token (will get auth error from Anthropic since test-oauth-token is not real)
curl -s http://localhost:8080/api/latest/anthropic/v1/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model": "claude-sonnet-4-20250514", "max_tokens": 10, "messages": [{"role": "user", "content": "Hi"}]}' | jq .
# 4. Test without token (should get 401)
curl -s http://localhost:8080/api/latest/anthropic/v1/messages | jq .
# 5. Test with invalid token (should get 401)
curl -s http://localhost:8080/api/latest/anthropic/v1/messages \
-H "Authorization: Bearer la_sk_invalid" | jq .cargo run --example basic_usageThis demonstrates token issuance, validation, and revocation programmatically.
.
├── .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
See CONTRIBUTING.md for development setup, coding guidelines, and the pull request process.