Skip to content

fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys - #29590

Merged
yuneng-berri merged 4 commits into
BerriAI:litellm_yj_june10from
hdaoud23:fix/datadog-cost-mgmt-pass-callback-specific-params
Jun 10, 2026
Merged

fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys#29590
yuneng-berri merged 4 commits into
BerriAI:litellm_yj_june10from
hdaoud23:fix/datadog-cost-mgmt-pass-callback-specific-params

Conversation

@hdaoud23

@hdaoud23 hdaoud23 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

What

DatadogCostManagementLogger is always constructed with cost_tag_keys=[], so the opt-in FinOps cost_tag_keys allowlist added in #28487 never reaches Datadog Cost Management — even when correctly configured under callback_settings.datadog_cost_management.cost_tag_keys.

Root cause

#28487 wired the consumption side but not the production side:

  • In litellm/proxy/common_utils/callback_utils.py, initialize_callbacks_on_proxy() correctly reads init_params from callback_specific_params["datadog_cost_management"] and passes them to DatadogCostManagementLogger(**init_params).
  • But its only caller in litellm/proxy/proxy_server.py (load_config(), the elif key == "callbacks": branch) calls initialize_callbacks_on_proxy(...) without the callback_specific_params argument — so it falls back to its default {}, and cost_tag_keys is dropped.

The callback_settings block is already loaded into a local variable a few lines earlier in load_config() (callback_settings = config.get("callback_settings", {})), it just isn't forwarded.

Reserved/canonical tags (env, service, provider, model, …) still appear because _extract_tags emits those unconditionally; only the allowlist-gated cost_tag_keys path is starved.

Fix

Pass the already-in-scope callback_settings local to the initializer:

elif key == "callbacks":
    initialize_callbacks_on_proxy(
        value=value,
        premium_user=premium_user,
        config_file_path=config_file_path,
        litellm_settings=litellm_settings,
        callback_specific_params=callback_settings,   # <-- added
    )

Verification

Tested against v1.88.0-rc.1: with this change, a key whose metadata.tags carry capability:*, platform:*, ai_product:*, etc. produce cost entries in Datadog Cost Management with those tags present. Without it, cost_tag_keys is [] and only reserved tags are emitted.

@CLAassistant

CLAassistant commented Jun 3, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where DatadogCostManagementLogger always initialized with cost_tag_keys=[], silently ignoring any cost_tag_keys configured under callback_settings.datadog_cost_management. The root cause was that load_config() called initialize_callbacks_on_proxy() without forwarding the already-loaded callback_settings variable.

  • proxy_server.py: Passes callback_settings as callback_specific_params to initialize_callbacks_on_proxy, closing the gap between config loading and callback initialization.
  • callback_utils.py: Adds an isinstance(dict) guard to the lakera_prompt_injection branch (matching the pattern already used by datadog_cost_management) to prevent a TypeError when callback_settings now flows through and a non-dict value is present under that key.
  • Tests: Two focused regression tests are added — one asserting the forwarded argument at the load_config call site, one covering the non-dict guard for the lakera branch.

Confidence Score: 5/5

Safe to merge — the change is a one-line forwarding fix with defensive guards and targeted regression tests.

The fix is minimal and surgical: it forwards a dict that already exists in scope at the call site, defaults to {} when absent (preserving previous behaviour for configs without callback_settings), and the new isinstance(dict) guard on the lakera branch prevents a crash that the forwarding would otherwise expose. Both regression tests exercise exactly the paths that were broken.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/proxy_server.py Passes the already-loaded callback_settings dict as callback_specific_params to initialize_callbacks_on_proxy, fixing the root-cause drop of cost_tag_keys
litellm/proxy/common_utils/callback_utils.py Adds isinstance(dict) guard to the lakera_prompt_injection branch, preventing a TypeError when a non-dict value is present under that key in callback_settings
tests/test_litellm/proxy/proxy_server/test_proxy_config.py New regression test that asserts initialize_callbacks_on_proxy receives callback_specific_params with the full callback_settings block from the YAML config
tests/test_litellm/proxy/common_utils/test_callback_utils.py New test covering the non-dict guard on the lakera_prompt_injection branch; uses module-level monkeypatching cleanly

Reviews (2): Last reviewed commit: "test: inject fake lakera_ai module to av..." | Re-trigger Greptile

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@hdaoud23

hdaoud23 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Good call — added a regression test in tests/test_litellm/proxy/proxy_server/test_proxy_config.py:

test_ProxyConfig_load_config_forwards_callback_specific_params loads a config with callback_settings.datadog_cost_management.cost_tag_keys + litellm_settings.callbacks: ["datadog_cost_management"], stubs initialize_callbacks_on_proxy, runs the real ProxyConfig.load_config, and asserts it was invoked with a non-empty callback_specific_params equal to the loaded callback_settings.

Verified it's a genuine guard:

  • with this PR's fix → callback_specific_params = {"datadog_cost_management": {"cost_tag_keys": [...]}} → passes
  • without the fix → callback_specific_params is None → fails

So a future refactor that drops the argument at the call site would be caught.

@michelligabriele

michelligabriele commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

LGTM

@yuneng-berri yuneng-berri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks another integration we already have:

TypeError: litellm.proxy.guardrails.guardrail_hooks.lakera_ai.lakeraAI_Moderation() argument after ** must be a mapping, not str

Config:

litellm_settings:
callbacks: ["lakera_prompt_injection"]
callback_settings:
lakera_prompt_injection: "any-string"

…ainst non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hdaoud23

hdaoud23 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

@yuneng-berri thanks for catching this — you're right, and I've pushed a fix (bab6a8f).

Root cause: the lakera_prompt_injection branch in initialize_callbacks_on_proxy reads callback_specific_params["lakera_prompt_injection"] and does lakeraAI_Moderation(**init_params) with no type guard — unlike the presidio and datadog_cost_management branches, which both guard with isinstance(..., dict). Before this PR callback_specific_params was always {}, so the lakera path was dormant; forwarding the real callback_settings exposes it, so callback_settings: {lakera_prompt_injection: "any-string"} becomes **"any-string"TypeError: argument after ** must be a mapping, not str.

Fix: guard the lakera branch with isinstance(dict), identical in shape to the existing datadog/presidio branches — a non-dict value now falls back to {} instead of being unpacked.

Verified against your exact repro:

  • before the guard → TypeError: … argument after ** must be a mapping, not str
  • after the guard → no error, init_params = {} (string ignored); a dict value is still passed through unchanged

Added a regression test (test_initialize_callbacks_on_proxy_lakera_ignores_non_dict_callback_settings) asserting initialize_callbacks_on_proxy ignores a non-dict lakera_prompt_injection value instead of crashing.

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hdaoud23
hdaoud23 requested a review from yuneng-berri June 8, 2026 18:15
@hdaoud23 hdaoud23 closed this Jun 9, 2026
@hdaoud23 hdaoud23 reopened this Jun 9, 2026
@yuneng-berri
yuneng-berri changed the base branch from litellm_internal_staging to litellm_yj_june10 June 10, 2026 18:01
@yuneng-berri
yuneng-berri merged commit 29db81c into BerriAI:litellm_yj_june10 Jun 10, 2026
139 of 140 checks passed
yuneng-berri added a commit that referenced this pull request Jun 10, 2026
…dict callback_settings

#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. #29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard
yuneng-berri added a commit that referenced this pull request Jun 10, 2026
…dict callback_settings

#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. #29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard
yuneng-berri added a commit that referenced this pull request Jun 10, 2026
…dict callback_settings (#30153)

#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. #29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard
yuneng-berri added a commit that referenced this pull request Jun 10, 2026
…d guard consumers against non-dict values (#30161)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys (#29590)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys

* test(proxy): regression test that load_config forwards callback_specific_params

* fix(proxy): guard lakera_prompt_injection callback_specific_params against non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: inject fake lakera_ai module to avoid importing the real one

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(callbacks): guard compression/websearch interceptors against non-dict callback_settings (#30153)

#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. #29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard

* fix(callbacks): normalize non-dict callback_specific_params to empty dict

A blank callback_settings: key in YAML loads as None, and
config.get('callback_settings', {}) returns None because dict.get only
falls back to the default when the key is absent. Forwarding that value
verbatim to initialize_callbacks_on_proxy made the first
'<name>' in callback_specific_params membership test raise
TypeError: argument of type 'NoneType' is not iterable, aborting proxy
startup. Same failure for any non-dict root such as callback_settings: true.

Normalize the value at the function boundary so both callsites (and any
future ones) initialize callbacks with their defaults instead of crashing.

---------

Co-authored-by: Hedi Daoud <150018939+hdaoud23@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
michaelxer pushed a commit to michaelxer/litellm that referenced this pull request Jun 17, 2026
…d guard consumers against non-dict values (BerriAI#30161)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys (BerriAI#29590)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys

* test(proxy): regression test that load_config forwards callback_specific_params

* fix(proxy): guard lakera_prompt_injection callback_specific_params against non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: inject fake lakera_ai module to avoid importing the real one

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(callbacks): guard compression/websearch interceptors against non-dict callback_settings (BerriAI#30153)

BerriAI#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. BerriAI#29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard

* fix(callbacks): normalize non-dict callback_specific_params to empty dict

A blank callback_settings: key in YAML loads as None, and
config.get('callback_settings', {}) returns None because dict.get only
falls back to the default when the key is absent. Forwarding that value
verbatim to initialize_callbacks_on_proxy made the first
'<name>' in callback_specific_params membership test raise
TypeError: argument of type 'NoneType' is not iterable, aborting proxy
startup. Same failure for any non-dict root such as callback_settings: true.

Normalize the value at the function boundary so both callsites (and any
future ones) initialize callbacks with their defaults instead of crashing.

---------

Co-authored-by: Hedi Daoud <150018939+hdaoud23@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
michaelxer pushed a commit to michaelxer/litellm that referenced this pull request Jun 17, 2026
…d guard consumers against non-dict values (BerriAI#30161)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys (BerriAI#29590)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys

* test(proxy): regression test that load_config forwards callback_specific_params

* fix(proxy): guard lakera_prompt_injection callback_specific_params against non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: inject fake lakera_ai module to avoid importing the real one

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(callbacks): guard compression/websearch interceptors against non-dict callback_settings (BerriAI#30153)

BerriAI#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. BerriAI#29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard

* fix(callbacks): normalize non-dict callback_specific_params to empty dict

A blank callback_settings: key in YAML loads as None, and
config.get('callback_settings', {}) returns None because dict.get only
falls back to the default when the key is absent. Forwarding that value
verbatim to initialize_callbacks_on_proxy made the first
'<name>' in callback_specific_params membership test raise
TypeError: argument of type 'NoneType' is not iterable, aborting proxy
startup. Same failure for any non-dict root such as callback_settings: true.

Normalize the value at the function boundary so both callsites (and any
future ones) initialize callbacks with their defaults instead of crashing.

---------

Co-authored-by: Hedi Daoud <150018939+hdaoud23@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
koladefaj pushed a commit to koladefaj/litellm that referenced this pull request Jun 17, 2026
…d guard consumers against non-dict values (BerriAI#30161)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys (BerriAI#29590)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys

* test(proxy): regression test that load_config forwards callback_specific_params

* fix(proxy): guard lakera_prompt_injection callback_specific_params against non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: inject fake lakera_ai module to avoid importing the real one

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(callbacks): guard compression/websearch interceptors against non-dict callback_settings (BerriAI#30153)

BerriAI#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. BerriAI#29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard

* fix(callbacks): normalize non-dict callback_specific_params to empty dict

A blank callback_settings: key in YAML loads as None, and
config.get('callback_settings', {}) returns None because dict.get only
falls back to the default when the key is absent. Forwarding that value
verbatim to initialize_callbacks_on_proxy made the first
'<name>' in callback_specific_params membership test raise
TypeError: argument of type 'NoneType' is not iterable, aborting proxy
startup. Same failure for any non-dict root such as callback_settings: true.

Normalize the value at the function boundary so both callsites (and any
future ones) initialize callbacks with their defaults instead of crashing.

---------

Co-authored-by: Hedi Daoud <150018939+hdaoud23@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
factnn pushed a commit to factnn/litellm that referenced this pull request Jun 18, 2026
…d guard consumers against non-dict values (BerriAI#30161)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys (BerriAI#29590)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys

* test(proxy): regression test that load_config forwards callback_specific_params

* fix(proxy): guard lakera_prompt_injection callback_specific_params against non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: inject fake lakera_ai module to avoid importing the real one

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(callbacks): guard compression/websearch interceptors against non-dict callback_settings (BerriAI#30153)

BerriAI#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. BerriAI#29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard

* fix(callbacks): normalize non-dict callback_specific_params to empty dict

A blank callback_settings: key in YAML loads as None, and
config.get('callback_settings', {}) returns None because dict.get only
falls back to the default when the key is absent. Forwarding that value
verbatim to initialize_callbacks_on_proxy made the first
'<name>' in callback_specific_params membership test raise
TypeError: argument of type 'NoneType' is not iterable, aborting proxy
startup. Same failure for any non-dict root such as callback_settings: true.

Normalize the value at the function boundary so both callsites (and any
future ones) initialize callbacks with their defaults instead of crashing.

---------

Co-authored-by: Hedi Daoud <150018939+hdaoud23@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…d guard consumers against non-dict values (BerriAI#30161)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys (BerriAI#29590)

* fix(datadog): pass callback_specific_params so DatadogCostManagementLogger receives cost_tag_keys

* test(proxy): regression test that load_config forwards callback_specific_params

* fix(proxy): guard lakera_prompt_injection callback_specific_params against non-dict

Addresses review feedback: forwarding callback_settings as callback_specific_params
(so DatadogCostManagementLogger receives cost_tag_keys) exposed the
lakera_prompt_injection branch, which did lakeraAI_Moderation(**callback_specific_params
["lakera_prompt_injection"]) with no type guard. A config like
`callback_settings: {lakera_prompt_injection: "any-string"}` then hit `**"any-string"`
-> TypeError: argument after ** must be a mapping, not str.

Guard the lakera branch with isinstance(dict), matching the existing presidio and
datadog_cost_management branches (non-dict values fall back to {}). Add a regression
test asserting initialize_callbacks_on_proxy ignores a non-dict value instead of crashing.


* test: inject fake lakera_ai module to avoid importing the real one

CI fix for the lakera regression test: it stubbed litellm.proxy.proxy_server with
a SimpleNamespace and then monkeypatch.setattr'd the real lakera_ai module, which
forces importing it — and lakera_ai does `from litellm.proxy.proxy_server import
LiteLLM_TeamTable`, absent on the stub -> ImportError under proxy-infra tests.

Inject a fake lakera_ai module into sys.modules instead, so the callbacks branch's
`from ...lakera_ai import lakeraAI_Moderation` resolves to the stub without loading
the real module. The guard under test (isinstance(dict) in the lakera branch) is
unchanged.


---------


* fix(callbacks): guard compression/websearch interceptors against non-dict callback_settings (BerriAI#30153)

BerriAI#29590 forwards the full callback_settings dict into initialize_callbacks_on_proxy, which activates the compression_interception and websearch_interception consumers. Their initialize_from_proxy_config read the callback_settings subkey without an isinstance(dict) guard, so a non-dict value such as `compression_interception: true` reached from_config_yaml(...).get(...) and aborted proxy startup with AttributeError. BerriAI#29590 added that guard for lakera_prompt_injection but not for these two

Mirror the isinstance(dict) guard already used by the lakera, presidio, and datadog branches so a non-dict value is ignored and the callback initializes with defaults. A parametrized test feeds every callback_settings consumer a non-dict value through initialize_callbacks_on_proxy to catch a future consumer that forgets the guard

* fix(callbacks): normalize non-dict callback_specific_params to empty dict

A blank callback_settings: key in YAML loads as None, and
config.get('callback_settings', {}) returns None because dict.get only
falls back to the default when the key is absent. Forwarding that value
verbatim to initialize_callbacks_on_proxy made the first
'<name>' in callback_specific_params membership test raise
TypeError: argument of type 'NoneType' is not iterable, aborting proxy
startup. Same failure for any non-dict root such as callback_settings: true.

Normalize the value at the function boundary so both callsites (and any
future ones) initialize callbacks with their defaults instead of crashing.

---------

Co-authored-by: Hedi Daoud <150018939+hdaoud23@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants