1717
1818import redis
1919import redis .asyncio as async_redis
20+ from redis .credentials import CredentialProvider
2021
2122from litellm import get_secret , get_secret_str
2223from litellm ._redis_credential_provider import (
@@ -134,6 +135,7 @@ def _get_redis_cluster_kwargs(client=None):
134135 "ssl_check_hostname" ,
135136 "ssl_ca_certs" ,
136137 "redis_connect_func" , # Needed for sync clusters and IAM detection
138+ "credential_provider" ,
137139 "gcp_service_account" ,
138140 "gcp_ssl_ca_certs" ,
139141 "azure_redis_ad_token" ,
@@ -549,14 +551,22 @@ def _init_redis_sentinel(redis_kwargs) -> redis.Redis:
549551 return sentinel .master_for (service_name , ** connection_kwargs )
550552
551553
554+ def _sentinel_auth_kwargs (connection_kwargs : dict , sentinel_password : str | None ) -> dict :
555+ """The Sentinel monitors are separate servers that authenticate with their own password, so the
556+ data node's credential provider never belongs on them: leaving it there makes redis-py send the
557+ data node's token to a monitor, which fails whether the monitor is unauthenticated or has its
558+ own password."""
559+ kept : Final = ((k , v ) for k , v in connection_kwargs .items () if k != "credential_provider" )
560+ return dict (kept , password = sentinel_password )
561+
562+
552563def _init_async_redis_sentinel (redis_kwargs ) -> async_redis .Redis :
553564 sentinel_nodes : Final = redis_kwargs .get ("sentinel_nodes" )
554565 sentinel_password : Final = redis_kwargs .get ("sentinel_password" )
555566 service_name : Final = redis_kwargs .get ("service_name" )
556567 connection_kwargs : Final = _get_redis_sentinel_connection_kwargs (redis_kwargs )
557568 connection_kwargs .setdefault ("socket_timeout" , REDIS_SOCKET_TIMEOUT )
558- sentinel_kwargs : Final = dict (connection_kwargs )
559- sentinel_kwargs ["password" ] = sentinel_password
569+ sentinel_kwargs : Final = _sentinel_auth_kwargs (connection_kwargs , sentinel_password )
560570
561571 if not sentinel_nodes or not service_name :
562572 raise ValueError ("Both 'sentinel_nodes' and 'service_name' are required for Redis Sentinel." )
@@ -574,6 +584,36 @@ def _init_async_redis_sentinel(redis_kwargs) -> async_redis.Redis:
574584 return sentinel .master_for (service_name , ** connection_kwargs )
575585
576586
587+ def _async_credential_provider (redis_connect_func : object | None ) -> CredentialProvider | None :
588+ """The Azure AD and GCP IAM connect funcs run their AUTH exchange with the blocking client
589+ API, so on an async connection their ``send_command``/``read_response`` calls return
590+ coroutines nobody awaits and every connect fails. Async paths authenticate through a
591+ ``CredentialProvider`` instead, which redis-py consults per connection so the token stays
592+ fresh. Any other ``redis_connect_func`` is left where it is, since redis-py awaits it
593+ itself when it is a coroutine function."""
594+ gcp_service_account : Final = getattr (redis_connect_func , "_gcp_service_account" , None )
595+ if gcp_service_account is not None :
596+ return GCPIAMCredentialProvider (gcp_service_account )
597+
598+ azure_credential : Final = getattr (redis_connect_func , "_azure_credential" , None )
599+ if azure_credential is not None :
600+ return AzureADCredentialProvider (azure_credential , username = os .environ .get ("REDIS_USERNAME" ) or None )
601+
602+ return None
603+
604+
605+ def _async_auth_kwargs (redis_kwargs : dict ) -> dict :
606+ """Swaps a connect func an async path cannot run for the equivalent credential provider,
607+ which supersedes any static username or password redis-py would otherwise reject it with."""
608+ credential_provider : Final = _async_credential_provider (redis_kwargs .get ("redis_connect_func" ))
609+ if credential_provider is None :
610+ return redis_kwargs
611+
612+ superseded : Final = frozenset ({"redis_connect_func" , "username" , "password" })
613+ kept : Final = ((k , v ) for k , v in redis_kwargs .items () if k not in superseded )
614+ return dict (kept , credential_provider = credential_provider ) # mutable-ok: the branches below mutate these kwargs
615+
616+
577617def get_redis_client (** env_overrides ):
578618 redis_kwargs : Final = _get_redis_client_logic (** env_overrides )
579619
@@ -600,7 +640,7 @@ def get_redis_async_client(
600640 connection_pool : async_redis .BlockingConnectionPool | None = None ,
601641 ** env_overrides ,
602642) -> async_redis .Redis | async_redis .RedisCluster :
603- redis_kwargs : Final = _get_redis_client_logic (** env_overrides )
643+ redis_kwargs : Final = _async_auth_kwargs ( _get_redis_client_logic (** env_overrides ) )
604644
605645 if "startup_nodes" in redis_kwargs :
606646 from redis .cluster import ClusterNode
@@ -611,28 +651,12 @@ def get_redis_async_client(
611651 if arg in args :
612652 cluster_kwargs [arg ] = redis_kwargs [arg ]
613653
614- # Handle GCP IAM authentication for async clusters
615- redis_connect_func = cluster_kwargs .pop ("redis_connect_func" , None )
616-
617- # Use a CredentialProvider so the IAM token is regenerated on every new
618- # connection — mirrors the sync path where redis_connect_func is invoked
619- # per connection. Without this, the token would expire after ~1 hour.
620- if redis_connect_func and hasattr (redis_connect_func , "_gcp_service_account" ):
621- cluster_kwargs ["credential_provider" ] = GCPIAMCredentialProvider (redis_connect_func ._gcp_service_account )
622- # Handle Azure AD authentication for async clusters via CredentialProvider
623- # so the credential's internal cache + silent refresh runs per connection
624- # (mirrors GCP IAM above; avoids static-token-baked-in-pool expiry).
625- elif redis_connect_func and hasattr (redis_connect_func , "_azure_credential" ):
626- cluster_kwargs ["credential_provider" ] = AzureADCredentialProvider (
627- redis_connect_func ._azure_credential ,
628- username = os .environ .get ("REDIS_USERNAME" ) or None ,
629- )
630-
631654 new_startup_nodes : Final [list [ClusterNode ]] = []
632655
633656 for item in redis_kwargs ["startup_nodes" ]:
634657 new_startup_nodes .append (ClusterNode (** item ))
635658 cluster_kwargs .pop ("startup_nodes" , None )
659+ cluster_kwargs .pop ("redis_connect_func" , None )
636660
637661 # Default to a periodic health check + TCP keepalive so a connection silently dropped
638662 # by a cluster restart (e.g. ElastiCache Serverless maintenance) is revalidated and
@@ -667,19 +691,6 @@ def get_redis_async_client(
667691 if "sentinel_nodes" in redis_kwargs and "service_name" in redis_kwargs :
668692 return _init_async_redis_sentinel (redis_kwargs )
669693
670- # Wrap GCP / Azure AD auth in a CredentialProvider for the standard async
671- # Redis client. The async client doesn't support redis_connect_func, but it
672- # does honour credential_provider — which is called per connection, so the
673- # underlying SDK can refresh tokens silently before they expire.
674- redis_connect_func = redis_kwargs .pop ("redis_connect_func" , None )
675- if redis_connect_func and hasattr (redis_connect_func , "_azure_credential" ):
676- redis_kwargs ["credential_provider" ] = AzureADCredentialProvider (
677- redis_connect_func ._azure_credential ,
678- username = os .environ .get ("REDIS_USERNAME" ) or None ,
679- )
680- elif redis_connect_func and hasattr (redis_connect_func , "_gcp_service_account" ):
681- redis_kwargs ["credential_provider" ] = GCPIAMCredentialProvider (redis_connect_func ._gcp_service_account )
682-
683694 _pretty_print_redis_config (redis_kwargs = redis_kwargs )
684695
685696 if connection_pool is not None :
@@ -693,7 +704,7 @@ def get_redis_async_client(
693704def get_redis_connection_pool (
694705 ** env_overrides ,
695706) -> async_redis .BlockingConnectionPool | None :
696- redis_kwargs : Final = _get_redis_client_logic (** env_overrides )
707+ redis_kwargs : Final = _async_auth_kwargs ( _get_redis_client_logic (** env_overrides ) )
697708 verbose_logger .debug ("get_redis_connection_pool: redis_kwargs" , redis_kwargs )
698709
699710 if "startup_nodes" in redis_kwargs :
@@ -714,18 +725,6 @@ def get_redis_connection_pool(
714725 )
715726 return async_redis .BlockingConnectionPool .from_url (** pool_kwargs )
716727
717- # Wrap GCP / Azure AD auth in a CredentialProvider so pool-managed
718- # connections re-fetch tokens via the SDK's internal cache + silent refresh
719- # rather than reusing a single token captured at pool creation.
720- redis_connect_func : Final = redis_kwargs .pop ("redis_connect_func" , None )
721- if redis_connect_func and hasattr (redis_connect_func , "_azure_credential" ):
722- redis_kwargs ["credential_provider" ] = AzureADCredentialProvider (
723- redis_connect_func ._azure_credential ,
724- username = os .environ .get ("REDIS_USERNAME" ) or None ,
725- )
726- elif redis_connect_func and hasattr (redis_connect_func , "_gcp_service_account" ):
727- redis_kwargs ["credential_provider" ] = GCPIAMCredentialProvider (redis_connect_func ._gcp_service_account )
728-
729728 if redis_kwargs .pop ("ssl" , None ):
730729 redis_kwargs ["connection_class" ] = async_redis .SSLConnection
731730 return async_redis .BlockingConnectionPool (timeout = REDIS_CONNECTION_POOL_TIMEOUT , ** redis_kwargs )
0 commit comments