mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
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:
parent
850fe595ac
commit
cde463b92d
4 changed files with 103 additions and 9 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue