diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index b8c3f1917b0..f4ae597792c 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -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", } ) diff --git a/litellm/types/router.py b/litellm/types/router.py index c51e4ecbc27..6e5686f0165 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -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, ...]: diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index 12d97b200ae..d3ae04eea2d 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -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.""" diff --git a/tests/test_litellm/types/test_router.py b/tests/test_litellm/types/test_router.py index c0e77e5867b..f377a59f21c 100644 --- a/tests/test_litellm/types/test_router.py +++ b/tests/test_litellm/types/test_router.py @@ -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)