From 62ad84fb6440dcc576ebacdfc8f6fddbbb2efd61 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Mon, 31 Mar 2025 22:48:43 -0700 Subject: [PATCH] UI (new_usage.tsx): Report 'total_tokens' + report success/failure calls (#9675) * feat(internal_user_endpoints.py): return 'total_tokens' in `/user/daily/analytics` * test(test_internal_user_endpoints.py): add unit test to assert spend metrics and dailyspend metadata always report the same fields * build(schema.prisma): record success + failure calls to daily user table allows understanding why model requests might exceed provider requests (e.g. user hit rate limit error) * fix(internal_user_endpoints.py): report success / failure requests in API * fix(proxy/utils.py): default to success status can be missing or none at times for successful requests * feat(new_usage.tsx): show success/failure calls on UI * style(new_usage.tsx): ui cleanup * fix: fix linting error * fix: fix linting error * feat(litellm-proxy-extras/): add new migration files --- ...itellm_proxy_extras-0.1.2-py3-none-any.whl | Bin 0 -> 8384 bytes .../dist/litellm_proxy_extras-0.1.2.tar.gz | Bin 0 -> 5719 bytes .../migration.sql | 4 + litellm-proxy-extras/poetry.lock | 7 + litellm-proxy-extras/pyproject.toml | 4 +- litellm/proxy/_new_secret_config.yaml | 10 +- litellm/proxy/_types.py | 2 + .../internal_user_endpoints.py | 22 ++- litellm/proxy/schema.prisma | 3 +- litellm/proxy/utils.py | 73 +++++++++- poetry.lock | 87 +----------- pyproject.toml | 2 +- requirements.txt | 2 +- schema.prisma | 3 + .../test_internal_user_endpoints.py | 27 ++++ .../src/components/new_usage.tsx | 131 ++++++++++++------ 16 files changed, 240 insertions(+), 137 deletions(-) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.1.2-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.1.2.tar.gz create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20250331215456_track_success_and_failed_requests_daily_agg_table/migration.sql create mode 100644 litellm-proxy-extras/poetry.lock diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.1.2-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.1.2-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..a034034c24e993414aa60a1c7fb9fe8f2736e236 GIT binary patch literal 8384 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 M?rDZ8>A!ycFRvUvCjbBd literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.1.2.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.1.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b3157d42cdd9fe94f59a85ea68fb94237119b7c5 GIT binary patch literal 5719 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 J27ds^004BFf}j8Z literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20250331215456_track_success_and_failed_requests_daily_agg_table/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20250331215456_track_success_and_failed_requests_daily_agg_table/migration.sql new file mode 100644 index 0000000000..9f1693500d --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20250331215456_track_success_and_failed_requests_daily_agg_table/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "LiteLLM_DailyUserSpend" ADD COLUMN "failed_requests" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "successful_requests" INTEGER NOT NULL DEFAULT 0; + diff --git a/litellm-proxy-extras/poetry.lock b/litellm-proxy-extras/poetry.lock new file mode 100644 index 0000000000..f526fec8da --- /dev/null +++ b/litellm-proxy-extras/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.1,<4.0, !=3.9.7" +content-hash = "2cf39473e67ff0615f0a61c9d2ac9f02b38cc08cbb1bdb893d89bee002646623" diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index c130a7fa9b..aea27371fe 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.1.1" +version = "0.1.2" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.1.1" +version = "0.1.2" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index a4b7910ba5..1f8d442b7d 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -32,8 +32,8 @@ litellm_settings: callbacks: ["prometheus"] # json_logs: true -# router_settings: -# routing_strategy: usage-based-routing-v2 # 👈 KEY CHANGE -# redis_host: os.environ/REDIS_HOST -# redis_password: os.environ/REDIS_PASSWORD -# redis_port: os.environ/REDIS_PORT +router_settings: + routing_strategy: usage-based-routing-v2 # 👈 KEY CHANGE + redis_host: os.environ/REDIS_HOST + redis_password: os.environ/REDIS_PASSWORD + redis_port: os.environ/REDIS_PORT diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 7f13717e29..9536442475 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2736,6 +2736,8 @@ class DailyUserSpendTransaction(TypedDict): completion_tokens: int spend: float api_requests: int + successful_requests: int + failed_requests: int class DBSpendUpdateTransactions(TypedDict): diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 8124b7fd20..ec7c6740bc 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -1259,6 +1259,8 @@ class SpendMetrics(BaseModel): prompt_tokens: int = Field(default=0) completion_tokens: int = Field(default=0) total_tokens: int = Field(default=0) + successful_requests: int = Field(default=0) + failed_requests: int = Field(default=0) api_requests: int = Field(default=0) @@ -1284,7 +1286,10 @@ class DailySpendMetadata(BaseModel): total_spend: float = Field(default=0.0) total_prompt_tokens: int = Field(default=0) total_completion_tokens: int = Field(default=0) + total_tokens: int = Field(default=0) total_api_requests: int = Field(default=0) + total_successful_requests: int = Field(default=0) + total_failed_requests: int = Field(default=0) page: int = Field(default=1) total_pages: int = Field(default=1) has_more: bool = Field(default=False) @@ -1307,6 +1312,8 @@ class LiteLLM_DailyUserSpend(BaseModel): completion_tokens: int = 0 spend: float = 0.0 api_requests: int = 0 + successful_requests: int = 0 + failed_requests: int = 0 class GroupedData(TypedDict): @@ -1322,6 +1329,8 @@ def update_metrics( group_metrics.completion_tokens += record.completion_tokens group_metrics.total_tokens += record.prompt_tokens + record.completion_tokens group_metrics.api_requests += record.api_requests + group_metrics.successful_requests += record.successful_requests + group_metrics.failed_requests += record.failed_requests return group_metrics @@ -1443,6 +1452,10 @@ async def get_user_daily_activity( take=page_size, ) + daily_spend_data_pydantic_list = [ + LiteLLM_DailyUserSpend(**record.model_dump()) for record in daily_spend_data + ] + # Process results results = [] total_metrics = SpendMetrics() @@ -1450,7 +1463,7 @@ async def get_user_daily_activity( # Group data by date and other dimensions grouped_data: Dict[str, Dict[str, Any]] = {} - for record in daily_spend_data: + for record in daily_spend_data_pydantic_list: date_str = record.date if date_str not in grouped_data: grouped_data[date_str] = { @@ -1474,7 +1487,9 @@ async def get_user_daily_activity( total_metrics.total_tokens += ( record.prompt_tokens + record.completion_tokens ) - total_metrics.api_requests += 1 + total_metrics.api_requests += record.api_requests + total_metrics.successful_requests += record.successful_requests + total_metrics.failed_requests += record.failed_requests # Convert grouped data to response format for date_str, data in grouped_data.items(): @@ -1495,7 +1510,10 @@ async def get_user_daily_activity( total_spend=total_metrics.spend, total_prompt_tokens=total_metrics.prompt_tokens, total_completion_tokens=total_metrics.completion_tokens, + total_tokens=total_metrics.total_tokens, total_api_requests=total_metrics.api_requests, + total_successful_requests=total_metrics.successful_requests, + total_failed_requests=total_metrics.failed_requests, page=page, total_pages=-(-total_count // page_size), # Ceiling division has_more=(page * page_size) < total_count, diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index d98f1fa981..faf110ca96 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -327,6 +327,8 @@ model LiteLLM_DailyUserSpend { completion_tokens Int @default(0) spend Float @default(0.0) api_requests Int @default(0) + successful_requests Int @default(0) + failed_requests Int @default(0) created_at DateTime @default(now()) updated_at DateTime @updatedAt @@ -352,4 +354,3 @@ enum JobStatus { INACTIVE } - diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 900b26f3f1..f612c88ccc 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -10,7 +10,17 @@ import traceback from datetime import datetime, timedelta from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Literal, + Optional, + Union, + cast, + overload, +) from litellm.proxy._types import ( DB_CONNECTION_ERROR_TYPES, @@ -18,6 +28,7 @@ from litellm.proxy._types import ( DailyUserSpendTransaction, ProxyErrorTypes, ProxyException, + SpendLogsMetadata, SpendLogsPayload, ) from litellm.types.guardrails import GuardrailEventHooks @@ -1145,6 +1156,41 @@ class PrismaClient: ) # Client to connect to Prisma db verbose_proxy_logger.debug("Success - Created Prisma Client") + def get_request_status( + self, payload: Union[dict, SpendLogsPayload] + ) -> Literal["success", "failure"]: + """ + Determine if a request was successful or failed based on payload metadata. + + Args: + payload (Union[dict, SpendLogsPayload]): Request payload containing metadata + + Returns: + Literal["success", "failure"]: Request status + """ + try: + # Get metadata and convert to dict if it's a JSON string + payload_metadata: Union[Dict, SpendLogsMetadata, str] = payload.get( + "metadata", {} + ) + if isinstance(payload_metadata, str): + payload_metadata_json: Union[Dict, SpendLogsMetadata] = cast( + Dict, json.loads(payload_metadata) + ) + else: + payload_metadata_json = payload_metadata + + # Check status in metadata dict + return ( + "failure" + if payload_metadata_json.get("status") == "failure" + else "success" + ) + + except (json.JSONDecodeError, AttributeError): + # Default to success if metadata parsing fails + return "success" + def add_spend_log_transaction_to_daily_user_transaction( self, payload: Union[dict, SpendLogsPayload] ): @@ -1156,12 +1202,15 @@ class PrismaClient: If key exists, update the transaction with the new spend and usage """ expected_keys = ["user", "startTime", "api_key", "model", "custom_llm_provider"] + if not all(key in payload for key in expected_keys): verbose_proxy_logger.debug( f"Missing expected keys: {expected_keys}, in payload, skipping from daily_user_spend_transactions" ) return + request_status = self.get_request_status(payload) + verbose_proxy_logger.info(f"Logged request status: {request_status}") if isinstance(payload["startTime"], datetime): start_time = payload["startTime"].isoformat() date = start_time.split("T")[0] @@ -1174,6 +1223,7 @@ class PrismaClient: return try: daily_transaction_key = f"{payload['user']}_{date}_{payload['api_key']}_{payload['model']}_{payload['custom_llm_provider']}" + if daily_transaction_key in self.daily_user_spend_transactions: daily_transaction = self.daily_user_spend_transactions[ daily_transaction_key @@ -1182,6 +1232,11 @@ class PrismaClient: daily_transaction["prompt_tokens"] += payload["prompt_tokens"] daily_transaction["completion_tokens"] += payload["completion_tokens"] daily_transaction["api_requests"] += 1 + + if request_status == "success": + daily_transaction["successful_requests"] += 1 + else: + daily_transaction["failed_requests"] += 1 else: daily_transaction = DailyUserSpendTransaction( user_id=payload["user"], @@ -1194,6 +1249,8 @@ class PrismaClient: completion_tokens=payload["completion_tokens"], spend=payload["spend"], api_requests=1, + successful_requests=1 if request_status == "success" else 0, + failed_requests=1 if request_status != "success" else 0, ) self.daily_user_spend_transactions[ @@ -2603,6 +2660,12 @@ class ProxyUpdateSpend: ], "spend": transaction["spend"], "api_requests": transaction["api_requests"], + "successful_requests": transaction[ + "successful_requests" + ], + "failed_requests": transaction[ + "failed_requests" + ], }, "update": { "prompt_tokens": { @@ -2617,6 +2680,14 @@ class ProxyUpdateSpend: "api_requests": { "increment": transaction["api_requests"] }, + "successful_requests": { + "increment": transaction[ + "successful_requests" + ] + }, + "failed_requests": { + "increment": transaction["failed_requests"] + }, }, }, ) diff --git a/poetry.lock b/poetry.lock index 157291f5f3..b6200d3180 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1151,69 +1151,6 @@ files = [ [package.extras] protobuf = ["grpcio-tools (>=1.70.0)"] -[[package]] -name = "grpcio" -version = "1.71.0" -description = "HTTP/2-based RPC framework" -optional = true -python-versions = ">=3.9" -files = [ - {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, - {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5"}, - {file = "grpcio-1.71.0-cp310-cp310-win32.whl", hash = "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509"}, - {file = "grpcio-1.71.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a"}, - {file = "grpcio-1.71.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef"}, - {file = "grpcio-1.71.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3"}, - {file = "grpcio-1.71.0-cp311-cp311-win32.whl", hash = "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444"}, - {file = "grpcio-1.71.0-cp311-cp311-win_amd64.whl", hash = "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b"}, - {file = "grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537"}, - {file = "grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79"}, - {file = "grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a"}, - {file = "grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8"}, - {file = "grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379"}, - {file = "grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637"}, - {file = "grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb"}, - {file = "grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366"}, - {file = "grpcio-1.71.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d"}, - {file = "grpcio-1.71.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32"}, - {file = "grpcio-1.71.0-cp39-cp39-win32.whl", hash = "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455"}, - {file = "grpcio-1.71.0-cp39-cp39-win_amd64.whl", hash = "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a"}, - {file = "grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c"}, -] - -[package.extras] -protobuf = ["grpcio-tools (>=1.71.0)"] - [[package]] name = "grpcio-status" version = "1.70.0" @@ -1230,22 +1167,6 @@ googleapis-common-protos = ">=1.5.5" grpcio = ">=1.70.0" protobuf = ">=5.26.1,<6.0dev" -[[package]] -name = "grpcio-status" -version = "1.71.0" -description = "Status proto mapping for gRPC" -optional = true -python-versions = ">=3.9" -files = [ - {file = "grpcio_status-1.71.0-py3-none-any.whl", hash = "sha256:843934ef8c09e3e858952887467f8256aac3910c55f077a359a65b2b3cde3e68"}, - {file = "grpcio_status-1.71.0.tar.gz", hash = "sha256:11405fed67b68f406b3f3c7c5ae5104a79d2d309666d10d61b152e91d28fb968"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.71.0" -protobuf = ">=5.26.1,<6.0dev" - [[package]] name = "gunicorn" version = "23.0.0" @@ -1678,13 +1599,13 @@ referencing = ">=0.31.0" [[package]] name = "litellm-proxy-extras" -version = "0.1.1" +version = "0.1.2" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" files = [ - {file = "litellm_proxy_extras-0.1.1-py3-none-any.whl", hash = "sha256:2b3c4c5474bacbde2424c1cd13b21f85c65e9c4346f6159badd49a210eedef5c"}, - {file = "litellm_proxy_extras-0.1.1.tar.gz", hash = "sha256:a1eb911ad2e3742238863d314a8bd6d02dd0cc213ba040b2c0593f132fbf3117"}, + {file = "litellm_proxy_extras-0.1.2-py3-none-any.whl", hash = "sha256:2caa7bdba5a533cd1781b55e3f7c581138d2a5b68a7e6d737327669dd21d5e08"}, + {file = "litellm_proxy_extras-0.1.2.tar.gz", hash = "sha256:218e97980ab5a34eed7dcd1564a910c9a790168d672cdec3c464eba9b7cb1518"}, ] [[package]] @@ -4135,4 +4056,4 @@ proxy = ["PyJWT", "apscheduler", "backoff", "boto3", "cryptography", "fastapi", [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "16cbf20784776377805f5e33c6bc97dce76303132aa3d81c7e6fe743f0ee3fc1" +content-hash = "524b2f8276ba057f8dc8a79dd460c1a243ef4aece7c08a8bf344e029e07b8841" diff --git a/pyproject.toml b/pyproject.toml index 34564bd049..bf120de885 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.34.34", optional = true} redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "1.5.0", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "0.1.1", optional = true} +litellm-proxy-extras = {version = "0.1.2", optional = true} [tool.poetry.extras] proxy = [ diff --git a/requirements.txt b/requirements.txt index b3f0f1d073..5fac9b8f06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests cryptography==43.0.1 tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.1.1 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.1.2 # for proxy extras - e.g. prisma migrations ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.0 # for env diff --git a/schema.prisma b/schema.prisma index 39ba0b10d2..faf110ca96 100644 --- a/schema.prisma +++ b/schema.prisma @@ -327,6 +327,8 @@ model LiteLLM_DailyUserSpend { completion_tokens Int @default(0) spend Float @default(0.0) api_requests Int @default(0) + successful_requests Int @default(0) + failed_requests Int @default(0) created_at DateTime @default(now()) updated_at DateTime @updatedAt @@ -351,3 +353,4 @@ enum JobStatus { ACTIVE INACTIVE } + diff --git a/tests/litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/litellm/proxy/management_endpoints/test_internal_user_endpoints.py index 697be8b3c9..54fda943eb 100644 --- a/tests/litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -55,3 +55,30 @@ async def test_ui_view_users_with_null_email(mocker, caplog): assert response == [ LiteLLM_UserTableFiltered(user_id="test-user-null-email", user_email=None) ] + + +def test_user_daily_activity_types(): + """ + Assert all fiels in SpendMetrics are reported in DailySpendMetadata as "total_" + """ + from litellm.proxy.management_endpoints.internal_user_endpoints import ( + DailySpendMetadata, + SpendMetrics, + ) + + # Create a SpendMetrics instance + spend_metrics = SpendMetrics() + + # Create a DailySpendMetadata instance + daily_spend_metadata = DailySpendMetadata() + + # Assert all fields in SpendMetrics are reported in DailySpendMetadata as "total_" + for field in spend_metrics.__dict__: + if field.startswith("total_"): + assert hasattr( + daily_spend_metadata, field + ), f"Field {field} is not reported in DailySpendMetadata" + else: + assert not hasattr( + daily_spend_metadata, field + ), f"Field {field} is reported in DailySpendMetadata" diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index e472fc69ef..547d4a13d4 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -33,6 +33,8 @@ interface SpendMetrics { completion_tokens: number; total_tokens: number; api_requests: number; + successful_requests: number; + failed_requests: number; } interface BreakdownMetrics { @@ -59,7 +61,7 @@ const NewUsagePage: React.FC = ({ // Derived states from userSpendData const totalSpend = userSpendData.metadata?.total_spend || 0; - + // Calculate top models from the breakdown data const getTopModels = () => { const modelSpend: { [key: string]: SpendMetrics } = {}; @@ -71,7 +73,9 @@ const NewUsagePage: React.FC = ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, - api_requests: 0 + api_requests: 0, + successful_requests: 0, + failed_requests: 0 }; } modelSpend[model].spend += metrics.spend; @@ -79,6 +83,8 @@ const NewUsagePage: React.FC = ({ modelSpend[model].completion_tokens += metrics.completion_tokens; modelSpend[model].total_tokens += metrics.total_tokens; modelSpend[model].api_requests += metrics.api_requests; + modelSpend[model].successful_requests += metrics.successful_requests || 0; + modelSpend[model].failed_requests += metrics.failed_requests || 0; }); }); @@ -87,6 +93,8 @@ const NewUsagePage: React.FC = ({ key: model, spend: metrics.spend, requests: metrics.api_requests, + successful_requests: metrics.successful_requests, + failed_requests: metrics.failed_requests, tokens: metrics.total_tokens })) .sort((a, b) => b.spend - a.spend) @@ -104,7 +112,9 @@ const NewUsagePage: React.FC = ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, - api_requests: 0 + api_requests: 0, + successful_requests: 0, + failed_requests: 0 }; } providerSpend[provider].spend += metrics.spend; @@ -112,6 +122,8 @@ const NewUsagePage: React.FC = ({ providerSpend[provider].completion_tokens += metrics.completion_tokens; providerSpend[provider].total_tokens += metrics.total_tokens; providerSpend[provider].api_requests += metrics.api_requests; + providerSpend[provider].successful_requests += metrics.successful_requests || 0; + providerSpend[provider].failed_requests += metrics.failed_requests || 0; }); }); @@ -120,6 +132,8 @@ const NewUsagePage: React.FC = ({ provider, spend: metrics.spend, requests: metrics.api_requests, + successful_requests: metrics.successful_requests, + failed_requests: metrics.failed_requests, tokens: metrics.total_tokens })); }; @@ -135,7 +149,9 @@ const NewUsagePage: React.FC = ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, - api_requests: 0 + api_requests: 0, + successful_requests: 0, + failed_requests: 0 }; } keySpend[key].spend += metrics.spend; @@ -143,6 +159,8 @@ const NewUsagePage: React.FC = ({ keySpend[key].completion_tokens += metrics.completion_tokens; keySpend[key].total_tokens += metrics.total_tokens; keySpend[key].api_requests += metrics.api_requests; + keySpend[key].successful_requests += metrics.successful_requests; + keySpend[key].failed_requests += metrics.failed_requests; }); }); @@ -185,6 +203,7 @@ const NewUsagePage: React.FC = ({ Project Spend {new Date().toLocaleString('default', { month: 'long' })} 1 - {new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate()} + = ({ /> + + + Usage Metrics + + + Total Requests + + {userSpendData.metadata?.total_api_requests?.toLocaleString() || 0} + + + + Successful Requests + + {userSpendData.metadata?.total_successful_requests?.toLocaleString() || 0} + + + + Failed Requests + + {userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0} + + + + Total Tokens + + {userSpendData.metadata?.total_tokens?.toLocaleString() || 0} + + + + Average Cost per Request + + ${((totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1)).toFixed(4)} + + + + + + {/* Daily Spend Chart */} @@ -215,6 +272,8 @@ const NewUsagePage: React.FC = ({

{data.date}

Spend: ${data.metrics.spend.toFixed(2)}

Requests: {data.metrics.api_requests}

+

Successful: {data.metrics.successful_requests}

+

Failed: {data.metrics.failed_requests}

Tokens: {data.metrics.total_tokens}

); @@ -240,7 +299,9 @@ const NewUsagePage: React.FC = ({ {/* Top Models */} - Top Models +
+ Top Models +
= ({

{data.key}

Spend: ${data.spend.toFixed(2)}

-

Requests: {data.requests.toLocaleString()}

+

Total Requests: {data.requests.toLocaleString()}

+

Successful: {data.successful_requests.toLocaleString()}

+

Failed: {data.failed_requests.toLocaleString()}

Tokens: {data.tokens.toLocaleString()}

); @@ -270,7 +333,9 @@ const NewUsagePage: React.FC = ({ {/* Spend by Provider */} - Spend by Provider +
+ Spend by Provider +
= ({ Provider Spend - Requests + Successful + Failed Tokens - {getProviderSpend().map((provider) => ( - - {provider.provider} - - ${provider.spend < 0.00001 - ? "less than 0.00" - : provider.spend.toFixed(2)} + {getProviderSpend() + .filter(provider => provider.spend > 0) + .map((provider) => ( + + {provider.provider} + + ${provider.spend < 0.00001 + ? "less than 0.00001" + : provider.spend.toFixed(2)} + + + {provider.successful_requests.toLocaleString()} + + + {provider.failed_requests.toLocaleString()} - {provider.requests.toLocaleString()} {provider.tokens.toLocaleString()} ))} @@ -313,31 +386,7 @@ const NewUsagePage: React.FC = ({ {/* Usage Metrics */} - - - Usage Metrics - - - Total Requests - - {userSpendData.metadata?.total_api_requests?.toLocaleString() || 0} - - - - Total Tokens - - {userSpendData.metadata?.total_tokens?.toLocaleString() || 0} - - - - Average Cost per Request - - ${((totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1)).toFixed(4)} - - - - - +