mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(gcs_bucket): store request payloads under the spend log request id so the logs viewer can find them
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
cc910fdf24
commit
498b85422c
6 changed files with 97 additions and 52 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"reportAny": {
|
||||
"limit": 14076
|
||||
"limit": 14075
|
||||
},
|
||||
"reportArgumentType": {
|
||||
"limit": 2216
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
"limit": 5601
|
||||
},
|
||||
"reportMissingTypeArgument": {
|
||||
"limit": 15306
|
||||
"limit": 15304
|
||||
},
|
||||
"reportMissingTypeStubs": {
|
||||
"limit": 40
|
||||
|
|
@ -105,13 +105,13 @@
|
|||
"limit": 109
|
||||
},
|
||||
"reportUnknownMemberType": {
|
||||
"limit": 38350
|
||||
"limit": 38348
|
||||
},
|
||||
"reportUnknownParameterType": {
|
||||
"limit": 19626
|
||||
"limit": 19624
|
||||
},
|
||||
"reportUnknownVariableType": {
|
||||
"limit": 29890
|
||||
"limit": 29888
|
||||
},
|
||||
"reportUnnecessaryCast": {
|
||||
"limit": 111
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
|
|||
from litellm.litellm_core_utils.cloud_storage_security import (
|
||||
sanitize_cloud_object_component,
|
||||
)
|
||||
from litellm.litellm_core_utils.spend_log_request_id import get_spend_logs_id
|
||||
from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus
|
||||
from litellm.types.integrations.gcs_bucket import *
|
||||
|
|
@ -289,7 +290,7 @@ class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils):
|
|||
else:
|
||||
object_name = self._generate_success_object_name(
|
||||
request_date_str=current_date,
|
||||
response_id=response_obj.get("id", ""),
|
||||
response_id=get_spend_logs_id(kwargs.get("call_type") or "acompletion", response_obj, kwargs) or "",
|
||||
)
|
||||
|
||||
# used for testing
|
||||
|
|
|
|||
59
litellm/litellm_core_utils/spend_log_request_id.py
Normal file
59
litellm/litellm_core_utils/spend_log_request_id.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""The id a request is filed under in LiteLLM_SpendLogs and in the per-request payload
|
||||
stores (GCS) the logs viewer reads back through ``/spend/logs/ui/{request_id}``.
|
||||
|
||||
Both sides must derive the id the same way or the viewer looks up a payload under a key
|
||||
it was never stored under, so the derivation lives here rather than in either caller.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from litellm.types.utils import CallTypes
|
||||
|
||||
BATCH_COST_REQUEST_ID_SUFFIX: Final = "_batch_cost"
|
||||
|
||||
_RESPONSE_ID_KEYED_CALL_TYPES: Final = frozenset(
|
||||
{
|
||||
CallTypes.acreate_batch.value,
|
||||
CallTypes.aretrieve_batch.value,
|
||||
CallTypes.acreate_file.value,
|
||||
}
|
||||
)
|
||||
"""Batch and file rows key off the object's own id so repeated polls of the same
|
||||
object collapse into one row instead of billing it once per poll. Every other call
|
||||
type keys off the proxy-generated per-call id: request_id is the LiteLLM_SpendLogs
|
||||
primary key and the flush inserts with skip_duplicates, so keying off the provider's
|
||||
response id silently drops every row after the first whenever a provider (commonly a
|
||||
self-hosted OpenAI-compatible server) reuses completion ids."""
|
||||
|
||||
|
||||
def _standard_logging_id(kwargs: Mapping[str, object]) -> str | None:
|
||||
match kwargs.get("standard_logging_object"):
|
||||
case {"id": str() as standard_logging_id}:
|
||||
return standard_logging_id
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def get_spend_logs_id(call_type: str, response_obj: Mapping[str, object], kwargs: Mapping[str, object]) -> str | None:
|
||||
candidate_ids: Final = (
|
||||
(
|
||||
response_obj.get("id"),
|
||||
_standard_logging_id(kwargs),
|
||||
kwargs.get("litellm_call_id"),
|
||||
)
|
||||
if call_type in _RESPONSE_ID_KEYED_CALL_TYPES
|
||||
else (
|
||||
kwargs.get("litellm_call_id"),
|
||||
_standard_logging_id(kwargs),
|
||||
response_obj.get("id"),
|
||||
)
|
||||
)
|
||||
resolved_id: Final = next(
|
||||
(candidate for candidate in candidate_ids if isinstance(candidate, str) and candidate), None
|
||||
)
|
||||
if resolved_id is not None and call_type == CallTypes.aretrieve_batch.value:
|
||||
return f"{resolved_id}{BATCH_COST_REQUEST_ID_SUFFIX}"
|
||||
return resolved_id
|
||||
|
|
@ -30,11 +30,13 @@ from litellm.litellm_core_utils.litellm_logging import (
|
|||
request_model_access_groups_from_litellm_params,
|
||||
)
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps, strip_null_bytes
|
||||
from litellm.litellm_core_utils.spend_log_request_id import (
|
||||
get_spend_logs_id,
|
||||
)
|
||||
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload, SpendLogsRouterMetadata
|
||||
from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error
|
||||
from litellm.proxy.utils import PrismaClient, hash_token
|
||||
from litellm.types.utils import (
|
||||
CallTypes,
|
||||
CostBreakdown,
|
||||
StandardLoggingGuardrailInformation,
|
||||
StandardLoggingMCPToolCall,
|
||||
|
|
@ -211,49 +213,6 @@ def _get_spend_logs_metadata(
|
|||
return clean_metadata
|
||||
|
||||
|
||||
BATCH_COST_REQUEST_ID_SUFFIX: Final = "_batch_cost"
|
||||
|
||||
_RESPONSE_ID_KEYED_CALL_TYPES: Final = frozenset(
|
||||
{
|
||||
CallTypes.acreate_batch.value,
|
||||
CallTypes.aretrieve_batch.value,
|
||||
CallTypes.acreate_file.value,
|
||||
}
|
||||
)
|
||||
"""Batch and file rows key off the object's own id so repeated polls of the same
|
||||
object collapse into one row instead of billing it once per poll. Every other call
|
||||
type keys off the proxy-generated per-call id: request_id is the LiteLLM_SpendLogs
|
||||
primary key and the flush inserts with skip_duplicates, so keying off the provider's
|
||||
response id silently drops every row after the first whenever a provider (commonly a
|
||||
self-hosted OpenAI-compatible server) reuses completion ids."""
|
||||
|
||||
|
||||
def get_spend_logs_id(call_type: str, response_obj: dict, kwargs: dict) -> str | None:
|
||||
standard_logging_payload = kwargs.get("standard_logging_object")
|
||||
standard_logging_id: Final = (
|
||||
standard_logging_payload.get("id") if isinstance(standard_logging_payload, dict) else None
|
||||
)
|
||||
candidate_ids: Final = (
|
||||
(
|
||||
response_obj.get("id"),
|
||||
standard_logging_id,
|
||||
kwargs.get("litellm_call_id"),
|
||||
)
|
||||
if call_type in _RESPONSE_ID_KEYED_CALL_TYPES
|
||||
else (
|
||||
kwargs.get("litellm_call_id"),
|
||||
standard_logging_id,
|
||||
response_obj.get("id"),
|
||||
)
|
||||
)
|
||||
resolved_id: Final = next(
|
||||
(candidate for candidate in candidate_ids if isinstance(candidate, str) and candidate), None
|
||||
)
|
||||
if resolved_id is not None and call_type == CallTypes.aretrieve_batch.value:
|
||||
return f"{resolved_id}{BATCH_COST_REQUEST_ID_SUFFIX}"
|
||||
return resolved_id
|
||||
|
||||
|
||||
_MISSING_ATTRIBUTE: Final = object()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.integrations.gcs_bucket.gcs_bucket import GCSBucketLogger
|
||||
from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
|
||||
from litellm.litellm_core_utils.spend_log_request_id import get_spend_logs_id
|
||||
|
||||
|
||||
class TestGCSBucketBase:
|
||||
|
|
@ -128,3 +131,26 @@ class TestGCSBucketBase:
|
|||
assert object_name.endswith("-target_uploadType_media")
|
||||
assert ".." not in object_name
|
||||
assert "?" not in object_name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_viewer_finds_payloads_stored_for_requests_sharing_a_provider_response_id(self):
|
||||
logger = GCSBucketLogger.__new__(GCSBucketLogger)
|
||||
provider_response = {"id": "chatcmpl-reused"}
|
||||
stored_objects = {
|
||||
logger._get_object_name(
|
||||
kwargs={"call_type": "acompletion", "litellm_call_id": call_id},
|
||||
logging_payload={"id": "chatcmpl-reused"},
|
||||
response_obj=provider_response,
|
||||
): json.dumps({"litellm_call_id": call_id})
|
||||
for call_id in ("call-id-1", "call-id-2")
|
||||
}
|
||||
assert len(stored_objects) == 2
|
||||
logger.download_gcs_object = AsyncMock(side_effect=lambda object_name: stored_objects.get(object_name))
|
||||
|
||||
payload = await logger.get_request_response_payload(
|
||||
request_id=get_spend_logs_id("acompletion", provider_response, {"litellm_call_id": "call-id-2"}),
|
||||
start_time_utc=datetime.now(timezone.utc),
|
||||
end_time_utc=None,
|
||||
)
|
||||
|
||||
assert payload == {"litellm_call_id": "call-id-2"}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"LIT001": {
|
||||
"limit": 22367
|
||||
"limit": 22365
|
||||
},
|
||||
"LIT002": {
|
||||
"limit": 26777
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"limit": 0
|
||||
},
|
||||
"LIT010": {
|
||||
"limit": 16507
|
||||
"limit": 16506
|
||||
},
|
||||
"LIT011": {
|
||||
"limit": 5535
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue