Skip to content

feat(core): add OAuth support to the built-in Anthropic transformer#1408

Open
neatshell wants to merge 2 commits into
musistudio:mainfrom
neatshell:feat/anthropic_oauth
Open

feat(core): add OAuth support to the built-in Anthropic transformer#1408
neatshell wants to merge 2 commits into
musistudio:mainfrom
neatshell:feat/anthropic_oauth

Conversation

@neatshell
Copy link
Copy Markdown

@neatshell neatshell commented May 24, 2026

Summary

Adds first-class OAuth (Claude Code subscription) support to the built-in
Anthropic transformer in @CCR/core, so users with a Claude Code login
can route requests through CCR to api.anthropic.com by toggling a single
option on the built-in transformer:

"transformer": { "use": [["Anthropic", { "OAuth": true }]] }

What changed

  • packages/core/src/transformer/anthropic.transformer.ts

    • New OAuth option on the Anthropic transformer.
    • Resolves the access token in this order:
      1. CCR_OAUTH_TOKEN env var
      2. ~/.claude/.credentials.json (Linux / dev installs)
      3. macOS Keychain entry Claude Code-credentials (via security find-generic-password)
      4. The provider's api_key (fallback for explicit overrides)
    • Caches the resolved token for 60 s to avoid repeatedly invoking
      security / re-reading the credentials file on every request.
    • Sends Authorization: Bearer <token>, drops x-api-key, and appends
      oauth-2025-04-20 to the anthropic-beta header while preserving any
      existing beta flags from the upstream request.
    • Sanitizes the request body to the field set accepted by the Anthropic
      OAuth endpoint (model, messages, system, max_tokens, metadata,
      stop_sequences, stream, temperature, top_p, top_k, tools,
      tool_choice, thinking) and strips non-type keys from any nested
      cache_control objects, which the OAuth endpoint rejects.
  • packages/core/src/services/provider.ts

    • When the registry returns an instance (not a constructor) for a named
      transformer, instantiate a fresh one from its constructor using the
      provider's options. This is what makes
      ["Anthropic", { "OAuth": true }] actually pass OAuth: true through
      instead of silently reusing the default-constructed singleton.
  • packages/core/src/api/routes.ts

    • In the bypass/passthrough path, prefer the provider's own transformer
      instance over the global one when calling auth(), so per-provider
      options (like OAuth: true) are honored. Also forwards the current
      request headers as TransformerContext so the transformer can merge
      anthropic-beta correctly.
  • packages/core/{package.json,vitest.config.ts} +
    packages/core/src/transformer/__tests__/anthropic.transformer.test.ts

    • Adds vitest + vite-tsconfig-paths as dev deps and an npm test /
      npm run test:watch script.
    • Adds a unit test suite covering token resolution precedence, header
      rewriting, field/cache_control sanitization, and the non-OAuth
      UseBearer / default x-api-key paths.

Compatibility

  • Default behavior is unchanged: without OAuth: true the transformer
    behaves exactly as before (x-api-key, or Bearer when UseBearer: true).
  • No new runtime dependencies (uses Node built-ins fs / child_process).

How to use it

Edit ~/.claude-code-router/config.json and add a provider that uses the
built-in Anthropic transformer with OAuth: true:

{
   ...,
  "Providers": [
    {
      "name": "AnthropicOAuth",
      "api_base_url": "https://api.anthropic.com/v1/messages",
      "api_key": "unused",
      "models": [
        "claude-opus-4-7",
        "claude-sonnet-4-6",
        "claude-haiku-4-5-20251001"
      ],
      "transformer": { "use": [["Anthropic", { "OAuth": true }]] }
    }
  ],
  "Router": {
    "default":      "AnthropicOAuth,claude-sonnet-4-6",
    "background":   "AnthropicOAuth,claude-haiku-4-5-20251001",
    "think":        "AnthropicOAuth,claude-opus-4-7",
    "longContext":  "AnthropicOAuth,claude-sonnet-4-6",
    "webSearch":    "AnthropicOAuth,claude-sonnet-4-6",
    "longContextThreshold": 60000
  },
  ...
}

Notes:

  • api_key is required by the schema but ignored when OAuth: true and
    a token is found via env / credentials file / Keychain; set it to any
    placeholder (e.g. "unused").
  • api_base_url should point at the real Anthropic endpoint
    (https://api.anthropic.com/v1/messages).
  • On macOS, the token is read from the Keychain entry
    Claude Code-credentials that the Claude Code CLI creates on login.
    On Linux / WSL, it's read from ~/.claude/.credentials.json.
  • To override either, export CCR_OAUTH_TOKEN before starting CCR.

After editing the file, restart the router:

ccr restart

Test plan

  • pnpm --filter @CCR/core test — new vitest suite passes.
  • Manual: ccr code against claude-haiku-4-5-20251001 — non-streaming,
    streaming (SSE), tool use, and cache_control sanitization (request
    with cache_control: { type: "ephemeral", foo: "bar" } and an unknown
    top-level field returns 200; without the sanitizer the OAuth endpoint
    400s on the extra field).
  • Manual: same matrix against claude-sonnet-4-6.
  • Manual: same matrix against claude-opus-4-7.
  • Manual: existing non-OAuth Anthropic provider (x-api-key) still
    works unchanged with the same build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant