diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index b7f11ec270..d0d132c313 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -871,6 +871,12 @@ async def proxy_startup_event(app: FastAPI): # noqa: PLR0915 redis_usage_cache=redis_usage_cache, ) + ## Validate use_redis_transaction_buffer requires Redis cache ## + ProxyStartupEvent._validate_redis_transaction_buffer_config( + general_settings=general_settings, + redis_usage_cache=redis_usage_cache, + ) + ## SEMANTIC TOOL FILTER ## # Read litellm_settings from config for semantic filter initialization try: @@ -5618,6 +5624,40 @@ class ProxyStartupEvent: llm_router=llm_router, redis_usage_cache=redis_usage_cache ) + @staticmethod + def _validate_redis_transaction_buffer_config( + general_settings: dict, + redis_usage_cache: Optional[RedisCache], + ): + """ + Validates that when use_redis_transaction_buffer is enabled, + a Redis cache is properly configured in litellm_settings. + + Without Redis, spend updates are silently dropped because: + - In-memory queues are drained but never pushed to Redis + - The pod lock manager cannot acquire locks for DB commits + - No fallback to direct DB writes occurs + """ + from litellm.proxy.db.db_transaction_queue.redis_update_buffer import ( + RedisUpdateBuffer, + ) + + if ( + RedisUpdateBuffer._should_commit_spend_updates_to_redis() + and redis_usage_cache is None + ): + raise ValueError( + "`use_redis_transaction_buffer` is enabled in general_settings, " + "but no Redis cache is configured. Spend tracking will silently " + "fail without Redis. Please add a Redis cache configuration in " + "litellm_settings:\n\n" + "litellm_settings:\n" + " cache: true\n" + " cache_params:\n" + " type: redis\n" + " url: os.environ/REDIS_URL\n" + ) + @classmethod async def _initialize_semantic_tool_filter( cls, diff --git a/tests/test_litellm/proxy/db/db_transaction_queue/test_redis_update_buffer.py b/tests/test_litellm/proxy/db/db_transaction_queue/test_redis_update_buffer.py index 2a380370c3..a77b8d49b6 100644 --- a/tests/test_litellm/proxy/db/db_transaction_queue/test_redis_update_buffer.py +++ b/tests/test_litellm/proxy/db/db_transaction_queue/test_redis_update_buffer.py @@ -10,6 +10,7 @@ sys.path.insert( ) # Adds the parent directory to the system path from litellm.proxy.db.db_transaction_queue.redis_update_buffer import RedisUpdateBuffer +from litellm.proxy.proxy_server import ProxyStartupEvent from litellm.types.caching import RedisPipelineRpushOperation @@ -192,3 +193,53 @@ async def test_get_all_transactions_from_redis_buffer_pipeline_no_redis(): buffer = RedisUpdateBuffer(redis_cache=None) result = await buffer.get_all_transactions_from_redis_buffer_pipeline() assert result == (None, None, None, None, None, None, None) + + +def test_validate_redis_transaction_buffer_raises_without_redis(): + """ + When use_redis_transaction_buffer=true but no Redis cache is configured, + the proxy should refuse to start with a clear error message. + """ + general_settings = {"use_redis_transaction_buffer": True} + + with patch( + "litellm.proxy.proxy_server.general_settings", general_settings + ): + with pytest.raises(ValueError, match="use_redis_transaction_buffer"): + ProxyStartupEvent._validate_redis_transaction_buffer_config( + general_settings=general_settings, + redis_usage_cache=None, + ) + + +def test_validate_redis_transaction_buffer_passes_with_redis(): + """ + When use_redis_transaction_buffer=true and Redis cache is configured, + validation should pass without error. + """ + general_settings = {"use_redis_transaction_buffer": True} + mock_redis_cache = MagicMock() + + with patch( + "litellm.proxy.proxy_server.general_settings", general_settings + ): + # Should not raise + ProxyStartupEvent._validate_redis_transaction_buffer_config( + general_settings=general_settings, + redis_usage_cache=mock_redis_cache, + ) + + +def test_validate_redis_transaction_buffer_passes_when_disabled(): + """ + When use_redis_transaction_buffer is not set or false, + validation should pass regardless of Redis configuration. + """ + with patch( + "litellm.proxy.proxy_server.general_settings", {} + ): + # Should not raise even without Redis + ProxyStartupEvent._validate_redis_transaction_buffer_config( + general_settings={}, + redis_usage_cache=None, + )