Skip to content

feat(anthropic): workload identity federation and pluggable identity sources - #44448

Merged
mateo-berri merged 33 commits into
mainfrom
litellm_anthropic_wif_backend_devin
Oct 4, 2026
Merged

mateo-berri merged 33 commits into
mainfrom
litellm_anthropic_wif_backend_devin

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Backend half of #38818 (itself the internal copy of the fork PR #38013 by @derhornspieler), targeting main. Everything under litellm/, tests/ and terraform/ from that PR is here; the dashboard (ui/), the credential form variants and the POST /provider/models/discover endpoint are left out and follow in a separate PR, so this PR ships the config.yaml, env var and POST /credentials setup paths only

TLDR

Supersedes #39935 (identical commits, same tip fce9d8f)

Problem this solves:

  • Anthropic-direct auth requires a long-lived static sk-ant key
  • Anthropic shipped Workload Identity Federation, but LiteLLM had no support
  • Vertex and Azure already federate here, Anthropic could not

How it solves it:

  • Shared RFC 7523 JWT-bearer token exchange engine, Anthropic is its first consumer
  • Short-lived sk-ant-oat01 tokens minted from an OIDC assertion
  • Four ways to obtain that assertion: token file, env token, LiteLLM-signed issuer, Keycloak
  • Set up through config.yaml, env vars, or a named credential; no dashboard changes
  • GET /credentials/{name}/jwks exports the public key set of a LiteLLM-signed credential for the Console
  • Every Anthropic surface uses it: chat, /v1/messages, files, batches, skills, passthrough, token counting
  • Federation config is server-owned: refused in a request body, pinned to api.anthropic.com unless an operator allowlists a gateway, and only a proxy admin can put a deployment into federation or change its credentials
  • /health never displays the federation token, the secret references, or the token file paths
  • Token exchange and token cache outcomes are exported as the litellm_anthropic_wif_* and litellm_anthropic_wif_cache_* Prometheus families and charted in the all-metrics Grafana dashboard
  • The team admin gate decrypts deployments stored in the database, so it covers those too
  • Refusals on the credential endpoints keep the proxy's usual error shape
  • PATCH /credentials/{name} runs the same proxy-admin gate over federation fields it copies from a deployment named through model_id, matching POST /credentials
  • OpenAI federation from feat(proxy): configure OpenAI workload identity federation from the Add Model and LLM Credentials forms #39613 (LIT-6869) rides along on the backend side: the identity trio is read per deployment or credential before the env vars, and chat, streaming, Responses and embeddings use the exchanged token; the trio is server-owned the same way

User Flow

Before: a platform team with a no-static-secrets policy cannot point LiteLLM at an Anthropic federation rule

  1. They declare model: anthropic/claude-haiku-4-5 in config.yaml with no api_key, and set the federation ids on the proxy's environment
  2. They send POST http://localhost:4000/v1/chat/completions with {"model": "claude-haiku-4-5-wif", "messages": [...]}
  3. The proxy answers 401 Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params
  4. Because there is no federation setting to guard, any team admin who can manage a team-scoped deployment can already set every field the deployment does have, and a caller can put any Anthropic field they like in a request body

After: the same team configures the provider once in config.yaml and every deployment behind it authenticates without a static key

  1. In the Claude Console they create a federation issuer, a service account, and a rule, which yields an fdrl_..., an org id, and an svac_...
  2. They add a credential_list entry with custom_llm_provider: anthropic and the identity source's fields, secrets as os.environ/... references, plus the three Console ids, and point deployments at it through litellm_credential_name (or set ANTHROPIC_IDENTITY_SOURCE and the ANTHROPIC_* ids in the environment with no credential at all)
  3. For the LiteLLM-signed issuer they send GET http://localhost:4000/credentials/anthropic-wif-issuer/jwks with the admin key and get 200 with the public key set, which they paste into the Console's issuer definition
  4. They send the same POST http://localhost:4000/v1/chat/completions and get 200 with the model's reply, minted from a short-lived token rather than a static key; the same holds for POST /v1/messages and POST /v1/responses, and POST http://localhost:4000/v1/messages/count_tokens answers Anthropic's own count for the body instead of the proxy's local estimate
  5. If the credential has the issuer fields but not the Console ids yet, and the proxy environment carries no ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN, the same POST answers 401 naming anthropic_organization_id and anthropic_federation_rule_id instead of silently trying a static key; with either of those set the static key answers instead, since it outranks federation
  6. Anyone, team admin or proxy admin alike, who puts a federation field straight into a POST http://localhost:4000/v1/chat/completions or POST http://localhost:4000/model/new body is refused with 401 naming the field, because these fields choose which server secret is read and where it is sent; a team admin cannot reach POST http://localhost:4000/credentials at all (proxy admin only)

Relevant issues

Fixes #28607

Docs: BerriAI/litellm-docs#1000 documents the config.yaml and environment flow this PR ships

Linear ticket

Resolves LIT-6107

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

Config-only rig, no dashboard. Two proxy pairs, every proxy in its own worktree and venv with 2 uvicorn workers, STORE_MODEL_IN_DB=True against a Postgres of its own (so POST /model/new persists), LITELLM_MASTER_KEY=sk-1234 under LITELLM_DANGEROUSLY_PERMIT_WEAK_OR_UNSET_MASTER_KEY=true, and a LITELLM_LICENSE so no premium gate gets in the way. The first pair boots from wif_config.yaml (litellm/proxy/dev_config.yaml plus the block below): Before at b024950 (the merge base) on port 60131, After at fce9d8f on port 60132. anthropic-wif-issuer names a real federation issuer, federation rule and service account in a Claude Console organization: the issuer was registered with the JWKS the first case exports, the rule is enabled for all workspaces, so the credential also names the workspace to mint for, and the only workspace the service account belongs to is the organization's default workspace, which GET /v1/organizations/workspaces does not list (its id comes from GET /v1/organizations/service_accounts/{id}/workspaces). A direct exchange with the same assertion got Anthropic's opaque 401 without workspace_id and 200 with it. anthropic-wif-noids carries no Console ids. The third deployment points at a local nginx on 127.0.0.1:60135 that forwards to api.anthropic.com, standing in for a customer's egress gateway, and the exchange host is allowlisted as host:port. No ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN in either environment and no .env in either worktree, since proxy_cli.py loads one when present and a static key would silently win; ISSUER_SIGNING_KEY_PEM holds the P-256 private key behind that JWKS. The After side's chat, messages, responses and gateway calls below are real federated calls: the proxy signs its own assertion, exchanges it for an sk-ant-oat01 token and the model answers. The count-tokens cases go through POST /v1/messages/count_tokens, the route that reaches the provider's counter (POST /utils/token_counter always answers the local estimate), with a body carrying a system prompt and one tool: the local estimate is 82 and a direct call to Anthropic's own count-tokens endpoint for that body returns 590

The four cases from /health through the credential PATCH need the database-backed team flow, so they run on the second pair booted the same way (Before on port 60133, After on port 60134) from rig_config.yaml, which adds the wif-inline-token-cfg deployment below and sets litellm_key_header_name: X-Team-Key. On each side the team admin key comes from POST /team/new {"team_alias":"rig-team"}, then POST /team/member_add with {"team_id":"<team>","member":{"user_id":"rig-team-admin","role":"admin"}}, then POST /key/generate {"user_id":"rig-team-admin","team_id":"<team>","key_alias":"rig-team-admin"}; $TEAM_ADMIN_KEY below is that key. The blocked case repeats those three calls for a second team so the aliases stay unique, and $TEAM2_ADMIN_KEY is that team's admin key. The credential PATCH case creates an internal user with POST /user/new {"user_id":"gate-internal-user","user_role":"internal_user"} and POST /key/generate {"user_id":"gate-internal-user","key_alias":"gate-internal-user","allowed_routes":["/credentials*"]}; $USER_KEY below is that key, and $MID is the id the first step of that case reads for the wif-inline-token-cfg deployment

The last two cases run on a third pair booted from wif_config_at.yaml below, a single claude-haiku-4-5-auth-token deployment of anthropic/claude-haiku-4-5 with no api_key and no credential: Before on port 60151, After on port 60152, same flags and a Postgres per side. That pair's environment holds ANTHROPIC_AUTH_TOKEN set to an Anthropic API key and no ANTHROPIC_API_KEY, the shape a customer gets from a token-based login or a gateway that hands out bearer tokens

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-4ec7c603
      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: d0081f2b-d961-4aab-9e4c-71be7e04e670
      anthropic_federation_rule_id: fdrl_01C41Y5CUS9f4X4sz53BL5N4
      anthropic_service_account_id: svac_01EVRMivmiYBKpFAzKrnvV7d
      anthropic_federation_workspace_id: wrkspc_01VeV13pBTuSwGCaqF5sq1PZ
  - 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-4ec7c603
      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}
  - model_name: claude-haiku-4-5-wif-gateway
    litellm_params: {model: anthropic/claude-haiku-4-5, litellm_credential_name: anthropic-wif-issuer, api_base: http://localhost:60135}
server {
  listen 127.0.0.1:60135;
  location / {
    proxy_pass https://api.anthropic.com;
    proxy_set_header Host api.anthropic.com;
    proxy_ssl_server_name on;
    proxy_ssl_name api.anthropic.com;
    proxy_http_version 1.1;
  }
}
nginx -c "$PWD/nginx.conf"
export LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS=localhost:60135
export LITELLM_MASTER_KEY=sk-1234 LITELLM_DANGEROUSLY_PERMIT_WEAK_OR_UNSET_MASTER_KEY=true STORE_MODEL_IN_DB=True
# Before, merge base b024950353 checked out in its own worktree, DATABASE_URL on its own Postgres
python litellm/proxy/proxy_cli.py --config wif_config.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 60131
# After, this branch at fce9d8f903, same again
python litellm/proxy/proxy_cli.py --config wif_config.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 60132
# Second pair for the health, team admin, blocked and credential PATCH cases, same flags and a Postgres per side
python litellm/proxy/proxy_cli.py --config rig_config.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 60133
python litellm/proxy/proxy_cli.py --config rig_config.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 60134
# Third pair for the two count-tokens cases with only ANTHROPIC_AUTH_TOKEN set, same flags and a Postgres per side
export ANTHROPIC_AUTH_TOKEN=<an Anthropic API key>; unset ANTHROPIC_API_KEY
python litellm/proxy/proxy_cli.py --config wif_config_at.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 60151
python litellm/proxy/proxy_cli.py --config wif_config_at.yaml --detailed_debug --use_v2_migration_resolver --num_workers 2 --port 60152
# rig_config.yaml, the deployment the /health case reads
- model_name: wif-inline-token-cfg
  litellm_params:
    model: anthropic/claude-haiku-4-5
    anthropic_identity_token: eyJfake.fake.fake
    anthropic_organization_id: d0081f2b-d961-4aab-9e4c-71be7e04e670
    anthropic_federation_rule_id: fdrl_01C41Y5CUS9f4X4sz53BL5N4
    anthropic_service_account_id: svac_01EVRMivmiYBKpFAzKrnvV7d
    anthropic_federation_workspace_id: wrkspc_01VeV13pBTuSwGCaqF5sq1PZ
# wif_config_at.yaml, the whole model list of the third pair
model_list:
- model_name: claude-haiku-4-5-auth-token
  litellm_params:
    model: anthropic/claude-haiku-4-5

Before (b024950)

JWKS export for a LiteLLM-signed credential

  1. Run

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

    {"detail":"Method Not Allowed"}
    HTTP 405
    

/v1/chat/completions on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60131/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: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars\n\nLiteLLM: model group 'claude-haiku-4-5-wif' failed with the error above. No fallback was attempted.","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

/v1/messages/count_tokens on a federated deployment

  1. Run (the federated deployment counts a body with a system prompt and a tool)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60131/v1/messages/count_tokens -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","system":"You are a terse assistant.","messages":[{"role":"user","content":"Say hi in three words."}],"tools":[{"name":"get_weather","description":"Get the current weather in a given location","input_schema":{"type":"object","properties":{"location":{"type":"string","description":"City and state, e.g. San Francisco, CA"}},"required":["location"]}}]}'
  2. Observed (no credential reaches Anthropic, so the proxy answers its local estimate: 82 tokens for a body Anthropic itself counts at 590)

    {"input_tokens":82}
    HTTP 200
    

/v1/messages on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60131/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: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars\n\nLiteLLM: model group 'claude-haiku-4-5-wif' failed with the error above. No fallback was attempted."}}
    HTTP 401
    

/v1/responses on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60131/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: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars\n\nLiteLLM: model group 'claude-haiku-4-5-wif' failed with the error above. No fallback was attempted.","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

/v1/chat/completions through an egress gateway allowlisted as host:port

  1. Run

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

    {"error":{"message":"litellm.AuthenticationError: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars\n\nLiteLLM: model group 'claude-haiku-4-5-wif-gateway' failed with the error above. No fallback was attempted.","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Federated credential missing the Console ids

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60131/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: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars\n\nLiteLLM: model group 'claude-haiku-4-5-wif-noids' failed with the error above. No fallback was attempted.","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' http://localhost:60131/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":"litellm.AuthenticationError: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars\n\nLiteLLM: model group 'claude-haiku-4-5-wif' failed with the error above. No fallback was attempted.","type":"authentication_error","param":null,"code":"401"}}
    HTTP 401
    

Federation fields typed into POST /model/new, next to the sanctioned credential

  1. Run (a federation field straight in the deployment's litellm_params)

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

    {"model_id":"de2e156f-d284-426d-b361-118234845f65","model_name":"wif-inline-probe-before-b024950353","litellm_params":{"model":"fQSM4Rah6yZ390EPGymzUYsJ6q2vyFO_j1iEU7_xMZL1vZLoKdEqBApVfBFYzNRRZ3tlA62_Zakmk6hAVKmbY-iA","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"anthropic_federation_rule_id":"7rgINm01swNgq0c-kc0pPs5HGSYqNSv-U4eB9wdGg5wZ0KXa9Si2pi5x8rTdz5AzO5T_fLo=","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"de2e156f-d284-426d-b361-118234845f65","db_model":false,"member_auto_router":false},"blocked":false,"created_at":"2026-10-03T20:38:20.091000Z","created_by":"default_user_id","updated_at":"2026-10-03T20:38:20.091000Z","updated_by":"default_user_id"}
    HTTP 200
    
  3. Run (the same proxy admin attaches the stored federated credential instead)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60131/model/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model_name":"wif-cred-probe-before-b024950353","litellm_params":{"model":"anthropic/claude-haiku-4-5","litellm_credential_name":"anthropic-wif-issuer"}}'
  4. Observed

    {"model_id":"8b22a2ca-353b-43b1-8c45-ec1fc465d767","model_name":"wif-cred-probe-before-b024950353","litellm_params":{"model":"9MU-eOt8QoiOFBVzmlLPhN5TJt-CBQM2flBaSZaAy9hJJjUttvH7tVxY1OTQSfGk0tr2sYRrRYZ8RYcH1MNzw29H","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"giZe8hM4ZCctaSm2FhN9TEjijjYZyNutZ7RJ6czogSXwWZB5Lr02tHJqYTj0E61PS1D_1GEQVCP64COD","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"8b22a2ca-353b-43b1-8c45-ec1fc465d767","db_model":false,"member_auto_router":false},"blocked":false,"created_at":"2026-10-03T20:38:20.180000Z","created_by":"default_user_id","updated_at":"2026-10-03T20:38:20.180000Z","updated_by":"default_user_id"}
    HTTP 200
    
  5. Run (which of the two the proxy now serves)

    curl -s http://localhost:60131/model/info -H "Authorization: Bearer sk-1234" | grep -oE 'wif-(inline|cred)-probe-before-b024950353' | sort -u
  6. Observed

    wif-cred-probe-before-b024950353
    wif-inline-probe-before-b024950353
    

GET /health with a team admin key on a deployment carrying an inline identity token

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' 'http://localhost:60133/health?model=wif-inline-token-cfg' -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY"
  2. Observed (the raw identity token and the Console ids come back to a team admin; the error field's stack trace and the raw request dict are trimmed)

    {"healthy_endpoints":[],"unhealthy_endpoints":[{"model":"anthropic/claude-haiku-4-5","error":"litellm.AuthenticationError: Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in your environment vars","model_id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","exception_status":401}],"healthy_count":0,"unhealthy_count":1}
    HTTP 503
    

Team admin edits on a federated team deployment

  1. Run (the proxy admin creates the team-scoped federated deployment)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60133/model/new -H 'X-Team-Key: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model_name":"team-wif-p2","litellm_params":{"model":"anthropic/claude-haiku-4-5","litellm_credential_name":"anthropic-wif-issuer"},"model_info":{"team_id":"db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe"}}'
  2. Observed

    {"model_id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","model_name":"model_name_db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe_5cda73f2-2ec9-4386-992d-bfac7d34e692","litellm_params":{"model":"bDGcvnkUG66QAnPD6vTSLNdsLfB5GEZyjEuMFHhXh4uRHJko4oUivxdShrbgBcIHvEtve7-5kvLsdPBFGbk_plfT","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"7MgehV-sSoHmB0bDjppV5KzY04_ClD7FZ89KMz-ZWd6pFzHhra8hJyE5lDwINiSWqAyXej1fQF6xk732","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","team_id":"db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe","db_model":false,"member_auto_router":false,"team_public_model_name":"team-wif-p2"},"blocked":false,"created_at":"2026-10-03T23:29:07.451000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:07.451000Z","updated_by":"default_user_id"}
    HTTP 200
    
  3. Run (the team admin sets rpm on it)

    curl -s -w '\nHTTP %{http_code}\n' -X PATCH http://localhost:60133/model/0d62639d-3d82-4e69-8c5f-80afab007aa6/update -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"litellm_params":{"rpm":5}}'
  4. Observed (the edit lands, it sets no federation field)

    {"model_id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","model_name":"model_name_db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe_5cda73f2-2ec9-4386-992d-bfac7d34e692","litellm_params":{"rpm":5,"model":"bDGcvnkUG66QAnPD6vTSLNdsLfB5GEZyjEuMFHhXh4uRHJko4oUivxdShrbgBcIHvEtve7-5kvLsdPBFGbk_plfT","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"7MgehV-sSoHmB0bDjppV5KzY04_ClD7FZ89KMz-ZWd6pFzHhra8hJyE5lDwINiSWqAyXej1fQF6xk732","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","team_id":"db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe","db_model":false,"member_auto_router":false,"team_public_model_name":"team-wif-p2"},"blocked":false,"created_at":"2026-10-03T23:29:07.451000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:07.646000Z","updated_by":"rig-team-admin"}
    HTTP 200
    
  5. Run (the same team admin points it at a static key instead)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60133/model/update -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"model_name":"team-wif-p2","litellm_params":{"model":"anthropic/claude-haiku-4-5","api_key":"sk-ant-bogus"},"model_info":{"id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","team_id":"db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe"}}'
  6. Observed (the credential swap lands)

    {"model_id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","model_name":"model_name_db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe_5cda73f2-2ec9-4386-992d-bfac7d34e692","litellm_params":{"rpm":5,"model":"jYocDKvHxemZwdpaZWSTI7QcESOJxgnFUUz3mlz-KDFUabViTyp3zUP66lfRyxiE2r1C55-DV66L4I_qp3yRPV0S","api_key":"Z88IVBrUOyEUaz5fgwZ1xZXBPY4wweTABDP-zom4NdJT3SzFjtImKGcQKcbceIpVOGLzBw==","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"7MgehV-sSoHmB0bDjppV5KzY04_ClD7FZ89KMz-ZWd6pFzHhra8hJyE5lDwINiSWqAyXej1fQF6xk732","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"0d62639d-3d82-4e69-8c5f-80afab007aa6","team_id":"db4d1edf-a2bd-4ea3-bc5d-00b2d14aa2fe","db_model":false,"member_auto_router":false,"team_public_model_name":"team-wif-p2"},"blocked":false,"created_at":"2026-10-03T23:29:07.451000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:07.790000Z","updated_by":"rig-team-admin"}
    HTTP 200
    
  7. Run (cleanup, the team admin deletes it)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60133/model/delete -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"id":"0d62639d-3d82-4e69-8c5f-80afab007aa6"}'
  8. Observed

    {"message":"Model: 0d62639d-3d82-4e69-8c5f-80afab007aa6 deleted successfully"}
    HTTP 200
    

Setting blocked on a new team deployment

  1. Run (the team admin creates a deployment with blocked: true)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60133/model/new -H "X-Team-Key: Bearer $TEAM2_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"model_name":"blocked-probe-fce9d8f903","litellm_params":{"model":"anthropic/claude-sonnet-4-5","api_key":"sk-ant-probe"},"model_info":{"team_id":"7fa892a7-e64a-4a18-b924-0ba1c90ee14c"},"blocked":true}'
  2. Observed (the flag is dropped and the deployment lands unblocked)

    {"model_id":"db2d5099-cb5a-4ff2-ae9a-b104abfcbfb9","model_name":"model_name_7fa892a7-e64a-4a18-b924-0ba1c90ee14c_62087e12-5b24-4032-97f3-7a24ee0269bf","litellm_params":{"model":"RRfiB58dDEXzh3GMz07g62FKbZX92zTOaya3Dzw-mo9YA78QRzYYh_n5CYEEyz4GYNC7tBdDy1B-sW55-2lTmCpq0w==","api_key":"uhFssyy7gqjkaTNPw11aRmPkAe7CGur8z8xzpbiqSP1bS7Uv5HOmDzN14r2BYLAgmMyGUQ==","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"db2d5099-cb5a-4ff2-ae9a-b104abfcbfb9","team_id":"7fa892a7-e64a-4a18-b924-0ba1c90ee14c","db_model":false,"member_auto_router":false,"team_public_model_name":"blocked-probe-fce9d8f903"},"blocked":false,"created_at":"2026-10-03T23:29:08.292000Z","created_by":"rig-team-2-admin","updated_at":"2026-10-03T23:29:08.292000Z","updated_by":"rig-team-2-admin"}
    HTTP 200
    
  3. Run (the proxy admin sends the same flag)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60133/model/new -H 'X-Team-Key: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model_name":"admin-blocked-probe-fce9d8f903","litellm_params":{"model":"anthropic/claude-sonnet-4-5","api_key":"sk-ant-probe"},"blocked":true}'
  4. Observed (the flag is dropped for the proxy admin too)

    {"model_id":"a314f42a-8d36-40ee-8079-f327ebb8d9b3","model_name":"admin-blocked-probe-fce9d8f903","litellm_params":{"model":"xTgQ-Xwbfh8xjwwZC6Jr28YWrEaOWNodeAjdZ3OfJ0IYqYr4BXF5Ykc3XZE5Lz4C_3I3oTn2uTJGM4rBh2rvCB82rg==","api_key":"FF04dJl3ByPEB6wKEuT6GxcQrP9kLt4y330rNEO1rRQrVzE4QsuI5MfvmJo_dNTuPDEc0A==","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"a314f42a-8d36-40ee-8079-f327ebb8d9b3","db_model":false,"member_auto_router":false},"blocked":false,"created_at":"2026-10-03T23:29:08.493000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:08.493000Z","updated_by":"default_user_id"}
    HTTP 200
    

PATCH /credentials/{name} copying a federated deployment's fields through model_id

  1. Run (the proxy admin reads the id of the config deployment that carries federation fields inline)

    curl -s http://localhost:60133/model/info -H "X-Team-Key: Bearer sk-1234" | jq -c '.data[] | select(.model_name=="wif-inline-token-cfg") | {id:.model_info.id, params:(.litellm_params|keys)}'
  2. Observed

    {"id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","params":["allow_client_keepalive_override","anthropic_federation_rule_id","anthropic_federation_workspace_id","anthropic_identity_token","anthropic_organization_id","anthropic_service_account_id","merge_reasoning_content_in_choices","model","use_in_pass_through","use_litellm_proxy","use_xai_oauth"]}
    
  3. Run (the proxy admin creates a plain credential holding only an api_key)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60133/credentials -H "X-Team-Key: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"credential_name":"plain-anthropic-fce9d8f903","credential_values":{"api_key":"sk-ant-placeholder"},"credential_info":{"custom_llm_provider":"anthropic"}}'
  4. Observed

    {"success":true,"message":"Credential created successfully"}
    HTTP 200
    
  5. Run (an internal user whose key allows only /credentials* PATCHes that credential naming the federated deployment through model_id)

    curl -s -w '\nHTTP %{http_code}\n' -X PATCH http://localhost:60133/credentials/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer $USER_KEY" -H 'Content-Type: application/json' -d '{"credential_name":"plain-anthropic-fce9d8f903","model_id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","credential_info":{}}'
  6. Observed (the PATCH lands, the merge base has no federation fields to copy)

    {"success":true,"message":"Credential updated successfully"}
    HTTP 200
    
  7. Run (which fields the credential holds now)

    curl -s http://localhost:60133/credentials/by_name/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer sk-1234" | jq -c '.credential_values | keys'
  8. Observed (the credential still holds only api_key)

    ["api_key"]
    
  9. Run (the proxy admin sends the same PATCH)

    curl -s -w '\nHTTP %{http_code}\n' -X PATCH http://localhost:60133/credentials/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"credential_name":"plain-anthropic-fce9d8f903","model_id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","credential_info":{}}'
  10. Observed (lands the same way)

{"success":true,"message":"Credential updated successfully"}
HTTP 200
  1. Run (which fields the credential holds now)
curl -s http://localhost:60133/credentials/by_name/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer sk-1234" | jq -c '.credential_values | keys'
  1. Observed (still only api_key)
["api_key"]

/v1/messages/count_tokens with only ANTHROPIC_AUTH_TOKEN set

  1. Run (a keyless deployment on a proxy whose environment holds only ANTHROPIC_AUTH_TOKEN, counting a body with a system prompt and a tool)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60151/v1/messages/count_tokens -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-auth-token","system":"You are a terse assistant.","messages":[{"role":"user","content":"Say hi in three words."}],"tools":[{"name":"get_weather","description":"Get the current weather in a given location","input_schema":{"type":"object","properties":{"location":{"type":"string","description":"City and state, e.g. San Francisco, CA"}},"required":["location"]}}]}'
  2. Observed (the auth token never reaches the count-tokens call and the proxy answers the same local estimate of 82)

    {"input_tokens":82}
    HTTP 200
    
  3. Run (chat on the same deployment)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60151/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-auth-token","messages":[{"role":"user","content":"Say hi in three words."}]}'
  4. Observed (chat authenticates with the same auth token on both sides)

    {"id":"chatcmpl-e09c29fc-c281-47dd-b33a-90c57b256da3","created":1791069797,"model":"claude-haiku-4-5-auth-token","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hi there, friend.","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":13,"total_tokens":21,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":8},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":13,"cache_write_tokens":0,"cache_creation_tokens":0,"cache_creation_token_details":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0}},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"inference_geo":"not_available","service_tier":"standard"}}
    HTTP 200
    

After (fce9d8f)

JWKS export for a LiteLLM-signed credential

  1. Run

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

    {
      "keys": [
        {
          "crv": "P-256",
          "kty": "EC",
          "x": "NtOW6ZJD7uiZtmRG-U5NzFticUUGw76ynoDHLIrXcKs",
          "y": "liD4kHokfughbZLFW-aU0LG8brz0jkg4qA_nXSuBdkM",
          "use": "sig",
          "alg": "ES256",
          "kid": "nLtBZqlLBZdO0Fs83Ca7cIzkC4TnAdouRwtVJelDw9g"
        }
      ]
    }
    HTTP 200
    

/v1/chat/completions on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/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

    {"id":"chatcmpl-0031e194-0e11-4d2d-bf94-709008105c52","created":1791070109,"model":"claude-haiku-4-5-wif","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hey, how are you?","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":9,"prompt_tokens":13,"total_tokens":22,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":9},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":13,"cache_write_tokens":0,"cache_creation_tokens":0,"cache_creation_token_details":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0}},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"inference_geo":"not_available","service_tier":"standard"}}
    HTTP 200
    

/v1/messages/count_tokens on a federated deployment

  1. Run (the federated deployment counts a body with a system prompt and a tool)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/v1/messages/count_tokens -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-wif","system":"You are a terse assistant.","messages":[{"role":"user","content":"Say hi in three words."}],"tools":[{"name":"get_weather","description":"Get the current weather in a given location","input_schema":{"type":"object","properties":{"location":{"type":"string","description":"City and state, e.g. San Francisco, CA"}},"required":["location"]}}]}'
  2. Observed (Anthropic's own count, 590, the number a direct call to Anthropic's count-tokens endpoint returns for this body, authenticated with the minted federated token)

    {"input_tokens":590}
    HTTP 200
    

/v1/messages on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/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

    {"model":"claude-haiku-4-5-wif","id":"msg_011CfgAv2kTy79dLbvVVsscF","type":"message","role":"assistant","content":[{"type":"text","text":"Hey, how's it going?"}],"container":null,"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":10,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null}
    HTTP 200
    

/v1/responses on a federated deployment

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/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

    {"id":"resp_TjHIHExnNPzXUV7amt1kQau-ftwvZbIksGP3CayzoX5-4JLVinC9mvwebPA1o3rUQE8U4uddG6nTtC3KejIE2QKgujuTAFn8iZz9mjSwlIDY7imshp6tNIGElkPCdeCZF8GODBfAZ8_wk2aRbr1qZQYBU0AOsOtll28XSnf7xzZv1V2jKTW7jzzSGHRVptVnlvXjT4WKqq3l2Es5vV_VBlywOzsqEAZNb5VsfhxPWU1MuA43Ln9dYqeUny-BwO0Nb4Lnvt6va5qc2SLtz22ydBFIxpzFHLjVx0a0QssVhfh24tlmaXXjr98uq5lUCDlPiNw4X1K3xrYDFFP3dOTRw7D4_RebAIlZE17TLrtN42PJsUoNjTXvqdw7Tw3xgr0_4wyNQhS0sYv7db5q_9lseJ8M7ULJ6moqtYvaBd0yoeLhawhoPhQ1iaEeYoiRy6qrP-wBFx46RtlQvw==","created_at":1791070123,"error":null,"incomplete_details":null,"instructions":null,"metadata":{},"model":"claude-haiku-4-5-wif","object":"response","output":[{"type":"message","id":"msg_fd3066d2-374c-498b-a91c-ca0c652e260b","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hey there, friend!","annotations":[]}],"phase":null}],"parallel_tool_calls":false,"temperature":0.0,"tool_choice":"auto","tools":[],"top_p":null,"max_output_tokens":null,"previous_response_id":null,"reasoning":null,"status":"completed","text":{},"truncation":null,"usage":{"input_tokens":13,"input_tokens_details":{"audio_tokens":null,"cached_tokens":0,"cached_tokens_details":null,"image_tokens":null,"text_tokens":13,"video_tokens":null,"cache_write_tokens":0},"output_tokens":8,"output_tokens_details":{"audio_tokens":null,"reasoning_tokens":0,"text_tokens":8},"total_tokens":21,"cost":null},"user":null,"store":null,"provider_specific_fields":{"citations":null,"thinking_blocks":null}}
    HTTP 200
    

/v1/chat/completions through an egress gateway allowlisted as host:port

  1. Run

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

    {"id":"chatcmpl-b77b513d-ec68-4d39-9834-1c18323d1555","created":1791070130,"model":"claude-haiku-4-5-wif-gateway","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hey there, friend.","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":13,"total_tokens":21,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":8},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":13,"cache_write_tokens":0,"cache_creation_tokens":0,"cache_creation_token_details":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0}},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"inference_geo":"not_available","service_tier":"standard"}}
    HTTP 200
    

Federated credential missing the Console ids

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/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\n\nLiteLLM: model group 'claude-haiku-4-5-wif-noids' failed with the error above. No fallback was attempted.","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' http://localhost:60132/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. A proxy admin configures it on the deployment or on a stored credential.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

Federation fields typed into POST /model/new, next to the sanctioned credential

  1. Run (a federation field straight in the deployment's litellm_params)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/model/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model_name":"wif-inline-probe-after-fce9d8f903","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. A proxy admin configures it on the deployment or on a stored credential.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    
  3. Run (the same proxy admin attaches the stored federated credential instead)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60132/model/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model_name":"wif-cred-probe-after-fce9d8f903","litellm_params":{"model":"anthropic/claude-haiku-4-5","litellm_credential_name":"anthropic-wif-issuer"}}'
  4. Observed

    {"model_id":"9b197b6a-a869-4c80-a618-e6b9df01cd15","model_name":"wif-cred-probe-after-fce9d8f903","litellm_params":{"model":"B_RxM2vobvg1qJQz0lj2jPC8QCbnAIm5M8hkrls5pXiCtO2hIlaey9q4umExnHYuWh5H7RV8DKFmR2NhMXv26fvU","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"3wwgncI7zuk6HF1EdVqTBZRC15jmLOd6QNVKCox-fYu4gJ3Ovq4_Uvx1EeDpGcbA1_YxKPqyZxpIu4Of","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"9b197b6a-a869-4c80-a618-e6b9df01cd15","db_model":false,"member_auto_router":false},"blocked":false,"created_at":"2026-10-03T23:29:03.377000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:03.377000Z","updated_by":"default_user_id"}
    HTTP 200
    
  5. Run (which of the two the proxy now serves)

    curl -s http://localhost:60132/model/info -H "Authorization: Bearer sk-1234" | grep -oE 'wif-(inline|cred)-probe-after-fce9d8f903' | sort -u
  6. Observed

    wif-cred-probe-after-fce9d8f903
    

GET /health with a team admin key on a deployment carrying an inline identity token

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' 'http://localhost:60134/health?model=wif-inline-token-cfg' -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY"
  2. Observed (the token and the Console ids are gone and the error says raw tokens must be oidc/ references; the error field's stack trace and the raw request dict are trimmed)

    {"healthy_endpoints":[],"unhealthy_endpoints":[{"model":"anthropic/claude-haiku-4-5","error":"litellm.AuthenticationError: anthropic_identity_token must be an oidc/ secret reference such as oidc/env/VAR_NAME, oidc/file//absolute/path, oidc/github/<audience>, or oidc/google/<audience>. Raw identity tokens and oidc/env_path/ references are not accepted; to pass a token directly, export it and reference it as oidc/env/VAR_NAME","model_id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","exception_status":401}],"healthy_count":0,"unhealthy_count":1}
    HTTP 503
    

Team admin edits on a federated team deployment

  1. Run (the proxy admin creates the team-scoped federated deployment)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60134/model/new -H 'X-Team-Key: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model_name":"team-wif-p2","litellm_params":{"model":"anthropic/claude-haiku-4-5","litellm_credential_name":"anthropic-wif-issuer"},"model_info":{"team_id":"4f5fa87b-f36f-45be-a984-71856154516d"}}'
  2. Observed

    {"model_id":"50bcab0c-f194-423e-a5b6-ec19ad1c1690","model_name":"model_name_4f5fa87b-f36f-45be-a984-71856154516d_68840c1f-89de-47d3-95cc-ee027c683247","litellm_params":{"model":"sBOLvK4zJObUC-GmiJgrsg-_16AkaHMNZnO6Z_swpnjiBUnmbJjjqVZO5sdPHRDoML0qRq3zIzgO1ZsKGctoKUiS","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"-bm-ygj5SMOh-fGGA8Eb33wRpYIDYxDTwxlXaxAsAQbgLTZOWMtqsA2lAZcFc9IJhw1njCUclhlRuIST","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"50bcab0c-f194-423e-a5b6-ec19ad1c1690","team_id":"4f5fa87b-f36f-45be-a984-71856154516d","db_model":false,"member_auto_router":false,"team_public_model_name":"team-wif-p2"},"blocked":false,"created_at":"2026-10-03T23:29:04.857000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:04.857000Z","updated_by":"default_user_id"}
    HTTP 200
    
  3. Run (the team admin sets rpm on it)

    curl -s -w '\nHTTP %{http_code}\n' -X PATCH http://localhost:60134/model/50bcab0c-f194-423e-a5b6-ec19ad1c1690/update -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"litellm_params":{"rpm":5}}'
  4. Observed (the edit lands, it sets no federation field)

    {"model_id":"50bcab0c-f194-423e-a5b6-ec19ad1c1690","model_name":"model_name_4f5fa87b-f36f-45be-a984-71856154516d_68840c1f-89de-47d3-95cc-ee027c683247","litellm_params":{"rpm":5,"model":"sBOLvK4zJObUC-GmiJgrsg-_16AkaHMNZnO6Z_swpnjiBUnmbJjjqVZO5sdPHRDoML0qRq3zIzgO1ZsKGctoKUiS","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"litellm_credential_name":"-bm-ygj5SMOh-fGGA8Eb33wRpYIDYxDTwxlXaxAsAQbgLTZOWMtqsA2lAZcFc9IJhw1njCUclhlRuIST","allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"50bcab0c-f194-423e-a5b6-ec19ad1c1690","team_id":"4f5fa87b-f36f-45be-a984-71856154516d","db_model":false,"member_auto_router":false,"team_public_model_name":"team-wif-p2"},"blocked":false,"created_at":"2026-10-03T23:29:04.857000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:05.017000Z","updated_by":"rig-team-admin"}
    HTTP 200
    
  5. Run (the same team admin points it at a static key instead)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60134/model/update -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"model_name":"team-wif-p2","litellm_params":{"model":"anthropic/claude-haiku-4-5","api_key":"sk-ant-bogus"},"model_info":{"id":"50bcab0c-f194-423e-a5b6-ec19ad1c1690","team_id":"4f5fa87b-f36f-45be-a984-71856154516d"}}'
  6. Observed (the credential swap is refused)

    {"error":{"message":"Only proxy admins can change the credentials of a deployment configured for workload identity federation ('anthropic_federation_rule_id').","type":"auth_error","param":"anthropic_federation_rule_id","code":"403"}}
    HTTP 403
    
  7. Run (cleanup, the team admin deletes it)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60134/model/delete -H "X-Team-Key: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"id":"50bcab0c-f194-423e-a5b6-ec19ad1c1690"}'
  8. Observed

    {"message":"Model: 50bcab0c-f194-423e-a5b6-ec19ad1c1690 deleted successfully"}
    HTTP 200
    

Setting blocked on a new team deployment

  1. Run (the team admin creates a deployment with blocked: true)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60134/model/new -H "X-Team-Key: Bearer $TEAM2_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"model_name":"blocked-probe-fce9d8f903","litellm_params":{"model":"anthropic/claude-sonnet-4-5","api_key":"sk-ant-probe"},"model_info":{"team_id":"b51f32e9-55b7-4e8e-a24e-f66e66499fe5"},"blocked":true}'
  2. Observed (the team admin is refused)

    {"error":{"message":"Only proxy admins can set a model's blocked flag.","type":"auth_error","param":"blocked","code":"403"}}
    HTTP 403
    
  3. Run (the proxy admin sends the same flag)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60134/model/new -H 'X-Team-Key: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model_name":"admin-blocked-probe-fce9d8f903","litellm_params":{"model":"anthropic/claude-sonnet-4-5","api_key":"sk-ant-probe"},"blocked":true}'
  4. Observed (the proxy admin's flag sticks)

    {"model_id":"ba1c0d3c-84a8-4f99-8bea-f3ca5904781d","model_name":"admin-blocked-probe-fce9d8f903","litellm_params":{"model":"zC8VL46F9PsO71UG2CnepgzsPngjs_IWYf6DTJLADZsx8MsfsvPB1AuhJm9J37erO3_0qf6sf5xMNzyB-o_BIpMwsg==","api_key":"gZWS4Tw7qC19dOPBfI3Uu-HdXe6uonXVWSZVZRE6OCG2f9VioJ40TlNn9hwa4bnDfwwa2A==","use_xai_oauth":false,"use_litellm_proxy":false,"use_in_pass_through":false,"allow_client_keepalive_override":false,"merge_reasoning_content_in_choices":false},"model_info":{"id":"ba1c0d3c-84a8-4f99-8bea-f3ca5904781d","db_model":false,"member_auto_router":false},"blocked":true,"created_at":"2026-10-03T23:29:05.348000Z","created_by":"default_user_id","updated_at":"2026-10-03T23:29:05.348000Z","updated_by":"default_user_id"}
    HTTP 200
    

PATCH /credentials/{name} copying a federated deployment's fields through model_id

  1. Run (the proxy admin reads the id of the config deployment that carries federation fields inline)

    curl -s http://localhost:60134/model/info -H "X-Team-Key: Bearer sk-1234" | jq -c '.data[] | select(.model_name=="wif-inline-token-cfg") | {id:.model_info.id, params:(.litellm_params|keys)}'
  2. Observed

    {"id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","params":["allow_client_keepalive_override","anthropic_federation_rule_id","anthropic_federation_workspace_id","anthropic_identity_token","anthropic_organization_id","anthropic_service_account_id","merge_reasoning_content_in_choices","model","use_in_pass_through","use_litellm_proxy","use_xai_oauth"]}
    
  3. Run (the proxy admin creates a plain credential holding only an api_key)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60134/credentials -H "X-Team-Key: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"credential_name":"plain-anthropic-fce9d8f903","credential_values":{"api_key":"sk-ant-placeholder"},"credential_info":{"custom_llm_provider":"anthropic"}}'
  4. Observed

    {"success":true,"message":"Credential created successfully"}
    HTTP 200
    
  5. Run (an internal user whose key allows only /credentials* PATCHes that credential naming the federated deployment through model_id)

    curl -s -w '\nHTTP %{http_code}\n' -X PATCH http://localhost:60134/credentials/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer $USER_KEY" -H 'Content-Type: application/json' -d '{"credential_name":"plain-anthropic-fce9d8f903","model_id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","credential_info":{}}'
  6. Observed (the internal user is refused, naming the first federation field the copy would have landed)

    {"error":{"message":"Only proxy admins can change 'anthropic_federation_rule_id', a server-owned workload identity federation parameter.","type":"auth_error","param":"anthropic_federation_rule_id","code":"403"}}
    HTTP 403
    
  7. Run (which fields the credential holds now)

    curl -s http://localhost:60134/credentials/by_name/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer sk-1234" | jq -c '.credential_values | keys'
  8. Observed (the credential is untouched)

    ["api_key"]
    
  9. Run (the proxy admin sends the same PATCH)

    curl -s -w '\nHTTP %{http_code}\n' -X PATCH http://localhost:60134/credentials/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"credential_name":"plain-anthropic-fce9d8f903","model_id":"688c043882c178e53ed1ed5eb726e58b7e8741c1c4247bd922fcd75ec8fb8736","credential_info":{}}'
  10. Observed (the proxy admin's copy lands)

{"success":true,"message":"Credential updated successfully"}
HTTP 200
  1. Run (which fields the credential holds now)
curl -s http://localhost:60134/credentials/by_name/plain-anthropic-fce9d8f903 -H "X-Team-Key: Bearer sk-1234" | jq -c '.credential_values | keys'
  1. Observed (the five federation fields now sit on the credential next to api_key)
["anthropic_federation_rule_id","anthropic_federation_workspace_id","anthropic_identity_token","anthropic_organization_id","anthropic_service_account_id","api_key"]

/v1/messages/count_tokens with only ANTHROPIC_AUTH_TOKEN set

  1. Run (a keyless deployment on a proxy whose environment holds only ANTHROPIC_AUTH_TOKEN, counting a body with a system prompt and a tool)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60152/v1/messages/count_tokens -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-auth-token","system":"You are a terse assistant.","messages":[{"role":"user","content":"Say hi in three words."}],"tools":[{"name":"get_weather","description":"Get the current weather in a given location","input_schema":{"type":"object","properties":{"location":{"type":"string","description":"City and state, e.g. San Francisco, CA"}},"required":["location"]}}]}'
  2. Observed (the same 590 from Anthropic, authenticated with the auth token as a Bearer)

    {"input_tokens":590}
    HTTP 200
    
  3. Run (chat on the same deployment)

    curl -s -w '\nHTTP %{http_code}\n' http://localhost:60152/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"claude-haiku-4-5-auth-token","messages":[{"role":"user","content":"Say hi in three words."}]}'
  4. Observed (chat authenticates with the same auth token on both sides)

    {"id":"chatcmpl-d023b6ec-9ca0-4124-876a-882d77169879","created":1791070107,"model":"claude-haiku-4-5-auth-token","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Hey there, friend!","role":"assistant","provider_specific_fields":{"citations":null,"thinking_blocks":null}}}],"usage":{"completion_tokens":8,"prompt_tokens":13,"total_tokens":21,"completion_tokens_details":{"reasoning_tokens":0,"text_tokens":8},"prompt_tokens_details":{"cached_tokens":0,"text_tokens":13,"cache_write_tokens":0,"cache_creation_tokens":0,"cache_creation_token_details":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0}},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"inference_geo":"not_available","service_tier":"standard"}}
    HTTP 200
    

Type

New Feature

Caveats (if any)

Severe

  • A static Anthropic key in the proxy environment silently outranks every federated deployment
    • The order is ANTHROPIC_API_KEY, then ANTHROPIC_AUTH_TOKEN, then federation, and it holds for chat, /v1/messages, /v1/responses, files, batches and model discovery alike. It is the order the Anthropic SDK itself uses, and inverting it would break every deployment that sets both, so it stays
    • Reduced rather than removed: a blank key or auth token now counts as unset instead of winning, and the proxy logs a warning naming the shadowed model the first time federation is skipped for it. proxy_cli.py calls load_dotenv(), so a .env in the proxy's working directory puts the key back even after the shell unset it, and a fleet that wants no static secrets has to clean that file too
  • Minted org-scoped tokens are written to disk by default
    • The shared store is what keeps several uvicorn workers reading one projected token file from each spending its single-use jti, so dropping it would break the multi-worker case this PR exists for. Entries are 0600 in a 0700 uid-owned directory, a staging file a failed write leaves behind is now unlinked so no live token survives it, and LITELLM_TOKEN_EXCHANGE_CACHE_DIR set to an empty string turns the store off
    • An expired-entry sweep was weighed and left out: an expired token is not a usable credential, and the sweep would add a background scan of a directory the engine already overwrites per key

Medium

  • No dashboard in this PR
  • A freshly added federated credential can answer the generic Missing Anthropic API Key 401 for up to one credential refresh interval on a multi-worker proxy without Redis
    • Pre-existing for every litellm_credential_name deployment (LIT-6901); config.yaml credentials are loaded by every worker at boot and do not hit it, POST /credentials ones do
  • Anthropic accepts each identity assertion once, so a static one is refused at its first refresh
    • An oidc/env/ value or a token file that rotates slower than the rule's token lifetime (60 s to 24 h, wizard default 10 min) gets jti_reused on the re-mint until it rotates, and the 401 hint says so; the LiteLLM-signed internal issuer and Keycloak mint a fresh assertion every time
  • Federation on eu.api.openai.com and us.api.openai.com is exercised only up to the upstream geography refusal, no geography-restricted project was available to see a 200 on a regional host

Low

  • Only Organization ID and Federation Rule ID are required to attempt an exchange: Anthropic's reference lists the service account id as required but a live exchange minted without it when the rule targets one service account, and the workspace id is required only when the rule is enabled in more than one workspace, which includes a rule enabled for all workspaces: this run's exchange got the opaque 401 until the credential named a workspace the service account belongs to, and the only one it belonged to was the organization's default workspace, whose id the workspaces listing hides and the service account's own workspace memberships listing returns
  • Federation reads ANTHROPIC_FEDERATION_WORKSPACE_ID, not the shorter ANTHROPIC_WORKSPACE_ID the Console reference uses, because the Bedrock Claude platform provider already reads that shorter name on a released path; the 401 hint says so when the workspace is ambiguous
  • Identity token files must sit under LITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS, and the token exchange only talks to api.anthropic.com unless the operator adds hosts to LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS, so a gateway or test double needs that set. Entries are a host or a host:port: naming a port trusts that port alone, leaving it off trusts every port on the host. The list is server-owned, read from the environment only, and cannot be set through the model or credential APIs
  • A freshly registered JWKS takes about a minute to become usable in the Console; a data plane 401 does not invalidate the cached token, which self-heals; minted tokens are cached on disk per host (LITELLM_TOKEN_EXCHANGE_CACHE_DIR, default $TMPDIR/litellm-token-exchange-<uid>), so workers on one host share one exchange and each host mints its own
  • Federation fields are refused inline on POST /model/new for the proxy admin too, not only for team admins: the sanctioned paths are a named credential (POST /credentials then litellm_credential_name) or config.yaml. Changing the credentials of a federated deployment, meaning its federation fields, its api_base or the credential attached to it, stays proxy-admin only; the team admin who owns it can still edit everything else on it, delete it, and press Test Connection
  • A request body that names a stored credential carrying federation fields is refused with 400, on /v1/chat/completions and every other LLM route, so a caller cannot pick which federated identity the proxy mints as
  • /anthropic/* passthrough no longer forwards the caller's own credential headers upstream once the deployment has a server-side Anthropic credential: authorization, x-api-key and the rest of the headers the proxy accepts a LiteLLM key in are stripped, plus general_settings.litellm_key_header_name when one is set. Base forwarded them all, so a client that relied on its own header reaching Anthropic through the relay has to stop sending the server credential instead. With no server credential nothing is stripped and BYOK is unchanged
  • The OpenAPI schema now lists CredentialItem-Input and CredentialItem-Output instead of one CredentialItem, because credential_values_to_delete is excluded from serialization. The wire shape is unchanged; a generated client pinned to the old schema name regenerates. ui/litellm-dashboard/src/lib/http/schema.d.ts is regenerated for the same reason and is the only file under ui/ this PR touches
  • A clientside-credential request that overrides api_base gets a different synthesized per-request model id than before, since the federation disable flag now rides those params. Cosmetic, the id is not persisted
  • A team admin setting blocked to true on POST /model/new for a team deployment gets 403 where base answered 200 and silently dropped the flag; blocked: false still answers 200, since that is the state every create lands in anyway, and a proxy admin still gets 200 with the flag honored
  • A team admin naming a credential that does not exist on POST /model/new gets 400 where base answered 403
  • /health/test_connection answers the model auth gate's own 403 or 400 where base wrapped the same refusal into a 500 Failed to test connection; for a team admin that includes a probe whose body redirects a federated deployment's api_base or swaps its credential, while an unmodified probe of that deployment still runs
  • A null inside credential_values on POST /credentials and PATCH /credentials/{name} is dropped rather than stored, where base stored it; removing a value goes through credential_values_to_delete. Storing the null is net-negative: the federation resolver refuses a foreign variant's field by key, so one stored anthropic_issuer_url: null wedged every deployment naming that credential
  • Blank identity-source fields (anthropic_identity_token_file: "" and the other variant fields) count as unset, the same blank-as-unset rule the static key follows, so a config template that leaves a field empty falls through to the next identity source instead of failing on an empty path or an unexpected variant
  • The warning that a static key shadows federation is memoized per deployment and configured rule id, so ANTHROPIC_FEDERATION_RULE_ID exported into a running proxy's environment after a deployment's first static-key call is not noticed until restart. Re-reading it per request is net-negative: it would be a secret-manager read on every static-key Anthropic call for a variable almost nobody sets
  • The tag management writer that appends a tag to a deployment's params is not behind the federation admin gate; it only touches tags, so it cannot change a federation field
  • Deployments without an api_base now count tokens through ANTHROPIC_API_BASE / ANTHROPIC_BASE_URL the way chat does; a gateway there that serves /v1/messages but not /v1/messages/count_tokens gets the logged local-tokenizer fallback where the base counted through Anthropic's host directly
  • A consumer sk-ant-oat token on count-tokens no longer gets anthropic-dangerous-direct-browser-access, which the base branch's count-tokens header ladder added for those tokens: count-tokens now builds its headers through the shared auth helper, the same one files, batches, skills and federated chat use, and none of those send that header
  • Deleting a credential that exists only in config.yaml answers 404 and leaves it serving, staging's fix(proxy): 404 a credential delete that matched nothing, and raise instead of return #36260 rule carried through the federation admin gate
  • Reading a credential back masks the token file path, and a client that PATCHes the masked object back stores the mask: pre-existing for every masked credential field (LIT-6900)
  • Image, audio, transcription, moderation, files, batches, fine-tuning, and assistants stay env-only for OpenAI federation: they build their OpenAI client elsewhere, LIT-6898 tracks them
  • The new tests reach for monkeypatch.setattr 68 times rather than injecting a dependency, which the repo prefers. Fixing it is net-negative: most of those calls patch module-level singletons (litellm.api_key, litellm.credential_list, litellm.module_level_client) that have no constructor to inject through, so removing them means reshaping settled provider modules well outside this PR. The token exchange engine, the one piece here that does have a seam, is tested through it
  • osv-scan was red on the pre-existing gitpython advisory in uv.lock on feat(anthropic): workload identity federation, pluggable identity sources, and provider-level setup (internal copy of #38013) #38818; this PR does not touch dependencies
  • PATCH /credentials/{name} with a model_id naming a deployment that does not exist now answers 404 Model not found before looking the credential up, where base answered 404 Credential not found in DB. when the credential was missing too: same status, different message, because the deployment's values are resolved first so the gate can inspect them
  • A deployment that points at a named credential through litellm_credential_name exposes no federation fields through model_id: named credentials are resolved per request and never merged into the router's copy of the deployment, so a PATCH copying from it carries only what the deployment holds inline, which config.yaml deployments do and API-created ones do not

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

Note

High Risk
Changes authentication across Anthropic and shared token-exchange infrastructure, including credential precedence, host allowlists, and on-disk minted-token caching—mistakes could break auth or leak org-scoped tokens.

Overview
Adds Anthropic workload identity federation: a new litellm/llms/anthropic/wif.py path and shared litellm/llms/base_llm/auth/ RFC 7523 JWT-bearer engine (token file/env, internal ES256 issuer, Keycloak client-credentials, on-disk multi-worker cache) mint short-lived sk-ant-oat01 tokens when static keys are absent.

Auth plumbing now threads litellm_params and WIF kwargs through get_litellm_params, batch credential extraction (so federated batch output fetch can bill), and Anthropic chat, messages passthrough, files, batches, skills, and count-tokens—using shared get_auth_header / aget_auth_header, stripping caller credential headers when the server mints, and async validate_environment / avalidate_anthropic_messages_environment so exchanges do not block the event loop.

Operational touches: Grafana all-metrics dashboard panels for litellm_anthropic_wif_* and cache families; HTTPHandler optional follow_redirects=False for Keycloak; async file/batch create paths await async credential validation.

Related fixes in the same diff: count-tokens and model discovery honor deployment api_base and ANTHROPIC_AUTH_TOKEN; batches use GenericLiteLLMParams.model_validate.

Reviewed by Cursor Bugbot for commit fce9d8f. Bugbot is set up for automated code reviews on this repo. Configure here.

mateo-berri and others added 30 commits September 5, 2026 12:05
…sources

Backend half of #38818 (internal copy of the fork PR #38013), rebuilt as one
commit on top of litellm_internal_staging without the dashboard changes.

Deployments on anthropic/ without a static api_key can exchange an OIDC
workload assertion for a short-lived sk-ant-oat01 token through a shared
RFC 7523 JWT-bearer engine. The assertion comes from a mounted token file,
an env token, a LiteLLM-signed issuer, or Keycloak, chosen per deployment,
per named credential, or through ANTHROPIC_IDENTITY_SOURCE. The federation
fields are server-owned: refused inline in request bodies and on
POST /model/new, proxy-admin only on credentials, and the token exchange
is pinned to api.anthropic.com unless LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS
adds a host. GET /credentials/{name}/jwks exports the public key set of a
LiteLLM-signed credential for the Claude Console.

The OpenAI federation trio from #39613 rides along on the backend side with
the same server-owned handling.

Fixes #28607
Resolves LIT-6107

Co-authored-by: derhornspieler <15236687+derhornspieler@users.noreply.github.com>
…s and accept host:port allowlist entries

The files handler enabled workload identity on batch-result downloads but never received the
deployment's litellm_params, so a deployment authenticating through a named credential could only
mint from process-wide env vars. It now threads litellm_params through to the auth header the way
the batch retrieve path already does.

LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS entries written as host:port were read by urlsplit as a scheme,
so the allowlist kept the raw entry while the exchange compared bare hostnames and refused the
gateway. Entries are now parsed as network locations whether or not they carry a scheme.
…thropic_workspace_id keeps working

The Bedrock Claude Platform route already reads anthropic_workspace_id from
optional_params, so banning that spelling as a server-owned federation
parameter broke a pre-existing client capability. The federation field is now
anthropic_federation_workspace_id (env ANTHROPIC_FEDERATION_WORKSPACE_ID),
which restores the base branch's behavior for Bedrock callers, drops the
Bedrock-specific hint from the refusal message, and deletes the unconditional
ban constant that no longer had a reader
…assertion

Anthropic accepts each identity assertion exactly once, so two uvicorn
workers reading the same token file both minting from it means the second
exchange is denied with jti_reused. Minted tokens now land in a per-user
0700 cache directory guarded by a file lock, so workers on the same host
reuse one exchange until the token expires or the assertion rotates. A 401
is only retried when the re-read assertion actually differs, and the denial
hint explains jti_reused. LITELLM_TOKEN_EXCHANGE_CACHE_DIR moves the cache
and an empty value disables it
…itellm_anthropic_wif_backend

# Conflicts:
#	tests/test_litellm/test_lazy_imports.py
…itellm_anthropic_wif_backend

# Conflicts:
#	tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py
An empty or whitespace-only ANTHROPIC_API_KEY counted as set, so a federated
deployment sent an empty x-api-key on every call instead of minting a token.
Blank values now read as unset, and a real static key on a federated deployment
logs once that it outranks federation and nothing is being federated.

The exchange-host allowlist matched hostnames only, so a second process on
another port of an allowed host was trusted with the workload's identity token.
An entry that names a port now trusts that port alone, while a bare host still
trusts every port.

The shared token store exists so the workers reading one projected token file do
not each spend its single-use jti. A source that mints its own assertion per
exchange shares nothing with another worker, so it no longer writes a live token
to disk for a lookup that can never hit.
The 401 denial hint now also says federation ignores ANTHROPIC_WORKSPACE_ID, which the Bedrock Claude platform provider already reads.
A buffered write only reaches the disk when the handle closes, so a full disk surfaces at close and left the staging file behind holding a usable token.
The admin gate read the stored deployment, so a team admin lost edit, delete
and Test Connection on any deployment carrying federation params. It now
returns early unless the submitted fields touch the federation surface, and a
Test Connection probe that points the deployment at its own api_base is still
refused, with the 403 no longer wrapped into a 500

The rest of the same review pass: POST /model/new refuses only a blocking
value of `blocked`, so a client that always sends `blocked: false` is not
turned away; a request body can no longer pick which federated identity to
mint as by naming a stored credential; an advisory refresh the executor
refuses disarms the entry instead of wedging the identity until the follower
timeout; the static-key shadow warning resolves its env fallback inside the
cache instead of once per request; credential writes drop nulls before
storing them; the token exchange validates the endpoint URL before reading an
assertion and keeps refusing redirects across a client heal; /health hides
every server-owned federation field from non-admins; and the async create_file
and create_batch paths say which setting is missing when the provider resolves
no URL
reject_federated_credential_reference runs from is_request_body_safe, which
pre_db_read_auth_checks calls on every route, so it also fired on POST
/model/new, /model/update, /model/{id}/update and /health/test_connection. A
proxy admin could no longer attach a federated credential to a deployment over
the API or the Admin UI, leaving a static config.yaml entry as the only way to
configure the feature the rejection told the caller to go configure, and
_reject_non_admin_wif_write never got to make the call it exists to make.

is_request_body_safe now takes the route and skips only the credential-reference
check on the routes that reach can_user_make_model_call. Federation fields typed
inline into a body stay refused everywhere, and a call naming a federated
credential still cannot pick the identity it mints as.
… sets

The health check module hand-copied the five workload identity fields whose
value is a credential, so a shared proxy surface named provider-specific
parameters and a newly added secret-bearing field would have gone on being
displayed until someone remembered both places

WIF_SECRET_BEARING_KEYS now sits beside the key sets it splits out of,
types/utils derives secret_bearing_wif_litellm_params from it, and the health
layer splats that tuple the same way it already splats the admin-only one
completion() forwards the Anthropic and OpenAI federation kwargs again now that
main inlines its forward tuple, and the forward tests drive completion() instead
of checking the funnel's key set. The health tests follow main's display
allowlist, the Bedrock workspace test opts into client-side credentials so the
unconditional federation ban is what it exercises, stacked comprehensions and
suppressions the newer checker reports as unused are cleared, and the child
interpreter tests run isolated.
Resolve the test_anthropic_chat_handler.py conflict to main's thinking-block
assertions and bring the branch back under the lint and type gates: split the
stacked comprehensions LIT014 now flags, drop the suppression comments LIT013
reports as unused, run child interpreters through the isolated helper, and
validate untyped litellm_params dicts with model_validate at the construction
sites in the files this branch touches instead of unpacking them, since every
new federation field on CredentialLiteLLMParams otherwise adds one unknown
argument diagnostic per site
…gistry

main's registry test (#43298) now fails the build for any credential-named
deployment param without a classification. The five federation fields that
carry a token, a token file path, or a signing or client secret reference are
Unplanted, matching WIF_SECRET_BEARING_KEYS; the four remaining Keycloak
settings name a URL, a client id, an auth method, or a scope and are NotSecret
…ves and chart their metrics

Register the 18 Anthropic and 3 OpenAI federation params as frozen
ConnectionSettings leaves so the owned-kwarg registry, the kwargs funnel
and the request-body ban list read one declaration. Pass the deployment
api_base through to the count-tokens handler instead of a pre-suffixed
URL, which doubled the /count_tokens path on main's prompt-cache
predictor. Add the five litellm_anthropic_wif_* families to the
all-metrics Grafana dashboard.
…_backend

# Conflicts:
#	litellm/llms/openai/workload_identity.py
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ SwiftWinds
You have signed the CLA already but the status is still pending? Let us recheck it.

@mateo-berri
mateo-berri enabled auto-merge (squash) October 3, 2026 22:38
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[High risk] Adds workload identity federation and pluggable auth to Anthropic and OpenAI integrations.

The PR is not yet ready to merge because the existing count-tokens consumer OAuth header change remains an unresolved repository requirement.

Findings

  1. P2 Provider logic in generic code ▶
  2. P2 Consumer OAuth header is dropped ▶
  3. P2 Product documentation added here ▶

Summary

The PR adds Anthropic workload identity federation and shared token exchange, then threads the resulting credentials through Anthropic request surfaces, proxy credential management, and metrics. The changes since the previous review consolidate count-tokens authentication with the shared Anthropic header resolver.

Reviews (5) · Last reviewed commit: "fix(anthropic): count tokens with ANTHRO..."

Comment thread litellm/proxy/credential_endpoints/endpoints.py Outdated
@codspeed

codspeed Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_anthropic_wif_backend_devin (fce9d8f) with main (5f96998)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (427158e) during the generation of this report, so 5f96998 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

The credential PATCH handler checked server-owned workload identity
federation fields only on the values the caller sent, while a body that
named a deployment through model_id had its credential values resolved
after that check. A non-admin could therefore copy a federated
deployment's WIF fields onto an ordinary credential. Resolve the incoming
values first and run the non-admin gate on them, matching the POST path
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

Comment thread litellm/llms/anthropic/count_tokens/token_counter.py
…ared auth header

Count-tokens walked its own credential ladder: a static key, else skip minting when
ANTHROPIC_AUTH_TOKEN is set, else mint a federated token. With only the auth token set it
forwarded nothing and the proxy silently fell back to its local tokenizer while chat on the
same deployment authenticated with that token. The handler now takes the auth header that
AnthropicModelInfo.aget_auth_header resolves, the same ladder chat, files, batches and skills
use, and merges the oauth beta a minted or consumer token carries with the token-counting beta
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

*AWS_CREDENTIAL_KWARGS_KEYS,
# A federated deployment holds no api_key, so without these the fetch that reads a
# finished batch's output has nothing to authenticate with and its cost is never billed.
*sorted(ANTHROPIC_WIF_KWARGS_KEYS),

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 Provider logic in generic code The batch credential extractor now lists Anthropic federation fields directly. The generic parameter plumbing also names Anthropic and OpenAI identity fields. This violates the repository directive to keep provider-specific code under llms/ and couples shared code to these providers’ authentication schemes. The repository requirement must be satisfied before merging.

Rule Used: What: Avoid writing provider-specific code outside of the llms/ directory. Why: This practice ensures better maintainability and reduces complexity over time. Good: ```python # Handle provider-specific logic within llms/vertex_ai/transformation.py ... (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!

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.

This tuple already enumerates AWS credential keys, and litellm_params types already name every provider's fields. The Anthropic entries follow both precedents.

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.

That’s a fair distinction. This extractor is already an explicit credential-forwarding boundary and enumerates provider-specific authentication keys (including AWS_CREDENTIAL_KWARGS_KEYS); adding the centralized ANTHROPIC_WIF_KWARGS_KEYS set is consistent with that pattern. Since the federation keys are declared in the shared workload-identity/types layer rather than handwritten in this batch module, this does not introduce a separate Anthropic implementation here. I’m withdrawing the provider-logic finding for this change.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment on lines +84 to +87
**auth_header,
"anthropic-beta": merge_anthropic_beta_headers(
auth_header.get("anthropic-beta"), ANTHROPIC_TOKEN_COUNTING_BETA_VERSION
),

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 Consumer OAuth header is dropped

Count-tokens requests using a consumer sk-ant-oat token previously sent anthropic-dangerous-direct-browser-access. The new shared auth-header path sends the bearer token and beta header but omits that header. This changes existing behavior without a user-controlled flag, contrary to the repository directive to avoid backwards-incompatible changes without one. The repository requirement must be satisfied before merging.

Rule Used: What: avoid backwards-incompatible changes without user-controlled flags Why: This breaks current behaviour for users using existing functionality Example of BAD: this PR (#22164) introduced run_post_custom... (source)

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.

That header only acknowledges browser CORS. The base branch's shared auth helper already sends sk-ant-oat bearers without it for files, batches and skills.

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.

You're right. I conflated the OAuth beta required for sk-ant-oat with anthropic-dangerous-direct-browser-access, which only opts a client into Anthropic's browser CORS behavior.

The count-tokens endpoint is server-to-server, and the shared auth helper intentionally uses the same bearer-token behavior already used by files, batches, and skills without that CORS header. Since the relevant behavior—the bearer authorization and OAuth beta—is preserved, I’m withdrawing this finding; no change is needed.

# LiteLLM All Prometheus Metrics dashboard

Every `litellm_*` metric family the proxy can expose on `/metrics` (136 families across 97 panels), grouped into rows: proxy traffic, latency, spend and tokens, cache, LLM API deployments, key and team rate limits, budgets, guardrails, MCP, managed files and batches, users and teams, the Redis circuit breaker, the spend log cleanup job, and the `prometheus_system` service callback metrics (per-service latency, request and failure rates, spend update queue sizes). Panel titles are the metric names so you can grep the JSON for the metric you care about
Every `litellm_*` metric family the proxy can expose on `/metrics` (141 families across 97 panels), grouped into rows: proxy traffic, latency, spend and tokens, cache, LLM API deployments, key and team rate limits, budgets, guardrails, MCP, managed files and batches, users and teams, the Redis circuit breaker, the spend log cleanup job, and the `prometheus_system` service callback metrics (per-service latency, request and failure rates, spend update queue sizes). Panel titles are the metric names so you can grep the JSON for the metric you care about

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 Product documentation added here

The changed README documents LiteLLM’s dashboard and exposed metric families in this repository. The repository directive requires product documentation to live in litellm-docs, and this cookbook README has no applicable exception. That requirement must be satisfied before merging.

Rule Used: Prevent documentation about the LiteLLM product from being added - needs to be in litellm-docs repo instead. This does NOT include any documentation about how to contribute to the LiteLLM repo, README.md, or code comments (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!

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.

This README predates the PR and catalogs the cookbook dashboard's own panels. The diff only bumps its metric family count from 136 to 141.

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.

You're right — this README is an existing cookbook artifact documenting the dashboard JSON, and this change only updates its metric-family count to match the newly added metrics. It does not add new LiteLLM product documentation or bypass the litellm-docs repository. I’m withdrawing this finding.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fce9d8f. Configure here.

@mateo-berri mateo-berri left a comment

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.

LGTM

@mateo-berri
mateo-berri merged commit 0ed1c08 into main Oct 4, 2026
211 of 213 checks passed
@mateo-berri
mateo-berri deleted the litellm_anthropic_wif_backend_devin branch October 4, 2026 00:08
mateo-berri added a commit that referenced this pull request Oct 8, 2026
…models

Main added the same discovery hook as BaseLLMModelInfo.discover_models
(#44448), so the PR's get_models_for_deployment hook and the HTTPHandler
header widening are dropped for main's versions and the Bedrock override
now reads the deployment's region and credentials from the mapping the
proxy passes. The inference profile pager is a bounded for loop (20 pages
of 1000) instead of recursion, which the recursion detector flagged.

This branch was successfully deployed

1 active deployment
e2e-changed — fce9d8f9 Deployed Oct 3, 2026 by mateo-berri via oauth #2951
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.

[Feature]: Support Anthropic Workload Identity Federation (OIDC JWT-bearer token exchange)

3 participants