fix(proxy): /config/update normalize existing success_callback before dedup

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"].
This commit is contained in:
Yuneng Jiang 2026-04-29 16:21:51 -07:00
parent b6e4ccf876
commit 1fd38eb5a5
2 changed files with 31 additions and 1 deletions

View file

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

View file

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