Repository navigation
fix(proxy): unregister logging callbacks removed from the stored config - #43428
Conversation
POST /config/callback/delete saved the config and resynced, but the resync only ever added callbacks, so a deleted callback kept exporting and kept showing in /get/config/callbacks as read-only on every worker. ProxyConfig now tracks which callback list entries each DB config sync registered and unregisters them once the stored config stops listing them. Callbacks it did not register (YAML, code) are never touched, and a failed config load skips the sync instead of treating the config as empty.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@greptileai review |
QA: before (8e6d99d) fails, after (b8eed45) passesLive e2e run, no mocks. Each leg ran two separate proxy processes with Chat used in both legs: Before (merge base 8e6d99d, ports 4101/4102)The deleted callback stays listed as read only on both processes and all 10 chats after the delete still land in Langfuse After (PR tip b8eed45, ports 4103/4104)Observations
|
Registering a custom-logger callback from the DB swaps an existing string entry for a logger instance. Deleting the DB entry then removed the instance and left the code-registered callback gone. The sync now records the entries it displaced and puts them back when it unregisters.
|
@greptileai review |
|
@greptileai review latest head |
|
bugbot run |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
TLDR
Problem this solves:
How it solves it:
User Flow
Before: an admin deletes a logging callback, gets a success message, and the proxy keeps exporting to it
langfuse_otelwith POST http://localhost:4000/config/update and traffic starts reaching LangfuseSuccessfully deleted callback: langfuse_otellangfuse_otel, now marked read only, on every workerAfter: the delete sticks on every worker
langfuse_otelwith POST http://localhost:4000/config/update and traffic starts reaching LangfuseSuccessfully deleted callback: langfuse_otelAffected release
The read-only listing appears since v1.102.0-rc.1 (#38974). The leftover exports are older and also happen on v1.101.0
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)The resync that runs after a delete, and every 30 seconds on each worker, could only add callbacks. So the callback object stayed in litellm's in-memory callback lists after its config entry was gone, and
/get/config/callbacksshows runtime-only callbacks as read only. The bug itself is older than v1.102.0: v1.101.0 also keeps exporting after the delete, but its callback list hid the leftoverGreptile on the rc backport found that registering
langfuse_otelfrom the DB swaps a code-registered string entry for a logger instance, so the delete dropped the code-registered callback too. The second fix commit records replaced entries and restores them, covered bytest_db_config_sync_restores_a_code_callback_it_replaced. A restart between the add and the delete was also checked on this branch: 10 chats after the delete produced 0 exports and every worker listed nothing. The new tests fail on main and pass here. Four of five planned source mutations are caught. The survivor removes and re-adds every still-configured callback on each sync, which ends in the same stateScreenshots / Proof of Fix
Proxy started with
--num_workers 2(the After run waits 30 seconds after the first worker is live so both workers finish startup), a local Postgres,STORE_MODEL_IN_DB=True, amock_responsemodel namedmock-gpt, andLANGFUSE_OTEL_HOSTpointing at a local OTLP capture server that counts export POSTs. No real provider calls are needed since the bug is in callback registration, not the LLM call. "OTLP exports" below counts export batches the capture server receivedBefore (8e6d99d)
curl -s localhost:4000/config/update -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"litellm_settings":{"success_callback":["langfuse_otel"]}}'returns{"message":"Config updated successfully"}, and 6 chats produce 1 export batchcurl -s localhost:4000/config/callback/delete -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"callback_name":"langfuse_otel"}'returns{"message":"Successfully deleted callback: langfuse_otel","removed_callback":"langfuse_otel","remaining_callbacks":[],...}curl -s localhost:4000/get/config/callbacks -H "Authorization: Bearer $LITELLM_MASTER_KEY"still returns[{"name": "langfuse_otel", ..., "type": "success", "read_only": true}]/get/config/callbackscalls across workers all return["langfuse_otel"]After (7026833)
/config/updatecall returns{"message":"Config updated successfully"}, and 6 chats produce 2 export batches/config/callback/deletecall returns the same success message/get/config/callbacksreturns[]/get/config/callbackscalls across workers all return[]/config/updatecall, then waiting 35 seconds, 10 chats produce 2 export batchesType
🐛 Bug Fix
Caveats (if any)
Medium
Low