mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
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:
parent
239425c925
commit
6e0b420b0f
2 changed files with 58 additions and 51 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue