From 1fd38eb5a52549874879db052fe33b66b68f7dd2 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 29 Apr 2026 16:21:51 -0700 Subject: [PATCH] fix(proxy): /config/update normalize existing success_callback before dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a litellm_settings row already holds mixed-case names (e.g. ["Langfuse"]) — written by another code path or by hand — the union-on-update path was running set([...]) over the raw existing list plus the lowercase-normalized incoming list, so "Langfuse" and "langfuse" survived as duplicates. delete_callback uses a lowercase lookup, leaving the mixed-case entry unreachable. Normalize the existing list with normalize_callback_names before the union so the merged list converges to lowercase. Adds a regression test covering the case where the DB starts with ["Langfuse", "SQS"] and the caller submits ["langfuse"]. --- litellm/proxy/proxy_server.py | 8 ++++++- .../test_update_config_endpoint.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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