fix(proxy): honor client-supplied x-litellm-call-id in spend log request_id

When a client sends x-litellm-call-id explicitly, it expects to use that
id to look up the resulting spend log record. Today, get_spend_logs_id
prefers response_obj.id (the providers own response id) over
litellm_call_id, so the client-supplied id is silently discarded. The
spend record exists, but with an unrelated provider id as request_id,
making /spend/logs/v2?request_id=my-uuid return empty.

Track whether the call_id came from the client header (vs. an
auto-generated UUID), and when it did, prefer it over response.id in
the spend log id resolution. Auto-generated call_ids continue to fall
back to response.id, preserving existing behavior for callers who
do not supply the header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darien Kindlund 2026-04-17 10:28:50 -04:00
parent 850fe595ac
commit cde463b92d
4 changed files with 103 additions and 9 deletions

View file

@ -783,9 +783,12 @@ class ProxyBaseLLMRequestProcessing:
):
self.data["model"] = user_api_key_dict.aliases[self.data["model"]]
self.data["litellm_call_id"] = request.headers.get(
"x-litellm-call-id", str(uuid.uuid4())
)
client_supplied_call_id = request.headers.get("x-litellm-call-id")
if client_supplied_call_id:
self.data["litellm_call_id"] = client_supplied_call_id
self.data["litellm_call_id_from_client"] = True
else:
self.data["litellm_call_id"] = str(uuid.uuid4())
DDSpanTagger.tag_call_id(self.data.get("litellm_call_id"))
DDSpanTagger.tag_request(
user_api_key_dict=user_api_key_dict,

View file

@ -58,9 +58,12 @@ async def google_generate_content(
)
# Create logging object with full request metadata so callbacks (e.g. S3) get user/trace_id
data["litellm_call_id"] = request.headers.get(
"x-litellm-call-id", str(uuid.uuid4())
)
client_supplied_call_id = request.headers.get("x-litellm-call-id")
if client_supplied_call_id:
data["litellm_call_id"] = client_supplied_call_id
data["litellm_call_id_from_client"] = True
else:
data["litellm_call_id"] = str(uuid.uuid4())
logging_obj, data = litellm.utils.function_setup(
original_function="agenerate_content",
rules_obj=litellm.utils.Rules(),
@ -121,9 +124,12 @@ async def google_stream_generate_content(
)
# Create logging object with full request metadata so streaming END callbacks (e.g. S3) get user/trace_id
data["litellm_call_id"] = request.headers.get(
"x-litellm-call-id", str(uuid.uuid4())
)
client_supplied_call_id = request.headers.get("x-litellm-call-id")
if client_supplied_call_id:
data["litellm_call_id"] = client_supplied_call_id
data["litellm_call_id_from_client"] = True
else:
data["litellm_call_id"] = str(uuid.uuid4())
logging_obj, data = litellm.utils.function_setup(
original_function="agenerate_content_stream",
rules_obj=litellm.utils.Rules(),

View file

@ -169,6 +169,12 @@ def get_spend_logs_id(
if call_type == "aretrieve_batch" or call_type == "acreate_file":
# Generate a hash from the response object
id: Optional[str] = generate_hash_from_response(response_obj)
elif kwargs.get("litellm_call_id_from_client"):
# Client explicitly supplied x-litellm-call-id; honor it so callers
# can correlate spend log records with their own request id.
id = cast(Optional[str], kwargs.get("litellm_call_id")) or cast(
Optional[str], response_obj.get("id")
)
else:
id = cast(Optional[str], response_obj.get("id")) or cast(
Optional[str], kwargs.get("litellm_call_id")

View file

@ -1469,3 +1469,82 @@ class TestIsMasterKey:
master = "sk-master-key-123"
hashed = hash_token(master)
assert _is_master_key(api_key=hashed, _master_key=master) is True
class TestGetSpendLogsId:
"""Tests for get_spend_logs_id precedence between response.id and litellm_call_id."""
def setup_method(self):
from litellm.proxy.spend_tracking.spend_tracking_utils import (
get_spend_logs_id,
)
self.fn = get_spend_logs_id
def test_default_prefers_response_id(self):
"""Without the client-supplied flag, response.id wins (existing behavior)."""
result = self.fn(
call_type="acompletion",
response_obj={"id": "chatcmpl-provider-id"},
kwargs={"litellm_call_id": "auto-generated-uuid"},
)
assert result == "chatcmpl-provider-id"
def test_default_falls_back_to_litellm_call_id(self):
"""Without the client-supplied flag, falls back to litellm_call_id when response.id missing."""
result = self.fn(
call_type="acompletion",
response_obj={},
kwargs={"litellm_call_id": "auto-generated-uuid"},
)
assert result == "auto-generated-uuid"
def test_client_supplied_flag_prefers_litellm_call_id(self):
"""With client-supplied flag, litellm_call_id wins over response.id."""
result = self.fn(
call_type="acompletion",
response_obj={"id": "chatcmpl-provider-id"},
kwargs={
"litellm_call_id": "client-correlation-uuid",
"litellm_call_id_from_client": True,
},
)
assert result == "client-correlation-uuid"
def test_client_supplied_flag_falls_back_to_response_id(self):
"""With client-supplied flag but missing litellm_call_id, falls back to response.id."""
result = self.fn(
call_type="acompletion",
response_obj={"id": "chatcmpl-provider-id"},
kwargs={
"litellm_call_id": None,
"litellm_call_id_from_client": True,
},
)
assert result == "chatcmpl-provider-id"
def test_client_supplied_flag_false_uses_default_precedence(self):
"""Explicit False on flag uses default (response.id wins)."""
result = self.fn(
call_type="acompletion",
response_obj={"id": "chatcmpl-provider-id"},
kwargs={
"litellm_call_id": "auto-uuid",
"litellm_call_id_from_client": False,
},
)
assert result == "chatcmpl-provider-id"
def test_aretrieve_batch_unchanged(self):
"""The batch hash path is independent of the flag."""
result = self.fn(
call_type="aretrieve_batch",
response_obj={"id": "batch-id", "data": "x"},
kwargs={
"litellm_call_id": "client-uuid",
"litellm_call_id_from_client": True,
},
)
# Hash-based id, not the client uuid
assert result is not None
assert result != "client-uuid"