From a96677c2990193eaad58b5497b337e61b187c449 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 4 Dec 2025 21:48:49 -0800 Subject: [PATCH 1/5] Fix /get/config/callbacks callback variables --- litellm/proxy/proxy_server.py | 20 +++++--- tests/test_litellm/proxy/test_proxy_server.py | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index e1d5a90dc79..fd576ee7c02 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9477,7 +9477,19 @@ async def get_config(): # noqa: PLR0915 elif _callback == "traceloop": env_vars = ["TRACELOOP_API_KEY"] elif _callback == "custom_callback_api": - env_vars = ["GENERIC_LOGGER_ENDPOINT"] + custom_callback_url = environment_variables.get( + "custom_callback_api_url" + ) + custom_callback_headers = environment_variables.get( + "custom_callback_api_headers" + ) + if custom_callback_url is not None and custom_callback_headers is not None: + env_vars = [ + "custom_callback_api_url", + "custom_callback_api_headers", + ] + else: + env_vars = ["GENERIC_LOGGER_ENDPOINT", "GENERIC_LOGGER_HEADER"] elif _callback == "otel": env_vars = ["OTEL_EXPORTER", "OTEL_ENDPOINT", "OTEL_HEADERS"] elif _callback == "langsmith": @@ -9495,11 +9507,7 @@ async def get_config(): # noqa: PLR0915 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 _data_to_return.append({"name": _callback, "variables": env_vars_dict}) elif _callback == "langfuse": diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 7b3157f8ee1..e75b87e1c87 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -246,6 +246,54 @@ 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 custom env vars present + config_data = { + "litellm_settings": {"success_callback": ["custom_callback_api"]}, + "general_settings": {}, + "environment_variables": { + "custom_callback_api_url": "https://callback.example.com", + "custom_callback_api_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"] == { + "custom_callback_api_url": "https://callback.example.com", + "custom_callback_api_headers": "Auth: token", + } + assert "GENERIC_LOGGER_ENDPOINT" not in custom_cb["variables"] + + # Mock Prisma class MockPrisma: def __init__(self, database_url=None, proxy_logging_obj=None, http_client=None): From 0dd4db34bd12ca3764e7eca47dfc0326e34b935c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 5 Dec 2025 14:37:48 -0800 Subject: [PATCH 2/5] Working setting generic callbacks on UI --- litellm/integrations/callback_configs.json | 10 +++++----- litellm/proxy/_types.py | 2 +- litellm/proxy/common_utils/callback_utils.py | 9 +-------- litellm/proxy/proxy_server.py | 2 +- .../proxy/common_utils/test_callback_utils.py | 10 +++------- tests/test_litellm/proxy/test_proxy_server.py | 11 +++++------ 6 files changed, 16 insertions(+), 28 deletions(-) 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 9edc93bfffa..964c6c4e404 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2577,7 +2577,7 @@ 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_HEADER"], 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 9e88bccb73e..4beec52c074 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, @@ -434,11 +431,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 c1a5fd6c924..13adda2f1b1 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9476,7 +9476,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/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 e75b87e1c87..6be6f1e3d01 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -252,13 +252,13 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch): """ from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth - # Mock config with custom_callback_api enabled and custom env vars present + # 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": { - "custom_callback_api_url": "https://callback.example.com", - "custom_callback_api_headers": "Auth: token", + "GENERIC_LOGGER_ENDPOINT": "https://callback.example.com", + "GENERIC_LOGGER_HEADER": "Auth: token", }, } @@ -288,10 +288,9 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch): assert custom_cb is not None assert custom_cb["variables"] == { - "custom_callback_api_url": "https://callback.example.com", - "custom_callback_api_headers": "Auth: token", + "GENERIC_LOGGER_ENDPOINT": "https://callback.example.com", + "GENERIC_LOGGER_HEADER": "Auth: token", } - assert "GENERIC_LOGGER_ENDPOINT" not in custom_cb["variables"] # Mock Prisma From 086e80ef837f6064a5a8c6ba75bc78be403bfa9b Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 5 Dec 2025 17:01:37 -0800 Subject: [PATCH 3/5] Define generic_api types --- litellm/proxy/_types.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 964c6c4e404..978585665f6 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", "GENERIC_LOGGER_HEADER"], + 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", ) From edf51a431a0b1a5d06df745aa3c6262db6a5fdb6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 5 Dec 2025 17:08:13 -0800 Subject: [PATCH 4/5] Fixed tests --- tests/test_litellm/proxy/test_proxy_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 6be6f1e3d01..706a96624d5 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -258,7 +258,7 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch): "general_settings": {}, "environment_variables": { "GENERIC_LOGGER_ENDPOINT": "https://callback.example.com", - "GENERIC_LOGGER_HEADER": "Auth: token", + "GENERIC_LOGGER_HEADERS": "Auth: token", }, } @@ -289,7 +289,7 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch): assert custom_cb is not None assert custom_cb["variables"] == { "GENERIC_LOGGER_ENDPOINT": "https://callback.example.com", - "GENERIC_LOGGER_HEADER": "Auth: token", + "GENERIC_LOGGER_HEADERS": "Auth: token", } From d3d005f9bf9ba8ed9bb1179b94b697fcee237550 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 6 Dec 2025 21:23:49 -0800 Subject: [PATCH 5/5] fixing tests --- tests/proxy_unit_tests/test_proxy_server.py | 24 ++++++--------------- 1 file changed, 6 insertions(+), 18 deletions(-) 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"