diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a80a277dc48..82f147b3f9d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12637,7 +12637,13 @@ async def update_config( # noqa: PLR0915 existing_cb = existing.get("success_callback") if isinstance(incoming_cb, list): if isinstance(existing_cb, list): - merged["success_callback"] = list(set(existing_cb + incoming_cb)) + # Normalize the existing list too — a row written by a + # different code path may still hold mixed-case names, + # which would otherwise dedup-miss against the lowercase + # incoming entries. + merged["success_callback"] = list( + set(normalize_callback_names(existing_cb) + incoming_cb) + ) else: merged["success_callback"] = list(set(incoming_cb)) diff --git a/tests/test_litellm/proxy/management_endpoints/test_update_config_endpoint.py b/tests/test_litellm/proxy/management_endpoints/test_update_config_endpoint.py index a425e55708c..2797b13b4e6 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_update_config_endpoint.py +++ b/tests/test_litellm/proxy/management_endpoints/test_update_config_endpoint.py @@ -208,6 +208,30 @@ def test_success_callback_unioned_with_existing(admin_auth, patched_proxy): assert set(stored) == {"langfuse", "prometheus"} +def test_success_callback_dedups_against_mixed_case_existing(admin_auth, patched_proxy): + """ + Regression: a litellm_settings row written by an older code path (or by + direct DB edit) may still hold mixed-case callback names like + ["Langfuse"]. When the user submits ["langfuse"], the union must + normalize the existing entries too — otherwise the DB ends up with both + "Langfuse" and "langfuse" and delete_callback (lowercase lookup) cannot + find the original. + """ + prisma = patched_proxy( + initial_rows={"litellm_settings": {"success_callback": ["Langfuse", "SQS"]}} + ) + + client = TestClient(app) + resp = client.post( + "/config/update", + json={"litellm_settings": {"success_callback": ["langfuse"]}}, + ) + + assert resp.status_code == 200 + stored = prisma.db.litellm_config.rows["litellm_settings"]["success_callback"] + assert set(stored) == {"langfuse", "sqs"} + + def test_success_callback_normalized_on_first_write(admin_auth, patched_proxy): """ Regression: when no litellm_settings row exists yet, incoming mixed-case