From 82afbbdba1eb20a0088b7781917b4effa5a403a0 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:53:44 -0700 Subject: [PATCH] fix(anthropic_wif): treat blank identity-source fields as unset --- litellm/llms/anthropic/wif.py | 8 ++++-- .../llms/anthropic/test_anthropic_wif.py | 22 ++++++++++++++++ .../credential_endpoints/test_endpoints.py | 26 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/wif.py b/litellm/llms/anthropic/wif.py index 2c28aec0413..0b5705db7be 100644 --- a/litellm/llms/anthropic/wif.py +++ b/litellm/llms/anthropic/wif.py @@ -160,7 +160,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: @@ -248,7 +248,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) @@ -412,6 +412,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/test_litellm/llms/anthropic/test_anthropic_wif.py b/tests/test_litellm/llms/anthropic/test_anthropic_wif.py index 95b89e9cc71..2c798cc3c5a 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_wif.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_wif.py @@ -1084,6 +1084,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/test_litellm/proxy/credential_endpoints/test_endpoints.py b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py index da679fbe6e6..7ef42cd74e2 100644 --- a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py @@ -361,6 +361,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(