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
This commit is contained in:
Yucheng 2026-08-31 13:18:37 -07:00
parent 239425c925
commit 6e0b420b0f
2 changed files with 58 additions and 51 deletions

View file

@ -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)

View file

@ -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"]
# ---------------------------------------------------------------------------