feat(anthropic): workload identity federation and pluggable identity sources - #39935
mateo-berri wants to merge 24 commits into
Conversation
…sources Backend half of #38818 (internal copy of the fork PR #38013), rebuilt as one commit on top of litellm_internal_staging without the dashboard changes. Deployments on anthropic/ without a static api_key can exchange an OIDC workload assertion for a short-lived sk-ant-oat01 token through a shared RFC 7523 JWT-bearer engine. The assertion comes from a mounted token file, an env token, a LiteLLM-signed issuer, or Keycloak, chosen per deployment, per named credential, or through ANTHROPIC_IDENTITY_SOURCE. The federation fields are server-owned: refused inline in request bodies and on POST /model/new, proxy-admin only on credentials, and the token exchange is pinned to api.anthropic.com unless LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS adds a host. GET /credentials/{name}/jwks exports the public key set of a LiteLLM-signed credential for the Claude Console. The OpenAI federation trio from #39613 rides along on the backend side with the same server-owned handling. Fixes #28607 Resolves LIT-6107 Co-authored-by: derhornspieler <15236687+derhornspieler@users.noreply.github.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds workload identity federation for Anthropic and extends the shared authentication infrastructure used by OpenAI federation.
Confidence Score: 5/5The PR appears safe to merge; no outstanding correctness, security, or repository-rule issue remains. The recent changes close the prior health-policy boundary concern through a provider-neutral sensitivity contract, and the deployment-route exemption remains protected by the downstream model-management authorization gate. No accepted new findings or outstanding previous findings remain.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/wif.py | Resolves Anthropic federation configuration, constrains exchange destinations, and constructs token-exchange specifications. |
| litellm/llms/base_llm/auth/token_exchange.py | Implements the shared RFC 7523 exchange engine with endpoint validation, caching, and synchronized refresh behavior. |
| litellm/proxy/auth/auth_utils.py | Allows federated credential references on deployment-management routes while retaining request-time rejection elsewhere. |
| litellm/proxy/common_utils/credential_hydration.py | Computes effective federation state so management authorization covers stored and named credentials. |
| litellm/proxy/health_check.py | Uses provider-neutral WIF sensitivity contracts to redact secrets and restrict identity metadata visibility. |
| litellm/types/workload_identity.py | Centralizes workload-identity parameter sets and the provider-neutral secret-bearing field contract. |
| litellm/llms/anthropic/batches/handler.py | Authenticates batch retrieval with deployment WIF parameters without blocking the asynchronous event loop. |
| litellm/batches/batch_utils.py | Preserves Anthropic federation configuration when completed batch output is fetched for accounting. |
Reviews (14): Last reviewed commit: "refactor(proxy): derive health display p..." | Re-trigger Greptile
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Files handler drops credential WIF params
- Added a litellm_params argument to AnthropicFilesHandler.afile_content and file_content and forwarded it to aget_auth_header so credential-backed workload identity federation can mint on the batch-result download path.
- ✅ Fixed: Allowlist misparses host:port entries
- Extracted a _normalize_trusted_host helper that prefixes schemeless entries with // before urlsplit and strips any trailing port, so host:port allowlist entries reduce to the bare hostname the exchange comparison uses.
Or push these changes by commenting:
@cursor push d3a1256a18
Preview (d3a1256a18)
diff --git a/litellm/llms/anthropic/files/handler.py b/litellm/llms/anthropic/files/handler.py
--- a/litellm/llms/anthropic/files/handler.py
+++ b/litellm/llms/anthropic/files/handler.py
@@ -43,6 +43,7 @@
api_key: str | None = None,
timeout: float | httpx.Timeout = 600.0,
max_retries: int | None = None,
+ litellm_params: dict | None = None, # mutable-ok: handed straight to aget_auth_header
) -> HttpxBinaryResponseContent:
"""
Async: Retrieve file content from Anthropic.
@@ -56,6 +57,10 @@
api_key: Anthropic API key
timeout: Request timeout
max_retries: Max retry attempts (unused for now)
+ litellm_params: Optional deployment/credential params carrying the
+ workload-identity federation fields (rule id, org id, identity
+ token file, etc.). Without these a credential-backed federated
+ deployment has no static api_key and no way to mint one.
Returns:
HttpxBinaryResponseContent: Binary content wrapped in compatible response format
@@ -74,7 +79,7 @@
# Get Anthropic API credentials
api_base = self.anthropic_model_info.get_api_base(api_base)
auth_header: Final = await self.anthropic_model_info.aget_auth_header(
- api_key, api_base, allow_workload_identity=True
+ api_key, api_base, litellm_params=litellm_params, allow_workload_identity=True
)
if auth_header is None:
@@ -118,6 +123,7 @@
api_key: str | None = None,
timeout: float | httpx.Timeout = 600.0,
max_retries: int | None = None,
+ litellm_params: dict | None = None, # mutable-ok: handed straight to aget_auth_header
) -> HttpxBinaryResponseContent | Coroutine[object, object, HttpxBinaryResponseContent]:
"""
Retrieve file content from Anthropic.
@@ -132,6 +138,8 @@
api_key: Anthropic API key
timeout: Request timeout
max_retries: Max retry attempts (unused for now)
+ litellm_params: Optional deployment/credential params carrying the
+ workload-identity federation fields, forwarded to aget_auth_header.
Returns:
HttpxBinaryResponseContent or Coroutine: Binary content wrapped in compatible response format
@@ -142,6 +150,7 @@
api_base=api_base,
api_key=api_key,
max_retries=max_retries,
+ litellm_params=litellm_params,
)
else:
return asyncio.run(
@@ -151,6 +160,7 @@
api_key=api_key,
timeout=timeout,
max_retries=max_retries,
+ litellm_params=litellm_params,
)
)
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
@@ -345,17 +345,29 @@
def _trusted_exchange_hosts() -> frozenset[str]:
"""Hostnames a federated exchange may reach: Anthropic's own, plus whatever the operator put in
- the environment. Comma separated, case folded, entries given as a URL reduced to their host."""
+ the environment. Comma separated, case folded, each entry reduced to its bare hostname whether
+ it was written as a URL, a plain host, or a ``host:port`` (a bare ``host:port`` would otherwise
+ parse as scheme-only and match nothing, since the compared exchange base carries no port)."""
configured: Final = os.getenv(_TRUSTED_EXCHANGE_HOSTS_ENV) or ""
extra: Final = (entry.strip() for entry in configured.split(",") if entry.strip())
return frozenset(
chain(
(_DEFAULT_TRUSTED_EXCHANGE_HOST,),
- ((urlsplit(entry).hostname or entry.split("/")[0]).lower() for entry in extra),
+ (_normalize_trusted_host(entry) for entry in extra),
)
)
+def _normalize_trusted_host(entry: str) -> str:
+ """Reduce one allowlist entry to its lowercased hostname. A missing scheme is added as ``//`` so
+ ``host:port`` is parsed as an authority rather than as a scheme."""
+ to_parse: Final = entry if "://" in entry else f"//{entry}"
+ host: Final = urlsplit(to_parse).hostname
+ if host is not None:
+ return host.lower()
+ return entry.split("/", 1)[0].split(":", 1)[0].lower()
+
+
def _raise_if_exchange_host_untrusted(exchange_base: str, model: str) -> None:
"""The federated exchange refuses any host the operator has not vouched for, whatever wrote the
deployment's api_base. Exact hostname match, never a substring: ``api.anthropic.com.evil.test``You can send follow-ups to the cloud agent here.
…s and accept host:port allowlist entries The files handler enabled workload identity on batch-result downloads but never received the deployment's litellm_params, so a deployment authenticating through a named credential could only mint from process-wide env vars. It now threads litellm_params through to the auth header the way the batch retrieve path already does. LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS entries written as host:port were read by urlsplit as a scheme, so the allowlist kept the raw entry while the exchange compared bare hostnames and refused the gateway. Entries are now parsed as network locations whether or not they carry a scheme.
…iod so the router suffix reads cleanly
|
bugbot run |
|
bugbot run |
…itellm_anthropic_wif_backend # Conflicts: # tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py
|
bugbot run |
There was a problem hiding this comment.
✅ 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 31f88a3. Configure here.
An empty or whitespace-only ANTHROPIC_API_KEY counted as set, so a federated deployment sent an empty x-api-key on every call instead of minting a token. Blank values now read as unset, and a real static key on a federated deployment logs once that it outranks federation and nothing is being federated. The exchange-host allowlist matched hostnames only, so a second process on another port of an allowed host was trusted with the workload's identity token. An entry that names a port now trusts that port alone, while a bare host still trusts every port. The shared token store exists so the workers reading one projected token file do not each spend its single-use jti. A source that mints its own assertion per exchange shares nothing with another worker, so it no longer writes a live token to disk for a lookup that can never hit.
The 401 denial hint now also says federation ignores ANTHROPIC_WORKSPACE_ID, which the Bedrock Claude platform provider already reads.
A buffered write only reaches the disk when the handle closes, so a full disk surfaces at close and left the staging file behind holding a usable token.
The admin gate read the stored deployment, so a team admin lost edit, delete and Test Connection on any deployment carrying federation params. It now returns early unless the submitted fields touch the federation surface, and a Test Connection probe that points the deployment at its own api_base is still refused, with the 403 no longer wrapped into a 500 The rest of the same review pass: POST /model/new refuses only a blocking value of `blocked`, so a client that always sends `blocked: false` is not turned away; a request body can no longer pick which federated identity to mint as by naming a stored credential; an advisory refresh the executor refuses disarms the entry instead of wedging the identity until the follower timeout; the static-key shadow warning resolves its env fallback inside the cache instead of once per request; credential writes drop nulls before storing them; the token exchange validates the endpoint URL before reading an assertion and keeps refusing redirects across a client heal; /health hides every server-owned federation field from non-admins; and the async create_file and create_batch paths say which setting is missing when the provider resolves no URL
| ADMIN_ONLY_HEALTH_DISPLAY_PARAMS: Final = ( | ||
| "api_base", | ||
| "api_version", | ||
| *(name for name in server_owned_wif_litellm_params if name not in ILLEGAL_DISPLAY_PARAMS), | ||
| ) |
There was a problem hiding this comment.
This shared proxy health policy now directly consumes provider-specific workload-identity parameters. The repository requires provider-specific logic to remain under llms/, so this requirement must be satisfied before merging. Please expose a provider-neutral sensitivity or display contract instead of making the health layer depend on WIF fields.
Rule Used: What: Avoid writing provider-specific code outside... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
reject_federated_credential_reference runs from is_request_body_safe, which
pre_db_read_auth_checks calls on every route, so it also fired on POST
/model/new, /model/update, /model/{id}/update and /health/test_connection. A
proxy admin could no longer attach a federated credential to a deployment over
the API or the Admin UI, leaving a static config.yaml entry as the only way to
configure the feature the rejection told the caller to go configure, and
_reject_non_admin_wif_write never got to make the call it exists to make.
is_request_body_safe now takes the route and skips only the credential-reference
check on the routes that reach can_user_make_model_call. Federation fields typed
inline into a body stay refused everywhere, and a call naming a federated
credential still cannot pick the identity it mints as.
… sets The health check module hand-copied the five workload identity fields whose value is a credential, so a shared proxy surface named provider-specific parameters and a newly added secret-bearing field would have gone on being displayed until someone remembered both places WIF_SECRET_BEARING_KEYS now sits beside the key sets it splits out of, types/utils derives secret_bearing_wif_litellm_params from it, and the health layer splats that tuple the same way it already splats the admin-only one
|
Too many files changed for review (118 files, 100 file limit). Bypass the limit by tagging |

Backend half of #38818 (itself the internal copy of the fork PR #38013 by @derhornspieler), rebuilt as one commit on top of
litellm_internal_staging. Everything underlitellm/,tests/andterraform/from that PR is here; the dashboard (ui/), the credential form variants and thePOST /provider/models/discoverendpoint are left out and follow in a separate PR, so this PR ships the config.yaml, env var andPOST /credentialssetup paths onlyTLDR
Problem this solves:
How it solves it:
GET /credentials/{name}/jwksexports the public key set of a LiteLLM-signed credential for the Console/healthnever displays the federation token, the secret references, or the token file pathsUser Flow
Before: a platform team with a no-static-secrets policy cannot point LiteLLM at an Anthropic federation rule
model: anthropic/claude-haiku-4-5in config.yaml with noapi_key, and set the federation ids on the proxy's environment{"model": "claude-haiku-4-5-wif", "messages": [...]}Missing Anthropic API Key - A call is being made to anthropic but no key is set either in the environment variables or via paramsAfter: the same team configures the provider once in config.yaml and every deployment behind it authenticates without a static key
fdrl_..., an org id, and ansvac_...credential_listentry withcustom_llm_provider: anthropicand the identity source's fields, secrets asos.environ/...references, plus the three Console ids, and point deployments at it throughlitellm_credential_name(or setANTHROPIC_IDENTITY_SOURCEand theANTHROPIC_*ids in the environment with no credential at all)ANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKEN, the same POST answers 401 naminganthropic_organization_idandanthropic_federation_rule_idinstead of silently trying a static key; with either of those set the static key answers instead, since it outranks federationRelevant issues
Fixes #28607
Docs: BerriAI/litellm-docs#1000 (written against the dashboard flow, needs trimming to the config flow before this merges)
Linear ticket
Resolves LIT-6107
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Config-only rig, no dashboard. Both proxies boot from the same
wif_config.yaml(litellm/proxy/dev_config.yamlplus the block below) with 2 uvicorn workers each, in their own worktree and venv: Before at the merge base on port 33608, After at this branch's tip on port 51022.anthropic-wif-issuercarries placeholder Console ids (no federation rule exists in this org, so the exchange gets exactly as far as Anthropic's token endpoint),anthropic-wif-noidscarries none. The third deployment points at a local nginx on127.0.0.1:53602that forwards toapi.anthropic.com, standing in for a customer's egress gateway, and the exchange host is allowlisted ashost:port. NoANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKENin either environment;ISSUER_SIGNING_KEY_PEMholds a P-256 private keyThe last three cases need the database-backed team flow, so they run on a second proxy pair booted the same way (2 workers each, Before on port 16670, After on port 16671) from
rig_config.yaml, which adds thewif-inline-token-cfgdeployment below, setslitellm_key_header_name: X-Team-Key, and runs withSTORE_MODEL_IN_DB=Trueagainst a Postgres per side. On each side the team admin key comes fromPOST /team/new {"team_alias":"rig-team"}, thenPOST /team/member_addwith{"team_id":"<team>","member":{"user_id":"rig-team-admin","role":"admin"}}, thenPOST /key/generate {"user_id":"rig-team-admin","team_id":"<team>","key_alias":"rig-team-admin"};$TEAM_ADMIN_KEYbelow is that key. Theblockedcase repeats those three calls for a second team so the aliases stay unique, and$TEAM2_ADMIN_KEYis that team's admin keyBefore (02522a5)
JWKS export for a LiteLLM-signed credential
Run
Observed
/v1/chat/completions on a federated deployment
Run
Observed
/v1/messages on a federated deployment
Run
Observed
/v1/responses on a federated deployment
Run
Observed
/v1/chat/completions through an egress gateway allowlisted as host:port
Run
Observed
Federated credential missing the Console ids
Run
Observed
Federation field typed into a chat body
Run
Observed
Federation fields typed into POST /model/new, next to the sanctioned credential
Run (a federation field straight in the deployment's
litellm_params)Observed
Run (the same proxy admin attaches the stored federated credential instead)
Observed
Run (which of the two the proxy now serves)
Observed
GET /health with a team admin key on a deployment carrying an inline identity token
Run
Observed (the raw identity token and the Console ids come back to a team admin; the
errorfield's stack trace and the raw request dict are trimmed)Team admin edits on a federated team deployment
Run (the proxy admin creates the team-scoped federated deployment)
Observed
Run (the team admin sets rpm on it)
Observed (the edit lands, it sets no federation field)
Run (the same team admin points it at a static key instead)
Observed (the credential swap lands)
Run (cleanup, the team admin deletes it)
Observed
Setting
blockedon a new team deploymentRun (the team admin creates a deployment with
blocked: true)Observed (the flag is dropped and the deployment lands unblocked)
Run (the proxy admin sends the same flag)
Observed (the flag is dropped for the proxy admin too)
After (a7e3b4c)
JWKS export for a LiteLLM-signed credential
Run
Observed
/v1/chat/completions on a federated deployment
Run
Observed
/v1/messages on a federated deployment
Run
Observed
/v1/responses on a federated deployment
Run
Observed
/v1/chat/completions through an egress gateway allowlisted as host:port
Run
Observed
Federated credential missing the Console ids
Run
Observed
Federation field typed into a chat body
Run
Observed
Federation fields typed into POST /model/new, next to the sanctioned credential
Run (a federation field straight in the deployment's
litellm_params)Observed
Run (the same proxy admin attaches the stored federated credential instead)
Observed
Run (which of the two the proxy now serves)
Observed
GET /health with a team admin key on a deployment carrying an inline identity token
Run
Observed (the token and the Console ids are gone and the error says raw tokens must be
oidc/references; theerrorfield's stack trace and the raw request dict are trimmed)Team admin edits on a federated team deployment
Run (the proxy admin creates the team-scoped federated deployment)
Observed
Run (the team admin sets rpm on it)
Observed (the edit lands, it sets no federation field)
Run (the same team admin points it at a static key instead)
Observed (the credential swap is refused)
Run (cleanup, the team admin deletes it)
Observed
Setting
blockedon a new team deploymentRun (the team admin creates a deployment with
blocked: true)Observed (the team admin is refused)
Run (the proxy admin sends the same flag)
Observed (the proxy admin's flag sticks)
Surprises from the run:
/model/newon base echoes encryptedlitellm_params; this PR leaves it aloneType
New Feature
Caveats (if any)
Severe
ANTHROPIC_API_KEY, thenANTHROPIC_AUTH_TOKEN, then federation, and it holds for chat,/v1/messages,/v1/responses, files, batches and model discovery alike. It is the order the Anthropic SDK itself uses, and inverting it would break every deployment that sets both, so it staysproxy_cli.pycallsload_dotenv(), so a.envin the proxy's working directory puts the key back even after the shell unset it, and a fleet that wants no static secrets has to clean that file toojti, so dropping it would break the multi-worker case this PR exists for. Entries are 0600 in a 0700 uid-owned directory, a staging file a failed write leaves behind is now unlinked so no live token survives it, andLITELLM_TOKEN_EXCHANGE_CACHE_DIRset to an empty string turns the store offHigh
org:admintoken the WIF Admin API wants); everything reachable without one is proven above, including the LiteLLM-signed assertion reaching Anthropic's token endpoint and being rejected there on the placeholder rule idMedium
POST /provider/models/discoverfrom feat(anthropic): workload identity federation, pluggable identity sources, and provider-level setup (internal copy of #38013) #38818 come back in a follow-up PR; until then an admin sets federation up through config.yaml, env vars orPOST /credentials, and a credential row created through the dashboard's generic form has no federation fields to fillMissing Anthropic API Key401 for up to one credential refresh interval on a multi-worker proxy without Redislitellm_credential_namedeployment (LIT-6901); config.yaml credentials are loaded by every worker at boot and do not hit it,POST /credentialsones dooidc/env/value or a token file that rotates slower than the rule's token lifetime (60 s to 24 h, wizard default 10 min) getsjti_reusedon the re-mint until it rotates, and the 401 hint says so; the LiteLLM-signed internal issuer and Keycloak mint a fresh assertion every timeeu.api.openai.comandus.api.openai.comis exercised only up to the upstream geography refusal, no geography-restricted project was available to see a 200 on a regional hostLow
ANTHROPIC_FEDERATION_WORKSPACE_ID, not the shorterANTHROPIC_WORKSPACE_IDthe Console reference uses, because the Bedrock Claude platform provider already reads that shorter name on a released path; the 401 hint says so when the workspace is ambiguousLITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS, and the token exchange only talks to api.anthropic.com unless the operator adds hosts toLITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS, so a gateway or test double needs that set. Entries are a host or ahost:port: naming a port trusts that port alone, leaving it off trusts every port on the host. The list is server-owned, read from the environment only, and cannot be set through the model or credential APIsLITELLM_TOKEN_EXCHANGE_CACHE_DIR, default$TMPDIR/litellm-token-exchange-<uid>), so workers on one host share one exchange and each host mints its ownPOST /model/newfor the proxy admin too, not only for team admins: the sanctioned paths are a named credential (POST /credentialsthenlitellm_credential_name) or config.yaml. Changing the credentials of a federated deployment, meaning its federation fields, its api_base or the credential attached to it, stays proxy-admin only; the team admin who owns it can still edit everything else on it, delete it, and press Test Connection/v1/chat/completionsand every other LLM route, so a caller cannot pick which federated identity the proxy mints as/anthropic/*passthrough no longer forwards the caller's own credential headers upstream once the deployment has a server-side Anthropic credential:authorization,x-api-keyand the rest of the headers the proxy accepts a LiteLLM key in are stripped, plusgeneral_settings.litellm_key_header_namewhen one is set. Base forwarded them all, so a client that relied on its own header reaching Anthropic through the relay has to stop sending the server credential instead. With no server credential nothing is stripped and BYOK is unchangedCredentialItem-InputandCredentialItem-Outputinstead of oneCredentialItem, becausecredential_values_to_deleteis excluded from serialization. The wire shape is unchanged; a generated client pinned to the old schema name regenerates.ui/litellm-dashboard/src/lib/http/schema.d.tsis regenerated for the same reason and is the only file underui/this PR touchesapi_basegets a different synthesized per-request model id than before, since the federation disable flag now rides those params. Cosmetic, the id is not persistedblockedto true onPOST /model/newfor a team deployment gets 403 where base answered 200 and silently dropped the flag;blocked: falsestill answers 200, since that is the state every create lands in anyway, and a proxy admin still gets 200 with the flag honoredtags, so it cannot change a federation fieldapi_basenow count tokens throughANTHROPIC_API_BASE/ANTHROPIC_BASE_URLthe way chat does; a gateway there that serves/v1/messagesbut not/v1/messages/count_tokensgets the logged local-tokenizer fallback where the base counted through Anthropic's host directlyANTHROPIC_AUTH_TOKEN: with only that set it logs the local-tokenizer fallback, exactly as the base branch didconfig.yamlanswers 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 gatemonkeypatch.setattr68 times rather than injecting a dependency, which the repo prefers. Fixing it is net-negative: most of those calls patch module-level singletons (litellm.api_key,litellm.credential_list,litellm.module_level_client) that have no constructor to inject through, so removing them means reshaping settled provider modules well outside this PR. The token exchange engine, the one piece here that does have a seam, is tested through itosv-scanwas red on the pre-existing gitpython advisory inuv.lockon feat(anthropic): workload identity federation, pluggable identity sources, and provider-level setup (internal copy of #38013) #38818; this PR does not touch dependenciesFinal Attestation
Note
High Risk
This changes authentication and token exchange across many Anthropic (and related OpenAI WIF) code paths, with new caching, host trust, and credential handling—mistakes could leak credentials, send requests to untrusted hosts, or break batch billing and multi-worker deployments.
Overview
Adds Anthropic workload identity federation (WIF): deployments without a static
api_keycan exchange an OIDC workload assertion for a short-livedsk-ant-oat01token via a new shared RFC 7523 JWT-bearer engine (litellm/llms/base_llm/auth/), with identity sources for mounted/env tokens, a LiteLLM-signed internal issuer, and Keycloak client_credentials.Anthropic auth is threaded through chat,
/v1/messagespassthrough, files, batches (including retrieve + batch output fetch for billing), skills, count-tokens, and model discovery.litellm_paramsnow carry WIF settings end-to-end; async paths offload token exchange so the event loop is not blocked. Minted federation credentials strip caller-supplied auth headers so a proxy Bearer does not ride alongside deployment credentials. Exchange hosts are pinned toapi.anthropic.comunlessLITELLM_ANTHROPIC_WIF_ALLOWED_HOSTSis set; tokens can be shared across workers via a file-backed cache.FORWARDED_KWARGS_KEYS/ optional params also include Anthropic and OpenAI WIF kwargs so federation config flows like other deployment credentials. Batch cost aggregation copies Anthropic WIF keys so finished-batch fetches can authenticate without anapi_key.Reviewed by Cursor Bugbot for commit 31f88a3. Bugbot is set up for automated code reviews on this repo. Configure here.