mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
Merge pull request #17522 from BerriAI/litellm_custom_webhook_fix
[Fix] Custom Callback on UI
This commit is contained in:
commit
e2e35c3f87
7 changed files with 70 additions and 40 deletions
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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", [])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue