diff --git a/docs/my-website/docs/proxy/dynamic_logging.md b/docs/my-website/docs/proxy/dynamic_logging.md index 3bc9f72b03..42df221bb8 100644 --- a/docs/my-website/docs/proxy/dynamic_logging.md +++ b/docs/my-website/docs/proxy/dynamic_logging.md @@ -211,4 +211,64 @@ x-litellm-disable-callbacks: LANGFUSE,datadog,PROMETHEUS x-litellm-disable-callbacks: langfuse,DATADOG,prometheus ``` +--- + +## Disabling Dynamic Callback Management (Enterprise) + +Some organizations have compliance requirements where **all requests must be logged under all circumstances**. For these cases, you can disable dynamic callback management entirely to ensure users cannot disable any logging callbacks. + +### Use Case + +This is designed for enterprise scenarios where: +- **Compliance requirements** mandate that all API requests must be logged +- **Audit trails** must be complete with no gaps +- **Security policies** require all traffic to be monitored +- **No exceptions** can be made for callback disabling + +### How to Disable + +Set `allow_dynamic_callback_disabling` to `false` in your config.yaml: + +```yaml showLineNumbers title="config.yaml" +litellm_settings: + allow_dynamic_callback_disabling: false +``` + +### Effect + +When disabled: +- The `x-litellm-disable-callbacks` header will be **ignored** +- All configured callbacks will **always execute** for every request +- Users cannot bypass logging through headers or request metadata +- All requests are guaranteed to be logged per your proxy configuration + +### Example: Compliance Logging Setup + +Here's a complete example for an organization requiring guaranteed logging: + +```yaml showLineNumbers title="config.yaml" +# config.yaml +model_list: + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4 + api_key: os.environ/OPENAI_API_KEY + +litellm_settings: + callbacks: ["langfuse", "datadog", "s3"] + # Disable dynamic callback disabling for compliance + allow_dynamic_callback_disabling: false +``` + +With this configuration: +- All requests will be logged to Langfuse, Datadog, and S3 +- Users cannot disable any of these callbacks via headers +- Complete audit trail is guaranteed for compliance requirements + +:::info + +**Default Behavior**: Dynamic callback disabling is **enabled by default** (`allow_dynamic_callback_disabling: true`). You must explicitly set it to `false` to enforce guaranteed logging. + +::: + diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py b/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py index ff3e9a744c..8824f4c02d 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py @@ -40,7 +40,7 @@ class EnterpriseCallbackControls: ######################################################### # premium user check ######################################################### - if not EnterpriseCallbackControls._premium_user_check(): + if not EnterpriseCallbackControls._should_allow_dynamic_callback_disabling(): return False ######################################################### if isinstance(callback, str): @@ -84,8 +84,15 @@ class EnterpriseCallbackControls: return None @staticmethod - def _premium_user_check(): + def _should_allow_dynamic_callback_disabling(): + import litellm from litellm.proxy.proxy_server import premium_user + + # Check if admin has disabled this feature + if litellm.allow_dynamic_callback_disabling is not True: + verbose_logger.debug("Dynamic callback disabling is disabled by admin via litellm.allow_dynamic_callback_disabling") + return False + if premium_user: return True verbose_logger.warning(f"Disabling callbacks using request headers is an enterprise feature. {CommonProxyErrors.not_premium_user.value}") diff --git a/litellm/__init__.py b/litellm/__init__.py index b47c7b74d3..6cf2aca313 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -423,6 +423,7 @@ fallbacks: Optional[List] = None context_window_fallbacks: Optional[List] = None content_policy_fallbacks: Optional[List] = None allowed_fails: int = 3 +allow_dynamic_callback_disabling: bool = True num_retries_per_request: Optional[int] = ( None # for the request overall (incl. fallbacks + model retries) ) diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py b/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py index cce0a27934..b160ca5130 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py @@ -22,13 +22,13 @@ class TestEnterpriseCallbackControls: @pytest.fixture def mock_premium_user(self): """Fixture to mock premium user check as True""" - with patch.object(EnterpriseCallbackControls, '_premium_user_check', return_value=True): + with patch.object(EnterpriseCallbackControls, '_should_allow_dynamic_callback_disabling', return_value=True): yield @pytest.fixture def mock_non_premium_user(self): """Fixture to mock premium user check as False""" - with patch.object(EnterpriseCallbackControls, '_premium_user_check', return_value=False): + with patch.object(EnterpriseCallbackControls, '_should_allow_dynamic_callback_disabling', return_value=False): yield @pytest.fixture @@ -214,3 +214,47 @@ class TestEnterpriseCallbackControls: # Test non-disabled callback is not disabled assert EnterpriseCallbackControls.is_callback_disabled_dynamically("prometheus", litellm_params, standard_callback_dynamic_params) is False + + def test_admin_can_disable_dynamic_callback_disabling(self, mock_request_headers): + """ + Test that when admin sets allow_dynamic_callback_disabling to False, + callbacks cannot be disabled dynamically even for premium users + """ + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} + litellm_params = {"proxy_server_request": {"url": "test"}} + standard_callback_dynamic_params = StandardCallbackDynamicParams() + + # Mock litellm.allow_dynamic_callback_disabling set to False + with patch('litellm.allow_dynamic_callback_disabling', False): + with patch('litellm.proxy.proxy_server.premium_user', True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + assert result is False + + def test_admin_can_enable_dynamic_callback_disabling(self, mock_request_headers): + """ + Test that when admin sets allow_dynamic_callback_disabling to True, + callbacks can be disabled dynamically for premium users + """ + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} + litellm_params = {"proxy_server_request": {"url": "test"}} + standard_callback_dynamic_params = StandardCallbackDynamicParams() + + # Mock litellm.allow_dynamic_callback_disabling set to True + with patch('litellm.allow_dynamic_callback_disabling', True): + with patch('litellm.proxy.proxy_server.premium_user', True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + assert result is True + + def test_default_admin_setting_allows_dynamic_callback_disabling(self, mock_request_headers): + """ + Test that when allow_dynamic_callback_disabling is not set, + it defaults to True and allows dynamic callback disabling for premium users + """ + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} + litellm_params = {"proxy_server_request": {"url": "test"}} + standard_callback_dynamic_params = StandardCallbackDynamicParams() + + # litellm.allow_dynamic_callback_disabling should default to True + with patch('litellm.proxy.proxy_server.premium_user', True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + assert result is True