provider-claude-code
v0.1.14Claude Code (Pro/Max subscription) Messages API provider worker; implements provider::claude-code::stream and provider::claude-code::refresh_models behind llm-router, using OAuth credentials from the auth-credentials vault or ~/.claude/.credentials.json.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64
exact versions are immutable; binary and bundle artifacts are digest-pinned.
readme
open as markdownprovider-claude-code
Claude Pro/Max subscription provider worker behind
llm-router. It lets the
stack generate against a personal Claude Code subscription (billed to the plan)
instead of a pay-per-token API key, by reusing the OAuth credentials the Claude
Code CLI already stores locally and speaking Anthropic's Messages API with a
Bearer token. It is the Claude analog of
provider-openai-codex.
Implements the provider protocol from tech-specs/2026-06-agentic/llm-router.md:
provider::claude-code::stream (Messages SSE → AssistantMessageEvent frames
into a router-owned channel) and provider::claude-code::refresh_models (fetches
and reconciles the model catalog, with a curated fallback). For API-key
billing (teams/CI/production), use provider-anthropic
instead — that provider speaks the same API with ANTHROPIC_API_KEY.
⚠️ Terms-of-service caveat — local/personal dev only. This drives a personal Claude Pro/Max subscription outside the official Claude Code CLI: it sends the Claude Code identity system prompt and the
anthropic-beta: oauth-2025-04-20header soapi.anthropic.comaccepts the subscription OAuth token. That is plausibly against Anthropic's terms and may risk account action. Use it for local, personal development. For team/CI/production, use official API-key billing (provider-anthropic). You assume the risk.
Credentials
This worker is a dumb token consumer — login and refresh live out-of-band:
- Vault (intended authority): the
auth-credentialsworker (auth::get_token), populated by anoauth-claude-codesign-in flow. A near-expiry token triggers the vault-owned refresh (oauth::claude-code::refresh); this provider never calls the OAuth token endpoint itself. That worker/flow is out of scope here — this provider only consumes and triggers. The credential record it expects is{ type: "oauth", access_token, refresh_token?, expires_at?(seconds), provider_extra: { subscription_type?, scopes? }, refresh_fn: "oauth::claude-code::refresh" }. - Local dev fallback: when no vault is running, the worker reads the
claudeCLI's own credential (read-only — the CLI owns its refresh, including rotating the refresh token):${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.jsonand, on macOS, the "Claude Code-credentials" login Keychain item (viasecurity find-generic-password), taking whichever expires later — the macOS CLI stops updating the file once it uses the Keychain. TheclaudeAiOauth.expiresAtis epoch milliseconds; it is stored as seconds in the vault shape. Requires host access to those stores, so it does not apply to sandboxed/microVM-managed workers. On boot the worker also does a one-time, read-only import of that credential into the vault when the vault is present but empty (never written back).
API-key credentials are rejected — they belong on provider-anthropic under
provider id anthropic.
Behavior
- Registration: self-declares via
router::provider::registerwith backoff, and re-declares on therouter::readytrigger. It advertises dynamic model listing andcredential_env_var: None(OAuth-only); identity binds via theregistration_tokenpersisted in state (scopeprovider-claude-code). - Models: attempts the authenticated
GET /v1/modelsat startup, on explicit refresh, after router readiness, and every ~15 minutes. Picker-visible results become namespaced router ids (claude-code/). If the models endpoint rejects the subscription OAuth token (401/403) — or returns nothing — a small curated fallback slice is reconciled instead of blanking the catalog, so streaming still works. Transient failures preserve the router's last-known-good slice. Namespacing preventsAmbiguousModelcollisions withprovider-anthropic. - Request: Messages API —
messages,tools,max_tokens,stream: true, optional adaptivethinking+output_config.effort, and automatic prompt caching.systemis always an array whose first block is the Claude Code identity line (a wire-only artifact required by the subscription backend); an optional router-supplied harness prompt follows as a second block. Headers:authorization: Bearer,anthropic-version: 2023-06-01,anthropic-beta: oauth-2025-04-20. - SSE:
content_block_delta(text_delta→ text,thinking_delta→ thinking,input_json_delta→ tool calls),message_delta→ usage,message_stop→ terminal. Unknown event types are ignored (forward-compat). - Liveness / errors:
pingat least every 30s of silence; 401/403 →auth_expired, 429 →rate_limited, context-overflow →context_overflow, 5xx/network →transient, other 4xx →permanent. The router owns retry policy.
Running
Standard worker CLI: --url (engine WebSocket, default ws://127.0.0.1:49134,
or III_URL), --manifest (print the registry manifest and exit), --config
(accepted but ignored — this worker has no file-based config).
# ensure `claude` has signed in (dev): ~/.claude/.credentials.json or, on macOS, the Keychain
cargo run -- --url ws://127.0.0.1:49134The prompt-cache anchors can be disabled with PROVIDER_CLAUDE_CODE_CACHE=0.
When the router forwards system_sections, the identity block is followed by
one text block per section and the marker lands on the block flagged
cache_boundary (the frozen agent-profile prefix) instead of the last block,
whatever its size (Anthropic applies its per-model token minimum over the
whole prefix and silently skips a short one), so the per-session tail no
longer invalidates the shared entry. That boundary
block and the tools marker use the 1-hour cache (ttl: "1h"); the messages
anchor stays at 5 minutes. PROVIDER_CLAUDE_CODE_CACHE_TTL=5m restores the
5-minute cache everywhere.
Tests
cargo test # unit modules, model-discovery/upstream TCP stubs, schema goldensRegenerate the wire-schema goldens with UPDATE_GOLDENS=1 cargo test.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
not configured: sign in with Claude Code … |
no vault credential and no local claude credential (~/.claude/.credentials.json, or the Keychain on macOS) |
run the oauth-claude-code sign-in, or claude (login) |
| local fallback is used on each request | auth-credentials vault not running |
start the vault for shared/refreshing credentials, or keep relying on the local claude credential fallback |
requires a Claude Pro/Max OAuth login … API keys belong on provider-anthropic |
credential is an API key | this provider is OAuth-only; use provider-anthropic for keys |
auth_expired on every request |
the local claude token expired and no refresh worker is registered |
run claude once to refresh it, or register the oauth-claude-code refresh flow |
| catalog shows only the curated fallback models | GET /v1/models rejected the OAuth bearer, or the models endpoint is unreachable |
expected — the subscription token may not be accepted on /v1/models; streaming still works, and the live list returns once the endpoint accepts the token |
| upstream 401 despite a valid token | the request no longer resembles Claude Code | keep the identity system block first and the anthropic-beta: oauth-2025-04-20 header; a future backend change may require a user-agent compat header |
| model routes ambiguously | a claude-code/* id collided with another provider |
keep ids namespaced; or pin provider: "claude-code" |