Skip to content

Commit 09b391d

Browse files
committed
fix(redis): keep the credential provider off the Sentinel monitors
1 parent 308c906 commit 09b391d

2 files changed

Lines changed: 43 additions & 2 deletions

File tree

litellm/_redis.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,14 +551,22 @@ def _init_redis_sentinel(redis_kwargs) -> redis.Redis:
551551
return sentinel.master_for(service_name, **connection_kwargs)
552552

553553

554+
def _sentinel_auth_kwargs(connection_kwargs: dict, sentinel_password: str | None) -> dict:
555+
"""The Sentinel monitors are separate servers with their own password, and redis-py refuses a
556+
password passed alongside a credential provider, so the data node's provider stays behind once
557+
a Sentinel password is configured."""
558+
superseded: Final = frozenset({"credential_provider"}) if sentinel_password else frozenset()
559+
kept: Final = ((k, v) for k, v in connection_kwargs.items() if k not in superseded)
560+
return dict(kept, password=sentinel_password)
561+
562+
554563
def _init_async_redis_sentinel(redis_kwargs) -> async_redis.Redis:
555564
sentinel_nodes: Final = redis_kwargs.get("sentinel_nodes")
556565
sentinel_password: Final = redis_kwargs.get("sentinel_password")
557566
service_name: Final = redis_kwargs.get("service_name")
558567
connection_kwargs: Final = _get_redis_sentinel_connection_kwargs(redis_kwargs)
559568
connection_kwargs.setdefault("socket_timeout", REDIS_SOCKET_TIMEOUT)
560-
sentinel_kwargs: Final = dict(connection_kwargs)
561-
sentinel_kwargs["password"] = sentinel_password
569+
sentinel_kwargs: Final = _sentinel_auth_kwargs(connection_kwargs, sentinel_password)
562570

563571
if not sentinel_nodes or not service_name:
564572
raise ValueError("Both 'sentinel_nodes' and 'service_name' are required for Redis Sentinel.")

tests/test_litellm/test_redis.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,3 +1024,36 @@ async def connect(connection):
10241024
client = get_redis_async_client()
10251025

10261026
assert isinstance(client, async_redis.RedisCluster)
1027+
1028+
1029+
@pytest.mark.parametrize(
1030+
"markers, provider_cls",
1031+
[
1032+
(AZURE_AD_CONNECT_FUNC, AzureADCredentialProvider),
1033+
(GCP_IAM_CONNECT_FUNC, GCPIAMCredentialProvider),
1034+
],
1035+
ids=["azure_ad", "gcp_iam"],
1036+
)
1037+
def test_async_sentinel_keeps_the_credential_provider_off_the_monitors(markers, provider_cls):
1038+
"""The Sentinel monitors authenticate with their own password, and redis-py refuses a password
1039+
passed alongside a credential provider, so only the data node may carry the provider.
1040+
"""
1041+
redis_kwargs = {
1042+
"sentinel_nodes": [("sentinel-1", 26379)],
1043+
"sentinel_password": "sentinel-secret",
1044+
"service_name": "mymaster",
1045+
"redis_connect_func": SimpleNamespace(**markers),
1046+
}
1047+
1048+
with patch("litellm._redis.async_redis.Sentinel") as mock_sentinel_cls:
1049+
with patch("litellm._redis._get_redis_client_logic", return_value=redis_kwargs):
1050+
get_redis_async_client()
1051+
1052+
sentinel_kwargs = mock_sentinel_cls.call_args[1]["sentinel_kwargs"]
1053+
assert sentinel_kwargs["password"] == "sentinel-secret"
1054+
assert "credential_provider" not in sentinel_kwargs
1055+
async_redis.Connection(host="sentinel-1", port=26379, **sentinel_kwargs)
1056+
1057+
master_kwargs = mock_sentinel_cls.return_value.master_for.call_args[1]
1058+
assert isinstance(master_kwargs["credential_provider"], provider_cls)
1059+
assert "password" not in master_kwargs

0 commit comments

Comments
 (0)