Skip to content

feat(anthropic): workload identity federation, pluggable identity sources, and provider-level setup (internal copy of #38013) - #38818

Open
mateo-berri wants to merge 89 commits into
litellm_internal_stagingfrom
litellm_internal_copy_38013
Open

feat(anthropic): workload identity federation, pluggable identity sources, and provider-level setup (internal copy of #38013)#38818
mateo-berri wants to merge 89 commits into
litellm_internal_stagingfrom
litellm_internal_copy_38013

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Internal copy of #38013 (fork branch derhornspieler:litellm_anthropic_wif_oidc_28607, superseded at 57b9e94), opened on a litellm_ head so CircleCI runs the legacy suites before merge. Commits and authorship are carried over unchanged

TLDR

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
  • No way to set a provider up once and reuse it
  • No way to test a stored provider credential before a model is added through it

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, including a self-signed issuer and Keycloak
  • Every Anthropic surface uses it: chat, /v1/messages, files, batches, skills, passthrough, token counting and discovery
  • 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 may put a deployment into or alter a federated configuration
  • One provider credential, created in one place: the LLM Credentials modal
  • That modal shows a LiteLLM-signed credential's public JWKS and has a Test Connection button backed by live model discovery
  • OpenAI federation from the forms rides in this PR too (feat(proxy): configure OpenAI workload identity federation from the Add Model and LLM Credentials forms #39613, LIT-6869, merged into the branch at 10d8449): Add Model and LLM Credentials offer a "Workload Identity Federation (token file)" auth method for OpenAI, the identity trio is read per deployment or credential before the env vars, and chat, streaming, Responses, embeddings, Test Connect and Discover Models use the exchanged token
  • The OpenAI trio is server-owned the same way: inline request bodies get 401 naming the field, credentials stay proxy-admin only

User Flow

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

  1. They open http://localhost:4000/ui/?page=models and pick Add Model, whose only Anthropic credential fields are api_base and api_key
  2. They set the federation variables on the proxy instead, leave api_key unset, and declare model: anthropic/claude-haiku-4-5
  3. They send POST http://localhost:4000/v1/chat/completions with {"model": "claude-haiku-4-5", "messages": [...]}
  4. 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
  5. 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

After: the same team configures the provider once and every discovered model 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 open http://localhost:4000/ui/?page=llm-credentials, press Add Credential, pick Anthropic, and select an auth method other than API key
  3. They fill that method's fields. Secret-bearing fields take a reference such as os.environ/KC_CLIENT_SECRET, never a pasted secret. Test Connection stays disabled with a hint to add the credential first, since federation values can only be tested once saved; only an API key and API base can be tested before saving
  4. For the self-signed issuer they add the credential with only the issuer fields, then open Edit on its row: below the fields the modal shows the public JWKS with a copy button and says where to paste it in the Console, without which Anthropic cannot verify anything
  5. Once the Console has created the rule and service account they paste Organization ID, Federation Rule ID, Service Account ID and, for a rule enabled in more than one workspace, Workspace ID into the same modal, press Update Credential, reopen Edit and press Test Connection. A green alert lists the models the credential can actually see, from a live call rather than a static list; a red one carries Anthropic's error, and on a 401 it says to read the rule's authentication history in the Console and, if Workspace ID is blank, that a rule spanning more than one workspace needs it
  6. They add models through Add Model with that credential picked under Existing Credentials
  7. They send the same POST http://localhost:4000/v1/chat/completions and get 200 with the model's reply
  8. Anyone, team admin or proxy admin alike, who puts a federation field straight into a 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 (401, proxy admin only), and the proxy admin attaches federation through a named credential or config.yaml instead
  9. For OpenAI the same forms now offer the federation method: the admin stores the identity provider id, service account id, and token file path as a credential at http://localhost:4000/ui/?page=llm-credentials, adds gpt-5.6 through it on the models page with the key field no longer required, and POST http://localhost:4000/v1/chat/completions with {"model": "gpt-wif", ...} answers 200 with the reply and real token usage. Before, the Add Model form refused to submit without a key and a keyless deployment answered 500 The api_key client option must be set

Relevant issues

Fixes #28607

Docs: BerriAI/litellm-docs#1000

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

Federated end to end (contributor's run at 197fd1e, before at fde3075)

Four models on anthropic/, no api_key anywhere, federation ids from the environment, and the identity token projected at a path inside the OIDC file allowlist:

model_list:
  - model_name: claude-haiku-4-5-wif
    litellm_params: {model: anthropic/claude-haiku-4-5}
  - model_name: claude-sonnet-5-wif
    litellm_params: {model: anthropic/claude-sonnet-5}
general_settings: {master_key: os.environ/LITELLM_MASTER_KEY, store_model_in_db: true}
ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID
ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/token
  1. curl -s localhost:4100/v1/chat/completions -d '{"model":"claude-haiku-4-5-wif","messages":[{"role":"user","content":"Reply with exactly: WIF OK"}],"max_tokens":16}'
  2. Before: {"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. ..."}}. After: content: WIF OK | model: claude-haiku-4-5-wif | tokens: 20
  3. The same request against /v1/messages with claude-sonnet-5-wif. Before: {"type":"error","error":{"type":"authentication_error","message":"x-api-key header is required"}}. After: content: WIF OK | usage: 19 in / 7 out
  4. curl -s localhost:4100/v1/messages/count_tokens -d '{"model":"claude-haiku-4-5-wif","messages":[{"role":"user","content":"count these tokens please"}]}'. Before: no credential, the count never reaches Anthropic. After: {"input_tokens":11}, Anthropic's own count
  5. curl -s localhost:4100/v1/chat/completions -d '{"model":"claude-haiku-4-5-wif", ..., "anthropic_identity_token":"attacker-supplied"}'. Before: the field is neither refused nor honoured and the call fails for want of a key. After: {"error":{"message":"Authentication Error, Rejected Request: anthropic_identity_token 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","code":"401"}}
  6. curl -s localhost:4100/public/providers/fields. Before: credential_variants: None. After: credential_variants: ['api_key', 'wif_token', 'wif_token_file', 'wif_internal_issuer', 'wif_keycloak']

The federated 200 legs were not re-run at the tip: the federation rule, service account, and identity token live only in a Claude Console organization the reviewer cannot provision, so the review runs below cover every leg that does not need a live federation rule. The tip changes nothing on the path those legs exercise beyond the three fixes proven below

Review run at f2240df vs base e585aab, carried to ff59884 since the later commits only touch the dashboard wizard, the lazy import of two proxy-extra packages, and the ANTHROPIC_IDENTITY_SOURCE env precedence, which the run never set, and to 8402ae9 which merges litellm_internal_staging into the branch (one import-order conflict in model_management_endpoints.py) plus the LIT002 budget accommodations that merge forced: two immutable rewrites in common_utils.py and eight suppressions on the legacy files/batches call sites in llm_http_handler.py, and to c4d2f03 whose only delta is an unrelated one-line CI fix (LIT-6519): the savings baseline test now pins the local cost map via the local_model_cost_map fixture, since main's registry update gave grok-4 a cache-read rate and broke the test's precondition on every branch reading the remote map, and to 068c1d1 which merges litellm_internal_staging again after #38863 landed the canonical fix for that test (a map-derived baseline pick), the one conflict resolved by taking staging's version of test_savings.py wholesale so the interim fixture pin is superseded (two proxies, --num_workers 2 each, real Anthropic key on the control models, real spend), and to c6f585e which fixes defect 8 below in the token counter, touching only token_counter.py and its regression tests; its count-tokens legs were re-driven live in the defect 8 A/B, and to c813cc7 which merges litellm_internal_staging a third time (seven conflicts, all resolved by keeping both sides: the WIF-aware _list_models in common_utils.py, the check_potential_json_str guard in model.py, the credential-hydration and encrypt-decrypt imports plus the blocked-flag and attach-credential checks in model_management_endpoints.py, the server-owned-header import next to staging's httpx client import in llm_passthrough_endpoints.py, staging's # test-quality-ok patch forms in its test, the WIF secret-pointer test alongside staging's tier-fallback class in test_router.py, and the models-and-endpoints tab tests rewritten on staging's VIEW_ONLY_ADMIN fixture with the Add Provider tab assertions folded in) plus the two accommodations that merge forced: ruff's blank-line trim in model_management_endpoints.py, and the Add Provider integration test's row lookup moving from .closest("tr") to a row role query since staging ratcheted the dashboard testing-library/no-node-access budget to 716 on 2026-09-01 and the old lookup was the 717th hit, with the merge tip re-driven in full on the rig rather than carried (46 S legs against the base, three of them new: S15k for a team admin registering a model through a stored credential, and S24a to S24c for fallbacks given as a config entry or as request-body dicts with and without a federation field inside; D01 to D16 with the proxy booted through a dotenv-patching wrapper so the worktree .env cannot re-supply ANTHROPIC_API_KEY; and the same 46 S legs on a worktree with staging a677242 merged in, identical to the tip apart from noise; the rig's LITELLM_LICENSE no longer verifies, so the team-admin legs ran on the non-premium path), and to 412890f, which fixes defect 9 below in the count-tokens URL builder, its handler call and their regression tests and touches nothing else, re-driven in full again: 54 S legs against base 2ffe6a1 (the eight new S25 legs drive count tokens and chat on deployments whose api_base is a bare host or ends in /v1/messages), D01 to D16 under the wrapper, and a merge of staging 987ab76 into the tip that applies clean, with no commit on staging touching count_tokens/ since the base, and to 272ccfd, which fixes defect 10 below by giving the token exchange and the count-tokens URL one shared base resolver and touches nothing else, re-driven in full: 59 S legs against base 2ffe6a1 (the five new S25 and S26 legs drive count tokens and chat on a deployment with api_base: "" and chat on federated deployments with an empty or bogus base), D01 to D16 plus D04b for a federated deployment with api_base: "" under the wrapper, the same S25 and D legs at 412890f for the before side, and six E legs on both sides with ANTHROPIC_API_BASE=https://gateway.invalid exported, and to c0739de, the fourth litellm_internal_staging merge, made from GitHub's Update branch, which merged staging's rename of the SSO helper in test_auth_utils.py as one call to the old name (the lint and proxy-auth checks caught it), to 55e303e, which repairs that one test line and nothing else, and to 4a785c8, the fifth staging merge (one conflict, in test_proxy_server.py, where both sides appended tests at the end of the file and both were kept), and to 0a6f606, the sixth staging merge (two conflicts: the import block of types/proxy/public_endpoints/public_endpoints.py, resolved as the union of both sides, and test_public_endpoints.py, where both sides appended tests at the end of the file and both were kept, and where taking staging's tightened reportUnnecessaryComparison ceiling in place of the branch's stale one surfaced four dead comparisons this PR had added: the two token-poster response is None guards now run through one require_posted_response helper whose parameter is typed optional, so the guard and the tests that cover it stay, and the two complete_api_base is None guards the PR duplicated into its new async file and batch wrappers are gone, since both URL builders are declared to return str), with the tip re-driven at 55e303e, at the fifth merge and at the sixth: the same 59 S legs against the base, identical to 272ccfd apart from noise, and D01 to D16 plus D04b with D12 and D13 repeated three times each (at the sixth merge the federation error text names the keycloak host as REDACTED, staging's #39380 at work, a body diff only), and to cb03462, the seventh staging merge, which brings in one Azure storage commit with no file in common with this PR and applies clean, and to 3794ba9, which resolves the count-tokens static key through AnthropicModelInfo.get_api_key so a secret-manager-only key is honored, touching only token_counter.py and its regression tests; the tip was re-driven in full at both, the same 59 S legs and D01 to D16 plus D04b, with no status difference between the two tips and only the documented noise in the bodies, and to 9036b90, the eighth staging merge, made after #36260 landed on staging and turned the branch CONFLICTING (four conflicts, all resolved by keeping both sides: DELETE /credentials/{name} in credential_endpoints/endpoints.py now runs the federation admin gate first and then staging's CredentialsRepository.delete_by_name, so a never-stored or config-only credential answers staging's 404 with the in-memory entry kept; the test_endpoints.py fixture rewritten around a prisma mock whose find_unique returns None with the repository patched at the endpoints import site only; staging's single-line # test-quality-ok patch forms kept in test_model_management_endpoints.py; and both the federation secret-pointer test and staging's reasoning-effort class kept in test_router.py) plus the accommodation the TQ008 patch budget forced (four displaced suppressions restored, one fixture patch swapped for the prisma mock, and a patch.dict in test_llm_pass_through_endpoints.py swapped for monkeypatch.setitem). The merge tip was driven live on a DB-backed proxy with two uvicorn workers and real Anthropic and OpenAI spend: 25 targeted legs on the conflicted endpoint (admin create and delete of a federation credential 200, a never-stored name 404, a config-only credential 404 while GET /credentials still lists it and chat through it still answers 200, an internal user refused 401 at the route on both deletes, a federation field inline in a chat body 401, /public/providers/fields listing the five Anthropic auth variants; a GET on the other worker within milliseconds of the delete still listed the row until staging's config sync tick cleared it, pre-existing and not a regression), then the same 59 S legs and D01 to D16 plus D04b in full, with no status difference from 3794ba9 apart from latency; the rig's license still does not verify, so the team-admin legs again ran on the non-premium path

Same config, same 30 requests in the same order on both sides, S01 to S23 in the run log. Identical on both sides: liveliness and readiness, /v1/chat/completions, /v1/messages, /v1/responses, /v1/messages/count_tokens and /anthropic/v1/messages passthrough on a static-key deployment and on an env-key deployment (all 200, pong), key generate, chat, key info spend and delete, the spend log row, /v1/model/info, /v1/models, /ui/, /health. The differences are the feature:

  1. POST /v1/chat/completions with "anthropic_federation_rule_id":"fdrl_x" in the body (S10). Base: 400 AnthropicException - invalid_request_error after the field reached Anthropic as an unknown parameter. Tip: 401 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.
  2. POST /provider/models/discover with {"custom_llm_provider":"anthropic"} (S13). Base: 404. Tip: 200 with the live model list; the same call carrying anthropic_federation_rule_id (S13b) is refused with the same 401
  3. POST /model/new carrying federation fields inline, as a team admin (S15e) and as the proxy admin (S15h): both 401 naming the field. A team admin on POST /credentials (S15g): 401 Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/credentials. A team admin creating a plain deployment (S15f) still gets 200
  4. GET /public/providers/fields (S12). Base: no credential_variants key. Tip: credential_variants: [api_key, wif_token, wif_token_file, wif_internal_issuer, wif_keycloak] on the anthropic entry, every other provider unchanged
  5. A deployment carrying bogus federation ids while ANTHROPIC_API_KEY is set in the proxy environment (S11) answers 200 pong on both sides: the static key wins over federation, see Caveats

Defects found in review, before at a7ed600 and after at f2240df, the dashboard defect after at 57b9e94, the base install defect after at f8b3184, the precedence and error-detail defects after at ff59884, the count-tokens api_base defect after at 412890f, and the count-tokens env-base defect after at 272ccfd (proxy booted without ANTHROPIC_API_KEY, --num_workers 2, router_settings: disable_cooldowns: true)

Config: claude-wif-keycloak (keycloak, anthropic_keycloak_client_secret_ref: os.environ/KC_SECRET, token url pointing at an unreachable https://127.0.0.1:9/...), claude-wif-tokenfile (token file inside LITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS), claude-wif-issuer (internal_issuer inline, anthropic_issuer_signing_key_ref: os.environ/ISSUER_KEY holding a P-256 PEM), claude-wif-issuer-cred (litellm_credential_name pointing at a credential_list entry mirroring the docs example), federation ids as os.environ/ references, every one a placeholder (fdrl_rigbogus...) since no live rule is available

  1. POST /v1/messages on claude-wif-keycloak (D03). Before: 401 anthropic_identity_source is 'keycloak', but anthropic_issuer_audience, anthropic_issuer_signing_key_ref, anthropic_issuer_subject, anthropic_issuer_ttl_seconds, anthropic_issuer_url belongs to a different identity source, so the endpoint could never federate: /v1/messages hands the resolver every deployment field with the unset ones as None, and the resolver rejected them as foreign. After: the request reaches the identity source, 401 Could not obtain the OIDC identity token (unreadable) from oidc/keycloak/...: could not reach the keycloak token endpoint https://127.0.0.1:9/realms/rig/protocol/openid-connect/token: ConnectError. The same flip on claude-wif-issuer-cred (D09) and on a credential-backed model (D13)

  2. POST /v1/chat/completions on claude-wif-keycloak (D02) and on claude-wif-issuer (D07) and claude-wif-issuer-cred (D08). Before: 401 ... keycloak client secret <withheld: not a secret reference> could not be read and 401 ... internal_issuer signing key <withheld: not a secret reference> could not be read: config.yaml loading expanded os.environ/KC_SECRET into the raw secret, and the identity source, which dereferences that pointer itself at use time, then saw a value that is not a reference. After: D02 reaches Keycloak (ConnectError from the unreachable rig endpoint), D07 and D08 mint and send the assertion and get Anthropic's own answer, 401 The token endpoint returned HTTP 400: error: invalid_request_error - federation_rule_id is not a well-formed fdrl_ tagged ID, which is exactly the placeholder rule id. The token-file model (D04, D05) returns that same Anthropic answer on both sides, and a credential created through POST /credentials (D10 to D13) was never affected because the credentials API never expanded the reference

  3. Async create_file and create_batch validated the provider credential on the event loop before dispatching, so a federated token exchange during an async file upload or batch create blocked every other request on that worker. No curl shows this deterministically; the regression tests in tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py assert the async hook is awaited (and a sync hook runs off the loop thread) and fail on the previous handler

  4. The Add Provider wizard on the models page kept the previous provider's form values after switching providers, so an Anthropic key typed on step two travelled into the OpenAI credential when the operator went back and picked OpenAI. The wizard was fixed to reset its form on a real provider change, and at 3a46fa2 the wizard is removed altogether; the LLM Credentials modal, now the only place a credential is created, does the same reset and its unit tests cover it

  5. A base pip install litellm (no extras) could no longer import litellm: the Anthropic provider now imports litellm/llms/base_llm/auth/jwt_signing.py, which imported PyJWT and cryptography at module level, and both ship only in the proxy extra (CircleCI base_sdk_install caught it on the internal copy). The module now imports them inside the two functions that sign or load keys and raises an ImportError naming litellm[proxy] when they are missing, so the internal issuer identity source fails at use time instead of every base SDK user failing at import; tests/base_sdk_tests/check_base_sdk_install.py passes 7/7 on a wheel installed into a fresh no-extras venv at f8b3184, and the subprocess test in test_jwt_signing.py blocks both packages and fails on the previous module

  6. Setting ANTHROPIC_IDENTITY_SOURCE in the proxy environment forced every deployment without a param-level kind onto that identity source, including deployments whose params configure a legacy anthropic_identity_token_file or anthropic_identity_token ref: those failed closed on the missing issuer or keycloak fields, with no kind value to opt back out (found by bugbot). Kind resolution now lets deployment params win: a param-level anthropic_identity_source first, a param-level token or token-file ref keeps that deployment on legacy resolution, and the env kind governs only deployments that set no identity params of their own; the regression tests in test_anthropic_wif.py pin a param token ref beating the env kind and fail on the previous resolver. A live A/B on the rig (env kind exported, no ambient key, D14) shows it end to end: the before proxy answers 401 Invalid anthropic_identity_source configuration naming the three missing InternalIssuerSource fields without ever reading the token file, and the after proxy reads the param token file, reaches Anthropic's token endpoint, and returns the placeholder-rule 401 federation_rule_id is not a well-formed fdrl_ tagged ID

  7. The ImportError from defect 5's lazy import surfaced as a bare unreadable with no detail, because the exchange engine captured a message only for ValueError (found by bugbot). _read_assertion now catches ImportError the same way, so the 401 names the litellm[proxy] install hint instead of an unexplained unreadable ref; the regression test in test_token_exchange.py asserts the detail carries litellm[proxy] and fails on the previous handler

  8. POST /v1/messages/count_tokens on a federated deployment (found by bugbot). The token counter treated only a deployment api_key or ANTHROPIC_API_KEY as static credentials, so a proxy relying on ANTHROPIC_AUTH_TOKEN still minted a federation token for counting, and the mint sat outside the counter's error handling, so a failed mint raised past the endpoint's local-tokenizer fallback: /v1/messages/count_tokens and /utils/token_counter?call_endpoint=true both answered 500 Internal server error: litellm.AuthenticationError .... The counter now skips minting whenever a static key or ANTHROPIC_AUTH_TOKEN is present, mirroring chat's validate_environment, and a failed mint degrades like any other Anthropic counting error (error=True, status 401), so the endpoint falls back to the local tokenizer. Live A/B at 068c1d1 vs c6f585e on the bogus-federation deployment: before, both routes return 500, and they still mint with ANTHROPIC_AUTH_TOKEN exported; after, both return 200 with the local count (tokenizer_type: huggingface_tokenizer) and the auth-token boot never mints; the static-key control counts 11 tokens on both sides. The regression tests in test_count_tokens_oauth.py fail on the previous counter

  9. POST /v1/messages/count_tokens and POST /utils/token_counter?call_endpoint=true on a deployment carrying an api_base (found by bugbot at c813cc7). Defect 8's fix started forwarding the deployment api_base to the count-tokens handler, but the handler took that value as the complete count-tokens URL, so a bare-host base posted to the host root and got Anthropic's 404, and a base written as .../v1/messages posted the count body to the messages endpoint and got a 400; either way the counter quietly fell back to the local tokenizer. The handler now derives <base>/v1/messages/count_tokens through the same suffix-stripping helper the token exchange and model discovery already use, so a base with or without /v1 or /v1/messages lands on the count endpoint. On the rig (claude-haiku-basehost with api_base: https://api.anthropic.com, claude-haiku-basemsgs with api_base: https://api.anthropic.com/v1/messages, --num_workers 2): base 2ffe6a1 and 412890f both answer tokenizer_type: anthropic_api on /utils/token_counter?call_endpoint=true and {"input_tokens":12} on /v1/messages/count_tokens for both deployments; c813cc7 answers tokenizer_type: huggingface_tokenizer for both while its log carries Anthropic CountTokens error: status=404 and status=400; chat on both deployments is 200 pong on every side. The regression tests in test_anthropic_count_tokens_transformation.py drive the handler through respx against https://gateway.example and fail on the previous handler

  10. POST /utils/token_counter?call_endpoint=true and POST /v1/messages/count_tokens on a deployment without an api_base (found by bugbot at 412890f). Defect 9's fix built the count-tokens URL from the deployment api_base alone, so a deployment with none skipped the ANTHROPIC_API_BASE / ANTHROPIC_BASE_URL that chat and the token exchange both follow, and an api_base: "" posted the count body to the bare relative path. The token exchange URL, its cache key and the count-tokens URL now come from one resolver: the deployment api_base, else the env base, else Anthropic's host, with a trailing slash or a chat-appended /v1/messages stripped. On the rig (--num_workers 2): claude-haiku-baseempty (api_base: "") answers tokenizer_type: huggingface_tokenizer on /utils/token_counter?call_endpoint=true at 412890f with Anthropic CountTokens error: status=500, message=CountTokens processing error: /v1/messages/count_tokens in the log, and anthropic_api at 272ccfd like base 2ffe6a1; /v1/messages/count_tokens answers {"input_tokens":12} on every side because that endpoint falls back to the local tokenizer silently. With ANTHROPIC_API_BASE=https://gateway.invalid exported, chat on claude-haiku fails with Cannot connect to host gateway.invalid:443 on both sides, and at 272ccfd the count fails the same way and falls back (huggingface_tokenizer), where base 2ffe6a1 still counted through Anthropic's host (anthropic_api); the explicit claude-haiku-basehost deployment counts through anthropic_api and chats 200 pong on every side. Chat on a federated deployment with api_base: "" (D04b) reaches the token endpoint on both sides, since chat resolves the base before minting; only the count path handed the mint the raw value. The regression tests in test_anthropic_count_tokens_transformation.py and test_anthropic_wif.py fail on the previous source

Observed alongside, none caused by this PR:

  • /v1/messages/count_tokens maps provider auth failures to 500 for non-federated deployments too, pre-existing; the federated leg is fixed as defect 8 and the general mapping is being fixed in fix(proxy): return provider auth errors from /v1/messages/count_tokens instead of masking or 500 #38902
  • Two workers: a /model/new model can miss its /credentials credential on the other worker, pre-existing (on the D rig, chat and messages on a federated model registered through a keycloak credential answer the federation error or Missing Anthropic API Key across six repeats of the same request, whichever worker picks it up)
  • Static or ambient ANTHROPIC_API_KEY silently wins over deployment federation, by design
  • Master key rotation drops the blocked flag on keys, pre-existing
  • Nine-character secrets mask to a single asterisk in error text, pre-existing

Ninth and tenth staging merges, with #39613 folded in

Staging merged in twice more after 9036b90. The ninth merge resolved two conflicts (staging's heuristic_v2 auto-router limit from #39468 and the write_row / team-model test replacement), then the stacked #39613 (OpenAI federation from the Add Model and LLM Credentials forms, LIT-6869, approved on its own PR at fe6a535) was merged into this branch at 10d8449, and the tenth merge resolved one conflict in tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py by keeping both sides' tests. make check passes at the merged tree, and the two pytest runs over every test directory the PR touches come back with only environment-bound reds (A, 6461 unit tests: 6458 passed, 13 skipped, 1 xfailed, and 2 that fail only because litellm's import-time load_dotenv() reads the worktree's gitignored .env (test_aretrieve_batch_missing_api_key_raises sees a real ANTHROPIC_API_KEY, test_openai_env_base[OPENAI_API_BASE] sees OPENAI_BASE_URL win), both green with the file moved aside; B: test_dual_cache 14 passed against a local Redis, the four TestMigrateDeployAttemptAccounting tests are red on staging's own test-unit proxy-extras shard at a5b3bc8 too (LIT-6918), and the Responses e2e suite targets CI's booted proxy, so the CircleCI run at the tip covers it)

Same rigs as the earlier review runs, at the merged tree (--num_workers 2, Postgres, random ports, real spend): the S run's 59 legs and the D run's 23 legs answer the same statuses as at 9036b90, and the Q run's 25 legs differ only on Q11, a GET /credentials racing a /model/new on the other worker, the pre-existing cross-worker credential window (LIT-6901). A D run booted with --detailed_debug proves staging's verbose-line redaction (#39526, #39538) on the federation fields: the kwargs line prints anthropic_keycloak_client_secret_ref='REDACTED', anthropic_issuer_signing_key_ref='REDACTED', anthropic_identity_token_file='REDACTED' and anthropic_keycloak_token_url='REDACTED', and the whole log has zero hits for the keycloak secret, either PEM header, client_secret= or signing_key=, while the ids (organization, federation rule, keycloak client id, issuer url, subject and audience) print in the clear

OpenAI federation legs at the merged tree (#39613's A to I, re-run)

The proxy for these legs strips OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID and OPENAI_IDENTITY_TOKEN_FILE after load_dotenv() (the worktree's .env otherwise puts the static key back inside the proxy, see Caveats), and a control deployment openai/gpt-5.6 with no key proves the env is clean: four chats in a row answer AuthenticationError: OpenAIException - The api_key client option must be set. With the federation credential (identity provider id, service account id, token file) stored through POST /credentials: Test Connect 200 success with OpenAI's rate-limit headers, POST /model/new through the credential 200 and GET /model/info showing only litellm_credential_name, chat 200 ok, streaming 200, POST /v1/responses 200, embeddings 200 (spend rows on https://api.openai.com/v1, real cost), Discover Models 200, the trio typed inline on /model/new 401 naming the field, a non-admin key on /credentials 401, and the chat, streaming, Responses and embeddings legs again 45s later all 200. The EU credential (api_base: https://eu.api.openai.com/v1): the chat sent 2s after /credentials on the two-worker proxy answers 500 The api_key client option must be set when the worker that has not refreshed its credential list picks it up (LIT-6901's window), and on a fresh boot every chat reaches https://eu.api.openai.com/v1 with the federation bearer and gets OpenAI's This endpoint is only accessible by projects with geography restrictions enabled (twice as upstream 401, twice as 403, proxied as such), the same result as #39613's leg I, while gpt-wif on the default host answers 200 alongside. The dashboard walkthrough for the OpenAI forms is in #39613's description and stands: no dashboard file changed after it

Eleventh staging merge (37f2de1)

Staging moved to a53c550 (#39661) and the PR went CONFLICTING again on litellm/llms/openai/workload_identity.py and its test: #39652 moved the OpenAI host check into is_openai_backed_api_base in litellm/llms/openai/common_utils.py (it accepts api.openai.com and any *.api.openai.com, PrivateLink and regional hosts included), a superset of this branch's _OPENAI_REGIONAL_HOST_SUFFIX handling, so the merge keeps staging's helper and drops the branch-local constants; _targets_openai_api now returns parsed.scheme == "https" and is_openai_backed_api_base(api_base). The resolved test module keeps staging's test_openai_backed_api_base_allows and test_lookalike_or_plaintext_api_base_disables in place of the branch's regional and lookalike cases. make check passed on the staged merge. The PR-touched unit-test groups ran locally in CI's shard shape (-n 4 --reruns 1): test_anthropic_skills_transformation.py plus test_router.py green; batches integrations litellm_core_utils router_utils types models 7763 passed and 1 failed (test_websearch_streaming_conversion); llms/anthropic llms/base_llm/auth llms/custom_httpx llms/openai 3353 passed and 1 failed (test_aretrieve_batch_missing_api_key_raises); proxy 21651 passed and 52 failed. Re-running those 54 reds serially with the local .env moved aside leaves 23: 21 test_semantic_tool_filter.py cases (No module named 'semantic_router', an optional dependency not installed locally) and 2 test_guardrail_coverage.py secret-detection cases (No module named 'detect_secrets'), and all of them fail identically on a pure staging tree at a53c550 through the same venv, so none is this branch's

QA at the eleventh-merge tree (37f2de1)

Same rigs as the tenth-merge run, on the merged tree with --num_workers 2, Postgres and random ports. The OpenAI federation legs (port 29518) answer the same statuses as at 055eaee modulo ids and rate-limit counters: the keyless control answers 500 four times, B to F 200, G and H 401, the E rerun 200, and leg I (the EU credential) answers 200 on its first two calls and then OpenAI's 403 This endpoint is only accessible by projects with geography restrictions enabled on chat, the same answer the EU re-run at 10d8449 got, so the request reaches https://eu.api.openai.com/v1 with the federation bearer and OpenAI refuses the project, not the proxy. Every keyless spend row is success with a real api_base and non-zero cost. The Anthropic S run (59 legs) and D run (23 legs) against a fresh Postgres 16 container answer the same leg counts and statuses as at 055eaee except S14b-credential-get, 404 at the tenth merge and 200 here: GET /credentials/by_name reads the worker-local litellm.credential_list, so on two workers the GET answers 200 only when it lands on the worker that served the POST /credentials, the pre-existing cross-worker credential window already listed under Caveats (the base answered 404 on the same leg, the original head 200, and nothing under litellm/proxy/credential_endpoints/ changed between the two merge tips). The D12 and D13 legs flip between Missing Anthropic API Key and the keycloak ConnectError federation failure for the same reason, both 401, as in every earlier run

CircleCI at 37f2de1

The legacy suites ran at the eleventh-merge tip after a run-ci label cycle, 40 of 42 jobs green, llm_translation_testing, local_testing_part1, local_testing_part2, pass_through_unit_testing, base_sdk_install, build_docker_database_image and auth_ui_unit_tests among them (upload-coverage does not run behind a red job). The two reds are staging's: litellm_router_unit_testing on test_no_linear_scans_in_router, red on litellm_internal_staging itself since #39468 and tracked as LIT-6911, and e2e_ui_testing on tests/users/searchUsers.spec.ts (the search placeholder #39604 renamed, LIT-6919), neither touching a file this PR changes. At the tenth-merge tip 055eaee the same two jobs were the only reds once pass_through_unit_testing, which flaked on a single run, passed on rerun. On the GitHub side all 33 required checks are green at both tips; the non-required osv-scan stays red on the pre-existing gitpython advisory in uv.lock, the same result on the base branch

Twelfth staging merge, discovery api_base fix and empty-credential-value fix (7a9ee45, 56cf0cd, 1caa2fa)

7a9ee45 answers a Bugbot thread: an empty api_base on an OpenAI deployment is treated as unset when discovering models instead of being parsed as a host. 56cf0cd merges staging at c4e9076 (#39037's Anthropic error envelope, the SCIM default-team fixes, the Vertex passthrough default location, the router pre-call context-window count and the daily spend summary grouping) and resolves litellm/router.py, llm_passthrough_endpoints.py and their two test modules by hand, keeping both sides' tests; this PR's own router.py delta stays the five-line secret-pointer guard. 1caa2fa keeps an empty credential value empty after decryption: decrypt_value_helper(...) or value handed the stored ciphertext back for a value that decrypts to the empty string, so a field cleared through the wizard came back as its encrypted form on the next read, and decrypted_or_stored now falls back to the stored value only when decryption answers None (a config.yaml value that was never encrypted). 5e5c436 is the wizard commit described below. At the tip the five pytest modules these commits touch pass locally (384 tests across test_anthropic_wif.py, test_public_endpoints.py, test_credential_hydration.py, test_proxy_config.py and test_openai_workload_identity.py), the 22 vitest cases in anthropicFederation.test.ts and AddProviderPanel.integration.test.tsx pass, and every required GitHub check, the lint and code-quality jobs included, is green

Thirteenth staging merge (669f028)

Staging moved to 2e73400 (#39399's Python version matrix, the per-worker admission control 503s from #39352, the SSE keepalives from #39273 and the pattern-router eviction from #39664) and the PR went CONFLICTING on tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py: #39399's cross-version fix rewrote the two parenthesized with (...) blocks this branch had reformatted, and the merge keeps staging's form of both. The five code files both sides touch (_health_endpoints.py, llm_passthrough_endpoints.py, proxy_server.py, router.py, utils.py) change in different functions on each side, so no line this PR adds moved. make check passes on the merge and the seven pytest modules that hold the federation code pass at the merged tree (594 passed). The fail-closed D rig from the 5e5c436 run was driven again at 669f028 (--num_workers 2, Postgres 16, port 41873): the same 22 legs answer the same statuses, every chat and /v1/messages through a LiteLLM-signed credential with no ids answers 401 naming anthropic_federation_rule_id and anthropic_organization_id, the rule-id-only legs name the singular field, the token-endpoint legs carry the denial hints, and the two calls that land in the cross-worker credential window (LIT-6901) and answer the generic Missing Anthropic API Key 401 are the first two token-endpoint legs here where they were two of the fail-closed legs at 5e5c436. 3a46fa2, the credential-modal commit described below, follows this merge

Fourteenth staging merge (4592e71)

Staging moved to d23bec8 (122 commits, among them #39801's health probe by named credential, the chronic-test repairs in dbf8fe0 and a5a7867, and 7d3b03d's redis event-loop stall test) and 3a46fa2 merged clean with no conflicts. On the nine files both sides touch (_health_endpoints.py, proxy_server.py, router.py, types/utils.py, coverage_allowlist.txt, test_streaming_iterator.py, test_public_endpoints.py, networking.tsx, schema.d.ts) this PR's added and removed lines are the same before and after the merge, so the merge changed nothing this PR does. make check passes at the tip, the mapped suites pass there (62 pytest across the credential endpoints, credential hydration, TestDiscoverProviderModels and the redis stall test, 43 vitest across CredentialModal, credential_form_helpers and the models page), and the fail-closed legs re-driven on a fresh two-worker rig at port 40886 (22 legs) answer 401 on every chat and /v1/messages call that is meant to fail closed and 200 on every credential, model and read step

Fifteenth staging merge (216da96)

Staging moved to 7573632 (142 commits, among them #39461's Any reduction, #39780's proxy-infra Python 3.10 fix, #39848's guardrail cost field for the Datadog integration, #39842's spend-log test for LIT-6949, #35853's retry policy for 503s and #36575's stateless MCP follow-up) and 4592e71 went CONFLICTING on one import line of litellm/llms/anthropic/files/transformation.py: #39461 dropped Any from the typing import and typed params as Final[dict[str, str]], while this branch adds from collections.abc import Mapping on the neighbouring line for _finalize_headers, so the merge keeps staging's narrowed import and the branch's Mapping import together. On the fourteen files both sides touch (batch_utils.py, the Anthropic messages and files transformations, model_management_endpoints.py, proxy_server.py, fallback_event_handlers.py, types/router.py, types/utils.py, utils.py, three test modules, networking.tsx and schema.d.ts) this PR's added and removed lines are the same before and after the merge, so the merge changed nothing this PR does. make check passes at the tip and the mapped suites pass there (97 pytest across the credential endpoints, credential hydration, TestDiscoverProviderModels, the Anthropic files transformation and the redis stall test, 43 vitest across CredentialModal, credential_form_helpers and the models page). Staging has since moved to aea5358 (#39843, #39844), whose one shared file litellm/llms/openai/responses/transformation.py merges clean with this tip (git merge-tree), so the PR reads MERGEABLE there without another merge.

Bugbot finding at 216da96, fixed at 82afbbd

Bugbot's pass on the fifteenth merge found one issue: _resolve_identity_source in litellm/llms/anthropic/wif.py filtered only None out of a deployment's federation params, so a blank string left behind by an API client's PATCH counted as set, a blank anthropic_keycloak_client_id on a LiteLLM-signed deployment tripped the foreign-field check, and a blank anthropic_issuer_ttl_seconds reached pydantic's integer field and failed validation. The fix treats "" as unset in both the resolver and _build_variant, which the JWKS export route calls too, so a blank optional field on a saved credential no longer 400s the public key block. Two regression tests cover it, test_blank_optional_and_foreign_fields_count_as_unset on the resolver and test_jwks_export_treats_blank_optional_fields_as_unset on the export route, both red on 216da96 (the first with the foreign-field error, the second with the 400) and green at the fix; the non-admin key-presence gate on POST /credentials is an authorization rule and stays as it was. make check passes at the tip and the two mapped test files pass there (141 tests). The merge base is unchanged at 7573632 and the tip still merges clean with staging

Register issuer step, before at 1caa2fa and after at 5e5c436

Superseded: the wizard these legs drove is removed at 3a46fa2, see the next section. The backend legs (fail-closed discovery, the refused workspace id, the variant metadata) still hold and are re-run there

The proxy runs from the PR worktree with ISSUER_SIGNING_KEY_PEM holding a P-256 PEM, ANTHROPIC_API_KEY unset, master key sk-1234, and the dashboard dev server on port 3000. No federation rule exists for this rig, so the ids are placeholders and every leg that reaches Anthropic gets its own 400 for the malformed fdrl_. The before side swaps only the two backend files this commit changes into the running proxy; the dashboard before side is the code as it stood

Before (1caa2fa)

Discover before the Console has generated the ids

  1. curl -s localhost:4000/credentials -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"credential_name":"wif-qa","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"}}', observed {"success":true,"message":"Credential created successfully"}
  2. curl -s localhost:4000/credentials/wif-qa/jwks -H "Authorization: Bearer sk-1234" | jq -c '.keys[0] | {kty, crv, alg, use}', observed {"kty":"EC","crv":"P-256","alg":"ES256","use":"sig"}
  3. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa"}', observed {"detail":{"error":"Model discovery failed: ANTHROPIC_API_BASE/ANTHROPIC_BASE_URL or ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN (or workload identity federation via ANTHROPIC_FEDERATION_RULE_ID/ANTHROPIC_ORGANIZATION_ID/ANTHROPIC_IDENTITY_TOKEN_FILE) is not set. Please set the environment variable, to query Anthropic's /models endpoint."}} and HTTP 502: the credential's identity source is skipped and the message points at environment variables the admin never meant to use

Discover once the ids are saved

  1. curl -s -X PATCH localhost:4000/credentials/wif-qa -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"credential_name":"wif-qa","credential_info":{"custom_llm_provider":"anthropic"},"credential_values":{"anthropic_organization_id":"00000000-0000-4000-8000-000000000000","anthropic_federation_rule_id":"fdrl_qa_fake","anthropic_service_account_id":"svac_qa_fake"}}', observed {"success":true,"message":"Credential updated successfully"}
  2. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa"}', observed {"detail":{"error":"Model discovery failed: 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"}} and HTTP 502: the placeholder rule id reached Anthropic's token endpoint

Workspace id typed into the discover body

  1. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa","anthropic_workspace_id":"default"}', observed {"error":{"message":"Authentication Error, Rejected Request: anthropic_workspace_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead. On the Bedrock Claude Platform route, pass workspace_id or aws_workspace_id instead.","type":"auth_error","param":"None","code":"401"}} and HTTP 401

Credential form metadata for the LiteLLM-signed variant

  1. curl -s localhost:4000/public/providers/fields | jq -c '.[] | select(.provider=="Anthropic") | .credential_variants.variants[] | select(.id=="wif_internal_issuer") | {optional_field_keys, last_four: (.field_keys[-4:])}', observed {"optional_field_keys":["anthropic_federation_rule_id"],"last_four":["anthropic_issuer_subject","anthropic_issuer_audience","anthropic_issuer_ttl_seconds","anthropic_issuer_signing_key_ref"]}: the Authentication step demands Organization ID before the Console can produce it, and the variant has no Service Account ID or Workspace ID field at all

Dashboard wizard

  1. At http://localhost:3000/models-and-endpoints/ the Add Provider wizard refuses to save the LiteLLM-signed credential until Organization ID is typed on the Authentication step
  2. After the JWKS is registered in the Console, the Register issuer step offers a single Federation Rule ID box, so a rule enabled in more than one workspace fails discovery with Anthropic's opaque 401 (workspace_id_required in the Console's authentication history), and adding the Workspace ID means Back twice, a re-save, and retyping the rule id

After (5e5c436)

Discover before the Console has generated the ids

  1. curl -s localhost:4000/credentials -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"credential_name":"wif-qa","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"}}', observed {"success":true,"message":"Credential created successfully"}
  2. curl -s localhost:4000/credentials/wif-qa/jwks -H "Authorization: Bearer sk-1234" | jq -c '.keys[0] | {kty, crv, alg, use}', observed {"kty":"EC","crv":"P-256","alg":"ES256","use":"sig"}
  3. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa"}', observed {"detail":{"error":"Model discovery failed: 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."}} and HTTP 502

Discover once the ids are saved

  1. curl -s -X PATCH localhost:4000/credentials/wif-qa -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"credential_name":"wif-qa","credential_info":{"custom_llm_provider":"anthropic"},"credential_values":{"anthropic_organization_id":"00000000-0000-4000-8000-000000000000","anthropic_federation_rule_id":"fdrl_qa_fake","anthropic_service_account_id":"svac_qa_fake"}}', observed {"success":true,"message":"Credential updated successfully"}
  2. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa"}', observed {"detail":{"error":"Model discovery failed: 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"}} and HTTP 502: the placeholder rule id reached Anthropic's token endpoint

Workspace id typed into the discover body

  1. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa","anthropic_workspace_id":"default"}', observed {"error":{"message":"Authentication Error, Rejected Request: anthropic_workspace_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead. On the Bedrock Claude Platform route, pass workspace_id or aws_workspace_id instead.","type":"auth_error","param":"None","code":"401"}} and HTTP 401

Credential form metadata for the LiteLLM-signed variant

  1. curl -s localhost:4000/public/providers/fields | jq -c '.[] | select(.provider=="Anthropic") | .credential_variants.variants[] | select(.id=="wif_internal_issuer") | {optional_field_keys, last_four: (.field_keys[-4:])}', observed {"optional_field_keys":["anthropic_organization_id","anthropic_federation_rule_id"],"last_four":["anthropic_organization_id","anthropic_federation_rule_id","anthropic_service_account_id","anthropic_workspace_id"]}: the Authentication step no longer needs the ids Anthropic generates later, and the variant carries all four in the order the Register issuer step shows them

Dashboard wizard

Driven in headless Chromium against the dev server, then repeated by hand for the screenshots

  1. Sign in at http://localhost:4000/ui/login with sk-1234, open http://localhost:3000/models-and-endpoints/, Add Provider, Anthropic, name wif-qa-ui, Next
  2. Authentication method Workload Identity Federation (LiteLLM-signed), Issuer URL https://litellm.local/qa, Subject litellm-wif-demo, Audience https://api.anthropic.com, Signing Key Reference os.environ/ISSUER_SIGNING_KEY_PEM, every id left blank, Save credential: the toast says the credential is saved
  3. The Register issuer step shows the JWKS ("kty": "EC") with Copy JWKS, then Organization ID, Federation Rule ID, Service Account ID and Workspace ID, each with a hint on where it lives in the Console and when it is needed, the line Still needed before discovery: Organization ID, Federation Rule ID. and Next disabled
  4. Type Organization ID 00000000-0000-4000-8000-000000000000, Federation Rule ID fdrl_qa_fake and Service Account ID svac_qa_fake: the line disappears and Next enables
  5. Next: the Discover models step shows Discovery failed with Model discovery failed: 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, and GET http://localhost:4000/credentials/by_name/wif-qa-ui already carries the three ids
  6. Back: the Register issuer step still shows all three ids with Workspace ID blank. Type default there, Back again: the Authentication step shows all four values, Save changes, and the Register issuer step still shows default
  7. Next: the same Anthropic 400, and the credential now carries all four ids. Delete the credential from LLM Credentials afterwards

One home for provider credentials, before at 5e5c436 and after at 3a46fa2

The proxy runs from the PR worktree with ISSUER_SIGNING_KEY_PEM holding a P-256 PEM, ANTHROPIC_API_KEY unset, master key sk-1234, and the dashboard dev server on port 3000. No federation rule exists for this rig, so the ids are placeholders and every leg that reaches Anthropic gets its own 400 for the malformed fdrl_. The credential in the curl legs is created the way the modal creates it, with custom_llm_provider in the dashboard's display casing

Before (5e5c436)

Models page

  1. Sign in at http://localhost:4000/ui/login with sk-1234, open http://localhost:3000/models-and-endpoints/: the tab strip is All Models, Add Model, Auto-Routers, Add Provider, LLM Credentials, Pass-Through Endpoints, Health Status, Model Retry Settings, Model Group Alias, Model Access Group Budgets, Price Data Reload. Add Provider and LLM Credentials both create an Anthropic federation credential; the JWKS and model discovery live only in the wizard
  2. LLM Credentials, Add Credential: the footer is Need Help? on the left and Add Credential then Cancel on the right, with no way to test the credential from the modal

JWKS and discovery for a credential the modal created

  1. curl -s localhost:4000/credentials -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"credential_name":"wif-qa-modal","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"}}', observed {"success":true,"message":"Credential created successfully"}
  2. curl -s -w '\nHTTP %{http_code}' localhost:4000/credentials/wif-qa-modal/jwks -H "Authorization: Bearer sk-1234", observed {"detail":{"error":"No anthropic credential named 'wif-qa-modal'."}} and HTTP 404: the route compared the stored Anthropic against anthropic and missed
  3. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa-modal"}', observed {"detail":{"error":"Credential 'wif-qa-modal' is configured for provider 'Anthropic', not 'anthropic'."}} and HTTP 400: same comparison, so only curl-created credentials could be discovered by name
  4. curl -s -X DELETE localhost:4000/credentials/wif-qa-modal -H "Authorization: Bearer sk-1234", observed {"success":true,"message":"Credential deleted successfully"}

After (3a46fa2)

Models page

Driven in headless Chromium against the dev server. The steps below are the ones to repeat by hand for the screenshots

  1. Sign in at http://localhost:4000/ui/login with sk-1234, open http://localhost:3000/models-and-endpoints/: the tab strip is All Models, Add Model, Auto-Routers, LLM Credentials, Pass-Through Endpoints, Health Status, Model Retry Settings, Model Group Alias, Model Access Group Budgets, Price Data Reload. No Add Provider
  2. LLM Credentials, Add Credential: the footer is Cancel and Need Help? on the left, Test Connection and Add Credential on the right; Test Connection is disabled with the hint Fill in the credential values first.
  3. Credential Name wif-qa-cred, Provider Anthropic, Authentication method Workload Identity Federation (LiteLLM-signed), Issuer URL https://litellm.local/qa, Subject litellm-wif-demo, Audience https://api.anthropic.com, Signing Key Reference os.environ/ISSUER_SIGNING_KEY_PEM, ids blank: Test Connection stays disabled with the hint Add the credential first, then test it from Edit. Only an API key and API base can be tested before saving., Add Credential: the toast says Credential added successfully and GET http://localhost:4000/credentials/by_name/wif-qa-cred carries the issuer fields with anthropic_identity_source: internal_issuer
  4. Row menu, Edit: below Workspace ID the modal shows Public JWKS with the Console instructions, the key set ("kty": "EC") and a Copy JWKS button; Test Connection is enabled
  5. Test Connection: a red alert with Model discovery failed: 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., so the request reached the identity source rather than a static key
  6. Type Organization ID 00000000-0000-4000-8000-000000000000, Federation Rule ID fdrl_qa_fake, Service Account ID svac_qa_fake: Test Connection disables with the hint Update the credential first. Test Connection checks the saved values., Update Credential: the toast says Credential updated successfully and the credential carries the three ids
  7. Edit again, Test Connection: a red alert with Model discovery failed: 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, the placeholder rule id reached Anthropic's token endpoint. Delete the credential from the row menu afterwards

JWKS and discovery for a credential the modal created

  1. curl -s localhost:4000/credentials -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"credential_name":"wif-qa-modal","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"}}', observed {"success":true,"message":"Credential created successfully"}
  2. curl -s -w '\nHTTP %{http_code}' localhost:4000/credentials/wif-qa-modal/jwks -H "Authorization: Bearer sk-1234", observed {"keys":[{"crv":"P-256","kty":"EC","x":"...","y":"...","use":"sig","alg":"ES256","kid":"..."}]} and HTTP 200
  3. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa-modal"}', observed {"detail":{"error":"Model discovery failed: 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."}} and HTTP 502: the request reached the identity source
  4. curl -s -X DELETE localhost:4000/credentials/wif-qa-modal -H "Authorization: Bearer sk-1234", observed {"success":true,"message":"Credential deleted successfully"}

Test Connection's inline path, what the add-mode button sends for an API key

  1. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"openai","api_key":"sk-not-a-real-key"}', observed {"detail":{"error":"Model discovery failed: Failed to get models: { \"error\": { \"message\": \"Incorrect API key provided: sk-not-a*****-key. ...\", \"type\": \"invalid_request_error\", \"code\": \"invalid_api_key\" } }"}} and HTTP 502, the text the modal shows in its red alert
  2. curl -s localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"openai","api_key":"'"$OPENAI_API_KEY"'"}' | jq -c '{models: (.models | length), first3: .models[:3]}', observed {"models":132,"first3":["text-embedding-ada-002","whisper-1","gpt-3.5-turbo"]}, the list the modal summarizes in its green alert

Federation field typed into the discover body

  1. curl -s -w '\nHTTP %{http_code}' localhost:4000/provider/models/discover -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"custom_llm_provider":"anthropic","litellm_credential_name":"wif-qa-modal","anthropic_workspace_id":"default"}', observed {"error":{"message":"Authentication Error, Rejected Request: anthropic_workspace_id is a server-owned workload identity federation parameter and cannot be set in a request body; configure it on the deployment instead. On the Bedrock Claude Platform route, pass workspace_id or aws_workspace_id instead.","type":"auth_error","param":"None","code":"401"}} and HTTP 401: this is why add mode refuses to test a federation credential inline and points at Edit

QA at 5e5c436

Same rigs as the eleventh-merge run, on the tip with --num_workers 2, Postgres 16 in Docker and random ports. The Anthropic S run (59 legs) and D run (23 legs) answer the same statuses as at 37f2de1, the differences being reply text, generated ids, and S12's provider-fields output, where the four issuer tooltips changed and optional_field_keys for the LiteLLM-signed variant now lists the organization and rule ids. The OpenAI federation legs (port 58122) answer the same statuses as before modulo the two-worker window: the keyless control 500 four times, B to D 200, E's first pass chat 500 and Responses 401 on the worker that had not refreshed its credential list while embeddings answered 200, the E rerun 45s later 200 three times, F 200, G and H 401, and leg I (the EU credential) 200, 200 and OpenAI's 403 geography refusal on chat

The fail-closed change was driven before and after on the D rig with a LiteLLM-signed credential that has no organization or rule id (POST /credentials with anthropic_identity_source: internal_issuer, a /model/new through it, then four chats and two /v1/messages). At 1caa2fa all six calls answer the generic 401 Missing Anthropic API Key. At 5e5c436 four of the six answer 401 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. and the other two, picked up by the worker that had not refreshed its credential list yet, still answer the generic message (Caveats). A PATCH adding both ids the way the wizard's Next does turns every chat into the token endpoint's 401, at the tip with the new hints (Anthropic answers every denied exchange with the same 401; the reason (for example workspace_id_required or jti_reused) is only shown in the Claude Console under Settings > Workload identity, in the rule's authentication history. plus the workspace and service account hints, four of four; at 1caa2fa the same 401 carries no hint). A PATCH with credential_values_to_delete: ["anthropic_federation_rule_id"] then turns the chats into the singular but anthropic_federation_rule_id is not set four times out of four at the tip (the generic message four times out of four at 1caa2fa), and GET /credentials/by_name shows the organization id kept and the rule id gone on both sides

CircleCI at 5e5c436

The legacy suites ran at the tip after a run-ci label cycle (workflow 3781f6f6): 40 of 43 jobs green, llm_translation_testing, local_testing_part1, local_testing_part2, pass_through_unit_testing, proxy_e2e_anthropic_messages_tests, auth_ui_unit_tests, e2e_ui_testing_server_root_path and using_litellm_on_windows among them, with upload-coverage not run behind the reds. The two reds are the same staging reds as at 37f2de1: litellm_router_unit_testing on test_no_linear_scans_in_router, which names config_deployments() and heuristic_v2_router_limit_violation(), the two model_list scans staging's #39468 added and which fail the same way on litellm_internal_staging (LIT-6911), and e2e_ui_testing on tests/users/searchUsers.spec.ts (narrows the table to the matching email, the placeholder #39604 renamed, LIT-6919), a file this PR does not touch. On the GitHub side all 33 required checks are green; the documentation and code-quality jobs went red once on the fleet-wide docs rename (LIT-6929) and are green on rerun, and osv-scan stays red on the pre-existing gitpython advisory, the same result on the base branch

QA at 4592e71

Same rigs, head at the tip and base at the new merge base d23bec8, each with --num_workers 2 on random ports (head S 34974 and D 44914, base S 41256 and D 43632, OpenAI federation 33026, dashboard proxy 47657 with the Next dev server on 37887). The shared local Postgres on 5432 ran out of connections part way through the first head S run and took that proxy down, so every rig moved to a per-run Postgres 16 container on 27801 and the run started over. The Anthropic S run (59 legs) and D run (23 legs) answer the same statuses on head and base as at 5e5c436, the differences being reply text and generated ids, and the health-check legs differ between head and base only where the head adds a route or closes a hole: the fail-closed 401 on H06, the JWKS route answering 200 (H13) and a 404 with a reason (H07) where the base answers 405, discovery answering 200 with 11 models (H08) and, for placeholder federation ids, the 502 that relays Anthropic's 400 (H14) where the base 404s, plus a display-cased custom_llm_provider typed straight into a discover body (something the dashboard never sends, it always posts the canonical id) getting a 400 where the base 404s. The OpenAI federation legs ran with a re-minted 24 hour subject token after the previous one had expired (an expired token reads like a regression and is not one): the 15 legs answer the statuses recorded at 5e5c436 except two that improved, E's first pass answering 200 on Responses where it answered 401 and D's /model/info listing both keyless deployments where it listed none, both inside the LIT-6901 two-worker window. Dashboard leg at the head: the Next dev server on port 37887 pointed at the head proxy on 47657 loaded /models-and-endpoints/, the LLM Credentials tab rendered its Add Credential button, and the Add New Credential modal opened with its provider select, with zero console errors and zero 4xx/5xx requests (?tab=llm-credentials is not a deep link on this page, the tab is clicked)

CircleCI at 4592e71

The legacy suites ran at the tip (workflow c93d2804): 43 of 44 jobs green, litellm_router_unit_testing, e2e_ui_testing, e2e_ui_testing_server_root_path, llm_translation_testing, local_testing_part1, local_testing_part2 and using_litellm_on_windows among them, so the two staging reds recorded at 5e5c436 are green here. The one red is proxy_store_model_in_db_tests on test_missing_model_parameter_curl[chat], red on litellm_internal_staging itself since its 03:09 UTC pipeline on 2026-09-04; LIT-6949 tracks it. On the GitHub side every required check is green (33 of 33). proxy-infra / Run tests (Python 3.10) was red on TestNumericFormFields::test_qualifiers_and_optionality_are_unwrapped, a non-required job of #39399's Python matrix that was red on staging's own runs at d23bec8 and after (LIT-6947, fixed by #39780); a close/reopen on 2026-09-05 refreshed the merge ref to a110b1c (this tip into df3b8a6) and the job is green there

QA at 216da96

Same rigs at the fifteenth merge, head at the tip and base at the new merge base 7573632, two workers each on random ports with the per-run Postgres 16 container on 27801 (head S 52568 and D 55848, base S 31493 and D 46115, OpenAI federation 40326, fail-closed 54615, dashboard proxy 52328 with the Next dev server on 38042). The 59 S legs and 23 D legs answer the same statuses on head and base as at 4592e71: the S differences are the nine legs where the head adds a route or closes a hole (S10 401 for a server-owned field in a body where the base 400s, S11 and S26a and S26b 200 on federated deployments with a bogus or empty base where the base 400s, S13 200 and S13b 401 on discovery where the base 404s, S15e and S15h 401 for a team admin or the proxy admin posting federation fields inline where the base 200s, and S23's cleanup 400 on the deployment the head refused to create) plus S14b, where the head's GET /credentials/by_name answered 404 on the worker that had not synced the credential yet (the LIT-6901 window, the same leg the 4592e71 run explains). The 16 health-check S legs and 11 D legs differ from the base exactly as before (H06 401, H07 404, H08 200 twice, H08b 400, H13 200 twice, H14 502 twice against the base's 200, 405, 404, 404, 405, 404) and the 22 fail-closed legs answer as at 4592e71: 401 naming the missing ids on every chat and messages call, 200 on the credential edits around them. The OpenAI federation run: 16 keyless calls 200 (store credential, Test Connect, Add Model, chat, streaming, Responses and embeddings, discovery through the credential with and without the empty api_base), the two inline-trio and non-admin legs 401, the control keyless static deployment 500 four times as it must, and one chat in the first pass of E plus the EU leg's chat 500 inside the two-worker window. The EU leg was driven twice more at the tip: the immediate variant split 2 to 3 between OpenAI's geography refusal and the missing-key 500 across five calls, and with the credential stored 20s before the model all four calls reached eu.api.openai.com and got OpenAI's geography refusal, so the window is LIT-6901's (the router resolves litellm_credential_name per request from the worker-local list, upstream code this PR does not touch) and the repro is noted on that ticket. Dashboard leg: the dev server on 38042 pointed at the head proxy on 52328 loaded /models-and-endpoints/, the LLM Credentials tab rendered Add Credential, the modal opened with its provider select, zero console errors and zero 4xx/5xx requests

QA at 82afbbd

Same rigs at the Bugbot fix, head at the tip and base at the unchanged merge base 7573632, two workers each on random ports with the per-run Postgres 16 container on 27801 (head S 49107 and D 50944, base S 28968 and D 38770, OpenAI federation 27307, fail-closed 34735, dashboard proxy 31256 with the Next dev server on 48870). Every leg answers the status it answered at 216da96: the 59 S and 23 D legs on the head are status-identical to the previous tip's run, the head-vs-base differences are the same nine legs, and the base run moved only on S14b, its GET /credentials/by_name answering 404 in the same LIT-6901 window the head hit at the previous tip (the window is upstream's, so it shows on either side). The 27 health-check legs differ from the base as before, with the two by_name reads right after POST /credentials (H03b, H12b) landing in that window on this run, 404 on the worker that had not synced yet where the previous run's reads hit the worker that had. The 22 fail-closed legs are status-identical, and the OpenAI federation run is the same 16 keyless 200s, the same two 401s and the control's four 500s, with E's first pass clean this time and the EU leg's chat still inside the window. Dashboard leg: the first attempt's Next dev server answered 404 on every route, /login/ included, serving the not-found page out of the persistent Turbopack cache under .next/dev that the previous leg's teardown had left half-written (nothing in this PR, the dev server is the same tree at both tips); with that directory cleared the dev server on 48870 pointed at the head proxy on 31256 loaded /models-and-endpoints/, the LLM Credentials tab rendered Add Credential, the modal opened with its provider select, zero console errors and zero 4xx/5xx requests

CircleCI at 82afbbd

The legacy suites ran at the tip (workflow 56bc1c76, 05:54Z to 06:07Z): all 43 jobs green, proxy_store_model_in_db_tests among them, since #39842's fix for LIT-6949 sits in the merge base now, and litellm_router_unit_testing, e2e_ui_testing, e2e_ui_testing_server_root_path, llm_translation_testing, local_testing_part1, local_testing_part2 and using_litellm_on_windows with it. On the GitHub side every check is green, the 33 required ones included, with proxy-infra / Run tests (Python 3.10) green on the tip directly (the tip runs against staging at or after #39780, so no close/reopen was needed). Bugbot reviewed the tip at 05:56Z and found no new issues; Greptile's only status is its 03:08Z "Too many files" skip

Type

New Feature

Caveats (if any)

Medium

  • A static OpenAI key still wins over federation
    • OPENAI_API_KEY in the proxy env or api_key on the deployment disables it silently, the same precedence the Anthropic side keeps as a Low below; the token-file field's tooltip says so, and a mixed fleet needs the env var gone
    • proxy_cli.py calls load_dotenv(), so a .env in the proxy's working directory carrying OPENAI_API_KEY (or ANTHROPIC_API_KEY) puts the static key back even when the shell unset it; a no-static-secrets proxy needs that file clean too, and the docs PR gets the note
  • A freshly added federated deployment can answer 500 for up to one credential refresh interval on a multi-worker proxy without Redis
    • Pre-existing for every litellm_credential_name deployment (LIT-6901), but the UI only offers OpenAI federation through a credential, so every federated deployment sits in that window; seen on the EU leg above, 2s after /credentials, and again at 216da96, where the same second split between the missing-key 500 and OpenAI's geography refusal and every call reached OpenAI once both workers held the credential (repro noted on LIT-6901)
    • On the Anthropic side the same window answers the generic Missing Anthropic API Key 401 instead of the fail-closed message naming the missing ids: 2 of the 6 calls sent right after POST /credentials on the two-worker rig; once both workers hold the credential every call names the ids
  • Federation on eu.api.openai.com and us.api.openai.com is exercised only up to the upstream geography refusal
    • The bearer is minted and presented (four chats on the EU host above), but no geography-restricted project was available to see a 200 on a regional host

Low

  • Only Organization ID and Federation Rule ID gate discovery: Anthropic's reference lists the service account id as required but a live exchange minted without it when the rule targets one service account (TestServiceAccountIdIsOptional), and the workspace id is required only when the rule is enabled in more than one workspace, so both stay optional with that guidance in their tooltips
  • Test Connection with real fdrl_, svac_ and wrkspc_ ids is not driven live at 3a46fa2, the rig has no federation rule; the placeholder ids reach Anthropic's token endpoint and get its 400, and the workspace_id_required recovery (fill Workspace ID, Update Credential, Test Connection) is covered by the unit tests and the headless run, not by a Console-backed exchange
  • Test Connection in the add modal covers only an API key and API base typed inline; every other value, federation fields included, is server-owned and refused in a discover body (the 401 above), so the button stays disabled with a hint to add the credential and test it from Edit
  • Test Connection in Edit tests the saved credential by name, so it disables while the form has unsaved changes; the hint says to update first
  • The public JWKS block appears only in Edit for a saved LiteLLM-signed credential, since the proxy derives it from the stored signing key reference; there is nothing to show before the credential exists
  • The docs PR (docs(anthropic): document workload identity federation litellm-docs#1000) lands after this one; its UI section walks the LLM Credentials modal as of cba7bb5f
  • Static api_key or an ambient ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN takes precedence over the deployment's federation fields, silently (S11): the same precedence every provider uses, and the docs PR gets a note; unset the env var on a no-static-secrets proxy
  • The federated 200 legs are the contributor's run at 197fd1e, not re-run at the tip, since the federation rule and identity token exist only in a Claude Console organization; everything reachable without one is proven at the tip
  • 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
  • A freshly registered inline JWKS takes about a minute to become usable; a data plane 401 does not invalidate the cached token, which self-heals; the token cache is per process, so each replica 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. Editing a federated deployment stays proxy-admin only, including changing its api_base or attaching a credential that carries federation values
  • 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. Keeping one name would mean splitting the request and response models, more churn than the rename costs
  • 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
  • anthropic_workspace_id is refused in a request body even on a Bedrock deployment, where it is harmless; it stays refused for uniformity
  • 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
  • /public/agents/fields does not list the federation variants, so the agents page offers API key only; the models page does
  • A non-Anthropic provider reusing the Anthropic config picks up a static key, pre-existing
  • 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. Special-casing the count would put it on a host the operator pointed the proxy away from, so it stays
  • Count-tokens still cannot authenticate with ANTHROPIC_AUTH_TOKEN: with only that set it logs the local-tokenizer fallback, exactly as the base branch did, since the base read api_key / ANTHROPIC_API_KEY and nothing else. The guard that skips federation in that case keeps the count's credential tier matching chat's _static_auth_header, which also prefers the auth token over federation, so the two surfaces never authenticate as different principals for one request. Sending that token as a Bearer on the count is a separate gap this PR neither introduces nor widens
  • 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; the dashboard's delete button on such a row shows the 404 rather than silently succeeding as it did before that staging change
  • The in-memory credential list on a multi-worker proxy trails a delete by up to one config sync tick, so a GET /credentials racing the delete on another worker can still list it; pre-existing publish-on-write behavior on staging, observed once on the rig and cleared on the next read
  • Typing the OpenAI trio straight into Add Model gets the 401, not a form hint: same trap as the Anthropic fields, LIT-6897 tracks a form-side fix
  • 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); the dashboard strips masked values before sending, API callers do not
  • 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
  • osv-scan is red on the pre-existing gitpython 3.1.58 advisory in uv.lock, the same result on the base branch; this PR does not touch dependencies
  • Two non-required checks were red at 4592e71 for staging's own reasons and are green at the tip: ci/circleci: proxy_store_model_in_db_tests on test_missing_model_parameter_curl[chat], red on litellm_internal_staging itself since 2026-09-04 (LIT-6949, fixed by test(store_model_in_db): assert the 400 contract in the unknown-model spend log test #39842, which the fifteenth merge brought in), and proxy-infra / Run tests (Python 3.10) on test_qualifiers_and_optionality_are_unwrapped (LIT-6947, fixed by fix(proxy): strip every TypedDict qualifier before numeric form-field detection #39780, which the same merge brought in). The litellm_router_unit_testing red recorded at 5e5c436 stays green
  • The admin panels on the models page are shown to view-only admins by a role masquerade; pre-existing and left for a follow-up

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

  • 068c1d1 passes /live-pr-risk

  • c6f585e passes /live-pr-risk

  • 412890f passes /live-pr-risk

  • 272ccfd passes /live-pr-risk

  • 55e303e passes /live-pr-risk

  • 4a785c8 passes /live-pr-risk

  • cb03462 passes /live-pr-risk

  • 3794ba9 passes /live-pr-risk

  • 9036b90 passes /live-pr-risk

  • 055eaee passes /live-pr-risk

  • 37f2de1 passes /live-pr-risk

  • 5e5c436 passes /live-pr-risk

  • 4592e71 passes /live-pr-risk

  • 216da96 passes /live-pr-risk

  • 82afbbd passes /live-pr-risk


Note

High Risk
Changes authentication across Anthropic and OpenAI paths (token minting, header handling, batch billing) and introduces new federation configuration with host allowlisting—security-critical infrastructure.

Overview
Adds Anthropic workload identity federation (WIF): deployments without a static api_key can mint short-lived sk-ant-oat01 tokens via a shared RFC 7523 JWT-bearer exchange engine, with identity from mounted OIDC files, inline refs, a LiteLLM-signed internal issuer, or Keycloak client_credentials.

Auth plumbing threads new anthropic_* / openai_* WIF keys through litellm_params (forwarded, optional, request-banned). Anthropic surfaces (chat, /v1/messages, files, batches, skills, count-tokens) resolve credentials through get_auth_header / validate_environment, strip caller-supplied auth headers when federation mints, and run blocking token exchange off the async event loop. Batch retrieve and batch output billing now pass litellm_params so federated deployments authenticate when fetching results.

Operator flows: discover_models() on providers enables live /v1/models discovery with deployment credentials; proxy exposes POST /provider/models/discover (allowlisted). OpenAI gains parallel WIF config for client creation and model discovery. Shared resolve_anthropic_base aligns token exchange, discovery, and count-tokens URLs across api_base shapes.

Security: exchange hosts are allowlisted (LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS); federation fields are server-owned (not client request bodies).

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

…oken exchange engine

Adds a provider-agnostic JWT-bearer token exchange engine (litellm/llms/base_llm/auth/)
with two-tier refresh, single-flight minting, negative caching, response caps, and
RFC 6749 error redaction, plus the Anthropic WIF adapter and wiring: env and
litellm_params config, sync and async facades, and beta-header merging fixes on the
skills, files, batches, messages, and passthrough surfaces

WIF is the lowest credential tier, so a deployment that does not configure it behaves
exactly as before. Verified end to end against the live token endpoint from a container:
both /chat/completions and /v1/messages complete over a minted sk-ant-oat01 token with no
api_key configured anywhere

Two protocol details were confirmed against the live endpoint rather than the docs. The
exchange needs no anthropic-beta header, so none is sent. service_account_id mints fine
when omitted for a single-service-account rule, so it stays optional

Resolves #28607
…nthropic providers

The federation exchange derives its token endpoint from the deployment's api_base and returns
an Anthropic-org credential, so it must only ever run for Anthropic itself. It did not.
VertexAIAnthropicConfig inherits AnthropicModelInfo.validate_environment, and the MiniMax and
Tencent /v1/messages configs inherit the Anthropic validate step, so a proxy configured with the
ANTHROPIC_* federation variables POSTed the workload's OIDC assertion to those providers' own
hosts, confirmed against a real deployment base

Eligibility is now declared per class and read from that class's own __dict__, so a subclass
written for another provider inherits nothing, and the get_auth_header facades refuse to mint
unless a caller states that it authenticates against Anthropic's own API. The six federation
fields join the banned request-body parameters: they select which server-side secret is read and,
with api_base, where it is sent, so a caller-supplied value was an exfiltration primitive for any
environment variable or mounted token file. The exchange client no longer follows redirects, since
only the initial token URL is validated, and a base that already ends in /v1 no longer yields a
doubled /v1/v1/oauth/token
…olled request paths

An independent review of the previous commit found the narrowing incomplete. The federation
fields select which server-side secret is read, and together with api_base decide where it is
sent, so they are deployment decisions on every surface:

- They are rejected from any request body unconditionally, ahead of the general banned-parameter
  check, because both client-side credential opt-ins would otherwise re-enable them. The inert
  workspace id keeps its existing behaviour, since Bedrock Claude Platform already accepts that
  spelling in a request body.
- /health/test_connection takes a litellm_params object that never reached the request-body check,
  and its existing guard only covers os.environ references, not oidc ones. It rejects them now.
- A client-redirected api_base clears the federation fields and marks the deployment, so a token is
  not minted for a caller-chosen host. The mark is what stops the environment-configured path,
  which cannot be cleared out of a dictionary

Batch retrieval now threads litellm_params, so it authenticates the same ways every other Anthropic
surface does rather than failing ahead of the transformation that resolves them

The exchange engine always publishes a result for its single-flight entry, so an unexpected failure
can no longer leave every later caller waiting on a leader that never finishes. Error bodies are
rendered from structured fields only, and a body that echoes the submitted assertion is dropped
rather than logged and returned. Endpoint normalization now works on the URL path, so a
single-label host is left alone and a pathological base cannot exhaust the stack
…eration

The workload assertion could only come from a mounted file or an environment variable, which assumes
a platform that already projects one. Two more sources sit behind an explicit
anthropic_identity_source discriminator, and its absence keeps today's resolver exactly as it was:

- internal_issuer signs a short-lived ES256 assertion with an operator-supplied key, resolved through
  the usual secret reference so it can live in a secret manager. Its JWKS is exported for the operator
  to register with Anthropic, without which the source cannot be used at all.
- keycloak fetches the assertion from a client_credentials grant, with the client secret likewise held
  behind a reference rather than in the configuration

The engine gains an optional assertion source on the spec, so a source that needs more than a string
can supply one without the reference ever carrying a secret: it stays a hash of the non-secret fields
and remains what the cache keys on and what errors name. Failures carry a redacted detail, so a
Keycloak hop is diagnosable rather than collapsing into one opaque message

The server-owned field list is now derived from one definition, so every field added here is rejected
from request bodies and cleared on a client base override without a second edit. Credential params
carry the federation fields too, which is what the files, batches, and passthrough surfaces read

Verified against the live token endpoint: an ES256 assertion from internal_issuer, with its exported
JWKS registered on the federation issuer, mints a token. Note that a freshly registered inline JWKS
takes up to about a minute to become usable
…eration

Federation was configurable but only per model, and only by hand. This adds the provider-level flow
the feature was missing, reusing what already exists rather than introducing parallel concepts

Provider metadata gains optional credential variants: a selector plus per-variant field lists, so a
provider whose credential shape branches describes that in the same metadata every provider already
publishes, and the existing generic renderer drives it with no provider-specific branch. Anthropic
publishes five, one per way of authenticating. Fields holding a secret reference render as text, not
as password inputs that would invite pasting the secret itself

Model discovery could not see a credential configured through litellm_params, only through the
environment, so a federated provider had nothing to discover. Providers now expose discovery that
receives the deployment params, and an admin-only endpoint discovers a provider's models from a
stored credential by name. The credential fields are never accepted in that request, since choosing
which server-side secret is read is not a caller's decision

Enable and disable reuse the existing blocked flag rather than a second notion of the same thing,
which meant letting model creation persist it so a model can be created already disabled instead of
being created live and then paused. Alternate names reuse the existing public name and group alias.
One credential now feeds many models, so switching a credential's auth variant has to be able to
remove the previous variant's fields, which credential updates could not express before

The Admin UI gains an Add Provider flow over all of it: pick the provider and auth method, fill that
method's fields, export the JWKS when the identity is self-signed, discover the provider's models,
then enable, rename, and alias them before creating. Two pre-existing UI defects surfaced while
building it: a variant-capable provider could render through the stale flat field list before its
metadata loaded and leave a validation rule behind that blocked submission, and the credential
editor left the previous variant's fields in place when the variant changed
…ject

/model/block and /model/unblock declare the proxy model table as their response model, and FastAPI
validates the returned row against it with from_attributes. The before-validator that parses the
JSON string columns assumed a mapping, so it called .get on the row object and both endpoints
answered 500 even though the block itself had already been applied

The validator now passes an object through untouched and keeps parsing the string columns it exists
for, which is the case the database actually produces. Found while exercising the provider setup's
enable and disable toggle against a live proxy
…types

The discovery and JWKS routes were missing from the generated types. Regenerating on a developer
machine can drop the enterprise routes when that package is not importable, which turns the sync
check into a large deletion; this spec was dumped from the built container image, so the diff is
only the two added endpoints
…-encode Keycloak credentials

The retry path re-read the assertion from its source to decide whether an error body
reflected it back. A rotating source (a token file rewritten between attempts, a
Keycloak fetch that mints a fresh token) hands back a different value, so the assertion
the failing request carried stayed in the error text. _Unauthorized now carries the
assertion the attempt used and the redaction runs against that, which also drops one
extra read of the secret per failure

Separately, RFC 6749 2.3.1 wants the client id and secret form-urlencoded before the
base64, so a reserved character in either one no longer corrupts what the far side
decodes back out of Basic auth
…g the event loop

The async file and batch handlers called the provider's sync validate_environment
directly. For the workload identity tier that hook performs a blocking token exchange,
so a mint stalled the whole loop. They now go through one facade that awaits the
provider's async hook when it has one and offloads the sync hook to a worker thread
otherwise, so every other surface keeps its existing behaviour

Batch retrieval also copies the litellm_params it hands down rather than sharing the
caller's dict, and get_anthropic_headers moves its credential selection into a helper
so the four auth tiers read as four branches instead of one nested chain
… fields

These fields pick which server-side secret is read and where the resulting assertion is
sent, so a team admin who can otherwise manage a team-scoped deployment must not be able
to set them. The create, update and patch model paths and both credential write paths now
reject them for anyone below proxy admin, mirroring the existing blocked-flag gate. The
field list is derived from anthropic_wif_litellm_params rather than copied, so a new
federation field is covered the day it is added

Also fixes two things CI caught: credential_values_to_delete is a PATCH instruction rather
than part of the credential, so it stays out of dumps that feed config loading and the
Prisma write, and the provider discovery route is registered in the backend allowlist
…es for failed rows

The tab list put add-provider ahead of auto-routers, which reordered the tabs an
existing test pins. It now sits after auto-routers, where the admin-only tabs start

The wizard also wrote a model_group_alias entry for every discovered row it tried,
including rows whose model creation had just failed, leaving an alias pointing at a
deployment that does not exist. Alias additions are now taken from the rows that
actually got created
Both posters built their HTTPHandler inline, so nothing exercised the guarantees that
matter there: redirects stay disabled, an HTTPStatusError comes back as its response
rather than escaping, and a None response raises instead of being dereferenced. Each
now takes a handler factory that defaults to the real one, which is the same injection
seam the rest of this package already uses for its poster and secret reader

Coverage for litellm/llms/base_llm/auth goes from 90% to 97%: client_credentials from
79% to 98%, with only its assert_never arm left, and token_exchange from 90% to 96%
…l schema

Excluding credential_values_to_delete from dumps makes pydantic emit separate input and
output schemas for CredentialItem, since the field is accepted on a write and absent from
every read. The generated types now say so: the credential read routes return the output
shape, which no longer carries the PATCH-only field, and only the write route accepts it
…fetime

The advisory and mandatory refresh windows were flat 120s and 30s, while the fallback
lifetime for a token minted without expires_in is 60s. Such a token was therefore born
inside its own advisory window, so every request armed another background exchange
against the provider's token endpoint and the cache never settled. Any real expires_in
of 120s or less did the same

Each window is now the smaller of its flat value and a fraction of the observed lifetime
(half for advisory, an eighth for mandatory), so a 60s token is served until roughly its
half life and then refreshed once. Tokens of 240s and above hit the flat values and keep
exactly the previous behaviour. The tradeoff is deliberate: a 60s token may now be served
with as little as 7.5s left, where before the mandatory wall was 30s, which for a 60s
token meant refreshing at half its life on every path
…d identity surface

The admin gate only inspected the incoming credential_values, so it was fail-open three
ways. A non-admin could PATCH with an empty value map and name a federation field in
credential_values_to_delete, dropping it off the stored credential. A PATCH carrying no
federation field could still mutate a credential whose stored values held them, because
the stored state was never consulted. DELETE had no gate at all

The gate now takes the effective surface of the operation: the fields the payload sets,
the keys it names for deletion, and the fields already on the credential being changed.
Presence is keyed on the field name rather than a non-null value, matching how the
resolver and get_litellm_params decide a field is set, so clearing a field to null no
longer walks past the gate and wedges every deployment referencing that credential

Create, delete and a rename that targets another credential resolve the existing record
through hydrate_named_credential, which reads the in-memory config list before the
database. A credential declared in config.yaml has no row, so a DB-only lookup let a
non-admin shadow or drop one. POST semantics for credentials carrying no federation
field are unchanged
…ed passthrough token

The Anthropic passthrough route forwards the caller's headers, so once the server owned
the credential the caller's own key travelled to Anthropic alongside the minted bearer.
Two credentials arrived in one request and the caller's could win, which defeats the
point of server-owned federation

The set of headers the server owns is now derived from SpecialHeaders, the same
enumeration the proxy accepts a LiteLLM key in, plus the configured
litellm_key_header_name, rather than the two names it listed before. That closes the
same leak for x-litellm-api-key, api-key, x-goog-api-key and Ocp-Apim-Subscription-Key.
The non-passthrough path already stripped these, so this brings the two into line

Bring-your-own-key is untouched: with no server credential the caller's key is the only
one there is, so it still forwards
check_workflow_startup_safety rejects a job whose deadline can preempt pytest: setup can
take 35 minutes plus 5 of runner overhead, so a 20 minute pytest budget needs the job
capped at 60, not 55. Three entries in the unit matrix still sat at 55 and failed the
guard on every pull request, including on litellm_internal_staging itself

Raising those three to 60 matches the ten entries already at that value and turns the
code-quality job green. This is unrelated to the rest of the branch, kept as its own
commit so it can be dropped or cherry-picked independently
…ervice

A deployment that mints short-lived tokens on a two tier refresh was unobservable: the
exchange emitted one warning and no metrics, so the first sign of a failing token
endpoint was user visible 401s. Registering the exchange as a service type puts it on
the same path redis and postgres already use, so success and failure counters and a
latency histogram reach /metrics and OTel spans from one emit, with no per backend
wiring

Each attempt reports its call type, separating a cold mint from a mandatory refresh and
from a background advisory refresh, and tokens served from cache are counted on their
own service so a cache hit never fakes an exchange latency. Failures carry the
ExchangeError variant as a low cardinality error class

The sink is a constructor parameter like the poster and the clock, so tests inject a
fake rather than patching. It hands events to a single worker thread, which keeps the
sync path free of the async hooks and of any need for a running loop, and every
emission is wrapped so a broken metrics backend can never fail or delay a mint. Labels
carry only the variant name and the redacted summary the error path already produces,
never an assertion, a token or a secret
…cannot exhaust memory

Every exchange and cache hit queued an event on the metrics executor with no ceiling, so a
telemetry backend that stalls turned request volume into unbounded queued work inside the
proxy. Reported by a review bot and independently by the adversarial verifiers on the
change that introduced it

The sink now tracks its own backlog and drops events once it reaches a thousand, on the
principle that losing a metric sample is cheaper than losing the process. A rejected
submit releases its slot too, so a failing executor cannot leak the count and wedge the
sink shut. Metrics stay best-effort throughout: nothing here can fail or delay a mint
…id not need

The repo's recursion guard rejects unignored recursive functions, and this branch had
added three. Two were plain iteration written as recursion: the model list walked its
pages recursively, and the base URL stripped chat suffixes by calling itself. The third
mattered more: a token exchange follower whose leader published nothing called get_token
again, so a contended entry grew the stack one frame per failed leader

All three are loops now. The suffix strip re-strips trailing slashes on each pass, which
the recursion got for free by re-entering a function that begins by stripping them, and
which a doubled suffix needs to keep matching

The passthrough header tests also pin SERVER_ROOT_PATH. They drive the real relay, and a
sibling test leaving that variable set re-prefixes the route, so the request 404s before
any header is built and the assertion never runs
…l write

The gate resolved the existing credential through hydrate_named_credential, which answers
from the in-memory list and only reads the database when memory has no entry. That order
is right when serving a request and wrong when deciding authorization: a pod whose copy
predates an admin adding federation fields sees none of them and lets a non-admin through.
In a multi-pod deployment the window is a config reload, and with store_model_in_db off it
never closes

Authorization now goes through its own helper that reads the in-memory entry and the row
and takes the union, so the gate refuses whenever either side says the credential is
server-owned. Resolution keeps memory-first, which is still correct for serving
…stinct identities

Eviction skipped every in-flight entry and then removed exactly one, so a burst of
distinct federation identities pushed the map past max_entries and left it there: each
later insert evicted one and added one, holding the high-water mark for the life of the
process. The cap the parameter advertises was effectively advisory

It now evicts as many as the overshoot needs, soonest-to-expire first, and treats an
entry with no token as evictable rather than skipping it. An entry a leader owns or a
follower waits on is still never a candidate, since dropping one would break single
flight, so a moment when every entry is in flight can still over-insert; that residue is
bounded by the concurrent mints themselves and clears as they finish
The recursion-to-loop rewrite carried its Final annotations onto locals that are now
assigned once per iteration, which basedpyright reports as an error: a Final cannot be
assigned within a loop. Each is a genuine per-iteration rebind, so they carry rebind-ok
with the reason instead
The exchange derived its token URL from the deployment's api_base and sent the
signed assertion wherever that pointed. Any write path that could set api_base on
a WIF deployment could therefore redirect a valid assertion to a host of its
choosing, and gating each of those paths individually has no termination condition
as new ones are added.

Enforce it where the exchange is actually built instead: the host must be
api.anthropic.com or an entry in LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS, matched on
the parsed hostname so a lookalike like api.anthropic.com.evil.test cannot pass by
containing the real one. Both the sync and async call sites check before any
assertion leaves the process.
…d exists

The Add Provider wizard required anthropic_federation_rule_id up front, but with
LiteLLM as the issuer that id does not exist yet: the operator gets it from
Anthropic after registering the JWKS URL the credential itself publishes. So the
step that needed a rule id could not be reached without one.

Mark the field optional for wif_internal_issuer and let the JWKS step PATCH it in
once it is known. The Keycloak flow is unchanged, where the rule id is known in
advance.
A council reviewed the four findings the review bots raised on the wizard and
ruled these three worth fixing before human review, since all of them are in code
this PR introduces.

Creation no longer treats a failed deployment lookup as an empty list. That lookup
is what makes a retry skip rows already created, so swallowing its error turned a
partial-failure retry into duplicate deployments. It now stops and says why, with
the rows still on screen.

Whether the wizard creates or updates a credential is derived from the name it
actually saved rather than a flag that was set once and never cleared. Renaming
after a save used to take the update path against a name the server had never
seen, which dead-ended the flow with no way forward but a reload.

Create is disabled while any model name is blank, instead of submitting an empty
model_name and producing a deployment that cannot be addressed.

Each fix carries a regression test that fails when the fix is reverted.
effectiveSessionRole deliberately reports a proxy_admin_viewer session as "Admin",
so gating the tab on all_admin_roles showed a read-only admin a wizard whose every
step is a write the proxy then refuses. The first step solicits a provider API key,
so a viewer types a real secret into a form that 403s on submit.

Only the raw-role isViewOnly separates the two, which is the same mechanism the
Playground already uses for this carve-out. Both conjuncts of the gate are covered:
dropping either one fails a test.

The five older admin panels on this page have the same gap, but they predate this
PR and fixing them is a page-wide change, so they are left for a follow-up.
…, and normalize the discovery base

Two defects the review bots found in this PR's own code, both confirmed against the
implementation before fixing.

The files surface built its headers by merging the resolved credential over the
caller's, which leaves a caller-supplied x-api-key in place next to a minted
federation Bearer. The chat surface already stripped those, so this was an
inconsistency rather than a new rule. That stripping is now one helper both
surfaces call, and it applies whatever the credential turned out to be, since a
header that authenticates the caller to LiteLLM should never reach Anthropic.
Batches is unaffected: it passes no caller headers through.

Model discovery appended /v1/models to the configured base without normalizing it,
so a deployment whose api_base already ended in /v1, or in the /v1/messages URL an
operator copied out of the docs, asked for /v1/v1/models and failed. It now reuses
the same suffix stripping the token exchange derives its URL from, so both agree on
what the deployment's base is.
…e surface gaps

Three defects the review bot found, all confirmed against the code before fixing.

anthropic_workspace_id was carved out of the request-body ban as inert. It is not
inert: it is the scope the federation token is minted for, and router.py merges
request kwargs over deployment params, so a caller who set it chose the scope
instead of the administrator. Proven against Anthropic before fixing, whose token
endpoint answered "workspace_id is not a well-formed wrkspc_ tagged ID" on a value
that came from the request body. The carve-out is gone, so it is banned like every
other minting parameter.

That does not cost the Bedrock Claude Platform route anything. It reads a workspace
from workspace_id or aws_workspace_id as well, neither of which is a federation
parameter or in all_litellm_params, so both still reach it from a request body, and
an administrator-configured anthropic_workspace_id still resolves from the
deployment. The refusal names those two spellings so that caller is not left
guessing.

Batches had the same caller-credential merge that files did, on its create path.
The retrieve path in the handler passes no caller headers, which is why the earlier
pass over this missed it.

merge_anthropic_beta_headers now accepts a list as well as a comma-separated string.
The Skills surface handled a list-valued anthropic-beta before it shared this
helper, and calling .split() on one raises.
@mateo-berri
mateo-berri requested a review from a team September 4, 2026 23:40
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 4, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

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.

@mateo-berri mateo-berri closed this Sep 5, 2026
auto-merge was automatically disabled September 5, 2026 03:08

Pull request was closed

@mateo-berri mateo-berri reopened this Sep 5, 2026
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (129 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@mateo-berri mateo-berri closed this Sep 5, 2026
auto-merge was automatically disabled September 5, 2026 03:21

Pull request was closed

@mateo-berri mateo-berri reopened this Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026

@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Empty identity fields treated as set
    • Filtered out empty strings alongside None in the identity-source params comprehension and in the internal-issuer _claims comprehension, so blank optional fields and leftover other-variant keys are treated as unset.

Create PR

Or push these changes by commenting:

@cursor push 2cd0c2ce9f
Preview (2cd0c2ce9f)
diff --git a/litellm/llms/anthropic/wif.py b/litellm/llms/anthropic/wif.py
--- a/litellm/llms/anthropic/wif.py
+++ b/litellm/llms/anthropic/wif.py
@@ -160,7 +160,7 @@
         legacy_ref: Final = _resolve_assertion_ref(litellm_params)
         return (legacy_ref, None) if legacy_ref is not None else None
     params: Final[Mapping[str, object]] = MappingProxyType(
-        {key: value for key, value in (litellm_params or _EMPTY_PARAMS).items() if value is not None}
+        {key: value for key, value in (litellm_params or _EMPTY_PARAMS).items() if value not in (None, "")}
     )
     match source_kind:
         case AnthropicIdentitySourceKind.internal_issuer.value:

diff --git a/litellm/llms/base_llm/auth/internal_issuer.py b/litellm/llms/base_llm/auth/internal_issuer.py
--- a/litellm/llms/base_llm/auth/internal_issuer.py
+++ b/litellm/llms/base_llm/auth/internal_issuer.py
@@ -38,7 +38,7 @@
                 ("exp", issued_at + config.ttl_seconds),
                 ("jti", str(uuid.uuid4())),
             )
-            if value is not None
+            if value not in (None, "")
         }
     )

You can send follow-ups to the cloud agent here.

Comment thread litellm/llms/anthropic/wif.py
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 5, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

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 82afbbd. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

Backend half of this PR, config-only and without the dashboard, is now #39935 on litellm_internal_staging. The dashboard returns in a follow-up

@mateo-berri
mateo-berri disabled auto-merge September 6, 2026 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants