fix(redis): apply Azure AD and GCP IAM auth to every async client path - #37740
Conversation
REDIS_URL-based async clients and every async connection pool dropped the managed-identity credential the caller configured, so they connected unauthenticated against an auth-enforcing Redis. The conversion from redis_connect_func to a CredentialProvider now happens once, before any branch, and covers the url, sentinel, cluster, and pool paths alike. Also adds credential_provider to the cluster kwargs allowlist, which silently filtered it out.
The AUTH exchange it runs is the blocking client API, so on an async connection send_command and read_response hand back coroutines nobody awaits and the connect fails outright.
A caller-supplied redis_connect_func has no way to run on an async connection, so log it instead of dropping it in silence.
Greptile SummaryThis PR centralizes async Redis credential-provider translation so Azure AD and GCP IAM authentication is consistently applied across URL, pool, Sentinel, and cluster client paths.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported duplicated kwargs mutation has been replaced by centralized authentication translation that returns a transformed dictionary.
|
| Filename | Overview |
|---|---|
| litellm/_redis.py | Centralizes async authentication translation in a copy-producing helper and applies it across all async Redis construction paths; the previously reported duplicated mutation is resolved. |
| tests/test_litellm/test_redis.py | Adds focused mocked regression tests covering Azure AD and GCP IAM authentication behavior for async URL, pool, cluster, and Sentinel clients. |
Reviews (4): Last reviewed commit: "fix(redis): never hand a data-node crede..." | Re-trigger Greptile
redis-py awaits a redis_connect_func that is a coroutine function, so dropping every connect func the async paths cannot convert took away an auth path that worked.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… twice Both async entrypoints edited the kwargs dict in place with the same five lines. One shared transform returns the swapped copy instead.
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Sentinel monitors keep data credentials
- Removed the sentinel_password gate so _sentinel_auth_kwargs always strips credential_provider from monitor kwargs, and added a no-password regression test for the Azure AD and GCP IAM cases.
Or push these changes by commenting:
@cursor push 714e013386
Preview (714e013386)
diff --git a/litellm/_redis.py b/litellm/_redis.py
--- a/litellm/_redis.py
+++ b/litellm/_redis.py
@@ -552,10 +552,10 @@
def _sentinel_auth_kwargs(connection_kwargs: dict, sentinel_password: str | None) -> dict:
- """The Sentinel monitors are separate servers with their own password, and redis-py refuses a
- password passed alongside a credential provider, so the data node's provider stays behind once
- a Sentinel password is configured."""
- superseded: Final = frozenset({"credential_provider"}) if sentinel_password else frozenset()
+ """The Sentinel monitors are separate servers with their own password, so the data node's
+ credential provider never belongs on them, and redis-py additionally refuses a password
+ passed alongside a credential provider."""
+ superseded: Final = frozenset({"credential_provider"})
kept: Final = ((k, v) for k, v in connection_kwargs.items() if k not in superseded)
return dict(kept, password=sentinel_password)
diff --git a/tests/test_litellm/test_redis.py b/tests/test_litellm/test_redis.py
--- a/tests/test_litellm/test_redis.py
+++ b/tests/test_litellm/test_redis.py
@@ -1057,3 +1057,34 @@
master_kwargs = mock_sentinel_cls.return_value.master_for.call_args[1]
assert isinstance(master_kwargs["credential_provider"], provider_cls)
assert "password" not in master_kwargs
+
+
+@pytest.mark.parametrize(
+ "markers, provider_cls",
+ [
+ (AZURE_AD_CONNECT_FUNC, AzureADCredentialProvider),
+ (GCP_IAM_CONNECT_FUNC, GCPIAMCredentialProvider),
+ ],
+ ids=["azure_ad", "gcp_iam"],
+)
+def test_async_sentinel_drops_data_node_provider_when_monitors_have_no_password(markers, provider_cls):
+ """Without a sentinel password the monitors are unauthenticated, so the data node's
+ Entra/IAM credential provider still must not follow onto them or Sentinel discovery
+ will attempt to authenticate the monitors with a data-node token."""
+ redis_kwargs = {
+ "sentinel_nodes": [("sentinel-1", 26379)],
+ "service_name": "mymaster",
+ "redis_connect_func": SimpleNamespace(**markers),
+ }
+
+ with patch("litellm._redis.async_redis.Sentinel") as mock_sentinel_cls:
+ with patch("litellm._redis._get_redis_client_logic", return_value=redis_kwargs):
+ get_redis_async_client()
+
+ sentinel_kwargs = mock_sentinel_cls.call_args[1]["sentinel_kwargs"]
+ assert "credential_provider" not in sentinel_kwargs
+ assert sentinel_kwargs.get("password") is None
+
+ master_kwargs = mock_sentinel_cls.return_value.master_for.call_args[1]
+ assert isinstance(master_kwargs["credential_provider"], provider_cls)
+ assert "password" not in master_kwargsYou can send follow-ups to the cloud agent here.
…l monitors The monitors are separate servers with their own password, so the data node's Entra or IAM token has no standing there. Dropping the provider only when a Sentinel password was configured left it in place for unauthenticated monitors, where redis-py sends it as an AUTH the monitor rejects and async Sentinel discovery fails.
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c09643a. Configure here.

TLDR
Problem this solves:
REDIS_URLnever authenticated at allHow it solves it:
credential_providerthrough the cluster kwargs filterUser Flow
Before: an admin running two proxy replicas that cache into a managed Redis with Microsoft Entra ID gets no shared cache at all, so every replica pays the provider separately
REDIS_URL=rediss://<their-cache>.<region>.redis.azure.net:10000,REDIS_USERNAME=<the identity's object id>, andREDIS_AZURE_AD_TOKEN=true, turn oncache: truewithcache_params.type: redis, and start two replicasGET /cache/pingon replica A and get503 Service Unavailablewith{"message": "Service Unhealthy"}POST /v1/chat/completionsto replica A and are billed for it,x-litellm-response-cost: 0.000645POST /v1/chat/completionsto replica B and are billed a second time,x-litellm-response-cost: 0.000675, coming back with a different response id and nox-litellm-cache-keyheaderAuthenticationError: Azure AD authentication failed for Redis, followed byRedis circuit breaker is openAfter: the same setup authenticates, so the second replica serves the answer out of Redis instead of buying it again
GET /cache/pingon replica A and get200 OKwith{"status":"healthy","cache_type":"redis","ping_response":true,"set_cache_response":"success"}POST /v1/chat/completionsto replica A and are billed for it,x-litellm-response-cost: 0.00035POST /v1/chat/completionsto replica B and get back the same response id as replica A carryingx-litellm-cache-key: 6c2debe1..., so no second call to the provider happensRelevant issues
Linear ticket
Related to LIT-5889
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Everything below ran against a real Azure Managed Redis (Balanced_B0, TLS, access keys Disabled, so Microsoft Entra ID is the only way in) and the real OpenAI API. No mocks, real spend. Before is the merge base
cb4eb82249on ports 47311 and 47313, After is the PR tipc09643ac4con ports 56981 and 56249Shared setup, identical on both sides. Two proxy replicas point at that one cache, which is what an HA deployment looks like and what makes a shared cache observable at all: a single replica hides the whole bug behind its own in-memory layer, still answering "Cache Hit!" while Redis is unreachable
Shared background, and the reason the fix takes the shape it does. Three redis-py legs against that same cache, driven by LiteLLM's own Azure AD helpers, showing that an async connection cannot authenticate through a
redis_connect_funcno matter which code path hands it oneLeg 2 fails because that connect function runs the AUTH exchange with the blocking client API, so on an async connection
send_commandandread_responsehand back coroutines nobody awaits. Python says so itself in the same runBefore (cb4eb82)
Cache health on replica A
curl -s -i http://127.0.0.1:47311/cache/ping -H "Authorization: Bearer sk-1234"The same completion on two replicas sharing one cache
curl -s -i http://127.0.0.1:47311/v1/chat/completions -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","messages":[{"role":"user","content":"Reply with exactly: lit5889 before probe"}]}'What actually reached the Redis server
After (c09643a)
Cache health on replica A
curl -s -i http://127.0.0.1:56981/cache/ping -H "Authorization: Bearer sk-1234"The same completion on two replicas sharing one cache
curl -s -i http://127.0.0.1:56981/v1/chat/completions -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","messages":[{"role":"user","content":"Reply with exactly: lit5889 after6 probe"}]}'What actually reached the Redis server
Type
🐛 Bug Fix
Caveats (if any)
REDIS_USERNAMEset to the identity's object idredis_connect_funcstill cannot run on asyncFinal Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
c09643a passes /live-pr-risk