fix(spend): fall back to the call id when a provider returns a null response id - #41026
Conversation
Greptile SummaryThis PR preserves provider response IDs when present and falls back to the LiteLLM call ID when a provider returns a null or otherwise falsy ID, preventing duplicate spend-log request IDs. It adds focused regression coverage for null and valid provider IDs Confidence Score: 4/5The behavior change appears correct, but the explicit repository comment policy must be satisfied before merging The previous finding was only partly addressed. The docstring was shortened, but lines 3967-3971 still explain straightforward test mechanics and therefore remain outside CLAUDE.md's allowed comment categories Files Needing Attention: tests/test_litellm/litellm_core_utils/test_litellm_logging.py
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/litellm_logging.py | Uses the per-call identifier when the provider response does not contain a usable ID |
| tests/test_litellm/litellm_core_utils/test_litellm_logging.py | Covers null-ID fallback and provider-ID precedence, but the previously flagged explanatory docstring remains |
Reviews (2): Last reviewed commit: "fix(spend): fall back to the call id whe..." | Re-trigger Greptile
| """A provider answering with a null `id` must not name every one of its rows "None". | ||
|
|
||
| `dict.get` returns its default only for an absent key, so a key present and null went | ||
| on to `str()` and each such request logged the same literal "None" as its request id. | ||
| That id is the spend row's primary key and the writer inserts with skip_duplicates, so | ||
| the first row landed and every one after it was dropped with no error raised. | ||
| """ |
There was a problem hiding this comment.
This six-line docstring explains straightforward mechanics, violating the directive to reserve comments for complex logic. Remove it before merging
| """A provider answering with a null `id` must not name every one of its rows "None". | |
| `dict.get` returns its default only for an absent key, so a key present and null went | |
| on to `str()` and each such request logged the same literal "None" as its request id. | |
| That id is the spend row's primary key and the writer inserts with skip_duplicates, so | |
| the first row landed and every one after it was dropped with no error raised. | |
| """ |
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…esponse id dict.get returns its default only for an absent key, so a response carrying "id": null reached str() and logged the literal string "None" as its request id. That id is the spend row's primary key and the writer inserts with skip_duplicates, so the first such row landed and every one after it was dropped without raising. A provider id that is actually present still wins, unchanged.
8cd6fba to
9881d8b
Compare
|
Trimmed the new test's docstring to match the file's convention and re-ran the QA at the new tip. @greptileai |
TLDR
Problem this solves:
"id": nullrecords the string "None"How it solves it:
User Flow
Before: an operator serving Gemini through Databricks sees one row in Request Logs no matter how many calls they make, and nothing on the page reports an error
"model": "databricks-gemini-3-5-flash-lite"and get 200 OK with a normal answerNone, and the other calls are absentAfter: every call is its own row
"model": "databricks-gemini-3-5-flash-lite"and get 200 OK with a normal answerNoneRelevant issues
Fixes #39749
Affected release
regression in v1.98.0
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -vScreenshots / Proof of Fix
A Postgres-backed proxy against a local upstream that answers with an OpenAI-shaped body whose
idis null, standing in for the Databricks serving-endpoint behaviour the issue reports. Three identical requests go through adatabricks/deployment and the spend table is read back. Three other provider prefixes are configured against the same upstream as controls: they build aModelResponse, which generates an id of its own, so the null only survives on thedatabricks/pathBefore (30f33a9)
200NoneAfter (9881d8b)
200None, and the controls are unchangedType
🐛 Bug Fix
Caveats (if any)
Medium
get_spend_logs_idwould leave that same string onstandard_logging_object["id"], where every other logging integration reads it, and it would match on a magic string rather than on the value being absentLow
get_spend_logs_idbegan reading this field ahead of the call id, which is what made the bad value reachableModelResponsegenerates its own idFinal Attestation