mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 10:44:24 +00:00
(Feat) - Allow viewing Request/Response Logs stored in GCS Bucket (#8449)
* BaseRequestResponseFetchFromCustomLogger * get_active_base_request_response_fetch_from_custom_logger * get_request_response_payload * ui_view_request_response_for_request_id * fix uiSpendLogDetailsCall * fix get_request_response_payload * ui fix RequestViewer * use 1 class AdditionalLoggingUtils * ui_view_request_response_for_request_id * cache the prefetch logs details * refactor prefetch * test view request/resp logs * fix code quality * fix get_request_response_payload * uninstall posthog prevent it from being added in ci/cd * fix posthog * fix traceloop test * fix linting error
This commit is contained in:
parent
64a4229606
commit
00c596a852
13 changed files with 706 additions and 201 deletions
36
litellm/integrations/additional_logging_utils.py
Normal file
36
litellm/integrations/additional_logging_utils.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
Base class for Additional Logging Utils for CustomLoggers
|
||||||
|
|
||||||
|
- Health Check for the logging util
|
||||||
|
- Get Request / Response Payload for the logging util
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AdditionalLoggingUtils(ABC):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def async_health_check(self) -> IntegrationHealthCheckStatus:
|
||||||
|
"""
|
||||||
|
Check if the service is healthy
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_request_response_payload(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
start_time_utc: Optional[datetime],
|
||||||
|
end_time_utc: Optional[datetime],
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Get the request and response payload for a given `request_id`
|
||||||
|
"""
|
||||||
|
return None
|
|
@ -1,19 +0,0 @@
|
||||||
"""
|
|
||||||
Base class for health check integrations
|
|
||||||
"""
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus
|
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckIntegration(ABC):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def async_health_check(self) -> IntegrationHealthCheckStatus:
|
|
||||||
"""
|
|
||||||
Check if the service is healthy
|
|
||||||
"""
|
|
||||||
pass
|
|
|
@ -38,14 +38,14 @@ from litellm.types.integrations.datadog import *
|
||||||
from litellm.types.services import ServiceLoggerPayload
|
from litellm.types.services import ServiceLoggerPayload
|
||||||
from litellm.types.utils import StandardLoggingPayload
|
from litellm.types.utils import StandardLoggingPayload
|
||||||
|
|
||||||
from ..base_health_check import HealthCheckIntegration
|
from ..additional_logging_utils import AdditionalLoggingUtils
|
||||||
|
|
||||||
DD_MAX_BATCH_SIZE = 1000 # max number of logs DD API can accept
|
DD_MAX_BATCH_SIZE = 1000 # max number of logs DD API can accept
|
||||||
|
|
||||||
|
|
||||||
class DataDogLogger(
|
class DataDogLogger(
|
||||||
CustomBatchLogger,
|
CustomBatchLogger,
|
||||||
HealthCheckIntegration,
|
AdditionalLoggingUtils,
|
||||||
):
|
):
|
||||||
# Class variables or attributes
|
# Class variables or attributes
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -543,3 +543,13 @@ class DataDogLogger(
|
||||||
status="unhealthy",
|
status="unhealthy",
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_request_response_payload(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
start_time_utc: Optional[datetimeObj],
|
||||||
|
end_time_utc: Optional[datetimeObj],
|
||||||
|
) -> Optional[dict]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Datdog Integration for getting request/response payloads not implemented as yet"
|
||||||
|
)
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
|
from litellm.integrations.additional_logging_utils import AdditionalLoggingUtils
|
||||||
from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
|
from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
|
||||||
from litellm.proxy._types import CommonProxyErrors
|
from litellm.proxy._types import CommonProxyErrors
|
||||||
|
from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus
|
||||||
from litellm.types.integrations.gcs_bucket import *
|
from litellm.types.integrations.gcs_bucket import *
|
||||||
from litellm.types.utils import StandardLoggingPayload
|
from litellm.types.utils import StandardLoggingPayload
|
||||||
|
|
||||||
|
@ -20,7 +24,7 @@ GCS_DEFAULT_BATCH_SIZE = 2048
|
||||||
GCS_DEFAULT_FLUSH_INTERVAL_SECONDS = 20
|
GCS_DEFAULT_FLUSH_INTERVAL_SECONDS = 20
|
||||||
|
|
||||||
|
|
||||||
class GCSBucketLogger(GCSBucketBase):
|
class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils):
|
||||||
def __init__(self, bucket_name: Optional[str] = None) -> None:
|
def __init__(self, bucket_name: Optional[str] = None) -> None:
|
||||||
from litellm.proxy.proxy_server import premium_user
|
from litellm.proxy.proxy_server import premium_user
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@ class GCSBucketLogger(GCSBucketBase):
|
||||||
batch_size=self.batch_size,
|
batch_size=self.batch_size,
|
||||||
flush_interval=self.flush_interval,
|
flush_interval=self.flush_interval,
|
||||||
)
|
)
|
||||||
|
AdditionalLoggingUtils.__init__(self)
|
||||||
|
|
||||||
if premium_user is not True:
|
if premium_user is not True:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -150,11 +155,16 @@ class GCSBucketLogger(GCSBucketBase):
|
||||||
"""
|
"""
|
||||||
Get the object name to use for the current payload
|
Get the object name to use for the current payload
|
||||||
"""
|
"""
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
current_date = self._get_object_date_from_datetime(datetime.now(timezone.utc))
|
||||||
if logging_payload.get("error_str", None) is not None:
|
if logging_payload.get("error_str", None) is not None:
|
||||||
object_name = f"{current_date}/failure-{uuid.uuid4().hex}"
|
object_name = self._generate_failure_object_name(
|
||||||
|
request_date_str=current_date,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
object_name = f"{current_date}/{response_obj.get('id', '')}"
|
object_name = self._generate_success_object_name(
|
||||||
|
request_date_str=current_date,
|
||||||
|
response_id=response_obj.get("id", ""),
|
||||||
|
)
|
||||||
|
|
||||||
# used for testing
|
# used for testing
|
||||||
_litellm_params = kwargs.get("litellm_params", None) or {}
|
_litellm_params = kwargs.get("litellm_params", None) or {}
|
||||||
|
@ -163,3 +173,65 @@ class GCSBucketLogger(GCSBucketBase):
|
||||||
object_name = _metadata["gcs_log_id"]
|
object_name = _metadata["gcs_log_id"]
|
||||||
|
|
||||||
return object_name
|
return object_name
|
||||||
|
|
||||||
|
async def get_request_response_payload(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
start_time_utc: Optional[datetime],
|
||||||
|
end_time_utc: Optional[datetime],
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Get the request and response payload for a given `request_id`
|
||||||
|
Tries current day, next day, and previous day until it finds the payload
|
||||||
|
"""
|
||||||
|
if start_time_utc is None:
|
||||||
|
raise ValueError(
|
||||||
|
"start_time_utc is required for getting a payload from GCS Bucket"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try current day, next day, and previous day
|
||||||
|
dates_to_try = [
|
||||||
|
start_time_utc,
|
||||||
|
start_time_utc + timedelta(days=1),
|
||||||
|
start_time_utc - timedelta(days=1),
|
||||||
|
]
|
||||||
|
date_str = None
|
||||||
|
for date in dates_to_try:
|
||||||
|
try:
|
||||||
|
date_str = self._get_object_date_from_datetime(datetime_obj=date)
|
||||||
|
object_name = self._generate_success_object_name(
|
||||||
|
request_date_str=date_str,
|
||||||
|
response_id=request_id,
|
||||||
|
)
|
||||||
|
encoded_object_name = quote(object_name, safe="")
|
||||||
|
response = await self.download_gcs_object(encoded_object_name)
|
||||||
|
|
||||||
|
if response is not None:
|
||||||
|
loaded_response = json.loads(response)
|
||||||
|
return loaded_response
|
||||||
|
except Exception as e:
|
||||||
|
verbose_logger.debug(
|
||||||
|
f"Failed to fetch payload for date {date_str}: {str(e)}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _generate_success_object_name(
|
||||||
|
self,
|
||||||
|
request_date_str: str,
|
||||||
|
response_id: str,
|
||||||
|
) -> str:
|
||||||
|
return f"{request_date_str}/{response_id}"
|
||||||
|
|
||||||
|
def _generate_failure_object_name(
|
||||||
|
self,
|
||||||
|
request_date_str: str,
|
||||||
|
) -> str:
|
||||||
|
return f"{request_date_str}/failure-{uuid.uuid4().hex}"
|
||||||
|
|
||||||
|
def _get_object_date_from_datetime(self, datetime_obj: datetime) -> str:
|
||||||
|
return datetime_obj.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
async def async_health_check(self) -> IntegrationHealthCheckStatus:
|
||||||
|
raise NotImplementedError("GCS Bucket does not support health check")
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from typing import Callable, List, Union
|
from typing import Callable, List, Set, Union
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
|
from litellm.integrations.additional_logging_utils import AdditionalLoggingUtils
|
||||||
from litellm.integrations.custom_logger import CustomLogger
|
from litellm.integrations.custom_logger import CustomLogger
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,3 +221,36 @@ class LoggingCallbackManager:
|
||||||
litellm._async_success_callback = []
|
litellm._async_success_callback = []
|
||||||
litellm._async_failure_callback = []
|
litellm._async_failure_callback = []
|
||||||
litellm.callbacks = []
|
litellm.callbacks = []
|
||||||
|
|
||||||
|
def _get_all_callbacks(self) -> List[Union[CustomLogger, Callable, str]]:
|
||||||
|
"""
|
||||||
|
Get all callbacks from litellm.callbacks, litellm.success_callback, litellm.failure_callback, litellm._async_success_callback, litellm._async_failure_callback
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
litellm.callbacks
|
||||||
|
+ litellm.success_callback
|
||||||
|
+ litellm.failure_callback
|
||||||
|
+ litellm._async_success_callback
|
||||||
|
+ litellm._async_failure_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_active_additional_logging_utils_from_custom_logger(
|
||||||
|
self,
|
||||||
|
) -> Set[AdditionalLoggingUtils]:
|
||||||
|
"""
|
||||||
|
Get all custom loggers that are instances of the given class type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
class_type: The class type to match against (e.g., AdditionalLoggingUtils)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set[CustomLogger]: Set of custom loggers that are instances of the given class type
|
||||||
|
"""
|
||||||
|
all_callbacks = self._get_all_callbacks()
|
||||||
|
matched_callbacks: Set[AdditionalLoggingUtils] = set()
|
||||||
|
for callback in all_callbacks:
|
||||||
|
if isinstance(callback, CustomLogger) and isinstance(
|
||||||
|
callback, AdditionalLoggingUtils
|
||||||
|
):
|
||||||
|
matched_callbacks.add(callback)
|
||||||
|
return matched_callbacks
|
||||||
|
|
|
@ -5,8 +5,11 @@ model_list:
|
||||||
api_key: my-fake-key
|
api_key: my-fake-key
|
||||||
api_base: https://exampleopenaiendpoint-production.up.railway.app/
|
api_base: https://exampleopenaiendpoint-production.up.railway.app/
|
||||||
|
|
||||||
litellm_settings:
|
|
||||||
cache: True
|
general_settings:
|
||||||
cache_params:
|
store_model_in_db: true
|
||||||
type: redis
|
|
||||||
|
|
||||||
|
litellm_settings:
|
||||||
|
callbacks: ["gcs_bucket"]
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import collections
|
import collections
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from functools import lru_cache
|
||||||
from typing import TYPE_CHECKING, Any, List, Optional
|
from typing import TYPE_CHECKING, Any, List, Optional
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
|
@ -1759,6 +1760,56 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||||
raise handle_exception_on_proxy(e)
|
raise handle_exception_on_proxy(e)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=128)
|
||||||
|
@router.get(
|
||||||
|
"/spend/logs/ui/{request_id}",
|
||||||
|
tags=["Budget & Spend Tracking"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
include_in_schema=False,
|
||||||
|
)
|
||||||
|
async def ui_view_request_response_for_request_id(
|
||||||
|
request_id: str,
|
||||||
|
start_date: Optional[str] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Time from which to start viewing key spend",
|
||||||
|
),
|
||||||
|
end_date: Optional[str] = fastapi.Query(
|
||||||
|
default=None,
|
||||||
|
description="Time till which to view key spend",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
View request / response for a specific request_id
|
||||||
|
|
||||||
|
- goes through all callbacks, checks if any of them have a @property -> has_request_response_payload
|
||||||
|
- if so, it will return the request and response payload
|
||||||
|
"""
|
||||||
|
custom_loggers = (
|
||||||
|
litellm.logging_callback_manager.get_active_additional_logging_utils_from_custom_logger()
|
||||||
|
)
|
||||||
|
start_date_obj: Optional[datetime] = None
|
||||||
|
end_date_obj: Optional[datetime] = None
|
||||||
|
if start_date is not None:
|
||||||
|
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S").replace(
|
||||||
|
tzinfo=timezone.utc
|
||||||
|
)
|
||||||
|
if end_date is not None:
|
||||||
|
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S").replace(
|
||||||
|
tzinfo=timezone.utc
|
||||||
|
)
|
||||||
|
|
||||||
|
for custom_logger in custom_loggers:
|
||||||
|
payload = await custom_logger.get_request_response_payload(
|
||||||
|
request_id=request_id,
|
||||||
|
start_time_utc=start_date_obj,
|
||||||
|
end_time_utc=end_date_obj,
|
||||||
|
)
|
||||||
|
if payload is not None:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/spend/logs",
|
"/spend/logs",
|
||||||
tags=["Budget & Spend Tracking"],
|
tags=["Budget & Spend Tracking"],
|
||||||
|
|
|
@ -11,6 +11,7 @@ sys.path.insert(0, os.path.abspath("../.."))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
@pytest.mark.skip(reason="Traceloop use `otel` integration instead")
|
||||||
def exporter():
|
def exporter():
|
||||||
from traceloop.sdk import Traceloop
|
from traceloop.sdk import Traceloop
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ def exporter():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("model", ["claude-3-5-haiku-20241022", "gpt-3.5-turbo"])
|
@pytest.mark.parametrize("model", ["claude-3-5-haiku-20241022", "gpt-3.5-turbo"])
|
||||||
|
@pytest.mark.skip(reason="Traceloop use `otel` integration instead")
|
||||||
def test_traceloop_logging(exporter, model):
|
def test_traceloop_logging(exporter, model):
|
||||||
litellm.completion(
|
litellm.completion(
|
||||||
model=model,
|
model=model,
|
||||||
|
|
208
tests/logging_callback_tests/test_view_request_resp_logs.py
Normal file
208
tests/logging_callback_tests/test_view_request_resp_logs.py
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath("../.."))
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import litellm
|
||||||
|
from litellm import completion
|
||||||
|
from litellm._logging import verbose_logger
|
||||||
|
from litellm.integrations.gcs_bucket.gcs_bucket import (
|
||||||
|
GCSBucketLogger,
|
||||||
|
StandardLoggingPayload,
|
||||||
|
)
|
||||||
|
from litellm.types.utils import StandardCallbackDynamicParams
|
||||||
|
|
||||||
|
|
||||||
|
# This is the response payload that GCS would return.
|
||||||
|
mock_response_data = {
|
||||||
|
"id": "chatcmpl-9870a859d6df402795f75dc5fca5b2e0",
|
||||||
|
"trace_id": None,
|
||||||
|
"call_type": "acompletion",
|
||||||
|
"cache_hit": None,
|
||||||
|
"stream": True,
|
||||||
|
"status": "success",
|
||||||
|
"custom_llm_provider": "openai",
|
||||||
|
"saved_cache_cost": 0.0,
|
||||||
|
"startTime": 1739235379.683053,
|
||||||
|
"endTime": 1739235379.84533,
|
||||||
|
"completionStartTime": 1739235379.84533,
|
||||||
|
"response_time": 0.1622769832611084,
|
||||||
|
"model": "my-fake-model",
|
||||||
|
"metadata": {
|
||||||
|
"user_api_key_hash": "88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b",
|
||||||
|
"user_api_key_alias": None,
|
||||||
|
"user_api_key_team_id": None,
|
||||||
|
"user_api_key_org_id": None,
|
||||||
|
"user_api_key_user_id": "default_user_id",
|
||||||
|
"user_api_key_team_alias": None,
|
||||||
|
"spend_logs_metadata": None,
|
||||||
|
"requester_ip_address": "127.0.0.1",
|
||||||
|
"requester_metadata": {},
|
||||||
|
"user_api_key_end_user_id": None,
|
||||||
|
"prompt_management_metadata": None,
|
||||||
|
},
|
||||||
|
"cache_key": None,
|
||||||
|
"response_cost": 3.7500000000000003e-05,
|
||||||
|
"total_tokens": 21,
|
||||||
|
"prompt_tokens": 9,
|
||||||
|
"completion_tokens": 12,
|
||||||
|
"request_tags": [],
|
||||||
|
"end_user": "",
|
||||||
|
"api_base": "https://exampleopenaiendpoint-production.up.railway.app",
|
||||||
|
"model_group": "fake-openai-endpoint",
|
||||||
|
"model_id": "b68d56d76b0c24ac9462ab69541e90886342508212210116e300441155f37865",
|
||||||
|
"requester_ip_address": "127.0.0.1",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": [{"type": "text", "text": "very gm to u"}]}
|
||||||
|
],
|
||||||
|
"response": {
|
||||||
|
"id": "chatcmpl-9870a859d6df402795f75dc5fca5b2e0",
|
||||||
|
"created": 1677652288,
|
||||||
|
"model": "gpt-3.5-turbo-0301",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"system_fingerprint": "fp_44709d6fcb",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"finish_reason": "stop",
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"content": "\n\nHello there, how may I assist you today?",
|
||||||
|
"role": "assistant",
|
||||||
|
"tool_calls": None,
|
||||||
|
"function_call": None,
|
||||||
|
"refusal": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"completion_tokens": 12,
|
||||||
|
"prompt_tokens": 9,
|
||||||
|
"total_tokens": 21,
|
||||||
|
"completion_tokens_details": None,
|
||||||
|
"prompt_tokens_details": None,
|
||||||
|
},
|
||||||
|
"service_tier": None,
|
||||||
|
},
|
||||||
|
"model_parameters": {"stream": False, "max_retries": 0, "extra_body": {}},
|
||||||
|
"hidden_params": {
|
||||||
|
"model_id": "b68d56d76b0c24ac9462ab69541e90886342508212210116e300441155f37865",
|
||||||
|
"cache_key": None,
|
||||||
|
"api_base": "https://exampleopenaiendpoint-production.up.railway.app/",
|
||||||
|
"response_cost": 3.7500000000000003e-05,
|
||||||
|
"additional_headers": {},
|
||||||
|
"litellm_overhead_time_ms": 2.126,
|
||||||
|
},
|
||||||
|
"model_map_information": {
|
||||||
|
"model_map_key": "gpt-3.5-turbo-0301",
|
||||||
|
"model_map_value": {},
|
||||||
|
},
|
||||||
|
"error_str": None,
|
||||||
|
"error_information": {"error_code": "", "error_class": "", "llm_provider": ""},
|
||||||
|
"response_cost_failure_debug_info": None,
|
||||||
|
"guardrail_information": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_payload_current_day():
|
||||||
|
"""
|
||||||
|
Verify that the payload is returned when it is found on the current day.
|
||||||
|
"""
|
||||||
|
gcs_logger = GCSBucketLogger()
|
||||||
|
# Use January 1, 2024 as the current day
|
||||||
|
start_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||||
|
request_id = mock_response_data["id"]
|
||||||
|
|
||||||
|
async def fake_download(object_name: str, **kwargs) -> bytes | None:
|
||||||
|
if "2024-01-01" in object_name:
|
||||||
|
return json.dumps(mock_response_data).encode("utf-8")
|
||||||
|
return None
|
||||||
|
|
||||||
|
gcs_logger.download_gcs_object = fake_download
|
||||||
|
|
||||||
|
payload = await gcs_logger.get_request_response_payload(
|
||||||
|
request_id, start_time, None
|
||||||
|
)
|
||||||
|
assert payload is not None
|
||||||
|
assert payload["id"] == request_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_payload_next_day():
|
||||||
|
"""
|
||||||
|
Verify that if the payload is not found on the current day,
|
||||||
|
but is available on the next day, it is returned.
|
||||||
|
"""
|
||||||
|
gcs_logger = GCSBucketLogger()
|
||||||
|
start_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||||
|
request_id = mock_response_data["id"]
|
||||||
|
|
||||||
|
async def fake_download(object_name: str, **kwargs) -> bytes | None:
|
||||||
|
if "2024-01-02" in object_name:
|
||||||
|
return json.dumps(mock_response_data).encode("utf-8")
|
||||||
|
return None
|
||||||
|
|
||||||
|
gcs_logger.download_gcs_object = fake_download
|
||||||
|
|
||||||
|
payload = await gcs_logger.get_request_response_payload(
|
||||||
|
request_id, start_time, None
|
||||||
|
)
|
||||||
|
assert payload is not None
|
||||||
|
assert payload["id"] == request_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_payload_previous_day():
|
||||||
|
"""
|
||||||
|
Verify that if the payload is not found on the current or next day,
|
||||||
|
but is available on the previous day, it is returned.
|
||||||
|
"""
|
||||||
|
gcs_logger = GCSBucketLogger()
|
||||||
|
start_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||||
|
request_id = mock_response_data["id"]
|
||||||
|
|
||||||
|
async def fake_download(object_name: str, **kwargs) -> bytes | None:
|
||||||
|
if "2023-12-31" in object_name:
|
||||||
|
return json.dumps(mock_response_data).encode("utf-8")
|
||||||
|
return None
|
||||||
|
|
||||||
|
gcs_logger.download_gcs_object = fake_download
|
||||||
|
|
||||||
|
payload = await gcs_logger.get_request_response_payload(
|
||||||
|
request_id, start_time, None
|
||||||
|
)
|
||||||
|
assert payload is not None
|
||||||
|
assert payload["id"] == request_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_payload_not_found():
|
||||||
|
"""
|
||||||
|
Verify that if none of the three days contain the payload, None is returned.
|
||||||
|
"""
|
||||||
|
gcs_logger = GCSBucketLogger()
|
||||||
|
start_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
||||||
|
request_id = mock_response_data["id"]
|
||||||
|
|
||||||
|
async def fake_download(object_name: str, **kwargs) -> bytes | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
gcs_logger.download_gcs_object = fake_download
|
||||||
|
|
||||||
|
payload = await gcs_logger.get_request_response_payload(
|
||||||
|
request_id, start_time, None
|
||||||
|
)
|
||||||
|
assert payload is None
|
|
@ -3221,3 +3221,41 @@ export const getGuardrailsList = async (accessToken: String) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const uiSpendLogDetailsCall = async (
|
||||||
|
accessToken: string,
|
||||||
|
logId: string,
|
||||||
|
start_date: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// Construct base URL
|
||||||
|
let url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/spend/logs/ui/${logId}?start_date=${encodeURIComponent(start_date)}`
|
||||||
|
: `/spend/logs/ui/${logId}?start_date=${encodeURIComponent(start_date)}`;
|
||||||
|
|
||||||
|
console.log("Fetching log details from:", url);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Fetched log details:", data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch log details:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueries, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
import { uiSpendLogsCall } from "../networking";
|
import { uiSpendLogsCall, uiSpendLogDetailsCall } from "../networking";
|
||||||
import { DataTable } from "./table";
|
import { DataTable } from "./table";
|
||||||
import { columns, LogEntry } from "./columns";
|
import { columns, LogEntry } from "./columns";
|
||||||
import { Row } from "@tanstack/react-table";
|
import { RequestViewer } from "./request_viewer";
|
||||||
|
import { prefetchLogDetails } from "./prefetch";
|
||||||
|
|
||||||
interface SpendLogsTableProps {
|
interface SpendLogsTableProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
@ -54,6 +55,8 @@ export default function SpendLogsTable({
|
||||||
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
||||||
const [selectedFilter, setSelectedFilter] = useState("Team ID");
|
const [selectedFilter, setSelectedFilter] = useState("Team ID");
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
@ -82,6 +85,7 @@ export default function SpendLogsTable({
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Update the logs query to use the imported prefetchLogDetails
|
||||||
const logs = useQuery<PaginatedResponse>({
|
const logs = useQuery<PaginatedResponse>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
"logs",
|
"logs",
|
||||||
|
@ -105,13 +109,21 @@ export default function SpendLogsTable({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert times to UTC before formatting
|
console.log("Fetching logs with params:", {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
selectedTeamId,
|
||||||
|
selectedKeyHash,
|
||||||
|
currentPage,
|
||||||
|
pageSize
|
||||||
|
});
|
||||||
|
|
||||||
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
|
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
|
||||||
const formattedEndTime = isCustomDate
|
const formattedEndTime = isCustomDate
|
||||||
? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss")
|
? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss")
|
||||||
: moment().utc().format("YYYY-MM-DD HH:mm:ss");
|
: moment().utc().format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
|
||||||
return await uiSpendLogsCall(
|
const response = await uiSpendLogsCall(
|
||||||
accessToken,
|
accessToken,
|
||||||
selectedKeyHash || undefined,
|
selectedKeyHash || undefined,
|
||||||
selectedTeamId || undefined,
|
selectedTeamId || undefined,
|
||||||
|
@ -121,30 +133,62 @@ export default function SpendLogsTable({
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize
|
pageSize
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("Received logs response:", response);
|
||||||
|
|
||||||
|
// Update prefetchLogDetails call with new parameters
|
||||||
|
prefetchLogDetails(response.data, formattedStartTime, accessToken, queryClient);
|
||||||
|
|
||||||
|
return response;
|
||||||
},
|
},
|
||||||
enabled: !!accessToken && !!token && !!userRole && !!userID,
|
enabled: !!accessToken && !!token && !!userRole && !!userID,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Move useQueries before the early return
|
||||||
|
const logDetailsQueries = useQueries({
|
||||||
|
queries: logs.data?.data?.map((log) => ({
|
||||||
|
queryKey: ["logDetails", log.request_id, moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")],
|
||||||
|
queryFn: () => uiSpendLogDetailsCall(accessToken!, log.request_id, moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")),
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
cacheTime: 10 * 60 * 1000,
|
||||||
|
enabled: !!log.request_id,
|
||||||
|
})) || []
|
||||||
|
});
|
||||||
|
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
console.log(
|
console.log("got None values for one of accessToken, token, userRole, userID");
|
||||||
"got None values for one of accessToken, token, userRole, userID",
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consolidate log details from queries
|
||||||
|
const logDetails: Record<string, any> = {};
|
||||||
|
logDetailsQueries.forEach((q, index) => {
|
||||||
|
const log = logs.data?.data[index];
|
||||||
|
if (log && q.data) {
|
||||||
|
logDetails[log.request_id] = q.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modify the filtered data to include log details
|
||||||
const filteredData =
|
const filteredData =
|
||||||
logs.data?.data?.filter((log) => {
|
logs.data?.data
|
||||||
|
?.filter((log) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
!searchTerm ||
|
!searchTerm ||
|
||||||
log.request_id.includes(searchTerm) ||
|
log.request_id.includes(searchTerm) ||
|
||||||
log.model.includes(searchTerm) ||
|
log.model.includes(searchTerm) ||
|
||||||
(log.user && log.user.includes(searchTerm));
|
(log.user && log.user.includes(searchTerm));
|
||||||
|
|
||||||
// No need for additional filtering since we're now handling this in the API call
|
|
||||||
return matchesSearch;
|
return matchesSearch;
|
||||||
}) || [];
|
})
|
||||||
|
.map(log => ({
|
||||||
|
...log,
|
||||||
|
// Include messages/response from cached details
|
||||||
|
messages: logDetails[log.request_id]?.messages || [],
|
||||||
|
response: logDetails[log.request_id]?.response || {},
|
||||||
|
})) || [];
|
||||||
|
|
||||||
// Add this function to handle manual refresh
|
// Add this function to handle manual refresh
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
|
@ -529,155 +573,3 @@ export default function SpendLogsTable({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
|
||||||
const formatData = (input: any) => {
|
|
||||||
if (typeof input === "string") {
|
|
||||||
try {
|
|
||||||
return JSON.parse(input);
|
|
||||||
} catch {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return input;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-gray-50 space-y-6">
|
|
||||||
{/* Combined Info Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium ">Request Details</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 p-4 ">
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Request ID:</span>
|
|
||||||
<span>{row.original.request_id}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Api Key:</span>
|
|
||||||
<span>{row.original.api_key}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Team ID:</span>
|
|
||||||
<span>{row.original.team_id}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Model:</span>
|
|
||||||
<span>{row.original.model}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Api Base:</span>
|
|
||||||
<span>{row.original.api_base}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Call Type:</span>
|
|
||||||
<span>{row.original.call_type}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Spend:</span>
|
|
||||||
<span>{row.original.spend}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Total Tokens:</span>
|
|
||||||
<span>{row.original.total_tokens}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Prompt Tokens:</span>
|
|
||||||
<span>{row.original.prompt_tokens}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Completion Tokens:</span>
|
|
||||||
<span>{row.original.completion_tokens}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Start Time:</span>
|
|
||||||
<span>{row.original.startTime}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">End Time:</span>
|
|
||||||
<span>{row.original.endTime}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Cache Hit:</span>
|
|
||||||
<span>{row.original.cache_hit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Cache Key:</span>
|
|
||||||
<span>{row.original.cache_key}</span>
|
|
||||||
</div>
|
|
||||||
{row?.original?.requester_ip_address && (
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Request IP Address:</span>
|
|
||||||
<span>{row?.original?.requester_ip_address}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Request Tags</h3>
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
|
||||||
{JSON.stringify(formatData(row.original.request_tags), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Request</h3>
|
|
||||||
{/* <div>
|
|
||||||
<button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
Expand
|
|
||||||
</button>
|
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
JSON
|
|
||||||
</button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
|
||||||
{JSON.stringify(formatData(row.original.messages), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Response Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Response</h3>
|
|
||||||
<div>
|
|
||||||
{/* <button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
Expand
|
|
||||||
</button>
|
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
JSON
|
|
||||||
</button> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
|
||||||
{JSON.stringify(formatData(row.original.response), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Metadata Card */}
|
|
||||||
{row.original.metadata &&
|
|
||||||
Object.keys(row.original.metadata).length > 0 && (
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Metadata</h3>
|
|
||||||
{/* <div>
|
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
JSON
|
|
||||||
</button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm ">
|
|
||||||
{JSON.stringify(row.original.metadata, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
23
ui/litellm-dashboard/src/components/view_logs/prefetch.ts
Normal file
23
ui/litellm-dashboard/src/components/view_logs/prefetch.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
import { uiSpendLogDetailsCall } from "../networking";
|
||||||
|
import { LogEntry } from "./columns";
|
||||||
|
|
||||||
|
export const prefetchLogDetails = (
|
||||||
|
logs: LogEntry[],
|
||||||
|
formattedStartTime: string,
|
||||||
|
accessToken: string,
|
||||||
|
queryClient: QueryClient
|
||||||
|
) => {
|
||||||
|
logs.forEach((log) => {
|
||||||
|
if (log.request_id) {
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ["logDetails", log.request_id, formattedStartTime],
|
||||||
|
queryFn: () => uiSpendLogDetailsCall(accessToken, log.request_id, formattedStartTime),
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(`Failed to prefetch details for log: ${log.request_id}`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
155
ui/litellm-dashboard/src/components/view_logs/request_viewer.tsx
Normal file
155
ui/litellm-dashboard/src/components/view_logs/request_viewer.tsx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import { Row } from "@tanstack/react-table";
|
||||||
|
import { LogEntry } from "./columns";
|
||||||
|
|
||||||
|
export function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
|
const formatData = (input: any) => {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
try {
|
||||||
|
return JSON.parse(input);
|
||||||
|
} catch {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-gray-50 space-y-6">
|
||||||
|
{/* Combined Info Card */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="p-4 border-b">
|
||||||
|
<h3 className="text-lg font-medium ">Request Details</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 p-4 ">
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Request ID:</span>
|
||||||
|
<span>{row.original.request_id}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Api Key:</span>
|
||||||
|
<span>{row.original.api_key}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Team ID:</span>
|
||||||
|
<span>{row.original.team_id}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Model:</span>
|
||||||
|
<span>{row.original.model}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Api Base:</span>
|
||||||
|
<span>{row.original.api_base}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Call Type:</span>
|
||||||
|
<span>{row.original.call_type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Spend:</span>
|
||||||
|
<span>{row.original.spend}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Total Tokens:</span>
|
||||||
|
<span>{row.original.total_tokens}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Prompt Tokens:</span>
|
||||||
|
<span>{row.original.prompt_tokens}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Completion Tokens:</span>
|
||||||
|
<span>{row.original.completion_tokens}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Start Time:</span>
|
||||||
|
<span>{row.original.startTime}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">End Time:</span>
|
||||||
|
<span>{row.original.endTime}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Cache Hit:</span>
|
||||||
|
<span>{row.original.cache_hit}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Cache Key:</span>
|
||||||
|
<span>{row.original.cache_key}</span>
|
||||||
|
</div>
|
||||||
|
{row?.original?.requester_ip_address && (
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Request IP Address:</span>
|
||||||
|
<span>{row?.original?.requester_ip_address}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Card */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h3 className="text-lg font-medium">Request Tags</h3>
|
||||||
|
</div>
|
||||||
|
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||||
|
{JSON.stringify(formatData(row.original.request_tags), null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Card */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h3 className="text-lg font-medium">Request</h3>
|
||||||
|
{/* <div>
|
||||||
|
<button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||||
|
Expand
|
||||||
|
</button>
|
||||||
|
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||||
|
{JSON.stringify(formatData(row.original.messages), null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Response Card */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h3 className="text-lg font-medium">Response</h3>
|
||||||
|
<div>
|
||||||
|
{/* <button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||||
|
Expand
|
||||||
|
</button>
|
||||||
|
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||||
|
JSON
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||||
|
{JSON.stringify(formatData(row.original.response), null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata Card */}
|
||||||
|
{row.original.metadata &&
|
||||||
|
Object.keys(row.original.metadata).length > 0 && (
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h3 className="text-lg font-medium">Metadata</h3>
|
||||||
|
{/* <div>
|
||||||
|
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||||
|
JSON
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<pre className="p-4 text-wrap overflow-auto text-sm ">
|
||||||
|
{JSON.stringify(row.original.metadata, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue