From 77482c46cdc37751f4b19590398df7dbb2a92d66 Mon Sep 17 00:00:00 2001 From: Jesus Velarde Date: Sat, 7 Nov 2026 17:24:04 -0800 Subject: [PATCH] fix(proxy): only require newly added credential references to exist on key writes --- .../callback_config_validation.py | 5 +++-- .../test_callback_config_validation.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/common_utils/callback_config_validation.py b/litellm/proxy/common_utils/callback_config_validation.py index 2c80683ec71..a486d6665fc 100644 --- a/litellm/proxy/common_utils/callback_config_validation.py +++ b/litellm/proxy/common_utils/callback_config_validation.py @@ -505,13 +505,14 @@ def credential_reference_rejection( return None requested_logging: Final = _logging_entries(requested_metadata) requested_names: Final = logging_credential_names(requested_logging) - references_involved: Final = bool(requested_names or logging_credential_names(stored_logging)) + stored_names: Final = logging_credential_names(stored_logging) + references_involved: Final = bool(requested_names or stored_names) if references_involved and not is_proxy_admin and requested_logging != stored_logging: return CredentialReferenceRejection( 403, "Only proxy admins can change the logging entries of a key that uses litellm_credential_name", ) - undefined: Final = sorted({name for name in requested_names if not is_defined(name)}) + undefined: Final = sorted({name for name in set(requested_names) - set(stored_names) if not is_defined(name)}) if undefined: return CredentialReferenceRejection( 400, f"metadata.logging references undefined credential(s): {', '.join(undefined)}" diff --git a/tests/unit/proxy/common_utils/test_callback_config_validation.py b/tests/unit/proxy/common_utils/test_callback_config_validation.py index ce70077fc67..b595569522a 100644 --- a/tests/unit/proxy/common_utils/test_callback_config_validation.py +++ b/tests/unit/proxy/common_utils/test_callback_config_validation.py @@ -541,3 +541,25 @@ def test_credential_reference_rejection_names_undefined_credentials_only_to_admi assert admin is not None and admin.status_code == 400 and "missing" in admin.message assert non_admin is not None and non_admin.status_code == 403 and "missing" not in non_admin.message + + +@pytest.mark.parametrize("is_proxy_admin", [True, False], ids=["admin", "non-admin"]) +def test_credential_reference_rejection_lets_a_stored_reference_to_a_deleted_credential_stay(is_proxy_admin: bool) -> None: + stale: Final = {**_REFERENCE, "litellm_credential_name": "deleted"} + + kept: Final = credential_reference_rejection( + {"logging": [stale], "note": "unrelated edit"}, + [stale], + is_proxy_admin=is_proxy_admin, + is_defined=lambda _: False, + ) + added: Final = credential_reference_rejection( + {"logging": [stale, {**_REFERENCE, "litellm_credential_name": "also-missing"}]}, + [stale], + is_proxy_admin=True, + is_defined=lambda _: False, + ) + + assert kept is None + assert added is not None and added.status_code == 400 + assert "also-missing" in added.message and "deleted" not in added.message