diff --git a/litellm/integrations/rubrik.py b/litellm/integrations/rubrik.py index a988c0b3604..48cf165ab3a 100644 --- a/litellm/integrations/rubrik.py +++ b/litellm/integrations/rubrik.py @@ -121,7 +121,13 @@ class RubrikLogger(CustomGuardrail, CustomBatchLogger): if self.key: self._headers["Authorization"] = f"Bearer {self.key}" - asyncio.create_task(self.periodic_flush()) + try: + asyncio.create_task(self.periodic_flush()) + except RuntimeError: + verbose_logger.debug( + "Rubrik: no running event loop at init time; " + "periodic_flush will not run automatically." + ) async def aclose(self): """Close the dedicated tool blocking HTTP client.""" diff --git a/tests/test_litellm/integrations/test_rubrik.py b/tests/test_litellm/integrations/test_rubrik.py index c2fe643a578..3a526fed270 100644 --- a/tests/test_litellm/integrations/test_rubrik.py +++ b/tests/test_litellm/integrations/test_rubrik.py @@ -59,9 +59,7 @@ class TestInitialization: def test_init_with_constructor_params(self): with patch("asyncio.create_task", Mock()): - handler = RubrikLogger( - api_key="ctor-key", api_base="http://ctor-host:9090" - ) + handler = RubrikLogger(api_key="ctor-key", api_base="http://ctor-host:9090") assert handler.key == "ctor-key" assert ( handler.tool_blocking_endpoint @@ -148,14 +146,23 @@ class TestInitialization: ): assert RubrikLogger().batch_size == 256 + def test_init_outside_event_loop_does_not_raise(self): + """Instantiation without a running event loop must not raise RuntimeError.""" + with patch.dict( + os.environ, + {"RUBRIK_WEBHOOK_URL": "http://localhost:8080", "RUBRIK_API_KEY": "k"}, + ): + # Do NOT patch asyncio.create_task — the real call should be + # guarded and fall back gracefully when there is no event loop. + handler = RubrikLogger() + assert handler.tool_blocking_endpoint.startswith("http://localhost:8080") + def test_headers_with_api_key(self, handler): assert handler._headers["Authorization"] == "Bearer test-api-key" assert handler._headers["Content-Type"] == "application/json" def test_headers_without_api_key(self): - with patch.dict( - os.environ, {"RUBRIK_WEBHOOK_URL": "http://host"}, clear=True - ): + with patch.dict(os.environ, {"RUBRIK_WEBHOOK_URL": "http://host"}, clear=True): with patch("asyncio.create_task", Mock()): h = RubrikLogger() assert "Authorization" not in h._headers @@ -352,9 +359,7 @@ def _echo_service(): @pytest.mark.asyncio class TestApplyGuardrail: async def test_skips_requests(self, handler): - inputs = make_inputs_with_tools( - [make_tool_call_dict("call_1", "test_tool")] - ) + inputs = make_inputs_with_tools([make_tool_call_dict("call_1", "test_tool")]) result = await handler.apply_guardrail( inputs=inputs, request_data={}, input_type="request" ) @@ -465,9 +470,7 @@ class TestApplyGuardrail: async def test_blocking_service_payload_format(self, handler): tc1 = make_tool_call_dict("call_1", "get_weather", '{"location": "SF"}') - tc2 = make_tool_call_dict( - "call_2", "send_email", '{"to": "user@example.com"}' - ) + tc2 = make_tool_call_dict("call_2", "send_email", '{"to": "user@example.com"}') inputs = make_inputs_with_tools([tc1, tc2]) captured_payload: Dict[str, Any] = {} @@ -606,9 +609,7 @@ class TestApplyGuardrailAnthropicFormat: inputs = make_inputs_with_tools([tc]) mock_client = AsyncMock() - mock_client.post = AsyncMock( - side_effect=httpx.TimeoutException("Timeout") - ) + mock_client.post = AsyncMock(side_effect=httpx.TimeoutException("Timeout")) handler.tool_blocking_client = mock_client result = await handler.apply_guardrail( @@ -729,5 +730,7 @@ class TestResolveModel: response = Mock() response.model = "" - result = RubrikLogger._resolve_model({"response": response}, {"model": "fallback"}) + result = RubrikLogger._resolve_model( + {"response": response}, {"model": "fallback"} + ) assert result == "unknown"