fix(proxy): /config/update normalize success_callback on first write
Some checks are pending
Unit Tests: Caching (Redis) / caching-redis (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run

Previously the normalize_callback_names call only ran when the existing
litellm_settings DB row already had a success_callback key. On the very
first write (no row yet, or row missing the key), incoming mixed-case
values like ["SQS", "sQs"] persisted as-is. delete_callback (lowercase
lookup) then could not find them, and a follow-up /config/update would
union normalized incoming with mixed-case stored entries, producing
duplicates.

Always normalize incoming success_callback before merging, and dedupe
both the standalone first-write case and the union-with-existing case.

Adds test_success_callback_normalized_on_first_write covering the
no-existing-row path; the existing union test still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yuneng Jiang 2026-04-27 23:42:43 -07:00
parent abbe5d7f85
commit b6e4ccf876
2 changed files with 41 additions and 14 deletions

View file

@ -12617,23 +12617,30 @@ async def update_config( # noqa: PLR0915
await _upsert_section("environment_variables", existing)
# litellm_settings: existing-wins merge (preserving legacy behavior),
# except success_callback is unioned with the request value.
# except success_callback is always normalized + deduped, and unioned
# with any existing list. Normalizing on every write — not only when
# an existing entry is present — keeps the DB free of mixed-case
# entries that delete_callback (lowercase lookup) cannot find.
if config_info.litellm_settings is not None:
existing = await _read_section("litellm_settings")
updated_litellm_settings = config_info.litellm_settings
updated_litellm_settings = dict(config_info.litellm_settings)
incoming_cb = updated_litellm_settings.get("success_callback")
if isinstance(incoming_cb, list):
updated_litellm_settings["success_callback"] = normalize_callback_names(
incoming_cb
)
merged = {**updated_litellm_settings, **existing}
if (
"success_callback" in updated_litellm_settings
and "success_callback" in existing
and isinstance(existing["success_callback"], list)
and isinstance(updated_litellm_settings["success_callback"], list)
):
normalized = normalize_callback_names(
updated_litellm_settings["success_callback"]
)
merged["success_callback"] = list(
set(existing["success_callback"] + normalized)
)
incoming_cb = updated_litellm_settings.get("success_callback")
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))
else:
merged["success_callback"] = list(set(incoming_cb))
await _upsert_section("litellm_settings", merged)
# router_settings: merge existing + request, request wins.

View file

@ -208,6 +208,26 @@ def test_success_callback_unioned_with_existing(admin_auth, patched_proxy):
assert set(stored) == {"langfuse", "prometheus"}
def test_success_callback_normalized_on_first_write(admin_auth, patched_proxy):
"""
Regression: when no litellm_settings row exists yet, incoming mixed-case
callbacks must still be lowercased and deduped before write. delete_callback
looks up by lowercase name, so a stored "SQS" would be unreachable, and a
follow-up /config/update with ["sqs"] would union mixed-case stored entries
with normalized incoming ones, producing duplicates.
"""
prisma = patched_proxy()
client = TestClient(app)
resp = client.post(
"/config/update",
json={"litellm_settings": {"success_callback": ["SQS", "sQs"]}},
)
assert resp.status_code == 200
stored = prisma.db.litellm_config.rows["litellm_settings"]["success_callback"]
assert set(stored) == {"sqs"}
def test_alert_to_webhook_url_enables_slack_alerting(admin_auth, patched_proxy):
prisma = patched_proxy()
client = TestClient(app)