Skip to content

refactor(anthropic): exchange workload identity through the Anthropic SDK - #39948

Open
mateo-berri wants to merge 1 commit into
litellm_anthropic_wif_backendfrom
litellm_anthropic_wif_sdk
Open

mateo-berri wants to merge 1 commit into
litellm_anthropic_wif_backendfrom
litellm_anthropic_wif_sdk

Conversation

@mateo-berri

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

  • WorkloadIdentityCredentials and TokenCache from anthropic.lib.credentials run the exchange
  • LiteLLM keeps identity sources, host allowlist, SSL settings, redaction, metrics
  • anthropic pinned to 1.3.0 everywhere it is listed, added to proxy-dev

Stacked on #39935: the base branch is litellm_anthropic_wif_backend rather than litellm_internal_staging, so the diff here is only the SDK swap. Merge #39935 first, then retarget this one

User Flow

Before: a proxy admin runs a federated Anthropic deployment and nothing fails; the token exchange is LiteLLM's own HTTP code

  1. They configure a credential with anthropic_identity_source, the Console ids and no api_key, then start the proxy
  2. They send POST http://localhost:4000/v1/chat/completions with "model": "claude-haiku-4-5-wif" and get a normal completion back
  3. On the Anthropic side the token exchange arrives with no anthropic-beta header and a litellm/<version> User-Agent
  4. Their Prometheus litellm_service_* samples for the exchange carry call_type values cold_mint, advisory_refresh, mandatory_refresh and cache_hit

After: the same flow succeeds the same way, and the exchange is the SDK's

  1. They configure the credential exactly as before and start the proxy
  2. They send the same POST http://localhost:4000/v1/chat/completions and get the same completion back
  3. On the Anthropic side the token exchange now carries anthropic-beta: oauth-2025-04-20,oidc-federation-2026-04-01 and User-Agent: anthropic-python/1.3.0
  4. Their Prometheus samples carry call_type values cold_mint, refresh and cache_hit

Relevant issues

Builds on #39935, which fixes #28607

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Same config-only rig as #39935, run twice from this worktree on port 4000, one proxy after the other: Before at the backend branch commit, After at this branch's tip. Both boot from the same wif_config.yaml: litellm/proxy/dev_config.yaml plus two credentials on the LiteLLM-signed issuer and two deployments on them. anthropic-wif-issuer carries placeholder Console ids (no federation rule exists in this org, so the exchange gets as far as Anthropic's token endpoint, which validates the rule id), anthropic-wif-noids carries none. No ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN in the environment; ISSUER_SIGNING_KEY_PEM holds a P-256 private key

credential_list:
  - credential_name: anthropic-wif-issuer
    credential_info: {custom_llm_provider: anthropic}
    credential_values:
      anthropic_identity_source: internal_issuer
      anthropic_issuer_url: https://litellm.local/qa
      anthropic_issuer_subject: litellm-wif-demo
      anthropic_issuer_audience: https://api.anthropic.com
      anthropic_issuer_signing_key_ref: os.environ/ISSUER_SIGNING_KEY_PEM
      anthropic_organization_id: 00000000-0000-4000-8000-000000000000
      anthropic_federation_rule_id: fdrl_qa_fake
      anthropic_service_account_id: svac_qa_fake
  - credential_name: anthropic-wif-noids
    credential_info: {custom_llm_provider: anthropic}
    credential_values:
      anthropic_identity_source: internal_issuer
      anthropic_issuer_url: https://litellm.local/qa
      anthropic_issuer_subject: litellm-wif-demo
      anthropic_issuer_audience: https://api.anthropic.com
      anthropic_issuer_signing_key_ref: os.environ/ISSUER_SIGNING_KEY_PEM
model_list:
  - model_name: claude-haiku-4-5-wif
    litellm_params: {model: anthropic/claude-haiku-4-5, litellm_credential_name: anthropic-wif-issuer}
  - model_name: claude-haiku-4-5-wif-noids
    litellm_params: {model: anthropic/claude-haiku-4-5, litellm_credential_name: anthropic-wif-noids}
python litellm/proxy/proxy_cli.py --config wif_config.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 4000

Each LLM leg waits out the router cooldown from the previous refusal (65s) so the 401 is the proxy's own answer rather than a No deployments available 429. The two runs are expected to match: the point of the After run is that Anthropic's live token endpoint accepts the SDK's request (its beta header, JSON body and User-Agent) and answers with the same federation rule validation error the hand-rolled engine got, which LiteLLM maps to the same 401. A rejected header or body shape would surface as a different error

Before (b738e13)

JWKS export for a LiteLLM-signed credential

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/credentials/anthropic-wif-issuer/jwks -H 'Authorization: Bearer sk-1234'
  2. Observed

    {
      "keys": [
        {
          "crv": "P-256",
          "kty": "EC",
          "x": "KUomVOja0JLfZTgwC-FMuLxqyxqhDOv-bgweEWSps5I",
          "y": "H13ZcfvN7ZU-CIUZL05hkmJTLSlA-CFzRR3eK1qnRJA",
          "use": "sig",
          "alg": "ES256",
          "kid": "Zh8hScTjErHgjQaASp7TYVMjFFR3mabbVPo_WYW_UYQ"
        }
      ]
    }
    HTTP 200
    

/v1/chat/completions on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"error":{"message":"litellm.AuthenticationError: Anthropic workload identity federation failed. The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID. Received Model Group=claude-haiku-4-5-wif\nAvailable Model Group Fallbacks=None","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

/v1/messages on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/messages -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","max_tokens":32,"messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"type":"error","error":{"type":"authentication_error","message":"litellm.AuthenticationError: Anthropic workload identity federation failed. The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID. Received Model Group=claude-haiku-4-5-wif\nAvailable Model Group Fallbacks=None"}}
    HTTP 401
    

/v1/responses on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/responses -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","input":"Say hi in three words."}'
  2. Observed

    {"error":{"message":"litellm.AuthenticationError: Anthropic workload identity federation failed. The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID. Received Model Group=claude-haiku-4-5-wif\nAvailable Model Group Fallbacks=None","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Federated credential missing the Console ids

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif-noids","messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"error":{"message":"litellm.AuthenticationError: anthropic_identity_source is 'internal_issuer', but anthropic_federation_rule_id and anthropic_organization_id are not set. Copy them from the federation rule's detail page under Settings > Workload identity in the Claude Console, or set ANTHROPIC_FEDERATION_RULE_ID and ANTHROPIC_ORGANIZATION_ID.. Received Model Group=claude-haiku-4-5-wif-noids\nAvailable Model Group Fallbacks=None","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Federation field typed into a chat body

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","anthropic_federation_rule_id":"fdrl_attacker","messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"error":{"message":"Authentication Error, Rejected Request: anthropic_federation_rule_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

Federation field typed into POST /model/new

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/model/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model_name":"wif-inline-probe","litellm_params":{"model":"anthropic/claude-haiku-4-5","anthropic_federation_rule_id":"fdrl_attacker"}}'
  2. Observed

    {"error":{"message":"Authentication Error, Rejected Request: anthropic_federation_rule_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

After (d48181f)

JWKS export for a LiteLLM-signed credential

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/credentials/anthropic-wif-issuer/jwks -H 'Authorization: Bearer sk-1234'
  2. Observed

    {
      "keys": [
        {
          "crv": "P-256",
          "kty": "EC",
          "x": "KUomVOja0JLfZTgwC-FMuLxqyxqhDOv-bgweEWSps5I",
          "y": "H13ZcfvN7ZU-CIUZL05hkmJTLSlA-CFzRR3eK1qnRJA",
          "use": "sig",
          "alg": "ES256",
          "kid": "Zh8hScTjErHgjQaASp7TYVMjFFR3mabbVPo_WYW_UYQ"
        }
      ]
    }
    HTTP 200
    

/v1/chat/completions on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"error":{"message":"litellm.AuthenticationError: Anthropic workload identity federation failed. The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID. Received Model Group=claude-haiku-4-5-wif\nAvailable Model Group Fallbacks=None","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

/v1/messages on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/messages -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","max_tokens":32,"messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"type":"error","error":{"type":"authentication_error","message":"litellm.AuthenticationError: Anthropic workload identity federation failed. The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID. Received Model Group=claude-haiku-4-5-wif\nAvailable Model Group Fallbacks=None"}}
    HTTP 401
    

/v1/responses on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/responses -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","input":"Say hi in three words."}'
  2. Observed

    {"error":{"message":"litellm.AuthenticationError: Anthropic workload identity federation failed. The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID. Received Model Group=claude-haiku-4-5-wif\nAvailable Model Group Fallbacks=None","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Federated credential missing the Console ids

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif-noids","messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"error":{"message":"litellm.AuthenticationError: anthropic_identity_source is 'internal_issuer', but anthropic_federation_rule_id and anthropic_organization_id are not set. Copy them from the federation rule's detail page under Settings > Workload identity in the Claude Console, or set ANTHROPIC_FEDERATION_RULE_ID and ANTHROPIC_ORGANIZATION_ID.. Received Model Group=claude-haiku-4-5-wif-noids\nAvailable Model Group Fallbacks=None","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Federation field typed into a chat body

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","anthropic_federation_rule_id":"fdrl_attacker","messages":[{"role":"user","content":"Say hi in three words."}]}'
  2. Observed

    {"error":{"message":"Authentication Error, Rejected Request: anthropic_federation_rule_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

Federation field typed into POST /model/new

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' localhost:4000/model/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model_name":"wif-inline-probe","litellm_params":{"model":"anthropic/claude-haiku-4-5","anthropic_federation_rule_id":"fdrl_attacker"}}'
  2. Observed

    {"error":{"message":"Authentication Error, Rejected Request: anthropic_federation_rule_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

Type

Refactoring

Caveats (if any)

Medium

  • Advisory refresh runs inline now: inside the last 120s, one caller pays a round trip
    • The engine refreshed in the background; the SDK's TokenCache has no background path
  • The SDK retries once on a 401 with a forced refresh, so metrics record a failed then a successful cold_mint
  • call_type shrinks to cold_mint, refresh, cache_hit; dashboards keyed on advisory_refresh or mandatory_refresh need updating
  • No negative cache: a failing endpoint is retried on every request, the SDK only backs off 5s while a stale token still serves

Low

  • The exchange now carries the SDK's anthropic-beta and User-Agent headers
  • Credentials are sync only; the async path runs them in asyncio.to_thread
  • The SDK speaks httpx2, so the exchange client is built separately with LiteLLM's SSL settings mirrored onto it
  • anthropic stays an optional extra; a federated deployment without it gets an install hint
  • 1.3.0 is the newest release the 3 day exclude-newer window allows; 1.4.0 clears it on 2026-09-07
  • anthropic joins the proxy-dev group so .venv-typecheck resolves the SDK; without it the new module measured as 39 untyped errors in the basedpyright gate
  • tests/test_litellm/llms/__init__.py is new so the shared test harness imports under pytest's default import mode
  • tests/test_litellm/test_lazy_imports.py::test_star_import_exports_public_api fails on the base branch too under Python 3.13, unrelated to this change

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

… SDK

Replace the hand-rolled RFC 7523 engine in base_llm/auth/token_exchange.py
with WorkloadIdentityCredentials and TokenCache from anthropic.lib.credentials.
LiteLLM keeps what the SDK does not do: the identity sources, the exchange host
allowlist, its own SSL settings on the exchange client, RFC 6749 error
redaction with credential-echo scrubbing, and the Prometheus service metrics.
The redaction and the https-only URL check move to base_llm/auth/oauth_endpoint.py,
which the OpenAI client-credentials flow still shares

anthropic is pinned to 1.3.0 in the proxy-runtime extra, the ci group, and the
build_from_pip image, and joins the proxy-dev group so the basedpyright gate
resolves the SDK types instead of measuring the new module as untyped
@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.32967% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/anthropic/wif_exchange.py 95.47% 12 Missing ⚠️
litellm/llms/anthropic/wif.py 61.53% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces LiteLLM's Anthropic workload-identity exchange engine with Anthropic SDK credential helpers

  • Adds SDK-backed token caching, refresh, error mapping, redaction, metrics, and an httpx2 exchange client
  • Extracts shared OAuth endpoint validation and redaction helpers
  • Updates Anthropic dependencies and rewrites the WIF tests around the SDK transport
  • Deletes the provider-agnostic token-exchange engine and its public exports

Confidence Score: 4/5

The implementation looks behaviorally sound, but the explicit HTTP-handler and backwards-compatibility requirements must be satisfied before merging

No concrete runtime failure remains, but the dedicated client and removal of the existing public exchange API both violate repository requirements

Files Needing Attention: litellm/llms/anthropic/wif_exchange.py, litellm/llms/base_llm/auth/init.py, litellm/llms/base_llm/auth/token_exchange.py

Important Files Changed

Filename Overview
litellm/llms/anthropic/wif_exchange.py Adds the SDK-backed WIF exchange, cache, metrics bridge, redaction mapping, and a rule-violating dedicated HTTP client
litellm/llms/anthropic/wif.py Routes synchronous and asynchronous WIF token resolution through the new SDK-backed exchange
litellm/llms/base_llm/auth/oauth_endpoint.py Extracts shared HTTPS validation, response guards, and credential-aware OAuth error redaction
litellm/llms/base_llm/auth/init.py Removes public token-exchange exports without the compatibility path required by repository rules
litellm/llms/base_llm/auth/token_exchange.py Deletes the former provider-agnostic exchange engine and its import surface
pyproject.toml Raises Anthropic to 1.3.0-compatible ranges and adds it to proxy development dependencies
tests/test_litellm/llms/anthropic/test_wif_exchange.py Adds extensive coverage for SDK wire behavior, refreshes, caching, error redaction, metrics, and transport hardening

Reviews (1): Last reviewed commit: "refactor(anthropic): exchange workload i..." | Re-trigger Greptile

Comment on lines +204 to +216
def new_exchange_client() -> httpx2.Client:
"""The client the SDK would build for itself ignores LiteLLM's SSL settings, so the exchange
gets one built the way ``HTTPHandler`` builds its own: the same CA bundle, verification switch
and client certificate. Redirects stay off: only the bound base URL passed the host allowlist,
and a 3xx must not replay the assertion elsewhere."""
from litellm.llms.custom_httpx.http_handler import get_ssl_configuration

return httpx2.Client(
verify=get_ssl_configuration(),
cert=os.getenv("SSL_CERTIFICATE", litellm.ssl_certificate),
timeout=httpx2.Timeout(EXCHANGE_TIMEOUT_SECONDS, connect=EXCHANGE_CONNECT_TIMEOUT_SECONDS),
follow_redirects=False,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Custom HTTP client

This directly constructs httpx2.Client instead of reusing LiteLLM's HTTP handlers, violating a repository requirement that must be satisfied before merging

Rule Used: What: Block any PR which writes it's own custom ht... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +29 to 45
from litellm.llms.base_llm.auth.oauth_endpoint import (
MAX_RESPONSE_BYTES,
JwtBearerTokenExchangeEngine,
default_token_exchange_engine,
redact_oauth_error_body,
validate_token_endpoint_url,
)
from litellm.llms.base_llm.auth.types import (
AssertionReader,
AssertionSource,
AssertionSourceError,
BodyEncoding,
ExchangeError,
ExchangeResult,
InsecureTokenUrl,
MalformedTokenResponse,
MintedToken,
SyncTokenPoster,
TokenEndpointError,
TokenExchangeSpec,
TokenTransportError,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Public exports removed

This removes the public token-exchange API without a compatibility path or flag, violating the repository's backwards-compatibility requirement that must be satisfied before merging

Rule Used: What: avoid backwards-incompatible changes without... (source)

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

LiteLLM Rust workflow

Workflow completed successfully for d48181f49270676f3ba790dfab8811130e8eb8b6

View workflow run

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