provider-claude-code
v0.1.7Claude 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 · x86
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
${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.jsondirectly (read-only — theclaudeCLI owns that file's refresh, including rotating the refresh token). The file'sclaudeAiOauth.expiresAtis epoch milliseconds; it is stored as seconds in the vault shape. Requires host access to that path, so it does not apply to sandboxed/microVM-managed workers, and macOS is not covered (Claude Code stores credentials in the Keychain there, not a file). On boot the worker also does a one-time, read-only import of that file 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 so ~/.claude/.credentials.json exists (dev)
cargo run -- --url ws://127.0.0.1:49134The prompt-cache anchors can be disabled with PROVIDER_CLAUDE_CODE_CACHE=0.
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 readable ~/.claude/.credentials.json |
run the oauth-claude-code sign-in, or claude (login) so ~/.claude/.credentials.json exists; on macOS the Keychain store is not read |
| 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/.credentials.json 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 .credentials.json token expired and no refresh worker is registered |
run claude once to refresh the file, 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" |