mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
(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 commit7f345df477
. * Revert "feature flag error logs" This reverts commit0e90c022bb
. * 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:
parent
0f87def2f5
commit
37e116235a
18 changed files with 845 additions and 294 deletions
|
@ -36,7 +36,7 @@ import TabItem from '@theme/TabItem';
|
|||
- Virtual Key Rate Limit
|
||||
- User Rate 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
|
||||
|
||||
|
|
|
@ -3114,10 +3114,26 @@ class StandardLoggingPayloadSetup:
|
|||
str(original_exception.__class__.__name__) if original_exception else ""
|
||||
)
|
||||
_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(
|
||||
error_code=error_status,
|
||||
error_class=error_class,
|
||||
llm_provider=_llm_provider_in_exception,
|
||||
traceback=traceback_info,
|
||||
error_message=error_message if original_exception else "",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -26,6 +26,8 @@ from litellm.types.utils import (
|
|||
ModelResponse,
|
||||
ProviderField,
|
||||
StandardCallbackDynamicParams,
|
||||
StandardLoggingPayloadErrorInformation,
|
||||
StandardLoggingPayloadStatus,
|
||||
StandardPassThroughResponseObject,
|
||||
TextCompletionResponse,
|
||||
)
|
||||
|
@ -1854,6 +1856,8 @@ class SpendLogsMetadata(TypedDict):
|
|||
] # special param to log k,v pairs to spendlogs for a call
|
||||
requester_ip_address: Optional[str]
|
||||
applied_guardrails: Optional[List[str]]
|
||||
status: StandardLoggingPayloadStatus
|
||||
error_information: Optional[StandardLoggingPayloadErrorInformation]
|
||||
|
||||
|
||||
class SpendLogsPayload(TypedDict):
|
||||
|
|
|
@ -64,10 +64,10 @@ def log_db_metrics(func):
|
|||
# in litellm custom callbacks kwargs is passed as arg[0]
|
||||
# https://docs.litellm.ai/docs/observability/custom_callback#callback-functions
|
||||
args is not None
|
||||
and len(args) > 0
|
||||
and isinstance(args[0], dict)
|
||||
and len(args) > 1
|
||||
and isinstance(args[1], dict)
|
||||
):
|
||||
passed_kwargs = args[0]
|
||||
passed_kwargs = args[1]
|
||||
parent_otel_span = _get_parent_otel_span_from_kwargs(
|
||||
kwargs=passed_kwargs
|
||||
)
|
||||
|
|
|
@ -1,25 +1,90 @@
|
|||
import asyncio
|
||||
import traceback
|
||||
from typing import Optional, Union, cast
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, Union, cast
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
_get_parent_otel_span_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.types.utils import StandardLoggingPayload
|
||||
from litellm.types.utils import (
|
||||
StandardLoggingPayload,
|
||||
StandardLoggingUserAPIKeyMetadata,
|
||||
)
|
||||
from litellm.utils import get_end_user_id_for_cost_tracking
|
||||
|
||||
|
||||
@log_db_metrics
|
||||
async def _PROXY_track_cost_callback(
|
||||
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
|
||||
async def _PROXY_track_cost_callback(
|
||||
self,
|
||||
kwargs, # kwargs to completion
|
||||
completion_response: litellm.ModelResponse, # response from completion
|
||||
completion_response: Optional[
|
||||
Union[litellm.ModelResponse, Any]
|
||||
], # response from completion
|
||||
start_time=None,
|
||||
end_time=None, # start/end time for completion
|
||||
):
|
||||
):
|
||||
from litellm.proxy.proxy_server import (
|
||||
prisma_client,
|
||||
proxy_logging_obj,
|
||||
|
@ -132,7 +197,9 @@ async def _PROXY_track_cost_callback(
|
|||
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(
|
||||
|
|
|
@ -8,4 +8,5 @@ model_list:
|
|||
|
||||
general_settings:
|
||||
store_model_in_db: true
|
||||
store_prompts_in_spend_logs: true
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
_get_parent_otel_span_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.proxy._types import *
|
||||
from litellm.proxy.analytics_endpoints.analytics_endpoints import (
|
||||
|
@ -178,7 +179,7 @@ from litellm.proxy.hooks.prompt_injection_detection import (
|
|||
_OPTIONAL_PromptInjectionDetection,
|
||||
)
|
||||
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.management_endpoints.budget_management_endpoints import (
|
||||
router as budget_management_router,
|
||||
|
@ -937,10 +938,7 @@ def load_from_azure_key_vault(use_azure_key_vault: bool = False):
|
|||
def cost_tracking():
|
||||
global prisma_client
|
||||
if prisma_client is not None:
|
||||
if isinstance(litellm._async_success_callback, list):
|
||||
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
|
||||
litellm.logging_callback_manager.add_litellm_callback(_ProxyDBLogger())
|
||||
|
||||
|
||||
def error_tracking():
|
||||
|
@ -3727,9 +3725,14 @@ async def chat_completion( # noqa: PLR0915
|
|||
timeout = getattr(
|
||||
e, "timeout", None
|
||||
) # 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(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
call_id=(
|
||||
_litellm_logging_obj.litellm_call_id if _litellm_logging_obj else None
|
||||
),
|
||||
version=version,
|
||||
response_cost=0,
|
||||
model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
|
||||
|
|
|
@ -47,6 +47,8 @@ def _get_spend_logs_metadata(
|
|||
requester_ip_address=None,
|
||||
additional_usage_values=None,
|
||||
applied_guardrails=None,
|
||||
status=None or "success",
|
||||
error_information=None,
|
||||
)
|
||||
verbose_proxy_logger.debug(
|
||||
"getting payload for SpendLogs, available keys in metadata: "
|
||||
|
@ -161,7 +163,6 @@ def get_logging_payload( # noqa: PLR0915
|
|||
import time
|
||||
|
||||
id = f"{id}_cache_hit{time.time()}" # SpendLogs does not allow duplicate request_id
|
||||
|
||||
try:
|
||||
payload: SpendLogsPayload = SpendLogsPayload(
|
||||
request_id=str(id),
|
||||
|
@ -193,7 +194,9 @@ def get_logging_payload( # noqa: PLR0915
|
|||
model_id=_model_id,
|
||||
requester_ip_address=clean_metadata.get("requester_ip_address", None),
|
||||
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),
|
||||
)
|
||||
|
||||
|
@ -293,12 +296,19 @@ async def get_spend_by_team_and_customer(
|
|||
|
||||
|
||||
def _get_messages_for_spend_logs_payload(
|
||||
payload: Optional[StandardLoggingPayload],
|
||||
standard_logging_payload: Optional[StandardLoggingPayload],
|
||||
metadata: Optional[dict] = None,
|
||||
) -> str:
|
||||
if payload is None:
|
||||
return "{}"
|
||||
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 "{}"
|
||||
|
||||
|
||||
|
|
|
@ -784,13 +784,17 @@ class ProxyLogging:
|
|||
else:
|
||||
_callback = callback # type: ignore
|
||||
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,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
original_exception=original_exception,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
verbose_proxy_logger.exception(
|
||||
f"[Non-Blocking] Error in post_call_failure_hook: {e}"
|
||||
)
|
||||
return
|
||||
|
||||
def _is_proxy_only_error(
|
||||
|
|
|
@ -1606,6 +1606,8 @@ class StandardLoggingPayloadErrorInformation(TypedDict, total=False):
|
|||
error_code: Optional[str]
|
||||
error_class: Optional[str]
|
||||
llm_provider: Optional[str]
|
||||
traceback: Optional[str]
|
||||
error_message: Optional[str]
|
||||
|
||||
|
||||
class StandardLoggingGuardrailInformation(TypedDict, total=False):
|
||||
|
|
80
tests/litellm/proxy/hooks/test_proxy_track_cost_callback.py
Normal file
80
tests/litellm/proxy/hooks/test_proxy_track_cost_callback.py
Normal 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"
|
|
@ -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/",
|
||||
"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",
|
||||
},
|
||||
"model_info": {
|
||||
|
|
|
@ -413,6 +413,7 @@ def test_get_error_information():
|
|||
assert result["error_code"] == "429"
|
||||
assert result["error_class"] == "RateLimitError"
|
||||
assert result["llm_provider"] == "openai"
|
||||
assert result["error_message"] == "litellm.RateLimitError: Test error"
|
||||
|
||||
|
||||
def test_get_response_time():
|
||||
|
|
|
@ -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
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
resp = ModelResponse(
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"stream": False,
|
||||
"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
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
resp = ModelResponse(
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"stream": False,
|
||||
"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
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
resp = ModelResponse(
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"stream": False,
|
||||
"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
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
resp = ModelResponse(
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"stream": True,
|
||||
"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
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
resp = ModelResponse(
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"stream": True,
|
||||
"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
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.caching.caching import Cache
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
litellm.cache = Cache()
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"model": "chatgpt-v-2",
|
||||
"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)
|
||||
|
||||
# update spend using track_cost callback, make 2nd request, it should fail
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
from litellm.proxy.proxy_server import user_api_key_cache
|
||||
|
||||
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
|
||||
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={
|
||||
"model": "chatgpt-v-2",
|
||||
"stream": False,
|
||||
|
@ -1874,9 +1873,9 @@ async def test_call_with_key_never_over_budget(prisma_client):
|
|||
import uuid
|
||||
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
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
|
||||
),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"model": "chatgpt-v-2",
|
||||
"stream": False,
|
||||
|
@ -1965,9 +1964,9 @@ async def test_call_with_key_over_budget_stream(prisma_client):
|
|||
import uuid
|
||||
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
proxy_db_logger = _ProxyDBLogger()
|
||||
|
||||
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
|
||||
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
|
||||
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
||||
)
|
||||
await track_cost_callback(
|
||||
await proxy_db_logger._PROXY_track_cost_callback(
|
||||
kwargs={
|
||||
"call_type": "acompletion",
|
||||
"model": "sagemaker-chatgpt-v-2",
|
||||
|
@ -2409,9 +2408,7 @@ async def track_cost_callback_helper_fn(generated_key: str, user_id: str):
|
|||
import uuid
|
||||
|
||||
from litellm import Choices, Message, ModelResponse, Usage
|
||||
from litellm.proxy.proxy_server import (
|
||||
_PROXY_track_cost_callback as track_cost_callback,
|
||||
)
|
||||
from litellm.proxy.proxy_server import _ProxyDBLogger
|
||||
|
||||
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
|
||||
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
|
||||
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={
|
||||
"call_type": "acompletion",
|
||||
"model": "sagemaker-chatgpt-v-2",
|
||||
|
|
|
@ -115,3 +115,97 @@ def test_missing_model_parameter_curl(curl_command):
|
|||
print("error in response", json.dumps(response, indent=4))
|
||||
|
||||
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"] == {}
|
||||
|
|
164
ui/litellm-dashboard/src/components/view_logs/ErrorViewer.tsx
Normal file
164
ui/litellm-dashboard/src/components/view_logs/ErrorViewer.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -56,6 +56,24 @@ export const columns: ColumnDef<LogEntry>[] = [
|
|||
accessorKey: "startTime",
|
||||
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",
|
||||
accessorKey: "request_id",
|
||||
|
@ -203,3 +221,51 @@ const formatMessage = (message: any): string => {
|
|||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,9 @@ import { DataTable } from "./table";
|
|||
import { columns, LogEntry } from "./columns";
|
||||
import { Row } from "@tanstack/react-table";
|
||||
import { prefetchLogDetails } from "./prefetch";
|
||||
import { RequestResponsePanel } from "./columns";
|
||||
import { ErrorViewer } from './ErrorViewer';
|
||||
|
||||
interface SpendLogsTableProps {
|
||||
accessToken: string | null;
|
||||
token: string | null;
|
||||
|
@ -574,57 +577,45 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
|||
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 (
|
||||
<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>
|
||||
<h3 className="text-lg font-medium">Request Details</h3>
|
||||
</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">
|
||||
<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>
|
||||
<span className="font-mono text-sm">{row.original.request_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">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>
|
||||
<span className="font-medium w-1/3">Provider:</span>
|
||||
<span>{row.original.custom_llm_provider || "-"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<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>{row.original.endTime}</span>
|
||||
</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">
|
||||
<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 className="font-medium w-1/3">IP Address:</span>
|
||||
<span>{row?.original?.requester_ip_address}</span>
|
||||
</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>
|
||||
|
||||
{/* 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="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 className="p-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
{/* Metadata Card - Only show if there's metadata */}
|
||||
{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
|
||||
onClick={() => navigator.clipboard.writeText(JSON.stringify(row.original.metadata, null, 2))}
|
||||
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>
|
||||
</div> */}
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm ">
|
||||
{JSON.stringify(row.original.metadata, null, 2)}
|
||||
</pre>
|
||||
<div className="p-4 overflow-auto max-h-64">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap break-all">{JSON.stringify(row.original.metadata, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue