Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion litellm/proxy/common_utils/callback_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ def initialize_callbacks_on_proxy( # noqa: PLR0915
)

init_params = {}
if "lakera_prompt_injection" in callback_specific_params:
if (
"lakera_prompt_injection" in callback_specific_params
and isinstance(
callback_specific_params["lakera_prompt_injection"], dict
)
):
init_params = callback_specific_params["lakera_prompt_injection"]
lakera_moderations_object = lakeraAI_Moderation(**init_params)
imported_list.append(lakera_moderations_object)
Expand Down
1 change: 1 addition & 0 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4068,6 +4068,7 @@ async def load_config( # noqa: PLR0915
premium_user=premium_user,
config_file_path=config_file_path,
litellm_settings=litellm_settings,
callback_specific_params=callback_settings,
)

elif key == "model_group_settings":
Expand Down
58 changes: 57 additions & 1 deletion tests/test_litellm/proxy/common_utils/test_callback_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy
import sys
import os
from types import SimpleNamespace
from types import ModuleType, SimpleNamespace

sys.path.insert(
0, os.path.abspath("../../..")
Expand Down Expand Up @@ -309,3 +309,59 @@ def test_encrypt_callback_vars_only_encrypts_credential_fields(monkeypatch):
assert cv["langfuse_host"] == "https://cloud.langfuse.com"
assert cv["langsmith_project"] == "my-proj"
assert cv["langsmith_base_url"] == "https://smith.example"


def test_initialize_callbacks_on_proxy_lakera_ignores_non_dict_callback_settings(
monkeypatch,
):
"""Regression: a non-dict value under callback_settings.lakera_prompt_injection
must not crash initialize_callbacks_on_proxy.

Forwarding callback_settings as callback_specific_params (so callbacks like
DatadogCostManagementLogger receive their init params) exposes the lakera
branch, which previously did lakeraAI_Moderation(**callback_specific_params[
"lakera_prompt_injection"]) with no isinstance(dict) guard. For a config like
{"lakera_prompt_injection": "x"} that is `**"x"` -> TypeError: argument after
** must be a mapping, not str. The branch now guards on isinstance(dict),
matching the presidio / datadog_cost_management branches.
"""
captured = {}

class _DummyLakera:
def __init__(self, **kwargs):
captured["kwargs"] = kwargs

# Inject a fake lakera_ai module so the branch's
# `from ...lakera_ai import lakeraAI_Moderation` resolves to our stub without
# importing the real module (which imports proxy_server symbols not present
# under the stubbed proxy_server below).
fake_lakera = ModuleType("litellm.proxy.guardrails.guardrail_hooks.lakera_ai")
fake_lakera.lakeraAI_Moderation = _DummyLakera
monkeypatch.setitem(
sys.modules,
"litellm.proxy.guardrails.guardrail_hooks.lakera_ai",
fake_lakera,
)
monkeypatch.setitem(
sys.modules,
"litellm.proxy.proxy_server",
SimpleNamespace(prisma_client=None),
)

original_callbacks = (
list(litellm.callbacks) if isinstance(litellm.callbacks, list) else []
)
litellm.callbacks = []
try:
# A non-dict value must be ignored (init_params stays {}), not **-unpacked.
initialize_callbacks_on_proxy(
value=["lakera_prompt_injection"],
premium_user=False,
config_file_path=".",
litellm_settings={},
callback_specific_params={"lakera_prompt_injection": "any-string"},
)
assert captured["kwargs"] == {}
assert any(isinstance(c, _DummyLakera) for c in litellm.callbacks)
finally:
litellm.callbacks = original_callbacks
50 changes: 50 additions & 0 deletions tests/test_litellm/proxy/proxy_server/test_proxy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,56 @@ async def test_ProxyConfig_load_config_missing_file_raises(monkeypatch):
await pc.load_config(router=None, config_file_path="/no/file.yaml")


@pytest.mark.asyncio
async def test_ProxyConfig_load_config_forwards_callback_specific_params(
tmp_path, monkeypatch
):
"""Regression: callback_settings from config must be forwarded to
initialize_callbacks_on_proxy as callback_specific_params.

Callbacks like DatadogCostManagementLogger read their init params (e.g.
cost_tag_keys) from callback_specific_params[<callback_name>]. If the
argument is dropped at the call site, they silently initialize with empty
params and the configured allowlist never takes effect.
"""
f = tmp_path / "c.yaml"
f.write_text(
"model_list: []\n"
"general_settings: {}\n"
"callback_settings:\n"
" datadog_cost_management:\n"
" cost_tag_keys:\n"
" - capability\n"
" - platform\n"
" - ai_product\n"
'litellm_settings:\n'
' callbacks: ["datadog_cost_management"]\n'
)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)

captured = {}

def _fake_initialize_callbacks_on_proxy(**kwargs):
captured.update(kwargs)

monkeypatch.setattr(
"litellm.proxy.proxy_server.initialize_callbacks_on_proxy",
_fake_initialize_callbacks_on_proxy,
)

pc = ProxyConfig()
await pc.load_config(router=None, config_file_path=str(f))

# The callbacks branch must forward the loaded callback_settings.
assert captured.get("callback_specific_params") == {
"datadog_cost_management": {
"cost_tag_keys": ["capability", "platform", "ai_product"]
}
}


# ---------------------------------------------------------------------------
# ProxyConfig._init_non_llm_configs
# ---------------------------------------------------------------------------
Expand Down
Loading