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
|
- 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,138 +1,205 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@log_db_metrics
|
class _ProxyDBLogger(CustomLogger):
|
||||||
async def _PROXY_track_cost_callback(
|
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||||
kwargs, # kwargs to completion
|
await self._PROXY_track_cost_callback(
|
||||||
completion_response: litellm.ModelResponse, # response from completion
|
kwargs, response_obj, start_time, end_time
|
||||||
start_time=None,
|
|
||||||
end_time=None, # start/end time for completion
|
|
||||||
):
|
|
||||||
from litellm.proxy.proxy_server import (
|
|
||||||
prisma_client,
|
|
||||||
proxy_logging_obj,
|
|
||||||
update_cache,
|
|
||||||
update_database,
|
|
||||||
)
|
|
||||||
|
|
||||||
verbose_proxy_logger.debug("INSIDE _PROXY_track_cost_callback")
|
|
||||||
try:
|
|
||||||
verbose_proxy_logger.debug(
|
|
||||||
f"kwargs stream: {kwargs.get('stream', None)} + complete streaming response: {kwargs.get('complete_streaming_response', None)}"
|
|
||||||
)
|
|
||||||
parent_otel_span = _get_parent_otel_span_from_kwargs(kwargs=kwargs)
|
|
||||||
litellm_params = kwargs.get("litellm_params", {}) or {}
|
|
||||||
end_user_id = get_end_user_id_for_cost_tracking(litellm_params)
|
|
||||||
metadata = get_litellm_metadata_from_kwargs(kwargs=kwargs)
|
|
||||||
user_id = cast(Optional[str], metadata.get("user_api_key_user_id", None))
|
|
||||||
team_id = cast(Optional[str], metadata.get("user_api_key_team_id", None))
|
|
||||||
org_id = cast(Optional[str], metadata.get("user_api_key_org_id", None))
|
|
||||||
key_alias = cast(Optional[str], metadata.get("user_api_key_alias", None))
|
|
||||||
end_user_max_budget = metadata.get("user_api_end_user_max_budget", None)
|
|
||||||
sl_object: Optional[StandardLoggingPayload] = kwargs.get(
|
|
||||||
"standard_logging_object", None
|
|
||||||
)
|
|
||||||
response_cost = (
|
|
||||||
sl_object.get("response_cost", None)
|
|
||||||
if sl_object is not None
|
|
||||||
else kwargs.get("response_cost", None)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if response_cost is not None:
|
async def async_post_call_failure_hook(
|
||||||
user_api_key = metadata.get("user_api_key", None)
|
self,
|
||||||
if kwargs.get("cache_hit", False) is True:
|
request_data: dict,
|
||||||
response_cost = 0.0
|
original_exception: Exception,
|
||||||
verbose_proxy_logger.info(
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
f"Cache Hit: response_cost {response_cost}, for user_id {user_id}"
|
):
|
||||||
)
|
from litellm.proxy.proxy_server import update_database
|
||||||
|
|
||||||
verbose_proxy_logger.debug(
|
_metadata = dict(
|
||||||
f"user_api_key {user_api_key}, prisma_client: {prisma_client}"
|
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,
|
||||||
)
|
)
|
||||||
if _should_track_cost_callback(
|
)
|
||||||
user_api_key=user_api_key,
|
_metadata["user_api_key"] = user_api_key_dict.api_key
|
||||||
user_id=user_id,
|
_metadata["status"] = "failure"
|
||||||
team_id=team_id,
|
_metadata["error_information"] = (
|
||||||
end_user_id=end_user_id,
|
StandardLoggingPayloadSetup.get_error_information(
|
||||||
):
|
original_exception=original_exception,
|
||||||
## UPDATE DATABASE
|
)
|
||||||
await update_database(
|
)
|
||||||
token=user_api_key,
|
|
||||||
response_cost=response_cost,
|
|
||||||
user_id=user_id,
|
|
||||||
end_user_id=end_user_id,
|
|
||||||
team_id=team_id,
|
|
||||||
kwargs=kwargs,
|
|
||||||
completion_response=completion_response,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time,
|
|
||||||
org_id=org_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# update cache
|
existing_metadata: dict = request_data.get("metadata", None) or {}
|
||||||
asyncio.create_task(
|
existing_metadata.update(_metadata)
|
||||||
update_cache(
|
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: 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,
|
||||||
|
update_cache,
|
||||||
|
update_database,
|
||||||
|
)
|
||||||
|
|
||||||
|
verbose_proxy_logger.debug("INSIDE _PROXY_track_cost_callback")
|
||||||
|
try:
|
||||||
|
verbose_proxy_logger.debug(
|
||||||
|
f"kwargs stream: {kwargs.get('stream', None)} + complete streaming response: {kwargs.get('complete_streaming_response', None)}"
|
||||||
|
)
|
||||||
|
parent_otel_span = _get_parent_otel_span_from_kwargs(kwargs=kwargs)
|
||||||
|
litellm_params = kwargs.get("litellm_params", {}) or {}
|
||||||
|
end_user_id = get_end_user_id_for_cost_tracking(litellm_params)
|
||||||
|
metadata = get_litellm_metadata_from_kwargs(kwargs=kwargs)
|
||||||
|
user_id = cast(Optional[str], metadata.get("user_api_key_user_id", None))
|
||||||
|
team_id = cast(Optional[str], metadata.get("user_api_key_team_id", None))
|
||||||
|
org_id = cast(Optional[str], metadata.get("user_api_key_org_id", None))
|
||||||
|
key_alias = cast(Optional[str], metadata.get("user_api_key_alias", None))
|
||||||
|
end_user_max_budget = metadata.get("user_api_end_user_max_budget", None)
|
||||||
|
sl_object: Optional[StandardLoggingPayload] = kwargs.get(
|
||||||
|
"standard_logging_object", None
|
||||||
|
)
|
||||||
|
response_cost = (
|
||||||
|
sl_object.get("response_cost", None)
|
||||||
|
if sl_object is not None
|
||||||
|
else kwargs.get("response_cost", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
if response_cost is not None:
|
||||||
|
user_api_key = metadata.get("user_api_key", None)
|
||||||
|
if kwargs.get("cache_hit", False) is True:
|
||||||
|
response_cost = 0.0
|
||||||
|
verbose_proxy_logger.info(
|
||||||
|
f"Cache Hit: response_cost {response_cost}, for user_id {user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
verbose_proxy_logger.debug(
|
||||||
|
f"user_api_key {user_api_key}, prisma_client: {prisma_client}"
|
||||||
|
)
|
||||||
|
if _should_track_cost_callback(
|
||||||
|
user_api_key=user_api_key,
|
||||||
|
user_id=user_id,
|
||||||
|
team_id=team_id,
|
||||||
|
end_user_id=end_user_id,
|
||||||
|
):
|
||||||
|
## UPDATE DATABASE
|
||||||
|
await update_database(
|
||||||
token=user_api_key,
|
token=user_api_key,
|
||||||
|
response_cost=response_cost,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
end_user_id=end_user_id,
|
end_user_id=end_user_id,
|
||||||
response_cost=response_cost,
|
|
||||||
team_id=team_id,
|
team_id=team_id,
|
||||||
parent_otel_span=parent_otel_span,
|
kwargs=kwargs,
|
||||||
|
completion_response=completion_response,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
org_id=org_id,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
await proxy_logging_obj.slack_alerting_instance.customer_spend_alert(
|
# update cache
|
||||||
token=user_api_key,
|
asyncio.create_task(
|
||||||
key_alias=key_alias,
|
update_cache(
|
||||||
end_user_id=end_user_id,
|
token=user_api_key,
|
||||||
response_cost=response_cost,
|
user_id=user_id,
|
||||||
max_budget=end_user_max_budget,
|
end_user_id=end_user_id,
|
||||||
)
|
response_cost=response_cost,
|
||||||
else:
|
team_id=team_id,
|
||||||
raise Exception(
|
parent_otel_span=parent_otel_span,
|
||||||
"User API key and team id and user id missing from custom callback."
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
if kwargs["stream"] is not True or (
|
await proxy_logging_obj.slack_alerting_instance.customer_spend_alert(
|
||||||
kwargs["stream"] is True and "complete_streaming_response" in kwargs
|
token=user_api_key,
|
||||||
):
|
key_alias=key_alias,
|
||||||
if sl_object is not None:
|
end_user_id=end_user_id,
|
||||||
cost_tracking_failure_debug_info: Union[dict, str] = (
|
response_cost=response_cost,
|
||||||
sl_object["response_cost_failure_debug_info"] # type: ignore
|
max_budget=end_user_max_budget,
|
||||||
or "response_cost_failure_debug_info is None in standard_logging_object"
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cost_tracking_failure_debug_info = (
|
raise Exception(
|
||||||
"standard_logging_object not found"
|
"User API key and team id and user id missing from custom callback."
|
||||||
)
|
)
|
||||||
model = kwargs.get("model")
|
else:
|
||||||
raise Exception(
|
if kwargs["stream"] is not True or (
|
||||||
f"Cost tracking failed for model={model}.\nDebug info - {cost_tracking_failure_debug_info}\nAdd custom pricing - https://docs.litellm.ai/docs/proxy/custom_pricing"
|
kwargs["stream"] is True and "complete_streaming_response" in kwargs
|
||||||
|
):
|
||||||
|
if sl_object is not None:
|
||||||
|
cost_tracking_failure_debug_info: Union[dict, str] = (
|
||||||
|
sl_object["response_cost_failure_debug_info"] # type: ignore
|
||||||
|
or "response_cost_failure_debug_info is None in standard_logging_object"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cost_tracking_failure_debug_info = (
|
||||||
|
"standard_logging_object not found"
|
||||||
|
)
|
||||||
|
model = kwargs.get("model")
|
||||||
|
raise Exception(
|
||||||
|
f"Cost tracking failed for model={model}.\nDebug info - {cost_tracking_failure_debug_info}\nAdd custom pricing - https://docs.litellm.ai/docs/proxy/custom_pricing"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error in tracking cost callback - {str(e)}\n Traceback:{traceback.format_exc()}"
|
||||||
|
model = kwargs.get("model", "")
|
||||||
|
metadata = kwargs.get("litellm_params", {}).get("metadata", {})
|
||||||
|
error_msg += f"\n Args to _PROXY_track_cost_callback\n model: {model}\n metadata: {metadata}\n"
|
||||||
|
asyncio.create_task(
|
||||||
|
proxy_logging_obj.failed_tracking_alert(
|
||||||
|
error_message=error_msg,
|
||||||
|
failing_model=model,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Error in tracking cost callback - {str(e)}\n Traceback:{traceback.format_exc()}"
|
|
||||||
model = kwargs.get("model", "")
|
|
||||||
metadata = kwargs.get("litellm_params", {}).get("metadata", {})
|
|
||||||
error_msg += f"\n Args to _PROXY_track_cost_callback\n model: {model}\n metadata: {metadata}\n"
|
|
||||||
asyncio.create_task(
|
|
||||||
proxy_logging_obj.failed_tracking_alert(
|
|
||||||
error_message=error_msg,
|
|
||||||
failing_model=model,
|
|
||||||
)
|
)
|
||||||
)
|
verbose_proxy_logger.exception(
|
||||||
verbose_proxy_logger.exception("Error in tracking cost callback - %s", str(e))
|
"Error in tracking cost callback - %s", str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _should_track_cost_callback(
|
def _should_track_cost_callback(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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", ""),
|
||||||
|
|
|
@ -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 "{}"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
request_data=request_data,
|
_callback.async_post_call_failure_hook(
|
||||||
user_api_key_dict=user_api_key_dict,
|
request_data=request_data,
|
||||||
original_exception=original_exception,
|
user_api_key_dict=user_api_key_dict,
|
||||||
|
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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
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/",
|
"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": {
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"] == {}
|
||||||
|
|
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",
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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,146 +577,182 @@ 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 */}
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white rounded-lg shadow">
|
||||||
<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="flex">
|
<div className="space-y-2">
|
||||||
<span className="font-medium w-1/3">Request ID:</span>
|
|
||||||
<span>{row.original.request_id}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Api Key:</span>
|
|
||||||
<span>{row.original.api_key}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Team ID:</span>
|
|
||||||
<span>{row.original.team_id}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Model:</span>
|
|
||||||
<span>{row.original.model}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">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 className="flex">
|
|
||||||
<span className="font-medium w-1/3">Start Time:</span>
|
|
||||||
<span>{row.original.startTime}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">End Time:</span>
|
|
||||||
<span>{row.original.endTime}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Cache Hit:</span>
|
|
||||||
<span>{row.original.cache_hit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Cache Key:</span>
|
|
||||||
<span>{row.original.cache_key}</span>
|
|
||||||
</div>
|
|
||||||
{row?.original?.requester_ip_address && (
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="font-medium w-1/3">Request IP Address:</span>
|
<span className="font-medium w-1/3">Request ID:</span>
|
||||||
<span>{row?.original?.requester_ip_address}</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">Provider:</span>
|
||||||
|
<span>{row.original.custom_llm_provider || "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Start Time:</span>
|
||||||
|
<span>{row.original.startTime}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">End Time:</span>
|
||||||
|
<span>{row.original.endTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
{row?.original?.requester_ip_address && (
|
||||||
|
<div className="flex">
|
||||||
|
<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 */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Request Tags</h3>
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
|
||||||
{JSON.stringify(formatData(row.original.request_tags), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Request</h3>
|
|
||||||
{/* <div>
|
|
||||||
<button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
Expand
|
|
||||||
</button>
|
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
JSON
|
|
||||||
</button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
|
||||||
{JSON.stringify(formatData(row.original.messages), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Response Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
|
||||||
<h3 className="text-lg font-medium">Response</h3>
|
|
||||||
<div>
|
|
||||||
{/* <button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
Expand
|
|
||||||
</button>
|
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
|
||||||
JSON
|
|
||||||
</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
|
||||||
{JSON.stringify(formatData(row.original.response), null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metadata Card */}
|
{/* Request/Response Panel */}
|
||||||
{row.original.metadata &&
|
<div className="grid grid-cols-2 gap-4">
|
||||||
Object.keys(row.original.metadata).length > 0 && (
|
{/* Request Side */}
|
||||||
<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">Request</h3>
|
||||||
{/* <div>
|
<button
|
||||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
onClick={() => navigator.clipboard.writeText(JSON.stringify(formatData(row.original.messages), null, 2))}
|
||||||
JSON
|
className="p-1 hover:bg-gray-200 rounded"
|
||||||
</button>
|
title="Copy request"
|
||||||
</div> */}
|
>
|
||||||
</div>
|
<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">
|
||||||
<pre className="p-4 text-wrap overflow-auto text-sm ">
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
{JSON.stringify(row.original.metadata, null, 2)}
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
</pre>
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
<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 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue