(Feat) - Show Error Logs on LiteLLM UI (#8904)

* fix test_moderations_bad_model

* use async_post_call_failure_hook

* basic logging errors in DB

* show status on ui

* show status on ui

* ui show request / response side by side

* stash fixes

* working, track raw request

* track error info in metadata

* fix showing error / request / response logs

* show traceback on error viewer

* ui with traceback of error

* fix async_post_call_failure_hook

* fix(http_parsing_utils.py): orjson can throw errors on some emoji's in text, default to json.loads

* test_get_error_information

* fix code quality

* rename proxy track cost callback test

* _should_store_errors_in_spend_logs

* feature flag error logs

* Revert "_should_store_errors_in_spend_logs"

This reverts commit 7f345df477.

* Revert "feature flag error logs"

This reverts commit 0e90c022bb.

* test_spend_logs_payload

* fix OTEL log_db_metrics

* fix import json

* fix ui linting error

* test_async_post_call_failure_hook

* test_chat_completion_bad_model_with_spend_logs

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>
This commit is contained in:
Ishaan Jaff 2025-02-28 20:10:09 -08:00 committed by GitHub
parent 0f87def2f5
commit 37e116235a
18 changed files with 845 additions and 294 deletions

View file

@ -36,7 +36,7 @@ import TabItem from '@theme/TabItem';
- Virtual Key Rate Limit - Virtual Key Rate Limit
- User Rate Limit - User Rate Limit
- Team Limit - Team Limit
- The `_PROXY_track_cost_callback` updates spend / usage in the LiteLLM database. [Here is everything tracked in the DB per request](https://github.com/BerriAI/litellm/blob/ba41a72f92a9abf1d659a87ec880e8e319f87481/schema.prisma#L172) - The `_ProxyDBLogger` updates spend / usage in the LiteLLM database. [Here is everything tracked in the DB per request](https://github.com/BerriAI/litellm/blob/ba41a72f92a9abf1d659a87ec880e8e319f87481/schema.prisma#L172)
## Frequently Asked Questions ## Frequently Asked Questions

View file

@ -3114,10 +3114,26 @@ class StandardLoggingPayloadSetup:
str(original_exception.__class__.__name__) if original_exception else "" str(original_exception.__class__.__name__) if original_exception else ""
) )
_llm_provider_in_exception = getattr(original_exception, "llm_provider", "") _llm_provider_in_exception = getattr(original_exception, "llm_provider", "")
# Get traceback information (first 100 lines)
traceback_info = ""
if original_exception:
tb = getattr(original_exception, "__traceback__", None)
if tb:
import traceback
tb_lines = traceback.format_tb(tb)
traceback_info = "".join(tb_lines[:100]) # Limit to first 100 lines
# Get additional error details
error_message = str(original_exception)
return StandardLoggingPayloadErrorInformation( return StandardLoggingPayloadErrorInformation(
error_code=error_status, error_code=error_status,
error_class=error_class, error_class=error_class,
llm_provider=_llm_provider_in_exception, llm_provider=_llm_provider_in_exception,
traceback=traceback_info,
error_message=error_message if original_exception else "",
) )
@staticmethod @staticmethod

View file

@ -26,6 +26,8 @@ from litellm.types.utils import (
ModelResponse, ModelResponse,
ProviderField, ProviderField,
StandardCallbackDynamicParams, StandardCallbackDynamicParams,
StandardLoggingPayloadErrorInformation,
StandardLoggingPayloadStatus,
StandardPassThroughResponseObject, StandardPassThroughResponseObject,
TextCompletionResponse, TextCompletionResponse,
) )
@ -1854,6 +1856,8 @@ class SpendLogsMetadata(TypedDict):
] # special param to log k,v pairs to spendlogs for a call ] # special param to log k,v pairs to spendlogs for a call
requester_ip_address: Optional[str] requester_ip_address: Optional[str]
applied_guardrails: Optional[List[str]] applied_guardrails: Optional[List[str]]
status: StandardLoggingPayloadStatus
error_information: Optional[StandardLoggingPayloadErrorInformation]
class SpendLogsPayload(TypedDict): class SpendLogsPayload(TypedDict):

View file

@ -64,10 +64,10 @@ def log_db_metrics(func):
# in litellm custom callbacks kwargs is passed as arg[0] # in litellm custom callbacks kwargs is passed as arg[0]
# https://docs.litellm.ai/docs/observability/custom_callback#callback-functions # https://docs.litellm.ai/docs/observability/custom_callback#callback-functions
args is not None args is not None
and len(args) > 0 and len(args) > 1
and isinstance(args[0], dict) and isinstance(args[1], dict)
): ):
passed_kwargs = args[0] passed_kwargs = args[1]
parent_otel_span = _get_parent_otel_span_from_kwargs( parent_otel_span = _get_parent_otel_span_from_kwargs(
kwargs=passed_kwargs kwargs=passed_kwargs
) )

View file

@ -1,22 +1,87 @@
import asyncio import asyncio
import traceback import traceback
from typing import Optional, Union, cast from datetime import datetime
from typing import Any, Optional, Union, cast
import litellm import litellm
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.core_helpers import (
_get_parent_otel_span_from_kwargs, _get_parent_otel_span_from_kwargs,
get_litellm_metadata_from_kwargs, get_litellm_metadata_from_kwargs,
) )
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.auth_checks import log_db_metrics from litellm.proxy.auth.auth_checks import log_db_metrics
from litellm.types.utils import StandardLoggingPayload from litellm.types.utils import (
StandardLoggingPayload,
StandardLoggingUserAPIKeyMetadata,
)
from litellm.utils import get_end_user_id_for_cost_tracking from litellm.utils import get_end_user_id_for_cost_tracking
class _ProxyDBLogger(CustomLogger):
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
await self._PROXY_track_cost_callback(
kwargs, response_obj, start_time, end_time
)
async def async_post_call_failure_hook(
self,
request_data: dict,
original_exception: Exception,
user_api_key_dict: UserAPIKeyAuth,
):
from litellm.proxy.proxy_server import update_database
_metadata = dict(
StandardLoggingUserAPIKeyMetadata(
user_api_key_hash=user_api_key_dict.api_key,
user_api_key_alias=user_api_key_dict.key_alias,
user_api_key_user_email=user_api_key_dict.user_email,
user_api_key_user_id=user_api_key_dict.user_id,
user_api_key_team_id=user_api_key_dict.team_id,
user_api_key_org_id=user_api_key_dict.org_id,
user_api_key_team_alias=user_api_key_dict.team_alias,
user_api_key_end_user_id=user_api_key_dict.end_user_id,
)
)
_metadata["user_api_key"] = user_api_key_dict.api_key
_metadata["status"] = "failure"
_metadata["error_information"] = (
StandardLoggingPayloadSetup.get_error_information(
original_exception=original_exception,
)
)
existing_metadata: dict = request_data.get("metadata", None) or {}
existing_metadata.update(_metadata)
existing_metadata["proxy_server_request"] = (
request_data.get("proxy_server_request", {}) or {}
)
request_data["litellm_params"] = {}
request_data["litellm_params"]["metadata"] = existing_metadata
await update_database(
token=user_api_key_dict.api_key,
response_cost=0.0,
user_id=user_api_key_dict.user_id,
end_user_id=user_api_key_dict.end_user_id,
team_id=user_api_key_dict.team_id,
kwargs=request_data,
completion_response=original_exception,
start_time=datetime.now(),
end_time=datetime.now(),
org_id=user_api_key_dict.org_id,
)
@log_db_metrics @log_db_metrics
async def _PROXY_track_cost_callback( async def _PROXY_track_cost_callback(
self,
kwargs, # kwargs to completion kwargs, # kwargs to completion
completion_response: litellm.ModelResponse, # response from completion completion_response: Optional[
Union[litellm.ModelResponse, Any]
], # response from completion
start_time=None, start_time=None,
end_time=None, # start/end time for completion end_time=None, # start/end time for completion
): ):
@ -132,7 +197,9 @@ async def _PROXY_track_cost_callback(
failing_model=model, failing_model=model,
) )
) )
verbose_proxy_logger.exception("Error in tracking cost callback - %s", str(e)) verbose_proxy_logger.exception(
"Error in tracking cost callback - %s", str(e)
)
def _should_track_cost_callback( def _should_track_cost_callback(

View file

@ -8,4 +8,5 @@ model_list:
general_settings: general_settings:
store_model_in_db: true store_model_in_db: true
store_prompts_in_spend_logs: true

View file

@ -114,6 +114,7 @@ from litellm.litellm_core_utils.core_helpers import (
_get_parent_otel_span_from_kwargs, _get_parent_otel_span_from_kwargs,
get_litellm_metadata_from_kwargs, get_litellm_metadata_from_kwargs,
) )
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.proxy._types import * from litellm.proxy._types import *
from litellm.proxy.analytics_endpoints.analytics_endpoints import ( from litellm.proxy.analytics_endpoints.analytics_endpoints import (
@ -178,7 +179,7 @@ from litellm.proxy.hooks.prompt_injection_detection import (
_OPTIONAL_PromptInjectionDetection, _OPTIONAL_PromptInjectionDetection,
) )
from litellm.proxy.hooks.proxy_failure_handler import _PROXY_failure_handler from litellm.proxy.hooks.proxy_failure_handler import _PROXY_failure_handler
from litellm.proxy.hooks.proxy_track_cost_callback import _PROXY_track_cost_callback from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
from litellm.proxy.management_endpoints.budget_management_endpoints import ( from litellm.proxy.management_endpoints.budget_management_endpoints import (
router as budget_management_router, router as budget_management_router,
@ -937,10 +938,7 @@ def load_from_azure_key_vault(use_azure_key_vault: bool = False):
def cost_tracking(): def cost_tracking():
global prisma_client global prisma_client
if prisma_client is not None: if prisma_client is not None:
if isinstance(litellm._async_success_callback, list): litellm.logging_callback_manager.add_litellm_callback(_ProxyDBLogger())
verbose_proxy_logger.debug("setting litellm success callback to track cost")
if (_PROXY_track_cost_callback) not in litellm._async_success_callback: # type: ignore
litellm.logging_callback_manager.add_litellm_async_success_callback(_PROXY_track_cost_callback) # type: ignore
def error_tracking(): def error_tracking():
@ -3727,9 +3725,14 @@ async def chat_completion( # noqa: PLR0915
timeout = getattr( timeout = getattr(
e, "timeout", None e, "timeout", None
) # returns the timeout set by the wrapper. Used for testing if model-specific timeout are set correctly ) # returns the timeout set by the wrapper. Used for testing if model-specific timeout are set correctly
_litellm_logging_obj: Optional[LiteLLMLoggingObj] = data.get(
"litellm_logging_obj", None
)
custom_headers = get_custom_headers( custom_headers = get_custom_headers(
user_api_key_dict=user_api_key_dict, user_api_key_dict=user_api_key_dict,
call_id=(
_litellm_logging_obj.litellm_call_id if _litellm_logging_obj else None
),
version=version, version=version,
response_cost=0, response_cost=0,
model_region=getattr(user_api_key_dict, "allowed_model_region", ""), model_region=getattr(user_api_key_dict, "allowed_model_region", ""),

View file

@ -47,6 +47,8 @@ def _get_spend_logs_metadata(
requester_ip_address=None, requester_ip_address=None,
additional_usage_values=None, additional_usage_values=None,
applied_guardrails=None, applied_guardrails=None,
status=None or "success",
error_information=None,
) )
verbose_proxy_logger.debug( verbose_proxy_logger.debug(
"getting payload for SpendLogs, available keys in metadata: " "getting payload for SpendLogs, available keys in metadata: "
@ -161,7 +163,6 @@ def get_logging_payload( # noqa: PLR0915
import time import time
id = f"{id}_cache_hit{time.time()}" # SpendLogs does not allow duplicate request_id id = f"{id}_cache_hit{time.time()}" # SpendLogs does not allow duplicate request_id
try: try:
payload: SpendLogsPayload = SpendLogsPayload( payload: SpendLogsPayload = SpendLogsPayload(
request_id=str(id), request_id=str(id),
@ -193,7 +194,9 @@ def get_logging_payload( # noqa: PLR0915
model_id=_model_id, model_id=_model_id,
requester_ip_address=clean_metadata.get("requester_ip_address", None), requester_ip_address=clean_metadata.get("requester_ip_address", None),
custom_llm_provider=kwargs.get("custom_llm_provider", ""), custom_llm_provider=kwargs.get("custom_llm_provider", ""),
messages=_get_messages_for_spend_logs_payload(standard_logging_payload), messages=_get_messages_for_spend_logs_payload(
standard_logging_payload=standard_logging_payload, metadata=metadata
),
response=_get_response_for_spend_logs_payload(standard_logging_payload), response=_get_response_for_spend_logs_payload(standard_logging_payload),
) )
@ -293,12 +296,19 @@ async def get_spend_by_team_and_customer(
def _get_messages_for_spend_logs_payload( def _get_messages_for_spend_logs_payload(
payload: Optional[StandardLoggingPayload], standard_logging_payload: Optional[StandardLoggingPayload],
metadata: Optional[dict] = None,
) -> str: ) -> str:
if payload is None:
return "{}"
if _should_store_prompts_and_responses_in_spend_logs(): if _should_store_prompts_and_responses_in_spend_logs():
return json.dumps(payload.get("messages", {})) metadata = metadata or {}
if metadata.get("status", None) == "failure":
_proxy_server_request = metadata.get("proxy_server_request", {})
_request_body = _proxy_server_request.get("body", {}) or {}
return json.dumps(_request_body, default=str)
else:
if standard_logging_payload is None:
return "{}"
return json.dumps(standard_logging_payload.get("messages", {}))
return "{}" return "{}"

View file

@ -784,13 +784,17 @@ class ProxyLogging:
else: else:
_callback = callback # type: ignore _callback = callback # type: ignore
if _callback is not None and isinstance(_callback, CustomLogger): if _callback is not None and isinstance(_callback, CustomLogger):
await _callback.async_post_call_failure_hook( asyncio.create_task(
_callback.async_post_call_failure_hook(
request_data=request_data, request_data=request_data,
user_api_key_dict=user_api_key_dict, user_api_key_dict=user_api_key_dict,
original_exception=original_exception, original_exception=original_exception,
) )
)
except Exception as e: except Exception as e:
raise e verbose_proxy_logger.exception(
f"[Non-Blocking] Error in post_call_failure_hook: {e}"
)
return return
def _is_proxy_only_error( def _is_proxy_only_error(

View file

@ -1606,6 +1606,8 @@ class StandardLoggingPayloadErrorInformation(TypedDict, total=False):
error_code: Optional[str] error_code: Optional[str]
error_class: Optional[str] error_class: Optional[str]
llm_provider: Optional[str] llm_provider: Optional[str]
traceback: Optional[str]
error_message: Optional[str]
class StandardLoggingGuardrailInformation(TypedDict, total=False): class StandardLoggingGuardrailInformation(TypedDict, total=False):

View file

@ -0,0 +1,80 @@
import json
import os
import sys
import pytest
from fastapi.testclient import TestClient
sys.path.insert(
0, os.path.abspath("../../../..")
) # Adds the parent directory to the system path
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger
from litellm.types.utils import StandardLoggingPayload
@pytest.mark.asyncio
async def test_async_post_call_failure_hook():
# Setup
logger = _ProxyDBLogger()
# Mock user_api_key_dict
user_api_key_dict = UserAPIKeyAuth(
api_key="test_api_key",
key_alias="test_alias",
user_email="test@example.com",
user_id="test_user_id",
team_id="test_team_id",
org_id="test_org_id",
team_alias="test_team_alias",
end_user_id="test_end_user_id",
)
# Mock request data
request_data = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"metadata": {"original_key": "original_value"},
"proxy_server_request": {"request_id": "test_request_id"},
}
# Mock exception
original_exception = Exception("Test exception")
# Mock update_database function
with patch(
"litellm.proxy.proxy_server.update_database", new_callable=AsyncMock
) as mock_update_database:
# Call the method
await logger.async_post_call_failure_hook(
request_data=request_data,
original_exception=original_exception,
user_api_key_dict=user_api_key_dict,
)
# Assertions
mock_update_database.assert_called_once()
# Check the arguments passed to update_database
call_args = mock_update_database.call_args[1]
assert call_args["token"] == "test_api_key"
assert call_args["response_cost"] == 0.0
assert call_args["user_id"] == "test_user_id"
assert call_args["end_user_id"] == "test_end_user_id"
assert call_args["team_id"] == "test_team_id"
assert call_args["org_id"] == "test_org_id"
assert call_args["completion_response"] == original_exception
# Check that metadata was properly updated
assert "litellm_params" in call_args["kwargs"]
metadata = call_args["kwargs"]["litellm_params"]["metadata"]
assert metadata["user_api_key"] == "test_api_key"
assert metadata["status"] == "failure"
assert "error_information" in metadata
assert metadata["original_key"] == "original_value"
assert "proxy_server_request" in metadata
assert metadata["proxy_server_request"]["request_id"] == "test_request_id"

View file

@ -96,6 +96,8 @@ def test_spend_logs_payload(model_id: Optional[str]):
}, },
"api_base": "https://openai-gpt-4-test-v-1.openai.azure.com/", "api_base": "https://openai-gpt-4-test-v-1.openai.azure.com/",
"caching_groups": None, "caching_groups": None,
"error_information": None,
"status": "success",
"raw_request": "\n\nPOST Request Sent from LiteLLM:\ncurl -X POST \\\nhttps://openai-gpt-4-test-v-1.openai.azure.com//openai/ \\\n-H 'Authorization: *****' \\\n-d '{'model': 'chatgpt-v-2', 'messages': [{'role': 'system', 'content': 'you are a helpful assistant.\\n'}, {'role': 'user', 'content': 'bom dia'}], 'stream': False, 'max_tokens': 10, 'user': '116544810872468347480', 'extra_body': {}}'\n", "raw_request": "\n\nPOST Request Sent from LiteLLM:\ncurl -X POST \\\nhttps://openai-gpt-4-test-v-1.openai.azure.com//openai/ \\\n-H 'Authorization: *****' \\\n-d '{'model': 'chatgpt-v-2', 'messages': [{'role': 'system', 'content': 'you are a helpful assistant.\\n'}, {'role': 'user', 'content': 'bom dia'}], 'stream': False, 'max_tokens': 10, 'user': '116544810872468347480', 'extra_body': {}}'\n",
}, },
"model_info": { "model_info": {

View file

@ -413,6 +413,7 @@ def test_get_error_information():
assert result["error_code"] == "429" assert result["error_code"] == "429"
assert result["error_class"] == "RateLimitError" assert result["error_class"] == "RateLimitError"
assert result["llm_provider"] == "openai" assert result["llm_provider"] == "openai"
assert result["error_message"] == "litellm.RateLimitError: Test error"
def test_get_response_time(): def test_get_response_time():

View file

@ -507,9 +507,9 @@ def test_call_with_user_over_budget(prisma_client):
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
resp = ModelResponse( resp = ModelResponse(
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac", id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
@ -526,7 +526,7 @@ def test_call_with_user_over_budget(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"stream": False, "stream": False,
"litellm_params": { "litellm_params": {
@ -604,9 +604,9 @@ def test_call_with_end_user_over_budget(prisma_client):
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
resp = ModelResponse( resp = ModelResponse(
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac", id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
@ -623,7 +623,7 @@ def test_call_with_end_user_over_budget(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"stream": False, "stream": False,
"litellm_params": { "litellm_params": {
@ -711,9 +711,9 @@ def test_call_with_proxy_over_budget(prisma_client):
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
resp = ModelResponse( resp = ModelResponse(
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac", id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
@ -730,7 +730,7 @@ def test_call_with_proxy_over_budget(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"stream": False, "stream": False,
"litellm_params": { "litellm_params": {
@ -802,9 +802,9 @@ def test_call_with_user_over_budget_stream(prisma_client):
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
resp = ModelResponse( resp = ModelResponse(
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac", id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
@ -821,7 +821,7 @@ def test_call_with_user_over_budget_stream(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"stream": True, "stream": True,
"complete_streaming_response": resp, "complete_streaming_response": resp,
@ -908,9 +908,9 @@ def test_call_with_proxy_over_budget_stream(prisma_client):
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
resp = ModelResponse( resp = ModelResponse(
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac", id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
@ -927,7 +927,7 @@ def test_call_with_proxy_over_budget_stream(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"stream": True, "stream": True,
"complete_streaming_response": resp, "complete_streaming_response": resp,
@ -1519,9 +1519,9 @@ def test_call_with_key_over_budget(prisma_client):
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.caching.caching import Cache from litellm.caching.caching import Cache
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
litellm.cache = Cache() litellm.cache = Cache()
import time import time
@ -1544,7 +1544,7 @@ def test_call_with_key_over_budget(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"model": "chatgpt-v-2", "model": "chatgpt-v-2",
"stream": False, "stream": False,
@ -1636,9 +1636,7 @@ def test_call_with_key_over_budget_no_cache(prisma_client):
print("result from user auth with new key", result) print("result from user auth with new key", result)
# update spend using track_cost callback, make 2nd request, it should fail # update spend using track_cost callback, make 2nd request, it should fail
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
)
from litellm.proxy.proxy_server import user_api_key_cache from litellm.proxy.proxy_server import user_api_key_cache
user_api_key_cache.in_memory_cache.cache_dict = {} user_api_key_cache.in_memory_cache.cache_dict = {}
@ -1668,7 +1666,8 @@ def test_call_with_key_over_budget_no_cache(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( proxy_db_logger = _ProxyDBLogger()
await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"model": "chatgpt-v-2", "model": "chatgpt-v-2",
"stream": False, "stream": False,
@ -1874,9 +1873,9 @@ async def test_call_with_key_never_over_budget(prisma_client):
import uuid import uuid
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
request_id = f"chatcmpl-{uuid.uuid4()}" request_id = f"chatcmpl-{uuid.uuid4()}"
@ -1897,7 +1896,7 @@ async def test_call_with_key_never_over_budget(prisma_client):
prompt_tokens=210000, completion_tokens=200000, total_tokens=41000 prompt_tokens=210000, completion_tokens=200000, total_tokens=41000
), ),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"model": "chatgpt-v-2", "model": "chatgpt-v-2",
"stream": False, "stream": False,
@ -1965,9 +1964,9 @@ async def test_call_with_key_over_budget_stream(prisma_client):
import uuid import uuid
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
) proxy_db_logger = _ProxyDBLogger()
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}" request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
resp = ModelResponse( resp = ModelResponse(
@ -1985,7 +1984,7 @@ async def test_call_with_key_over_budget_stream(prisma_client):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"call_type": "acompletion", "call_type": "acompletion",
"model": "sagemaker-chatgpt-v-2", "model": "sagemaker-chatgpt-v-2",
@ -2409,9 +2408,7 @@ async def track_cost_callback_helper_fn(generated_key: str, user_id: str):
import uuid import uuid
from litellm import Choices, Message, ModelResponse, Usage from litellm import Choices, Message, ModelResponse, Usage
from litellm.proxy.proxy_server import ( from litellm.proxy.proxy_server import _ProxyDBLogger
_PROXY_track_cost_callback as track_cost_callback,
)
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}" request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
resp = ModelResponse( resp = ModelResponse(
@ -2429,7 +2426,8 @@ async def track_cost_callback_helper_fn(generated_key: str, user_id: str):
model="gpt-35-turbo", # azure always has model written like this model="gpt-35-turbo", # azure always has model written like this
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410), usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
) )
await track_cost_callback( proxy_db_logger = _ProxyDBLogger()
await proxy_db_logger._PROXY_track_cost_callback(
kwargs={ kwargs={
"call_type": "acompletion", "call_type": "acompletion",
"model": "sagemaker-chatgpt-v-2", "model": "sagemaker-chatgpt-v-2",

View file

@ -115,3 +115,97 @@ def test_missing_model_parameter_curl(curl_command):
print("error in response", json.dumps(response, indent=4)) print("error in response", json.dumps(response, indent=4))
assert "litellm.BadRequestError" in response["error"]["message"] assert "litellm.BadRequestError" in response["error"]["message"]
@pytest.mark.asyncio
async def test_chat_completion_bad_model_with_spend_logs():
"""
Tests that Error Logs are created for failed requests
"""
import json
key = generate_key_sync()
# Use httpx to make the request and capture headers
url = "http://0.0.0.0:4000/v1/chat/completions"
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
payload = {
"model": "non-existent-model",
"messages": [{"role": "user", "content": "Hello!"}],
}
with httpx.Client() as client:
response = client.post(url, headers=headers, json=payload)
# Extract the litellm call ID from headers
litellm_call_id = response.headers.get("x-litellm-call-id")
print(f"Status code: {response.status_code}")
print(f"Headers: {dict(response.headers)}")
print(f"LiteLLM Call ID: {litellm_call_id}")
# Parse the JSON response body
try:
response_body = response.json()
print(f"Error response: {json.dumps(response_body, indent=4)}")
except json.JSONDecodeError:
print(f"Could not parse response body as JSON: {response.text}")
assert (
litellm_call_id is not None
), "Failed to get LiteLLM Call ID from response headers"
print("waiting for flushing error log to db....")
await asyncio.sleep(15)
# Now query the spend logs
url = "http://0.0.0.0:4000/spend/logs?request_id=" + litellm_call_id
headers = {"Authorization": f"Bearer sk-1234", "Content-Type": "application/json"}
with httpx.Client() as client:
response = client.get(
url,
headers=headers,
)
assert (
response.status_code == 200
), f"Failed to get spend logs: {response.status_code}"
spend_logs = response.json()
# Print the spend logs payload
print(f"Spend logs response: {json.dumps(spend_logs, indent=4)}")
# Verify we have logs for the failed request
assert len(spend_logs) > 0, "No spend logs found"
# Check if the error is recorded in the logs
log_entry = spend_logs[0] # Should be the specific log for our litellm_call_id
# Verify the structure of the log entry
assert log_entry["request_id"] == litellm_call_id
assert log_entry["model"] == "non-existent-model"
assert log_entry["model_group"] == "non-existent-model"
assert log_entry["spend"] == 0.0
assert log_entry["total_tokens"] == 0
assert log_entry["prompt_tokens"] == 0
assert log_entry["completion_tokens"] == 0
# Verify metadata fields
assert log_entry["metadata"]["status"] == "failure"
assert "user_api_key" in log_entry["metadata"]
assert "error_information" in log_entry["metadata"]
# Verify error information
error_info = log_entry["metadata"]["error_information"]
assert "traceback" in error_info
assert error_info["error_code"] == "400"
assert error_info["error_class"] == "BadRequestError"
assert "litellm.BadRequestError" in error_info["error_message"]
assert "non-existent-model" in error_info["error_message"]
# Verify request details
assert log_entry["cache_hit"] == "False"
assert log_entry["messages"]["model"] == "non-existent-model"
assert log_entry["messages"]["messages"][0]["role"] == "user"
assert log_entry["messages"]["messages"][0]["content"] == "Hello!"
assert log_entry["response"] == {}

View file

@ -0,0 +1,164 @@
import React from 'react';
interface ErrorViewerProps {
errorInfo: {
error_class?: string;
error_message?: string;
traceback?: string;
llm_provider?: string;
error_code?: string | number;
};
}
export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
const [expandedFrames, setExpandedFrames] = React.useState<{[key: number]: boolean}>({});
const [allExpanded, setAllExpanded] = React.useState(false);
// Toggle individual frame
const toggleFrame = (index: number) => {
setExpandedFrames(prev => ({
...prev,
[index]: !prev[index]
}));
};
// Toggle all frames
const toggleAllFrames = () => {
const newState = !allExpanded;
setAllExpanded(newState);
if (tracebackFrames.length > 0) {
const newExpandedState: {[key: number]: boolean} = {};
tracebackFrames.forEach((_, idx) => {
newExpandedState[idx] = newState;
});
setExpandedFrames(newExpandedState);
}
};
// Parse traceback into frames
const parseTraceback = (traceback: string) => {
if (!traceback) return [];
// Extract file paths, line numbers and code from traceback
const fileLineRegex = /File "([^"]+)", line (\d+)/g;
const matches = Array.from(traceback.matchAll(fileLineRegex));
// Create simplified frames
return matches.map(match => {
const filePath = match[1];
const lineNumber = match[2];
const fileName = filePath.split('/').pop() || filePath;
// Extract the context around this frame
const matchIndex = match.index || 0;
const nextMatchIndex = traceback.indexOf('File "', matchIndex + 1);
const frameContent = nextMatchIndex > -1
? traceback.substring(matchIndex, nextMatchIndex).trim()
: traceback.substring(matchIndex).trim();
// Try to extract the code line
const lines = frameContent.split('\n');
let code = '';
if (lines.length > 1) {
code = lines[lines.length - 1].trim();
}
return {
filePath,
fileName,
lineNumber,
code,
inFunction: frameContent.includes(' in ')
? frameContent.split(' in ')[1].split('\n')[0]
: ''
};
});
};
const tracebackFrames = errorInfo.traceback ? parseTraceback(errorInfo.traceback) : [];
return (
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">
<h3 className="text-lg font-medium flex items-center text-red-600">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
Error Details
</h3>
</div>
<div className="p-4">
<div className="bg-red-50 rounded-md p-4 mb-4">
<div className="flex">
<span className="text-red-800 font-medium w-20">Type:</span>
<span className="text-red-700">{errorInfo.error_class || "Unknown Error"}</span>
</div>
<div className="flex mt-2">
<span className="text-red-800 font-medium w-20">Message:</span>
<span className="text-red-700">{errorInfo.error_message || "Unknown error occurred"}</span>
</div>
</div>
{errorInfo.traceback && (
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium">Traceback</h4>
<div className="flex items-center space-x-4">
<button
onClick={toggleAllFrames}
className="text-gray-500 hover:text-gray-700 flex items-center text-sm"
>
{allExpanded ? "Collapse All" : "Expand All"}
</button>
<button
onClick={() => navigator.clipboard.writeText(errorInfo.traceback || "")}
className="text-gray-500 hover:text-gray-700 flex items-center"
title="Copy traceback"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span className="ml-1">Copy</span>
</button>
</div>
</div>
<div className="bg-white rounded-md border border-gray-200 overflow-hidden shadow-sm">
{tracebackFrames.map((frame, index) => (
<div key={index} className="border-b border-gray-200 last:border-b-0">
<div
className="px-4 py-2 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => toggleFrame(index)}
>
<div className="flex items-center">
<span className="text-gray-400 mr-2 w-12 text-right">{frame.lineNumber}</span>
<span className="text-gray-600 font-medium">{frame.fileName}</span>
<span className="text-gray-500 mx-1">in</span>
<span className="text-indigo-600 font-medium">{frame.inFunction || frame.fileName}</span>
</div>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${expandedFrames[index] ? 'transform rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
{(expandedFrames[index] || false) && frame.code && (
<div className="px-12 py-2 font-mono text-sm text-gray-800 bg-gray-50 overflow-x-auto border-t border-gray-100">
{frame.code}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};

View file

@ -56,6 +56,24 @@ export const columns: ColumnDef<LogEntry>[] = [
accessorKey: "startTime", accessorKey: "startTime",
cell: (info: any) => <TimeCell utcTime={info.getValue()} />, cell: (info: any) => <TimeCell utcTime={info.getValue()} />,
}, },
{
header: "Status",
accessorKey: "metadata.status",
cell: (info: any) => {
const status = info.getValue() || "Success";
const isSuccess = status.toLowerCase() !== "failure";
return (
<span className={`px-2 py-1 rounded-md text-xs font-medium inline-block text-center w-16 ${
isSuccess
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{isSuccess ? "Success" : "Failure"}
</span>
);
},
},
{ {
header: "Request ID", header: "Request ID",
accessorKey: "request_id", accessorKey: "request_id",
@ -203,3 +221,51 @@ const formatMessage = (message: any): string => {
} }
return String(message); return String(message);
}; };
// Add this new component for displaying request/response with copy buttons
export const RequestResponsePanel = ({ request, response }: { request: any; response: any }) => {
const requestStr = typeof request === 'object' ? JSON.stringify(request, null, 2) : String(request || '{}');
const responseStr = typeof response === 'object' ? JSON.stringify(response, null, 2) : String(response || '{}');
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
return (
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="rounded-lg border border-gray-200 bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200">
<h3 className="text-sm font-medium">Request</h3>
<button
onClick={() => copyToClipboard(requestStr)}
className="p-1 hover:bg-gray-200 rounded"
title="Copy request"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<pre className="p-4 overflow-auto text-xs font-mono h-64 whitespace-pre-wrap break-words">{requestStr}</pre>
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200">
<h3 className="text-sm font-medium">Response</h3>
<button
onClick={() => copyToClipboard(responseStr)}
className="p-1 hover:bg-gray-200 rounded"
title="Copy response"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<pre className="p-4 overflow-auto text-xs font-mono h-64 whitespace-pre-wrap break-words">{responseStr}</pre>
</div>
</div>
);
};

View file

@ -8,6 +8,9 @@ import { DataTable } from "./table";
import { columns, LogEntry } from "./columns"; import { columns, LogEntry } from "./columns";
import { Row } from "@tanstack/react-table"; import { Row } from "@tanstack/react-table";
import { prefetchLogDetails } from "./prefetch"; import { prefetchLogDetails } from "./prefetch";
import { RequestResponsePanel } from "./columns";
import { ErrorViewer } from './ErrorViewer';
interface SpendLogsTableProps { interface SpendLogsTableProps {
accessToken: string | null; accessToken: string | null;
token: string | null; token: string | null;
@ -574,6 +577,25 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
return input; return input;
}; };
// Extract error information from metadata if available
const hasError = row.original.metadata?.status === "failure";
const errorInfo = hasError ? row.original.metadata?.error_information : null;
// Format the response with error details if present
const formattedResponse = () => {
if (hasError && errorInfo) {
return {
error: {
message: errorInfo.error_message || "An error occurred",
type: errorInfo.error_class || "error",
code: errorInfo.error_code || "unknown",
param: null
}
};
}
return formatData(row.original.response);
};
return ( return (
<div className="p-6 bg-gray-50 space-y-6"> <div className="p-6 bg-gray-50 space-y-6">
{/* Combined Info Card */} {/* Combined Info Card */}
@ -581,50 +603,19 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
<div className="p-4 border-b"> <div className="p-4 border-b">
<h3 className="text-lg font-medium">Request Details</h3> <h3 className="text-lg font-medium">Request Details</h3>
</div> </div>
<div className="space-y-2 p-4 "> <div className="grid grid-cols-2 gap-4 p-4">
<div className="space-y-2">
<div className="flex"> <div className="flex">
<span className="font-medium w-1/3">Request ID:</span> <span className="font-medium w-1/3">Request ID:</span>
<span>{row.original.request_id}</span> <span className="font-mono text-sm">{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>
<div className="flex"> <div className="flex">
<span className="font-medium w-1/3">Model:</span> <span className="font-medium w-1/3">Model:</span>
<span>{row.original.model}</span> <span>{row.original.model}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="font-medium w-1/3">Custom LLM Provider:</span> <span className="font-medium w-1/3">Provider:</span>
<span>{row.original.custom_llm_provider}</span> <span>{row.original.custom_llm_provider || "-"}</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>
<div className="flex"> <div className="flex">
<span className="font-medium w-1/3">Start Time:</span> <span className="font-medium w-1/3">Start Time:</span>
@ -634,84 +625,132 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
<span className="font-medium w-1/3">End Time:</span> <span className="font-medium w-1/3">End Time:</span>
<span>{row.original.endTime}</span> <span>{row.original.endTime}</span>
</div> </div>
</div>
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Tokens:</span>
<span>{row.original.total_tokens} ({row.original.prompt_tokens}+{row.original.completion_tokens})</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Cost:</span>
<span>${Number(row.original.spend || 0).toFixed(6)}</span>
</div>
<div className="flex"> <div className="flex">
<span className="font-medium w-1/3">Cache Hit:</span> <span className="font-medium w-1/3">Cache Hit:</span>
<span>{row.original.cache_hit}</span> <span>{row.original.cache_hit}</span>
</div> </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 && ( {row?.original?.requester_ip_address && (
<div className="flex"> <div className="flex">
<span className="font-medium w-1/3">Request IP Address:</span> <span className="font-medium w-1/3">IP Address:</span>
<span>{row?.original?.requester_ip_address}</span> <span>{row?.original?.requester_ip_address}</span>
</div> </div>
)} )}
<div className="flex">
<span className="font-medium w-1/3">Status:</span>
<span className={`px-2 py-1 rounded-md text-xs font-medium inline-block text-center w-16 ${
(row.original.metadata?.status || "Success").toLowerCase() !== "failure"
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{(row.original.metadata?.status || "Success").toLowerCase() !== "failure" ? "Success" : "Failure"}
</span>
</div>
</div>
</div> </div>
</div> </div>
{/* Request Card */} {/* Request/Response Panel */}
<div className="grid grid-cols-2 gap-4">
{/* Request Side */}
<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>
<button
onClick={() => navigator.clipboard.writeText(JSON.stringify(formatData(row.original.messages), null, 2))}
className="p-1 hover:bg-gray-200 rounded"
title="Copy request"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div className="p-4 overflow-auto max-h-96">
<pre className="text-xs font-mono whitespace-pre-wrap break-all">{JSON.stringify(formatData(row.original.messages), null, 2)}</pre>
</div>
</div>
{/* Response Side */}
<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
{hasError && (
<span className="ml-2 text-sm text-red-600">
HTTP code {errorInfo?.error_code || 400}
</span>
)}
</h3>
<button
onClick={() => navigator.clipboard.writeText(JSON.stringify(formattedResponse(), null, 2))}
className="p-1 hover:bg-gray-200 rounded"
title="Copy response"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div className="p-4 overflow-auto max-h-96 bg-gray-50">
<pre className="text-xs font-mono whitespace-pre-wrap break-all">{JSON.stringify(formattedResponse(), null, 2)}</pre>
</div>
</div>
</div>
{/* Error Card - Only show for failures */}
{hasError && errorInfo && <ErrorViewer errorInfo={errorInfo} />}
{/* Tags Card - Only show if there are tags */}
{row.original.request_tags && Object.keys(row.original.request_tags).length > 0 && (
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
<div className="flex justify-between items-center p-4 border-b"> <div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">Request Tags</h3> <h3 className="text-lg font-medium">Request Tags</h3>
</div> </div>
<pre className="p-4 text-wrap overflow-auto text-sm"> <div className="p-4">
{JSON.stringify(formatData(row.original.request_tags), null, 2)} <div className="flex flex-wrap gap-2">
</pre> {Object.entries(row.original.request_tags).map(([key, value]) => (
<span key={key} className="px-2 py-1 bg-gray-100 rounded-full text-xs">
{key}: {String(value)}
</span>
))}
</div> </div>
</div>
</div>
)}
{/* Request Card */} {/* Metadata Card - Only show if there's metadata */}
<div className="bg-white rounded-lg shadow"> {row.original.metadata && Object.keys(row.original.metadata).length > 0 && (
<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="bg-white rounded-lg shadow">
<div className="flex justify-between items-center p-4 border-b"> <div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">Metadata</h3> <h3 className="text-lg font-medium">Metadata</h3>
{/* <div> <button
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50"> onClick={() => navigator.clipboard.writeText(JSON.stringify(row.original.metadata, null, 2))}
JSON className="p-1 hover:bg-gray-200 rounded"
title="Copy metadata"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button> </button>
</div> */}
</div> </div>
<pre className="p-4 text-wrap overflow-auto text-sm "> <div className="p-4 overflow-auto max-h-64">
{JSON.stringify(row.original.metadata, null, 2)} <pre className="text-xs font-mono whitespace-pre-wrap break-all">{JSON.stringify(row.original.metadata, null, 2)}</pre>
</pre> </div>
</div> </div>
)} )}
</div> </div>