diff --git a/litellm/llms/anthropic/wif.py b/litellm/llms/anthropic/wif.py index adfd16c1e74..b096a11ace4 100644 --- a/litellm/llms/anthropic/wif.py +++ b/litellm/llms/anthropic/wif.py @@ -175,7 +175,7 @@ def _resolve_identity_source( 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 _is_set(value)} ) match source_kind: case AnthropicIdentitySourceKind.internal_issuer.value: @@ -263,7 +263,7 @@ def _build_variant( field_map: Mapping[str, str], ) -> _IdentitySourceVariant: fields: Final = MappingProxyType( - {field_map[key]: value for key, value in litellm_params.items() if key in field_map} + {field_map[key]: value for key, value in litellm_params.items() if key in field_map and _is_set(value)} ) try: return model.model_validate(fields) @@ -512,6 +512,10 @@ def _config_value(litellm_params: Mapping[str, object] | None, param_key: str, e return _param_str(litellm_params, param_key) or _env_str(env_name) +def _is_set(value: object) -> bool: + return value is not None and value != "" + + def _param_str(litellm_params: Mapping[str, object] | None, key: str) -> str | None: if litellm_params is None: return None diff --git a/tests/unit/llms/anthropic/test_anthropic_wif.py b/tests/unit/llms/anthropic/test_anthropic_wif.py index a66daae48da..a054b4d130c 100644 --- a/tests/unit/llms/anthropic/test_anthropic_wif.py +++ b/tests/unit/llms/anthropic/test_anthropic_wif.py @@ -25,7 +25,6 @@ from litellm.llms.base_llm.auth.identity_source import ( ) from litellm.llms.base_llm.auth.jwt_signing import build_jwks, rfc7638_thumbprint from litellm.llms.base_llm.auth.token_exchange import JwtBearerTokenExchangeEngine -from litellm.types.router import GenericLiteLLMParams from litellm.llms.base_llm.auth.types import ( AssertionSourceError, ExchangeError, @@ -34,6 +33,7 @@ from litellm.llms.base_llm.auth.types import ( TokenEndpointError, TokenTransportError, ) +from litellm.types.router import GenericLiteLLMParams WIF_ENV_VARS: Final = ( "ANTHROPIC_FEDERATION_RULE_ID", @@ -1132,6 +1132,28 @@ class TestIdentitySourceValidationFailsClosed: } ) + def test_blank_optional_and_foreign_fields_count_as_unset(self): + configured = { + "anthropic_federation_rule_id": "fdrl_1", + "anthropic_organization_id": "org-1", + "anthropic_identity_source": "internal_issuer", + "anthropic_issuer_url": "https://issuer.internal.example", + "anthropic_issuer_subject": "workload-a", + "anthropic_issuer_signing_key_ref": ISSUER_SIGNING_KEY_REF, + } + with_blanks = { + **configured, + "anthropic_issuer_audience": "", + "anthropic_issuer_ttl_seconds": "", + "anthropic_keycloak_client_id": "", + } + + expected = resolve_anthropic_wif_params(configured) + actual = resolve_anthropic_wif_params(with_blanks) + + assert expected is not None and actual is not None + assert actual.assertion_ref == expected.assertion_ref + def test_secret_pasted_into_wrong_field_never_appears_in_the_error(self): secret_value = "super-secret-client-value-xyz" with pytest.raises(litellm.AuthenticationError) as exc_info: diff --git a/tests/unit/proxy/credential_endpoints/test_endpoints.py b/tests/unit/proxy/credential_endpoints/test_endpoints.py index b27cc0e417a..7d667f72502 100644 --- a/tests/unit/proxy/credential_endpoints/test_endpoints.py +++ b/tests/unit/proxy/credential_endpoints/test_endpoints.py @@ -439,6 +439,32 @@ class TestCredentialJwksExport: assert "JWKS_TEST_SIGNING_KEY" not in response.text assert "PRIVATE KEY" not in response.text + def test_jwks_export_treats_blank_optional_fields_as_unset(self, restore_credential_list, monkeypatch): + monkeypatch.setenv("JWKS_TEST_SIGNING_KEY", _generate_es256_pem()) + monkeypatch.setattr( + litellm, + "credential_list", + [ + CredentialItem( + credential_name="anthropic-issuer-blanks", + credential_values={ + "anthropic_identity_source": "internal_issuer", + "anthropic_issuer_url": "https://issuer.example.com", + "anthropic_issuer_subject": "my-workload", + "anthropic_issuer_signing_key_ref": "os.environ/JWKS_TEST_SIGNING_KEY", + "anthropic_issuer_audience": "", + "anthropic_issuer_ttl_seconds": "", + }, + credential_info={"custom_llm_provider": "anthropic"}, + ) + ], + ) + + response = _get_jwks("anthropic-issuer-blanks") + + assert response.status_code == 200, response.text + assert response.json()["keys"][0]["kty"] == "EC" + def test_jwks_export_accepts_the_dashboard_provider_casing(self, restore_credential_list, monkeypatch): monkeypatch.setenv("JWKS_TEST_SIGNING_KEY", _generate_es256_pem()) monkeypatch.setattr(