fix(anthropic): carry the federation disable sentinel through the kwargs funnel

When a client redirects api_base, the handler sets a sentinel so the deployment
stops federating and no server credential is minted for a host the caller chose.
get_litellm_params then rebuilds litellm_params from kwargs and did not carry that
field, so it was dropped in transit and the deployment federated anyway. A flag the
next hop discards is worse than no flag, because the code reads as protected.

The sentinel now rides the funnel like the other federation fields, which also makes
it request-banned, correctly: a caller must not be able to set it or clear it. It is
declared on the params model for the same reason the others are, so it survives the
strict dump rather than being rebuilt away.
This commit is contained in:
derhornspieler 2026-08-25 09:39:13 -04:00
parent 7076219284
commit c4a5459b49
4 changed files with 31 additions and 0 deletions

View file

@ -50,6 +50,11 @@ ANTHROPIC_WIF_KWARGS_KEYS: Final = frozenset(
"anthropic_keycloak_auth_method",
"anthropic_keycloak_client_secret_ref",
"anthropic_keycloak_scope",
# Set server-side when a client redirects api_base, to stop a federated deployment minting
# for a base the caller chose. It has to ride this funnel or it is dropped on the way and
# the deployment federates anyway; being carried here also request-bans it, which is right,
# since a caller must not be able to set it in either direction.
"anthropic_disable_workload_identity_federation",
}
)

View file

@ -297,6 +297,9 @@ class CredentialLiteLLMParams(BaseModel):
anthropic_keycloak_auth_method: str | None = None
anthropic_keycloak_client_secret_ref: str | None = None
anthropic_keycloak_scope: str | None = None
# Server-set when a client redirects api_base. Declared so it survives the strict dump the
# other federation fields above are declared for, rather than being rebuilt away in transit.
anthropic_disable_workload_identity_federation: bool | None = None
def anthropic_wif_fields_present(fields: Mapping[str, object]) -> tuple[str, ...]:

View file

@ -3426,6 +3426,28 @@ class TestWifServerOwnedParamsAreUnconditional:
class TestWifDisabledOnClientRedirectedBase:
def test_the_sentinel_survives_the_kwargs_funnel(self):
"""Setting the sentinel is only half of it. get_litellm_params rebuilds litellm_params from
kwargs, so a field it does not carry is dropped on the way and the deployment federates for
the caller-chosen base after all."""
from litellm.litellm_core_utils.get_litellm_params import FORWARDED_KWARGS_KEYS
from litellm.router_utils.clientside_credential_handler import (
DISABLE_WORKLOAD_IDENTITY_PARAM,
)
assert DISABLE_WORKLOAD_IDENTITY_PARAM in FORWARDED_KWARGS_KEYS
def test_the_sentinel_is_not_client_settable(self):
"""It is server-owned in both directions: a caller must not be able to set it, and must not
be able to clear it either."""
from litellm.router_utils.clientside_credential_handler import (
DISABLE_WORKLOAD_IDENTITY_PARAM,
)
from litellm.types.router import reject_server_owned_wif_params
with pytest.raises(ValueError, match=DISABLE_WORKLOAD_IDENTITY_PARAM):
reject_server_owned_wif_params({DISABLE_WORKLOAD_IDENTITY_PARAM: False})
def test_base_override_clears_wif_and_sets_the_sentinel(self):
"""A federation token minted for a client-chosen api_base would send the workload's assertion,
and then the minted bearer, to that host."""

View file

@ -109,6 +109,7 @@ def test_credential_litellm_params_declares_every_anthropic_wif_field():
def test_anthropic_wif_fields_round_trip_through_model_dump():
values = {field: f"value-for-{field}" for field in anthropic_wif_litellm_params}
values["anthropic_issuer_ttl_seconds"] = 300
values["anthropic_disable_workload_identity_federation"] = True
dumped = CredentialLiteLLMParams(**values).model_dump(exclude_none=True)