diff --git a/litellm/proxy/common_utils/callback_config_validation.py b/litellm/proxy/common_utils/callback_config_validation.py index 805c8897b9d..801fdbaa046 100644 --- a/litellm/proxy/common_utils/callback_config_validation.py +++ b/litellm/proxy/common_utils/callback_config_validation.py @@ -77,7 +77,7 @@ def cross_entry_family_error( callback_vars: Mapping[str, str] | None, stored_vars_by_entry: Sequence[Mapping[str, str]], ) -> str | None: - """Reject an entry that changes a credential family another entry already holds. + """Reject an entry that brings a new value to a family another entry holds. Every stored entry's variables are flattened into one dict before a request reads them, and the flattened dict is what the exporter authenticates and @@ -86,12 +86,14 @@ def cross_entry_family_error( with the key from the first, and the request carries that key to the new host. - Requiring one entry to own a family end to end removes the pairing. Repeating - a value the holding entry already stores is allowed, because flattening then - produces the same dict either way -- that is how the same integration gets - registered for both the success and the failure event. A team admin who does - want to move a family deletes the entry holding it first, which reveals - nothing. + A destination the caller controls is by definition a value the owning entries + do not already carry, so requiring every value to be one of theirs removes + the pairing. Repeating what they carry is allowed: that is how the same + integration gets registered for both the success and the failure event, and + it holds for the spellings the same credential has (``langfuse_secret`` and + ``langfuse_secret_key`` are one key) without anything here having to list + them. A team admin who does want to move a family deletes the entry holding + it first, which reveals nothing. Only the writers this endpoint newly admits are held to this, because a proxy admin already holds every credential the proxy has. @@ -101,16 +103,20 @@ def cross_entry_family_error( """ if not callback_vars: return None - stored: Final = { # mutable-ok: local view of the stored entries, never stored - var: value for entry in stored_vars_by_entry for var, value in entry.items() if _family_of(var) is not None - } - held: Final = {_family_of(var) for var in stored} # mutable-ok: local index, never stored + stored: Final = tuple( + (family, value) + for entry in stored_vars_by_entry + for var, value in entry.items() + if (family := _family_of(var)) is not None + ) + held_values: Final = frozenset(stored) + held_families: Final = frozenset(family for family, _ in stored) return next( ( f"{family} is already configured by another callback entry on this team. " - f"Remove that entry before setting {var} here." + f"Remove that entry before setting {var} to a new value here." for var, value, family in ((v, callback_vars[v], _family_of(v)) for v in callback_vars) - if family in held and stored.get(var) != value + if family in held_families and (family, value) not in held_values ), None, ) diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py index 7b812fd6e61..ec0a3de21ca 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py @@ -1541,6 +1541,8 @@ async def test_proxy_admin_still_told_the_team_is_unknown(): # the same integration registered for a second event: identical values # flatten to the identical dict, so there is nothing to redirect ({"langfuse_host": "https://us.cloud.langfuse.com", "langfuse_public_key": "pk", "langfuse_secret_key": "sk"}, [{"langfuse_host": "https://us.cloud.langfuse.com", "langfuse_public_key": "pk", "langfuse_secret_key": "sk"}], False), + # the same credential under its other spelling is the same credential + ({"langfuse_secret": "sk"}, [{"langfuse_public_key": "pk", "langfuse_secret_key": "sk"}], False), # the same shape with one value moved is the redirect again ({"langfuse_host": "http://attacker.invalid", "langfuse_public_key": "pk", "langfuse_secret_key": "sk"}, [{"langfuse_host": "https://us.cloud.langfuse.com", "langfuse_public_key": "pk", "langfuse_secret_key": "sk"}], True), ],