Skip to content

Commit e2e35c3

Browse files
authored
Merge pull request #17522 from BerriAI/litellm_custom_webhook_fix
[Fix] Custom Callback on UI
2 parents f9a4ba3 + d3d005f commit e2e35c3

File tree

7 files changed

+70
-40
lines changed

7 files changed

+70
-40
lines changed

litellm/integrations/callback_configs.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,21 @@
4242
"description": "Braintrust Logging Integration"
4343
},
4444
{
45-
"id": "custom_callback_api",
45+
"id": "generic_api",
4646
"displayName": "Custom Callback API",
4747
"logo": "custom.svg",
4848
"supports_key_team_logging": true,
4949
"dynamic_params": {
50-
"custom_callback_api_url": {
50+
"GENERIC_LOGGER_ENDPOINT": {
5151
"type": "text",
5252
"ui_name": "Callback URL",
5353
"description": "Your custom webhook/API endpoint URL to receive logs",
5454
"required": true
5555
},
56-
"custom_callback_api_headers": {
56+
"GENERIC_LOGGER_HEADERS": {
5757
"type": "text",
58-
"ui_name": "Headers (JSON)",
59-
"description": "Custom HTTP headers as JSON string (e.g., {\"Authorization\": \"Bearer token\"})",
58+
"ui_name": "Headers",
59+
"description": "Custom HTTP headers as a comma-separated string (e.g., Authorization: Bearer token, Content-Type: application/json)",
6060
"required": false
6161
}
6262
},

litellm/proxy/_types.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2577,7 +2577,13 @@ class AllCallbacks(LiteLLMPydanticObjectBase):
25772577

25782578
custom_callback_api: CallbackOnUI = CallbackOnUI(
25792579
litellm_callback_name="custom_callback_api",
2580-
litellm_callback_params=["GENERIC_LOGGER_ENDPOINT"],
2580+
litellm_callback_params=["GENERIC_LOGGER_ENDPOINT", "GENERIC_LOGGER_HEADERS"],
2581+
ui_callback_name="Custom Callback API",
2582+
)
2583+
2584+
generic_api: CallbackOnUI = CallbackOnUI(
2585+
litellm_callback_name="generic_api",
2586+
litellm_callback_params=["GENERIC_LOGGER_ENDPOINT", "GENERIC_LOGGER_HEADERS"],
25812587
ui_callback_name="Custom Callback API",
25822588
)
25832589

litellm/proxy/common_utils/callback_utils.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
from litellm.integrations.custom_logger import CustomLogger
77
from litellm.proxy._types import CommonProxyErrors, LiteLLMPromptInjectionParams
88
from litellm.proxy.types_utils.utils import get_instance_fn
9-
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
10-
decrypt_value_helper,
11-
)
129
from litellm.types.utils import (
1310
StandardLoggingGuardrailInformation,
1411
StandardLoggingPayload,
@@ -436,11 +433,7 @@ def process_callback(_callback: str, callback_type: str, environment_variables:
436433
if env_variable is None:
437434
env_vars_dict[_var] = None
438435
else:
439-
# decode + decrypt the value
440-
decrypted_value = decrypt_value_helper(
441-
value=env_variable, key=_var
442-
)
443-
env_vars_dict[_var] = decrypted_value
436+
env_vars_dict[_var] = env_variable
444437

445438
return {
446439
"name": _callback,

litellm/proxy/proxy_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9478,7 +9478,7 @@ async def get_config(): # noqa: PLR0915
94789478
_litellm_settings = config_data.get("litellm_settings", {})
94799479
_general_settings = config_data.get("general_settings", {})
94809480
environment_variables = config_data.get("environment_variables", {})
9481-
9481+
94829482
_success_callbacks = _litellm_settings.get("success_callback", [])
94839483
_failure_callbacks = _litellm_settings.get("failure_callback", [])
94849484
_success_and_failure_callbacks = _litellm_settings.get("callbacks", [])

tests/proxy_unit_tests/test_proxy_server.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2497,9 +2497,6 @@ async def test_get_config_callbacks_with_all_types(client_no_auth):
24972497

24982498
with patch.object(
24992499
proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data)
2500-
), patch(
2501-
"litellm.proxy.common_utils.callback_utils.decrypt_value_helper",
2502-
side_effect=lambda value, key=None: value
25032500
):
25042501
response = client_no_auth.get("/get/config/callbacks")
25052502

@@ -2549,7 +2546,7 @@ async def test_get_config_callbacks_with_all_types(client_no_auth):
25492546
async def test_get_config_callbacks_environment_variables(client_no_auth):
25502547
"""
25512548
Test that /get/config/callbacks correctly includes environment variables
2552-
for each callback type with proper decryption.
2549+
for each callback type. Values are returned as-is from the config (no decryption).
25532550
"""
25542551
from litellm.proxy.proxy_server import ProxyConfig
25552552

@@ -2561,8 +2558,8 @@ async def test_get_config_callbacks_environment_variables(client_no_auth):
25612558
"callbacks": ["otel"]
25622559
},
25632560
"environment_variables": {
2564-
"LANGFUSE_PUBLIC_KEY": "encrypted-public-key",
2565-
"LANGFUSE_SECRET_KEY": "encrypted-secret-key",
2561+
"LANGFUSE_PUBLIC_KEY": "test-public-key",
2562+
"LANGFUSE_SECRET_KEY": "test-secret-key",
25662563
"LANGFUSE_HOST": "https://cloud.langfuse.com",
25672564
"OTEL_EXPORTER": "otlp",
25682565
"OTEL_ENDPOINT": "http://localhost:4317",
@@ -2571,19 +2568,10 @@ async def test_get_config_callbacks_environment_variables(client_no_auth):
25712568
"general_settings": {}
25722569
}
25732570

2574-
# Mock decrypt to prepend "decrypted-" to values
2575-
def mock_decrypt(value, key=None):
2576-
if value and isinstance(value, str) and "encrypted" in value:
2577-
return f"decrypted-{value}"
2578-
return value
2579-
25802571
proxy_config = getattr(litellm.proxy.proxy_server, "proxy_config")
25812572

25822573
with patch.object(
25832574
proxy_config, "get_config", new=AsyncMock(return_value=mock_config_data)
2584-
), patch(
2585-
"litellm.proxy.common_utils.callback_utils.decrypt_value_helper",
2586-
side_effect=mock_decrypt
25872575
):
25882576
response = client_no_auth.get("/get/config/callbacks")
25892577

@@ -2600,12 +2588,12 @@ def mock_decrypt(value, key=None):
26002588
assert langfuse_callback["type"] == "success"
26012589
assert "variables" in langfuse_callback
26022590

2603-
# Verify langfuse env vars are present and decrypted
2591+
# Verify langfuse env vars are present (values returned as-is, no decryption)
26042592
langfuse_vars = langfuse_callback["variables"]
26052593
assert "LANGFUSE_PUBLIC_KEY" in langfuse_vars
2606-
assert langfuse_vars["LANGFUSE_PUBLIC_KEY"] == "decrypted-encrypted-public-key"
2594+
assert langfuse_vars["LANGFUSE_PUBLIC_KEY"] == "test-public-key"
26072595
assert "LANGFUSE_SECRET_KEY" in langfuse_vars
2608-
assert langfuse_vars["LANGFUSE_SECRET_KEY"] == "decrypted-encrypted-secret-key"
2596+
assert langfuse_vars["LANGFUSE_SECRET_KEY"] == "test-secret-key"
26092597
assert "LANGFUSE_HOST" in langfuse_vars
26102598
assert langfuse_vars["LANGFUSE_HOST"] == "https://cloud.langfuse.com"
26112599

tests/test_litellm/proxy/common_utils/test_callback_utils.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,9 @@ def test_get_remaining_tokens_and_requests_from_request_data():
3737
"litellm.proxy.common_utils.callback_utils.CustomLogger.get_callback_env_vars",
3838
return_value=["API_KEY", "MISSING_VAR"],
3939
)
40-
@patch(
41-
"litellm.proxy.common_utils.callback_utils.decrypt_value_helper",
42-
side_effect=lambda value, key: f"decrypted-{key}",
43-
)
44-
def test_process_callback_with_env_vars(mock_decrypt, mock_get_env_vars):
40+
def test_process_callback_with_env_vars(mock_get_env_vars):
4541
environment_variables = {
46-
"API_KEY": "ENC_VALUE",
42+
"API_KEY": "PLAIN_VALUE",
4743
"UNUSED": "SHOULD_BE_IGNORED",
4844
}
4945

@@ -56,7 +52,7 @@ def test_process_callback_with_env_vars(mock_decrypt, mock_get_env_vars):
5652
assert result["name"] == "my_callback"
5753
assert result["type"] == "input"
5854
assert result["variables"] == {
59-
"API_KEY": "decrypted-API_KEY",
55+
"API_KEY": "PLAIN_VALUE",
6056
"MISSING_VAR": None,
6157
}
6258

tests/test_litellm/proxy/test_proxy_server.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,53 @@ def test_update_config_fields_deep_merge_db_wins():
247247
assert rs["routing_mode"] == "cost_optimized"
248248

249249

250+
def test_get_config_custom_callback_api_env_vars(monkeypatch):
251+
"""
252+
Ensure /get/config/callbacks returns custom callback env vars when both custom values are provided.
253+
"""
254+
from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth
255+
256+
# Mock config with custom_callback_api enabled and generic logger env vars present
257+
config_data = {
258+
"litellm_settings": {"success_callback": ["custom_callback_api"]},
259+
"general_settings": {},
260+
"environment_variables": {
261+
"GENERIC_LOGGER_ENDPOINT": "https://callback.example.com",
262+
"GENERIC_LOGGER_HEADERS": "Auth: token",
263+
},
264+
}
265+
266+
# Mock proxy_config.get_config and router settings
267+
mock_router = MagicMock()
268+
mock_router.get_settings.return_value = {}
269+
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
270+
monkeypatch.setattr(
271+
proxy_config, "get_config", AsyncMock(return_value=config_data)
272+
)
273+
274+
# Bypass auth dependency
275+
original_overrides = app.dependency_overrides.copy()
276+
app.dependency_overrides[user_api_key_auth] = lambda: MagicMock()
277+
278+
client = TestClient(app)
279+
try:
280+
response = client.get("/get/config/callbacks")
281+
finally:
282+
app.dependency_overrides = original_overrides
283+
284+
assert response.status_code == 200
285+
callbacks = response.json()["callbacks"]
286+
custom_cb = next(
287+
(cb for cb in callbacks if cb["name"] == "custom_callback_api"), None
288+
)
289+
290+
assert custom_cb is not None
291+
assert custom_cb["variables"] == {
292+
"GENERIC_LOGGER_ENDPOINT": "https://callback.example.com",
293+
"GENERIC_LOGGER_HEADERS": "Auth: token",
294+
}
295+
296+
250297
# Mock Prisma
251298
class MockPrisma:
252299
def __init__(self, database_url=None, proxy_logging_obj=None, http_client=None):

0 commit comments

Comments
 (0)