diff --git a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py index b80907e8ec..92c97a5992 100644 --- a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py +++ b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py @@ -1,12 +1,28 @@ from typing import Dict, Optional -from litellm._logging import verbose_logger from litellm.types.utils import StandardCallbackDynamicParams def _is_env_reference(value: object) -> bool: return isinstance(value, str) and "os.environ/" in value + +def _raise_env_reference_error(param: str, *, source: str) -> None: + raise ValueError( + f"Callback param '{param}' (from {source}) contains an 'os.environ/' " + "reference. Environment references in request-supplied parameters are " + "no longer resolved server-side for security reasons.\n" + "To resolve:\n" + " 1. Remove the 'os.environ/' reference from your request body / " + "metadata.\n" + " 2. Either (a) configure this callback value in your proxy " + "config.yaml under 'litellm_settings' / 'general_settings', or " + "(b) pass the resolved secret value directly in the request.\n" + "See https://docs.litellm.ai/docs/proxy/logging for server-side " + "callback configuration." + ) + + # Hardcoded list of supported callback params to avoid runtime inspection issues with TypedDict _supported_callback_params = [ "langfuse_public_key", @@ -52,13 +68,7 @@ def initialize_standard_callback_dynamic_params( if param in kwargs: _param_value = kwargs.get(param) if _is_env_reference(_param_value): - verbose_logger.warning( - "Dropping callback param '%s': os.environ/ references " - "in request-supplied parameters are not resolved. " - "Configure this value server-side instead.", - param, - ) - continue + _raise_env_reference_error(param, source="request body") standard_callback_dynamic_params[param] = _param_value # type: ignore # 2. Fallback: check "metadata" or "litellm_params" -> "metadata" @@ -72,14 +82,7 @@ def initialize_standard_callback_dynamic_params( if param not in standard_callback_dynamic_params and param in metadata: _param_value = metadata.get(param) if _is_env_reference(_param_value): - verbose_logger.warning( - "Dropping callback param '%s' from metadata: " - "os.environ/ references in request-supplied " - "parameters are not resolved. Configure this " - "value server-side instead.", - param, - ) - continue + _raise_env_reference_error(param, source="metadata") standard_callback_dynamic_params[param] = _param_value # type: ignore return standard_callback_dynamic_params diff --git a/tests/test_litellm/litellm_core_utils/test_initialize_dynamic_callback_params.py b/tests/test_litellm/litellm_core_utils/test_initialize_dynamic_callback_params.py new file mode 100644 index 0000000000..91969a2b8e --- /dev/null +++ b/tests/test_litellm/litellm_core_utils/test_initialize_dynamic_callback_params.py @@ -0,0 +1,99 @@ +import os +import sys + +import pytest + +sys.path.insert(0, os.path.abspath("../../..")) + +from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( + initialize_standard_callback_dynamic_params, +) + + +def test_resolves_plain_values_at_top_level(): + kwargs = { + "langfuse_public_key": "pk-test", + "langfuse_secret_key": "sk-test", + } + + params = initialize_standard_callback_dynamic_params(kwargs) + + assert params.get("langfuse_public_key") == "pk-test" + assert params.get("langfuse_secret_key") == "sk-test" + + +def test_resolves_plain_values_from_metadata(): + kwargs = { + "metadata": { + "langfuse_public_key": "pk-meta", + "langfuse_host": "https://test.langfuse.com", + } + } + + params = initialize_standard_callback_dynamic_params(kwargs) + + assert params.get("langfuse_public_key") == "pk-meta" + assert params.get("langfuse_host") == "https://test.langfuse.com" + + +def test_env_reference_at_top_level_raises_with_guidance(): + kwargs = {"langfuse_public_key": "os.environ/LANGFUSE_PUBLIC_KEY"} + + with pytest.raises(ValueError) as exc_info: + initialize_standard_callback_dynamic_params(kwargs) + + message = str(exc_info.value) + assert "langfuse_public_key" in message + assert "request body" in message + assert "os.environ/" in message + assert "config.yaml" in message + + +def test_env_reference_in_metadata_raises_with_guidance(): + kwargs = { + "metadata": { + "langsmith_api_key": "os.environ/LANGSMITH_API_KEY", + } + } + + with pytest.raises(ValueError) as exc_info: + initialize_standard_callback_dynamic_params(kwargs) + + message = str(exc_info.value) + assert "langsmith_api_key" in message + assert "metadata" in message + + +def test_env_reference_in_litellm_params_metadata_raises(): + kwargs = { + "litellm_params": { + "metadata": { + "gcs_bucket_name": "os.environ/GCS_BUCKET", + } + } + } + + with pytest.raises(ValueError) as exc_info: + initialize_standard_callback_dynamic_params(kwargs) + + assert "gcs_bucket_name" in str(exc_info.value) + + +def test_non_string_values_are_not_flagged(): + kwargs = { + "langsmith_sampling_rate": 0.5, + "turn_off_message_logging": True, + } + + params = initialize_standard_callback_dynamic_params(kwargs) + + assert params.get("langsmith_sampling_rate") == 0.5 + assert params.get("turn_off_message_logging") is True + + +def test_empty_kwargs_returns_empty_params(): + params = initialize_standard_callback_dynamic_params(None) + assert dict(params) == {} + + params = initialize_standard_callback_dynamic_params({}) + assert dict(params) == {}