From 6e0b420b0f4e1a78f1612bb2fc9dfb9fda32abb1 Mon Sep 17 00:00:00 2001 From: Yucheng Date: Mon, 31 Aug 2026 13:18:37 -0700 Subject: [PATCH] fix: guard non-list config callbacks in get_config, use monkeypatch in tests - Line-concat type error: normalize_callback now returns empty list for non-list types (dict/tuple/set) instead of passing through unchanged; prevents TypeError when config values are non-list - Test quality TQ005: replace manual try/finally save-restore of litellm.callbacks with monkeypatch.setattr in test_get_config_callbacks_appends_runtime_only_callbacks and test_get_config_callbacks_redacts_runtime_only_row_secrets_for_view_only_admin - Ruff format: wrap _internal_callback_prefixes tuple and isinstance check across multiple lines to respect 120-char limit - All three new tests pass --- litellm/proxy/proxy_server.py | 19 +++- .../proxy/proxy_server/test_routes_config.py | 90 +++++++++---------- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 34097ef6d44..b2e70cf66a8 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -17043,7 +17043,11 @@ async def get_config( return [callback] elif callback is None: return [] - return callback + elif isinstance(callback, list): + return callback + # Convert dict, tuple, set, or any other type to empty list + # (config validation should prevent non-list types, but guard here) + return [] _success_callbacks = normalize_callback(_success_callbacks) _failure_callbacks = normalize_callback(_failure_callbacks) @@ -17098,10 +17102,19 @@ async def get_config( # Append runtime-only rows (those not in config). # Filter out internal proxy hooks (names starting with _PROXY or known internal names). - _internal_callback_prefixes: Final[tuple] = ("_PROXY", "_Async", "ShadowEval", "ServiceLogging", "SkillsInjection", "ResponsesID") + _internal_callback_prefixes: Final[tuple] = ( + "_PROXY", + "_Async", + "ShadowEval", + "ServiceLogging", + "SkillsInjection", + "ResponsesID", + ) for _runtime_cb_name, _runtime_cb_type in _runtime_items: # Skip internal proxy callbacks (these are infrastructure, not user-configured). - if isinstance(_runtime_cb_name, str) and any(_runtime_cb_name.startswith(p) for p in _internal_callback_prefixes): + if isinstance(_runtime_cb_name, str) and any( + _runtime_cb_name.startswith(p) for p in _internal_callback_prefixes + ): continue _normalized_runtime = _normalize_callback_alias(_runtime_cb_name) diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_config.py b/tests/test_litellm/proxy/proxy_server/test_routes_config.py index 9b60b8e492d..1242cf5f038 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_config.py @@ -1059,31 +1059,28 @@ def test_get_config_callbacks_appends_runtime_only_callbacks( # Mock runtime callbacks: register otel in addition to langfuse in config. import litellm - original = litellm.callbacks - try: - litellm.callbacks = ["otel"] - with auth_as(LitellmUserRoles.PROXY_ADMIN): - response = client.get("/get/config/callbacks") - assert response.status_code == 200 - body = response.json() + monkeypatch.setattr(litellm, "callbacks", ["otel"]) - callbacks = body["callbacks"] - callback_names = [cb["name"] for cb in callbacks] + with auth_as(LitellmUserRoles.PROXY_ADMIN): + response = client.get("/get/config/callbacks") + assert response.status_code == 200 + body = response.json() - # Both should be present - assert "langfuse" in callback_names - assert "otel" in callback_names + callbacks = body["callbacks"] + callback_names = [cb["name"] for cb in callbacks] - # Configured callback should NOT be marked read_only - langfuse_cb = next(cb for cb in callbacks if cb["name"] == "langfuse") - assert langfuse_cb.get("read_only") != True + # Both should be present + assert "langfuse" in callback_names + assert "otel" in callback_names - # Runtime-only callback should be marked read_only - otel_cb = next(cb for cb in callbacks if cb["name"] == "otel") - assert otel_cb["read_only"] is True - assert otel_cb["type"] == "success_and_failure" - finally: - litellm.callbacks = original + # Configured callback should NOT be marked read_only + langfuse_cb = next(cb for cb in callbacks if cb["name"] == "langfuse") + assert langfuse_cb.get("read_only") != True + + # Runtime-only callback should be marked read_only + otel_cb = next(cb for cb in callbacks if cb["name"] == "otel") + assert otel_cb["read_only"] is True + assert otel_cb["type"] == "success_and_failure" def test_get_config_callbacks_deduplicates_configured_and_runtime( @@ -1153,36 +1150,33 @@ def test_get_config_callbacks_redacts_runtime_only_row_secrets_for_view_only_adm # Mock runtime: register otel import litellm - original = litellm.callbacks - try: - litellm.callbacks = ["otel"] - # View-only admin - with auth_as(LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY): - response = client.get("/get/config/callbacks") - assert response.status_code == 200 - body = response.json() + monkeypatch.setattr(litellm, "callbacks", ["otel"]) - callbacks = body["callbacks"] - otel_cb = next((cb for cb in callbacks if cb["name"] == "otel"), None) - assert otel_cb is not None + # View-only admin + with auth_as(LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY): + response = client.get("/get/config/callbacks") + assert response.status_code == 200 + body = response.json() - # Secret env vars must be redacted - assert otel_cb["variables"]["OTEL_HEADERS"] == "REDACTED" - # Non-secret vars should pass through - assert otel_cb["variables"]["OTEL_ENDPOINT"] == _CALLBACK_ENV_FIXTURE["OTEL_ENDPOINT"] + callbacks = body["callbacks"] + otel_cb = next((cb for cb in callbacks if cb["name"] == "otel"), None) + assert otel_cb is not None - # Full admin sees secrets - with auth_as(LitellmUserRoles.PROXY_ADMIN): - admin_response = client.get("/get/config/callbacks") - assert admin_response.status_code == 200 - admin_body = admin_response.json() - admin_otel = next( - (cb for cb in admin_body["callbacks"] if cb["name"] == "otel"), None - ) - assert admin_otel is not None - assert admin_otel["variables"]["OTEL_HEADERS"] == _CALLBACK_ENV_FIXTURE["OTEL_HEADERS"] - finally: - litellm.callbacks = original + # Secret env vars must be redacted + assert otel_cb["variables"]["OTEL_HEADERS"] == "REDACTED" + # Non-secret vars should pass through + assert otel_cb["variables"]["OTEL_ENDPOINT"] == _CALLBACK_ENV_FIXTURE["OTEL_ENDPOINT"] + + # Full admin sees secrets + with auth_as(LitellmUserRoles.PROXY_ADMIN): + admin_response = client.get("/get/config/callbacks") + assert admin_response.status_code == 200 + admin_body = admin_response.json() + admin_otel = next( + (cb for cb in admin_body["callbacks"] if cb["name"] == "otel"), None + ) + assert admin_otel is not None + assert admin_otel["variables"]["OTEL_HEADERS"] == _CALLBACK_ENV_FIXTURE["OTEL_HEADERS"] # ---------------------------------------------------------------------------