diff --git a/litellm/integrations/callback_configs.json b/litellm/integrations/callback_configs.json index 7d452d9ef01..88f7908e9a2 100644 --- a/litellm/integrations/callback_configs.json +++ b/litellm/integrations/callback_configs.json @@ -42,21 +42,21 @@ "description": "Braintrust Logging Integration" }, { - "id": "custom_callback_api", + "id": "generic_api", "displayName": "Custom Callback API", "logo": "custom.svg", "supports_key_team_logging": true, "dynamic_params": { - "custom_callback_api_url": { + "GENERIC_LOGGER_ENDPOINT": { "type": "text", "ui_name": "Callback URL", "description": "Your custom webhook/API endpoint URL to receive logs", "required": true }, - "custom_callback_api_headers": { + "GENERIC_LOGGER_HEADERS": { "type": "text", - "ui_name": "Headers (JSON)", - "description": "Custom HTTP headers as JSON string (e.g., {\"Authorization\": \"Bearer token\"})", + "ui_name": "Headers", + "description": "Custom HTTP headers as a comma-separated string (e.g., Authorization: Bearer token, Content-Type: application/json)", "required": false } }, diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 2bccd7ed8ed..083ac07340a 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2577,7 +2577,13 @@ class AllCallbacks(LiteLLMPydanticObjectBase): custom_callback_api: CallbackOnUI = CallbackOnUI( litellm_callback_name="custom_callback_api", - litellm_callback_params=["GENERIC_LOGGER_ENDPOINT"], + litellm_callback_params=["GENERIC_LOGGER_ENDPOINT", "GENERIC_LOGGER_HEADERS"], + ui_callback_name="Custom Callback API", + ) + + generic_api: CallbackOnUI = CallbackOnUI( + litellm_callback_name="generic_api", + litellm_callback_params=["GENERIC_LOGGER_ENDPOINT", "GENERIC_LOGGER_HEADERS"], ui_callback_name="Custom Callback API", ) diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index 86125b3b519..90f82580f1e 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -6,9 +6,6 @@ from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import CommonProxyErrors, LiteLLMPromptInjectionParams from litellm.proxy.types_utils.utils import get_instance_fn -from litellm.proxy.common_utils.encrypt_decrypt_utils import ( - decrypt_value_helper, -) from litellm.types.utils import ( StandardLoggingGuardrailInformation, StandardLoggingPayload, @@ -436,11 +433,7 @@ def process_callback(_callback: str, callback_type: str, environment_variables: if env_variable is None: env_vars_dict[_var] = None else: - # decode + decrypt the value - decrypted_value = decrypt_value_helper( - value=env_variable, key=_var - ) - env_vars_dict[_var] = decrypted_value + env_vars_dict[_var] = env_variable return { "name": _callback, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index abe51914eb2..753255337ac 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9478,7 +9478,7 @@ async def get_config(): # noqa: PLR0915 _litellm_settings = config_data.get("litellm_settings", {}) _general_settings = config_data.get("general_settings", {}) environment_variables = config_data.get("environment_variables", {}) - + _success_callbacks = _litellm_settings.get("success_callback", []) _failure_callbacks = _litellm_settings.get("failure_callback", []) _success_and_failure_callbacks = _litellm_settings.get("callbacks", []) diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index b55259afee4..d7e338d657b 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -2497,9 +2497,6 @@ async def test_get_config_callbacks_with_all_types(client_no_auth): with patch.object( proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data) - ), patch( - "litellm.proxy.common_utils.callback_utils.decrypt_value_helper", - side_effect=lambda value, key=None: value ): response = client_no_auth.get("/get/config/callbacks") @@ -2549,7 +2546,7 @@ async def test_get_config_callbacks_with_all_types(client_no_auth): async def test_get_config_callbacks_environment_variables(client_no_auth): """ Test that /get/config/callbacks correctly includes environment variables - for each callback type with proper decryption. + for each callback type. Values are returned as-is from the config (no decryption). """ from litellm.proxy.proxy_server import ProxyConfig @@ -2561,8 +2558,8 @@ async def test_get_config_callbacks_environment_variables(client_no_auth): "callbacks": ["otel"] }, "environment_variables": { - "LANGFUSE_PUBLIC_KEY": "encrypted-public-key", - "LANGFUSE_SECRET_KEY": "encrypted-secret-key", + "LANGFUSE_PUBLIC_KEY": "test-public-key", + "LANGFUSE_SECRET_KEY": "test-secret-key", "LANGFUSE_HOST": "https://cloud.langfuse.com", "OTEL_EXPORTER": "otlp", "OTEL_ENDPOINT": "http://localhost:4317", @@ -2571,19 +2568,10 @@ async def test_get_config_callbacks_environment_variables(client_no_auth): "general_settings": {} } - # Mock decrypt to prepend "decrypted-" to values - def mock_decrypt(value, key=None): - if value and isinstance(value, str) and "encrypted" in value: - return f"decrypted-{value}" - return value - proxy_config = getattr(litellm.proxy.proxy_server, "proxy_config") with patch.object( proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data) - ), patch( - "litellm.proxy.common_utils.callback_utils.decrypt_value_helper", - side_effect=mock_decrypt ): response = client_no_auth.get("/get/config/callbacks") @@ -2600,12 +2588,12 @@ async def test_get_config_callbacks_environment_variables(client_no_auth): assert langfuse_callback["type"] == "success" assert "variables" in langfuse_callback - # Verify langfuse env vars are present and decrypted + # Verify langfuse env vars are present (values returned as-is, no decryption) langfuse_vars = langfuse_callback["variables"] assert "LANGFUSE_PUBLIC_KEY" in langfuse_vars - assert langfuse_vars["LANGFUSE_PUBLIC_KEY"] == "decrypted-encrypted-public-key" + assert langfuse_vars["LANGFUSE_PUBLIC_KEY"] == "test-public-key" assert "LANGFUSE_SECRET_KEY" in langfuse_vars - assert langfuse_vars["LANGFUSE_SECRET_KEY"] == "decrypted-encrypted-secret-key" + assert langfuse_vars["LANGFUSE_SECRET_KEY"] == "test-secret-key" assert "LANGFUSE_HOST" in langfuse_vars assert langfuse_vars["LANGFUSE_HOST"] == "https://cloud.langfuse.com" diff --git a/tests/test_litellm/proxy/common_utils/test_callback_utils.py b/tests/test_litellm/proxy/common_utils/test_callback_utils.py index d51437fc844..985e8d20be7 100644 --- a/tests/test_litellm/proxy/common_utils/test_callback_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_callback_utils.py @@ -37,13 +37,9 @@ def test_get_remaining_tokens_and_requests_from_request_data(): "litellm.proxy.common_utils.callback_utils.CustomLogger.get_callback_env_vars", return_value=["API_KEY", "MISSING_VAR"], ) -@patch( - "litellm.proxy.common_utils.callback_utils.decrypt_value_helper", - side_effect=lambda value, key: f"decrypted-{key}", -) -def test_process_callback_with_env_vars(mock_decrypt, mock_get_env_vars): +def test_process_callback_with_env_vars(mock_get_env_vars): environment_variables = { - "API_KEY": "ENC_VALUE", + "API_KEY": "PLAIN_VALUE", "UNUSED": "SHOULD_BE_IGNORED", } @@ -56,7 +52,7 @@ def test_process_callback_with_env_vars(mock_decrypt, mock_get_env_vars): assert result["name"] == "my_callback" assert result["type"] == "input" assert result["variables"] == { - "API_KEY": "decrypted-API_KEY", + "API_KEY": "PLAIN_VALUE", "MISSING_VAR": None, } diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index a3b9b90719f..179c794555c 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -247,6 +247,53 @@ def test_update_config_fields_deep_merge_db_wins(): assert rs["routing_mode"] == "cost_optimized" +def test_get_config_custom_callback_api_env_vars(monkeypatch): + """ + Ensure /get/config/callbacks returns custom callback env vars when both custom values are provided. + """ + from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth + + # Mock config with custom_callback_api enabled and generic logger env vars present + config_data = { + "litellm_settings": {"success_callback": ["custom_callback_api"]}, + "general_settings": {}, + "environment_variables": { + "GENERIC_LOGGER_ENDPOINT": "https://callback.example.com", + "GENERIC_LOGGER_HEADERS": "Auth: token", + }, + } + + # Mock proxy_config.get_config and router settings + mock_router = MagicMock() + mock_router.get_settings.return_value = {} + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router) + monkeypatch.setattr( + proxy_config, "get_config", AsyncMock(return_value=config_data) + ) + + # Bypass auth dependency + original_overrides = app.dependency_overrides.copy() + app.dependency_overrides[user_api_key_auth] = lambda: MagicMock() + + client = TestClient(app) + try: + response = client.get("/get/config/callbacks") + finally: + app.dependency_overrides = original_overrides + + assert response.status_code == 200 + callbacks = response.json()["callbacks"] + custom_cb = next( + (cb for cb in callbacks if cb["name"] == "custom_callback_api"), None + ) + + assert custom_cb is not None + assert custom_cb["variables"] == { + "GENERIC_LOGGER_ENDPOINT": "https://callback.example.com", + "GENERIC_LOGGER_HEADERS": "Auth: token", + } + + # Mock Prisma class MockPrisma: def __init__(self, database_url=None, proxy_logging_obj=None, http_client=None):