From 4c87084ff7bd99966a33b6337ef58a5a2f11d9b0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 14:07:14 -0700 Subject: [PATCH 001/208] UserAPIKeyAuthExceptionHandler --- litellm/proxy/auth/auth_checks.py | 42 ------ litellm/proxy/auth/auth_exception_handler.py | 130 +++++++++++++++++++ litellm/proxy/auth/user_api_key_auth.py | 55 +------- 3 files changed, 136 insertions(+), 91 deletions(-) create mode 100644 litellm/proxy/auth/auth_exception_handler.py diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 80cfb03de4..98685e1a7c 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -1009,8 +1009,6 @@ async def get_key_object( ) return _response - except DB_CONNECTION_ERROR_TYPES as e: - return await _handle_failed_db_connection_for_get_key_object(e=e) except Exception: traceback.print_exc() raise Exception( @@ -1018,46 +1016,6 @@ async def get_key_object( ) -async def _handle_failed_db_connection_for_get_key_object( - e: Exception, -) -> UserAPIKeyAuth: - """ - Handles httpx.ConnectError when reading a Virtual Key from LiteLLM DB - - Use this if you don't want failed DB queries to block LLM API reqiests - - Returns: - - UserAPIKeyAuth: If general_settings.allow_requests_on_db_unavailable is True - - Raises: - - Orignal Exception in all other cases - """ - from litellm.proxy.proxy_server import ( - general_settings, - litellm_proxy_admin_name, - proxy_logging_obj, - ) - - # If this flag is on, requests failing to connect to the DB will be allowed - if general_settings.get("allow_requests_on_db_unavailable", False) is True: - # log this as a DB failure on prometheus - proxy_logging_obj.service_logging_obj.service_failure_hook( - service=ServiceTypes.DB, - call_type="get_key_object", - error=e, - duration=0.0, - ) - - return UserAPIKeyAuth( - key_name="failed-to-connect-to-db", - token="failed-to-connect-to-db", - user_id=litellm_proxy_admin_name, - ) - else: - # raise the original exception, the wrapper on `get_key_object` handles logging db failure to prometheus - raise e - - @log_db_metrics async def get_org_object( org_id: str, diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py new file mode 100644 index 0000000000..88a94fd5b9 --- /dev/null +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -0,0 +1,130 @@ +""" +Handles Authentication Errors +""" + +import asyncio +from typing import TYPE_CHECKING, Any, Optional + +from fastapi import HTTPException, Request, status + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth +from litellm.proxy.auth.auth_utils import _get_request_ip_address +from litellm.types.services import ServiceTypes + +if TYPE_CHECKING: + from opentelemetry.trace import Span as _Span + + Span = _Span +else: + Span = Any + + +class UserAPIKeyAuthExceptionHandler: + + @staticmethod + async def _handle_authentication_error( + e: Exception, + request: Request, + request_data: dict, + route: str, + parent_otel_span: Optional[Span], + api_key: str, + ) -> UserAPIKeyAuth: + """ + Handles Connection Errors when reading a Virtual Key from LiteLLM DB + Use this if you don't want failed DB queries to block LLM API reqiests + + Reliability scenarios this covers: + - DB is down and having an outage + - Unable to read / recover a key from the DB + + Returns: + - UserAPIKeyAuth: If general_settings.allow_requests_on_db_unavailable is True + + Raises: + - Orignal Exception in all other cases + """ + from litellm.proxy.proxy_server import ( + general_settings, + litellm_proxy_admin_name, + proxy_logging_obj, + ) + + if UserAPIKeyAuthExceptionHandler.should_allow_request_on_db_unavailable(): + # log this as a DB failure on prometheus + proxy_logging_obj.service_logging_obj.service_failure_hook( + service=ServiceTypes.DB, + call_type="get_key_object", + error=e, + duration=0.0, + ) + + return UserAPIKeyAuth( + key_name="failed-to-connect-to-db", + token="failed-to-connect-to-db", + user_id=litellm_proxy_admin_name, + ) + else: + # raise the exception to the caller + requester_ip = _get_request_ip_address( + request=request, + use_x_forwarded_for=general_settings.get("use_x_forwarded_for", False), + ) + verbose_proxy_logger.exception( + "litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - {}\nRequester IP Address:{}".format( + str(e), + requester_ip, + ), + extra={"requester_ip": requester_ip}, + ) + + # Log this exception to OTEL, Datadog etc + user_api_key_dict = UserAPIKeyAuth( + parent_otel_span=parent_otel_span, + api_key=api_key, + ) + asyncio.create_task( + proxy_logging_obj.post_call_failure_hook( + request_data=request_data, + original_exception=e, + user_api_key_dict=user_api_key_dict, + error_type=ProxyErrorTypes.auth_error, + route=route, + ) + ) + + if isinstance(e, litellm.BudgetExceededError): + raise ProxyException( + message=e.message, + type=ProxyErrorTypes.budget_exceeded, + param=None, + code=400, + ) + if isinstance(e, HTTPException): + raise ProxyException( + message=getattr(e, "detail", f"Authentication Error({str(e)})"), + type=ProxyErrorTypes.auth_error, + param=getattr(e, "param", "None"), + code=getattr(e, "status_code", status.HTTP_401_UNAUTHORIZED), + ) + elif isinstance(e, ProxyException): + raise e + raise ProxyException( + message="Authentication Error, " + str(e), + type=ProxyErrorTypes.auth_error, + param=getattr(e, "param", "None"), + code=status.HTTP_401_UNAUTHORIZED, + ) + + @staticmethod + def should_allow_request_on_db_unavailable() -> bool: + """ + Returns True if the request should be allowed to proceed despite the DB connection error + """ + from litellm.proxy.proxy_server import general_settings + + if general_settings.get("allow_requests_on_db_unavailable", False) is True: + return True + return False diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index b78619ae65..a2850ca294 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -26,7 +26,6 @@ from litellm.proxy._types import * from litellm.proxy.auth.auth_checks import ( _cache_key_object, _get_user_role, - _handle_failed_db_connection_for_get_key_object, _is_user_proxy_admin, _virtual_key_max_budget_check, _virtual_key_soft_budget_check, @@ -38,6 +37,7 @@ from litellm.proxy.auth.auth_checks import ( get_user_object, is_valid_fallback_model, ) +from litellm.proxy.auth.auth_exception_handler import UserAPIKeyAuthExceptionHandler from litellm.proxy.auth.auth_utils import ( _get_request_ip_address, get_end_user_id_from_request_body, @@ -675,9 +675,7 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 if ( prisma_client is None ): # if both master key + user key submitted, and user key != master key, and no db connected, raise an error - return await _handle_failed_db_connection_for_get_key_object( - e=Exception("No connected db.") - ) + raise Exception("No connected db.") ## check for cache hit (In-Memory Cache) _user_role = None @@ -1018,55 +1016,14 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 else: raise Exception() except Exception as e: - requester_ip = _get_request_ip_address( + return await UserAPIKeyAuthExceptionHandler._handle_authentication_error( + e=e, request=request, - use_x_forwarded_for=general_settings.get("use_x_forwarded_for", False), - ) - verbose_proxy_logger.exception( - "litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - {}\nRequester IP Address:{}".format( - str(e), - requester_ip, - ), - extra={"requester_ip": requester_ip}, - ) - - # Log this exception to OTEL, Datadog etc - user_api_key_dict = UserAPIKeyAuth( + request_data=request_data, + route=route, parent_otel_span=parent_otel_span, api_key=api_key, ) - asyncio.create_task( - proxy_logging_obj.post_call_failure_hook( - request_data=request_data, - original_exception=e, - user_api_key_dict=user_api_key_dict, - error_type=ProxyErrorTypes.auth_error, - route=route, - ) - ) - - if isinstance(e, litellm.BudgetExceededError): - raise ProxyException( - message=e.message, - type=ProxyErrorTypes.budget_exceeded, - param=None, - code=400, - ) - if isinstance(e, HTTPException): - raise ProxyException( - message=getattr(e, "detail", f"Authentication Error({str(e)})"), - type=ProxyErrorTypes.auth_error, - param=getattr(e, "param", "None"), - code=getattr(e, "status_code", status.HTTP_401_UNAUTHORIZED), - ) - elif isinstance(e, ProxyException): - raise e - raise ProxyException( - message="Authentication Error, " + str(e), - type=ProxyErrorTypes.auth_error, - param=getattr(e, "param", "None"), - code=status.HTTP_401_UNAUTHORIZED, - ) @tracer.wrap() From 59040167ac7dd3e1864f76a51a8b29dff6ef96b0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 14:40:11 -0700 Subject: [PATCH 002/208] fix ProxyErrorTypes --- litellm/proxy/_types.py | 53 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 220a0d5ddb..54612868cc 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2067,16 +2067,64 @@ class SpendCalculateRequest(LiteLLMPydanticObjectBase): class ProxyErrorTypes(str, enum.Enum): budget_exceeded = "budget_exceeded" + """ + Object was over budget + """ + + token_not_found_in_db = "token_not_found_in_db" + """ + Requested token was not found in the database + """ + key_model_access_denied = "key_model_access_denied" + """ + Key does not have access to the model + """ + team_model_access_denied = "team_model_access_denied" + """ + Team does not have access to the model + """ + user_model_access_denied = "user_model_access_denied" + """ + User does not have access to the model + """ + expired_key = "expired_key" + """ + Key has expired + """ + auth_error = "auth_error" + """ + General authentication error + """ + internal_server_error = "internal_server_error" + """ + Internal server error + """ + bad_request_error = "bad_request_error" + """ + Bad request error + """ + not_found_error = "not_found_error" - validation_error = "bad_request_error" + """ + Not found error + """ + + validation_error = "validation_error" + """ + Validation error + """ + cache_ping_error = "cache_ping_error" + """ + Cache ping error + """ @classmethod def get_model_access_error_type_for_object( @@ -2093,9 +2141,6 @@ class ProxyErrorTypes(str, enum.Enum): return cls.user_model_access_denied -DB_CONNECTION_ERROR_TYPES = (httpx.ConnectError, httpx.ReadError, httpx.ReadTimeout) - - class SSOUserDefinedValues(TypedDict): models: List[str] user_id: str From ce49e2721789cfaa4a236103cef90742f7e51bfb Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 15:44:13 -0700 Subject: [PATCH 003/208] fixes for auth checks --- litellm/proxy/_types.py | 7 +++ litellm/proxy/auth/auth_checks.py | 47 +++++++++--------- litellm/proxy/auth/auth_exception_handler.py | 23 ++++++++- litellm/proxy/auth/user_api_key_auth.py | 50 ++++++++------------ 4 files changed, 71 insertions(+), 56 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 54612868cc..c0fb0750eb 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2141,6 +2141,13 @@ class ProxyErrorTypes(str, enum.Enum): return cls.user_model_access_denied +DB_CONNECTION_ERROR_TYPES = ( + httpx.ConnectError, + httpx.ReadError, + httpx.ReadTimeout, +) + + class SSOUserDefinedValues(TypedDict): models: List[str] user_id: str diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 98685e1a7c..9e71147706 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -987,33 +987,34 @@ async def get_key_object( ) # else, check db - try: - _valid_token: Optional[BaseModel] = await prisma_client.get_data( - token=hashed_token, - table_name="combined_view", - parent_otel_span=parent_otel_span, - proxy_logging_obj=proxy_logging_obj, + _valid_token: Optional[BaseModel] = await prisma_client.get_data( + token=hashed_token, + table_name="combined_view", + parent_otel_span=parent_otel_span, + proxy_logging_obj=proxy_logging_obj, + ) + + if _valid_token is None: + raise ProxyException( + message="Key doesn't exist in db. key={}. Create key via `/key/generate` call.".format( + hashed_token + ), + type=ProxyErrorTypes.token_not_found_in_db, + param="key", + code=status.HTTP_401_UNAUTHORIZED, ) - if _valid_token is None: - raise Exception + _response = UserAPIKeyAuth(**_valid_token.model_dump(exclude_none=True)) - _response = UserAPIKeyAuth(**_valid_token.model_dump(exclude_none=True)) + # save the key object to cache + await _cache_key_object( + hashed_token=hashed_token, + user_api_key_obj=_response, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) - # save the key object to cache - await _cache_key_object( - hashed_token=hashed_token, - user_api_key_obj=_response, - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, - ) - - return _response - except Exception: - traceback.print_exc() - raise Exception( - f"Key doesn't exist in db. key={hashed_token}. Create key via `/key/generate` call." - ) + return _response @log_db_metrics diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py index 88a94fd5b9..5b314ef931 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -9,7 +9,12 @@ from fastapi import HTTPException, Request, status import litellm from litellm._logging import verbose_proxy_logger -from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth +from litellm.proxy._types import ( + DB_CONNECTION_ERROR_TYPES, + ProxyErrorTypes, + ProxyException, + UserAPIKeyAuth, +) from litellm.proxy.auth.auth_utils import _get_request_ip_address from litellm.types.services import ServiceTypes @@ -52,7 +57,10 @@ class UserAPIKeyAuthExceptionHandler: proxy_logging_obj, ) - if UserAPIKeyAuthExceptionHandler.should_allow_request_on_db_unavailable(): + if ( + UserAPIKeyAuthExceptionHandler.should_allow_request_on_db_unavailable() + and UserAPIKeyAuthExceptionHandler.is_database_connection_error(e) + ): # log this as a DB failure on prometheus proxy_logging_obj.service_logging_obj.service_failure_hook( service=ServiceTypes.DB, @@ -128,3 +136,14 @@ class UserAPIKeyAuthExceptionHandler: if general_settings.get("allow_requests_on_db_unavailable", False) is True: return True return False + + @staticmethod + def is_database_connection_error(e: Exception) -> bool: + """ + Returns True if the exception is from a database outage / connection error + """ + import prisma + + return isinstance(e, DB_CONNECTION_ERROR_TYPES) or isinstance( + e, prisma.errors.PrismaError + ) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index a2850ca294..06fa560866 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -683,37 +683,25 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 api_key = hash_token(token=api_key) if valid_token is None: - try: - valid_token = await get_key_object( - hashed_token=api_key, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - parent_otel_span=parent_otel_span, - proxy_logging_obj=proxy_logging_obj, - ) - # update end-user params on valid token - # These can change per request - it's important to update them here - valid_token.end_user_id = end_user_params.get("end_user_id") - valid_token.end_user_tpm_limit = end_user_params.get( - "end_user_tpm_limit" - ) - valid_token.end_user_rpm_limit = end_user_params.get( - "end_user_rpm_limit" - ) - valid_token.allowed_model_region = end_user_params.get( - "allowed_model_region" - ) - # update key budget with temp budget increase - valid_token = _update_key_budget_with_temp_budget_increase( - valid_token - ) # updating it here, allows all downstream reporting / checks to use the updated budget - except Exception: - verbose_logger.info( - "litellm.proxy.auth.user_api_key_auth.py::user_api_key_auth() - Unable to find token={} in cache or `LiteLLM_VerificationTokenTable`. Defaulting 'valid_token' to None'".format( - api_key - ) - ) - valid_token = None + valid_token = await get_key_object( + hashed_token=api_key, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + parent_otel_span=parent_otel_span, + proxy_logging_obj=proxy_logging_obj, + ) + # update end-user params on valid token + # These can change per request - it's important to update them here + valid_token.end_user_id = end_user_params.get("end_user_id") + valid_token.end_user_tpm_limit = end_user_params.get("end_user_tpm_limit") + valid_token.end_user_rpm_limit = end_user_params.get("end_user_rpm_limit") + valid_token.allowed_model_region = end_user_params.get( + "allowed_model_region" + ) + # update key budget with temp budget increase + valid_token = _update_key_budget_with_temp_budget_increase( + valid_token + ) # updating it here, allows all downstream reporting / checks to use the updated budget if valid_token is None: raise Exception( From c6d5793bf6077ac94b7884ee1fde97521ac6ab52 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 17:50:27 -0700 Subject: [PATCH 004/208] add toxi proxy tests to ci/cd --- .circleci/config.yml | 63 ++++++++++++ .../setup_toxi_proxy.py | 96 +++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 tests/proxy_reliability_tests/setup_toxi_proxy.py diff --git a/.circleci/config.yml b/.circleci/config.yml index b93a9d81e8..87432c7a24 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -470,6 +470,61 @@ jobs: paths: - litellm_proxy_security_tests_coverage.xml - litellm_proxy_security_tests_coverage + litellm_proxy_reliability_tests: + docker: + - image: cimg/python:3.11 + auth: + username: ${DOCKERHUB_USERNAME} + password: ${DOCKERHUB_PASSWORD} + working_directory: ~/project + steps: + - checkout + - run: + name: Show git commit hash + command: | + echo "Git commit hash: $CIRCLE_SHA1" + - run: + name: Install Dependencies + command: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + pip install "requests==2.31.0" + pip install "pytest==7.3.1" + pip install "pytest-retry==1.6.3" + pip install "pytest-asyncio==0.21.1" + pip install "pytest-cov==5.0.0" + - run: + name: Setup Toxiproxy + command: | + python tests/proxy_reliability_tests/setup_toxiproxy.py + - run: + name: Run prisma ./docker/entrypoint.sh + command: | + set +e + chmod +x docker/entrypoint.sh + ./docker/entrypoint.sh + set -e + # Run pytest and generate JUnit XML report + - run: + name: Run tests + command: | + pwd + ls + python -m pytest tests/proxy_security_tests --cov=litellm --cov-report=xml -vv -x -v --junitxml=test-results/junit.xml --durations=5 + no_output_timeout: 120m + - run: + name: Rename the coverage files + command: | + mv coverage.xml litellm_proxy_security_tests_coverage.xml + mv .coverage litellm_proxy_security_tests_coverage + # Store test results + - store_test_results: + path: test-results + - persist_to_workspace: + root: . + paths: + - litellm_proxy_security_tests_coverage.xml + - litellm_proxy_security_tests_coverage litellm_proxy_unit_testing: # Runs all tests with the "proxy", "key", "jwt" filenames docker: - image: cimg/python:3.11 @@ -2451,6 +2506,12 @@ workflows: only: - main - /litellm_.*/ + - litellm_proxy_reliability_tests: + filters: + branches: + only: + - main + - /litellm_.*/ - litellm_assistants_api_testing: filters: branches: @@ -2592,6 +2653,7 @@ workflows: - caching_unit_tests - litellm_proxy_unit_testing - litellm_proxy_security_tests + - litellm_proxy_reliability_tests - langfuse_logging_unit_tests - local_testing - litellm_assistants_api_testing @@ -2657,6 +2719,7 @@ workflows: - e2e_ui_testing - litellm_proxy_unit_testing - litellm_proxy_security_tests + - litellm_proxy_reliability_tests - installing_litellm_on_python - installing_litellm_on_python_3_13 - proxy_logging_guardrails_model_info_tests diff --git a/tests/proxy_reliability_tests/setup_toxi_proxy.py b/tests/proxy_reliability_tests/setup_toxi_proxy.py new file mode 100644 index 0000000000..3625b0bd3a --- /dev/null +++ b/tests/proxy_reliability_tests/setup_toxi_proxy.py @@ -0,0 +1,96 @@ +import os +import platform +import subprocess +import time +import requests +import stat + + +def download_toxiproxy(): + # Determine system architecture and OS + system = platform.system().lower() + machine = platform.machine().lower() + + # Map architecture names + arch_map = { + "x86_64": "amd64", + "amd64": "amd64", + "arm64": "arm64", + "aarch64": "arm64", + } + + arch = arch_map.get(machine, machine) + + # Construct download URL (using latest version 2.5.0) + base_url = "https://github.com/Shopify/toxiproxy/releases/download/v2.5.0/" + if system == "linux": + filename = f"toxiproxy-server-linux-{arch}" + cli_filename = f"toxiproxy-cli-linux-{arch}" + elif system == "darwin": + filename = f"toxiproxy-server-darwin-{arch}" + cli_filename = f"toxiproxy-cli-darwin-{arch}" + else: + raise Exception("Unsupported operating system") + + # Download server + response = requests.get(f"{base_url}{filename}") + with open("toxiproxy-server", "wb") as f: + f.write(response.content) + + # Download CLI + response = requests.get(f"{base_url}{cli_filename}") + with open("toxiproxy-cli", "wb") as f: + f.write(response.content) + + # Make files executable + os.chmod("toxiproxy-server", stat.S_IRWXU) + os.chmod("toxiproxy-cli", stat.S_IRWXU) + + +def setup_toxiproxy(): + # Start toxiproxy-server + server_process = subprocess.Popen(["./toxiproxy-server"]) + + # Wait for server to start + time.sleep(2) + + # Create proxy + subprocess.run( + [ + "./toxiproxy-cli", + "create", + "postgres_proxy", + "-l", + "127.0.0.1:6666", + "-u", + "ep-dry-paper-a69g2y1q-pooler.us-west-2.aws.neon.tech:5432", + ] + ) + + return server_process + + +def main(): + try: + # Download ToxiProxy binaries + download_toxiproxy() + + # Setup ToxiProxy + server_process = setup_toxiproxy() + + print("ToxiProxy setup completed successfully!") + print("Proxy 'postgres_proxy' created and listening on 127.0.0.1:6666") + + # Keep the script running to maintain the proxy + try: + server_process.wait() + except KeyboardInterrupt: + server_process.terminate() + print("\nToxiProxy server stopped") + + except Exception as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + main() From 7b09d88680430d2fe5fd79c0f57e6079fa08113b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 17:52:12 -0700 Subject: [PATCH 005/208] fix setup --- tests/proxy_reliability_tests/setup_toxi_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/proxy_reliability_tests/setup_toxi_proxy.py b/tests/proxy_reliability_tests/setup_toxi_proxy.py index 3625b0bd3a..1338445f3a 100644 --- a/tests/proxy_reliability_tests/setup_toxi_proxy.py +++ b/tests/proxy_reliability_tests/setup_toxi_proxy.py @@ -59,11 +59,11 @@ def setup_toxiproxy(): [ "./toxiproxy-cli", "create", - "postgres_proxy", "-l", "127.0.0.1:6666", "-u", "ep-dry-paper-a69g2y1q-pooler.us-west-2.aws.neon.tech:5432", + "postgres_proxy", ] ) From 34c3825d1306c095730bdf1dd52f1e9c25c2b917 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 17:53:30 -0700 Subject: [PATCH 006/208] fix path --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87432c7a24..ea887bf9de 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -496,7 +496,7 @@ jobs: - run: name: Setup Toxiproxy command: | - python tests/proxy_reliability_tests/setup_toxiproxy.py + python tests/proxy_reliability_tests/setup_toxi_proxy.py - run: name: Run prisma ./docker/entrypoint.sh command: | From 53a586e87626c8816ad8febde009e53be5890c09 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 17:59:38 -0700 Subject: [PATCH 007/208] TOXI_PROXY_DATABASE_URL --- .circleci/config.yml | 75 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ea887bf9de..15eacf9f51 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -471,47 +471,88 @@ jobs: - litellm_proxy_security_tests_coverage.xml - litellm_proxy_security_tests_coverage litellm_proxy_reliability_tests: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} + machine: + image: ubuntu-2204:2023.10.1 + resource_class: xlarge working_directory: ~/project steps: - checkout - run: - name: Show git commit hash + name: Install Docker CLI (In case it's not already installed) command: | - echo "Git commit hash: $CIRCLE_SHA1" + sudo apt-get update + sudo apt-get install -y docker-ce docker-ce-cli containerd.io + - run: + name: Install Python 3.9 + command: | + curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh --output miniconda.sh + bash miniconda.sh -b -p $HOME/miniconda + export PATH="$HOME/miniconda/bin:$PATH" + conda init bash + source ~/.bashrc + conda create -n myenv python=3.9 -y + conda activate myenv + python --version - run: name: Install Dependencies command: | + pip install "pytest==7.3.1" + pip install "pytest-asyncio==0.21.1" + pip install aiohttp python -m pip install --upgrade pip python -m pip install -r requirements.txt - pip install "requests==2.31.0" pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" + pip install "pytest-mock==3.12.0" pip install "pytest-asyncio==0.21.1" - pip install "pytest-cov==5.0.0" + pip install "assemblyai==0.37.0" - run: name: Setup Toxiproxy command: | python tests/proxy_reliability_tests/setup_toxi_proxy.py - run: - name: Run prisma ./docker/entrypoint.sh + name: Build Docker image + command: docker build -t my-app:latest -f ./docker/Dockerfile.database . + - run: + name: Run Docker container + # intentionally give bad redis credentials here + # the OTEL test - should get this as a trace command: | - set +e - chmod +x docker/entrypoint.sh - ./docker/entrypoint.sh - set -e - # Run pytest and generate JUnit XML report + docker run -d \ + -p 4000:4000 \ + -e DATABASE_URL=$TOXI_PROXY_DATABASE_URL \ + -e STORE_MODEL_IN_DB="True" \ + -e LITELLM_MASTER_KEY="sk-1234" \ + -e LITELLM_LICENSE=$LITELLM_LICENSE \ + --name my-app \ + -v $(pwd)/litellm/proxy/example_config_yaml/store_model_db_config.yaml:/app/config.yaml \ + my-app:latest \ + --config /app/config.yaml \ + --port 4000 \ + --detailed_debug \ + - run: + name: Install curl and dockerize + command: | + sudo apt-get update + sudo apt-get install -y curl + sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz + sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz + sudo rm dockerize-linux-amd64-v0.6.1.tar.gz + - run: + name: Start outputting logs + command: docker logs -f my-app + background: true + - run: + name: Wait for app to be ready + command: dockerize -wait http://localhost:4000 -timeout 5m - run: name: Run tests command: | pwd ls - python -m pytest tests/proxy_security_tests --cov=litellm --cov-report=xml -vv -x -v --junitxml=test-results/junit.xml --durations=5 - no_output_timeout: 120m + python -m pytest -vv tests/store_model_in_db_tests -x --junitxml=test-results/junit.xml --durations=5 + no_output_timeout: + 120m - run: name: Rename the coverage files command: | From bf7241abd1cb6d92899df8796fbdb149df01af72 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 18:02:01 -0700 Subject: [PATCH 008/208] litellm_proxy_reliability_tests --- .circleci/config.yml | 193 ++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 96 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 15eacf9f51..61f98405a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -470,102 +470,6 @@ jobs: paths: - litellm_proxy_security_tests_coverage.xml - litellm_proxy_security_tests_coverage - litellm_proxy_reliability_tests: - machine: - image: ubuntu-2204:2023.10.1 - resource_class: xlarge - working_directory: ~/project - steps: - - checkout - - run: - name: Install Docker CLI (In case it's not already installed) - command: | - sudo apt-get update - sudo apt-get install -y docker-ce docker-ce-cli containerd.io - - run: - name: Install Python 3.9 - command: | - curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh --output miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - conda init bash - source ~/.bashrc - conda create -n myenv python=3.9 -y - conda activate myenv - python --version - - run: - name: Install Dependencies - command: | - pip install "pytest==7.3.1" - pip install "pytest-asyncio==0.21.1" - pip install aiohttp - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-mock==3.12.0" - pip install "pytest-asyncio==0.21.1" - pip install "assemblyai==0.37.0" - - run: - name: Setup Toxiproxy - command: | - python tests/proxy_reliability_tests/setup_toxi_proxy.py - - run: - name: Build Docker image - command: docker build -t my-app:latest -f ./docker/Dockerfile.database . - - run: - name: Run Docker container - # intentionally give bad redis credentials here - # the OTEL test - should get this as a trace - command: | - docker run -d \ - -p 4000:4000 \ - -e DATABASE_URL=$TOXI_PROXY_DATABASE_URL \ - -e STORE_MODEL_IN_DB="True" \ - -e LITELLM_MASTER_KEY="sk-1234" \ - -e LITELLM_LICENSE=$LITELLM_LICENSE \ - --name my-app \ - -v $(pwd)/litellm/proxy/example_config_yaml/store_model_db_config.yaml:/app/config.yaml \ - my-app:latest \ - --config /app/config.yaml \ - --port 4000 \ - --detailed_debug \ - - run: - name: Install curl and dockerize - command: | - sudo apt-get update - sudo apt-get install -y curl - sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz - sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz - sudo rm dockerize-linux-amd64-v0.6.1.tar.gz - - run: - name: Start outputting logs - command: docker logs -f my-app - background: true - - run: - name: Wait for app to be ready - command: dockerize -wait http://localhost:4000 -timeout 5m - - run: - name: Run tests - command: | - pwd - ls - python -m pytest -vv tests/store_model_in_db_tests -x --junitxml=test-results/junit.xml --durations=5 - no_output_timeout: - 120m - - run: - name: Rename the coverage files - command: | - mv coverage.xml litellm_proxy_security_tests_coverage.xml - mv .coverage litellm_proxy_security_tests_coverage - # Store test results - - store_test_results: - path: test-results - - persist_to_workspace: - root: . - paths: - - litellm_proxy_security_tests_coverage.xml - - litellm_proxy_security_tests_coverage litellm_proxy_unit_testing: # Runs all tests with the "proxy", "key", "jwt" filenames docker: - image: cimg/python:3.11 @@ -1984,6 +1888,103 @@ jobs: no_output_timeout: 120m # Clean up first container + litellm_proxy_reliability_tests: + machine: + image: ubuntu-2204:2023.10.1 + resource_class: xlarge + working_directory: ~/project + steps: + - checkout + - run: + name: Install Docker CLI (In case it's not already installed) + command: | + sudo apt-get update + sudo apt-get install -y docker-ce docker-ce-cli containerd.io + - run: + name: Install Python 3.9 + command: | + curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh --output miniconda.sh + bash miniconda.sh -b -p $HOME/miniconda + export PATH="$HOME/miniconda/bin:$PATH" + conda init bash + source ~/.bashrc + conda create -n myenv python=3.9 -y + conda activate myenv + python --version + - run: + name: Install Dependencies + command: | + pip install "pytest==7.3.1" + pip install "pytest-asyncio==0.21.1" + pip install aiohttp + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + pip install "pytest==7.3.1" + pip install "pytest-retry==1.6.3" + pip install "pytest-mock==3.12.0" + pip install "pytest-asyncio==0.21.1" + pip install "assemblyai==0.37.0" + - run: + name: Setup Toxiproxy + command: | + python tests/proxy_reliability_tests/setup_toxi_proxy.py + - run: + name: Build Docker image + command: docker build -t my-app:latest -f ./docker/Dockerfile.database . + - run: + name: Run Docker container + # intentionally give bad redis credentials here + # the OTEL test - should get this as a trace + command: | + docker run -d \ + -p 4000:4000 \ + -e DATABASE_URL=$TOXI_PROXY_DATABASE_URL \ + -e STORE_MODEL_IN_DB="True" \ + -e LITELLM_MASTER_KEY="sk-1234" \ + -e LITELLM_LICENSE=$LITELLM_LICENSE \ + --name my-app \ + -v $(pwd)/litellm/proxy/example_config_yaml/store_model_db_config.yaml:/app/config.yaml \ + my-app:latest \ + --config /app/config.yaml \ + --port 4000 \ + --detailed_debug \ + - run: + name: Install curl and dockerize + command: | + sudo apt-get update + sudo apt-get install -y curl + sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz + sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz + sudo rm dockerize-linux-amd64-v0.6.1.tar.gz + - run: + name: Start outputting logs + command: docker logs -f my-app + background: true + - run: + name: Wait for app to be ready + command: dockerize -wait http://localhost:4000 -timeout 5m + - run: + name: Run tests + command: | + pwd + ls + python -m pytest -vv tests/store_model_in_db_tests -x --junitxml=test-results/junit.xml --durations=5 + no_output_timeout: + 120m + - run: + name: Rename the coverage files + command: | + mv coverage.xml litellm_proxy_security_tests_coverage.xml + mv .coverage litellm_proxy_security_tests_coverage + # Store test results + - store_test_results: + path: test-results + - persist_to_workspace: + root: . + paths: + - litellm_proxy_security_tests_coverage.xml + - litellm_proxy_security_tests_coverage + proxy_build_from_pip_tests: # Change from docker to machine executor From 83b41f95e769a1f19d33d1b8ca934311153e6dd5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 18:05:41 -0700 Subject: [PATCH 009/208] Setup Toxiproxy --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 61f98405a7..b7dc08bced 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1927,7 +1927,8 @@ jobs: - run: name: Setup Toxiproxy command: | - python tests/proxy_reliability_tests/setup_toxi_proxy.py + python tests/proxy_reliability_tests/setup_toxi_proxy.py & + sleep 5 - run: name: Build Docker image command: docker build -t my-app:latest -f ./docker/Dockerfile.database . From 6f138c79a744cb1f9ea7f44089968f132e737580 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 18:19:11 -0700 Subject: [PATCH 010/208] run toxi proxy tests --- .circleci/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b7dc08bced..2577862036 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1934,8 +1934,6 @@ jobs: command: docker build -t my-app:latest -f ./docker/Dockerfile.database . - run: name: Run Docker container - # intentionally give bad redis credentials here - # the OTEL test - should get this as a trace command: | docker run -d \ -p 4000:4000 \ From 9e2d230339c001b894956a841689bf115176927e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 18:23:52 -0700 Subject: [PATCH 011/208] litellm_proxy_reliability_tests --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2577862036..8e376b6dc2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2552,7 +2552,8 @@ workflows: branches: only: - main - - /litellm_.*/ + - /litellm_stability_.*/ + - litellm_stable_release_branch - litellm_assistants_api_testing: filters: branches: From 53d9e33e782da6051418eff380e6b507fcafa329 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 18:59:26 -0700 Subject: [PATCH 012/208] fix setup toxi proxy --- .circleci/config.yml | 1 + tests/proxy_reliability_tests/setup_toxi_proxy.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e376b6dc2..04799ddd75 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1942,6 +1942,7 @@ jobs: -e LITELLM_MASTER_KEY="sk-1234" \ -e LITELLM_LICENSE=$LITELLM_LICENSE \ --name my-app \ + --network=host \ -v $(pwd)/litellm/proxy/example_config_yaml/store_model_db_config.yaml:/app/config.yaml \ my-app:latest \ --config /app/config.yaml \ diff --git a/tests/proxy_reliability_tests/setup_toxi_proxy.py b/tests/proxy_reliability_tests/setup_toxi_proxy.py index 1338445f3a..b44298aca5 100644 --- a/tests/proxy_reliability_tests/setup_toxi_proxy.py +++ b/tests/proxy_reliability_tests/setup_toxi_proxy.py @@ -60,7 +60,7 @@ def setup_toxiproxy(): "./toxiproxy-cli", "create", "-l", - "127.0.0.1:6666", + "0.0.0.0:6666", "-u", "ep-dry-paper-a69g2y1q-pooler.us-west-2.aws.neon.tech:5432", "postgres_proxy", From 438655858210efc9cbc9d3d594bbe66494c92c66 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 19:11:13 -0700 Subject: [PATCH 013/208] litellm_proxy_reliability_tests --- .circleci/config.yml | 137 ++++++++++++------------------------------- 1 file changed, 39 insertions(+), 98 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 04799ddd75..e4d6af8f0e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -686,6 +686,44 @@ jobs: paths: - llm_translation_coverage.xml - llm_translation_coverage + litellm_proxy_reliability_tests: + docker: + - image: cimg/python:3.11 + auth: + username: ${DOCKERHUB_USERNAME} + password: ${DOCKERHUB_PASSWORD} + working_directory: ~/project + + steps: + - checkout + - run: + name: Install Dependencies + command: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + pip install "pytest==7.3.1" + pip install "pytest-retry==1.6.3" + pip install "pytest-cov==5.0.0" + pip install "pytest-asyncio==0.21.1" + pip install "respx==0.21.1" + # Run pytest and generate JUnit XML report + - run: + name: Setup Toxiproxy + command: | + python tests/proxy_reliability_tests/setup_toxi_proxy.py & + sleep 5 + - run: + name: Run tests + command: | + pwd + ls + python -m pytest -vv tests/proxy_reliability_tests --cov=litellm --cov-report=xml -x -s -v --junitxml=test-results/junit.xml --durations=5 + no_output_timeout: 120m + - run: + name: Rename the coverage files + command: | + mv coverage.xml litellm_proxy_reliability_tests_coverage.xml + mv .coverage litellm_proxy_reliability_tests_coverage mcp_testing: docker: - image: cimg/python:3.11 @@ -1888,104 +1926,7 @@ jobs: no_output_timeout: 120m # Clean up first container - litellm_proxy_reliability_tests: - machine: - image: ubuntu-2204:2023.10.1 - resource_class: xlarge - working_directory: ~/project - steps: - - checkout - - run: - name: Install Docker CLI (In case it's not already installed) - command: | - sudo apt-get update - sudo apt-get install -y docker-ce docker-ce-cli containerd.io - - run: - name: Install Python 3.9 - command: | - curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh --output miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - conda init bash - source ~/.bashrc - conda create -n myenv python=3.9 -y - conda activate myenv - python --version - - run: - name: Install Dependencies - command: | - pip install "pytest==7.3.1" - pip install "pytest-asyncio==0.21.1" - pip install aiohttp - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-mock==3.12.0" - pip install "pytest-asyncio==0.21.1" - pip install "assemblyai==0.37.0" - - run: - name: Setup Toxiproxy - command: | - python tests/proxy_reliability_tests/setup_toxi_proxy.py & - sleep 5 - - run: - name: Build Docker image - command: docker build -t my-app:latest -f ./docker/Dockerfile.database . - - run: - name: Run Docker container - command: | - docker run -d \ - -p 4000:4000 \ - -e DATABASE_URL=$TOXI_PROXY_DATABASE_URL \ - -e STORE_MODEL_IN_DB="True" \ - -e LITELLM_MASTER_KEY="sk-1234" \ - -e LITELLM_LICENSE=$LITELLM_LICENSE \ - --name my-app \ - --network=host \ - -v $(pwd)/litellm/proxy/example_config_yaml/store_model_db_config.yaml:/app/config.yaml \ - my-app:latest \ - --config /app/config.yaml \ - --port 4000 \ - --detailed_debug \ - - run: - name: Install curl and dockerize - command: | - sudo apt-get update - sudo apt-get install -y curl - sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz - sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz - sudo rm dockerize-linux-amd64-v0.6.1.tar.gz - - run: - name: Start outputting logs - command: docker logs -f my-app - background: true - - run: - name: Wait for app to be ready - command: dockerize -wait http://localhost:4000 -timeout 5m - - run: - name: Run tests - command: | - pwd - ls - python -m pytest -vv tests/store_model_in_db_tests -x --junitxml=test-results/junit.xml --durations=5 - no_output_timeout: - 120m - - run: - name: Rename the coverage files - command: | - mv coverage.xml litellm_proxy_security_tests_coverage.xml - mv .coverage litellm_proxy_security_tests_coverage - # Store test results - - store_test_results: - path: test-results - - persist_to_workspace: - root: . - paths: - - litellm_proxy_security_tests_coverage.xml - - litellm_proxy_security_tests_coverage - - + proxy_build_from_pip_tests: # Change from docker to machine executor machine: From 9d10befa09f5cd16420195abaef99b9795bb9bc1 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 19:16:34 -0700 Subject: [PATCH 014/208] test_litellm_proxy_server_config_no_general_settings --- .../test_using_litellm_db_outage.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/proxy_reliability_tests/test_using_litellm_db_outage.py diff --git a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py new file mode 100644 index 0000000000..745a14bdc6 --- /dev/null +++ b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py @@ -0,0 +1,74 @@ +import asyncio +import os +import subprocess +import sys +import time +import traceback + +import pytest + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +import litellm + +import os +import subprocess +import time + +import pytest +import requests + + +def test_litellm_proxy_server_config_no_general_settings(): + # Install the litellm[proxy] package + # Start the server + try: + subprocess.run(["pip", "install", "litellm[proxy]"]) + subprocess.run(["pip", "install", "litellm[extra_proxy]"]) + filepath = os.path.dirname(os.path.abspath(__file__)) + config_fp = f"{filepath}/test_configs/test_config_no_auth.yaml" + server_process = subprocess.Popen( + [ + "python", + "-m", + "litellm.proxy.proxy_cli", + "--config", + config_fp, + ] + ) + + # Allow some time for the server to start + time.sleep(60) # Adjust the sleep time if necessary + + # Send a request to the /health/liveliness endpoint + response = requests.get("http://localhost:4000/health/liveliness") + + # Check if the response is successful + assert response.status_code == 200 + assert response.json() == "I'm alive!" + + # Test /chat/completions + response = requests.post( + "http://localhost:4000/chat/completions", + headers={"Authorization": "Bearer 1234567890"}, + json={ + "model": "test_openai_models", + "messages": [{"role": "user", "content": "Hello, how are you?"}], + }, + ) + + assert response.status_code == 200 + + except ImportError: + pytest.fail("Failed to import litellm.proxy_server") + except requests.ConnectionError: + pytest.fail("Failed to connect to the server") + finally: + # Shut down the server + server_process.terminate() + server_process.wait() + + # Additional assertions can be added here + assert True From b4e745323a49e630b0b1f98bdefcce62690e2d04 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 19:21:51 -0700 Subject: [PATCH 015/208] add test config --- litellm/proxy/proxy_config.yaml | 40 +++---------------- .../test_configs/test_config.yaml | 9 +++++ .../test_using_litellm_db_outage.py | 2 +- 3 files changed, 16 insertions(+), 35 deletions(-) create mode 100644 tests/proxy_reliability_tests/test_configs/test_config.yaml diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 26ce6cb8f8..4912a35f89 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -1,37 +1,9 @@ model_list: - - model_name: gpt-3.5-turbo-end-user-test + - model_name: fake-openai-endpoint litellm_params: - model: azure/chatgpt-v-2 - api_base: https://openai-gpt-4-test-v-1.openai.azure.com/ - api_version: "2023-05-15" - api_key: os.environ/AZURE_API_KEY + model: openai/fake + api_key: fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ - - -mcp_tools: - - name: "get_current_time" - description: "Get the current time" - input_schema: { - "type": "object", - "properties": { - "format": { - "type": "string", - "description": "The format of the time to return", - "enum": ["short"] - } - } - } - handler: "mcp_tools.get_current_time" - - name: "get_current_date" - description: "Get the current date" - input_schema: { - "type": "object", - "properties": { - "format": { - "type": "string", - "description": "The format of the date to return", - "enum": ["short"] - } - } - } - handler: "mcp_tools.get_current_date" +general_settings: + allow_requests_on_db_unavailable: True \ No newline at end of file diff --git a/tests/proxy_reliability_tests/test_configs/test_config.yaml b/tests/proxy_reliability_tests/test_configs/test_config.yaml new file mode 100644 index 0000000000..4912a35f89 --- /dev/null +++ b/tests/proxy_reliability_tests/test_configs/test_config.yaml @@ -0,0 +1,9 @@ +model_list: + - model_name: fake-openai-endpoint + litellm_params: + model: openai/fake + api_key: fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ + +general_settings: + allow_requests_on_db_unavailable: True \ No newline at end of file diff --git a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py index 745a14bdc6..acaaed05f3 100644 --- a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py +++ b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py @@ -28,7 +28,7 @@ def test_litellm_proxy_server_config_no_general_settings(): subprocess.run(["pip", "install", "litellm[proxy]"]) subprocess.run(["pip", "install", "litellm[extra_proxy]"]) filepath = os.path.dirname(os.path.abspath(__file__)) - config_fp = f"{filepath}/test_configs/test_config_no_auth.yaml" + config_fp = f"{filepath}/test_configs/test_config.yaml" server_process = subprocess.Popen( [ "python", From 6572ba7a0ef9ddb24345dde179610365d237e571 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 19:25:47 -0700 Subject: [PATCH 016/208] fix startup --- .../proxy_reliability_tests/test_using_litellm_db_outage.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py index acaaed05f3..52d3ec6222 100644 --- a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py +++ b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py @@ -4,7 +4,6 @@ import subprocess import sys import time import traceback - import pytest sys.path.insert( @@ -29,6 +28,10 @@ def test_litellm_proxy_server_config_no_general_settings(): subprocess.run(["pip", "install", "litellm[extra_proxy]"]) filepath = os.path.dirname(os.path.abspath(__file__)) config_fp = f"{filepath}/test_configs/test_config.yaml" + + # Set DATABASE_URL environment variable + os.environ["DATABASE_URL"] = os.getenv("TOXI_PROXY_DATABASE_URL") + server_process = subprocess.Popen( [ "python", From 0a401ee468de6a13c905c8b41c66a73b95d5b1c5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 19:27:15 -0700 Subject: [PATCH 017/208] test_litellm_proxy_server_config_no_general_settings --- .../test_using_litellm_db_outage.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py index 52d3ec6222..ea9c7e3f3a 100644 --- a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py +++ b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py @@ -19,18 +19,20 @@ import time import pytest import requests +TEST_MASTER_KEY = "sk-1234" + def test_litellm_proxy_server_config_no_general_settings(): # Install the litellm[proxy] package # Start the server try: subprocess.run(["pip", "install", "litellm[proxy]"]) - subprocess.run(["pip", "install", "litellm[extra_proxy]"]) filepath = os.path.dirname(os.path.abspath(__file__)) config_fp = f"{filepath}/test_configs/test_config.yaml" # Set DATABASE_URL environment variable os.environ["DATABASE_URL"] = os.getenv("TOXI_PROXY_DATABASE_URL") + os.environ["LITELLM_MASTER_KEY"] = TEST_MASTER_KEY server_process = subprocess.Popen( [ @@ -72,6 +74,3 @@ def test_litellm_proxy_server_config_no_general_settings(): # Shut down the server server_process.terminate() server_process.wait() - - # Additional assertions can be added here - assert True From f68cc26f1590fb6569b9a1dd1a37d99070dc5ee8 Mon Sep 17 00:00:00 2001 From: Nicholas Grabar Date: Tue, 25 Mar 2025 22:37:28 -0700 Subject: [PATCH 018/208] 8864 Add support for anyOf union type while handling null fields --- .gitignore | 2 +- litellm/llms/vertex_ai/common_utils.py | 43 +++++++----- .../vertex_ai/test_vertex_ai_common_utils.py | 70 +++++++++++++++++++ 3 files changed, 95 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index dab6d4ec81..a9d14852eb 100644 --- a/.gitignore +++ b/.gitignore @@ -82,4 +82,4 @@ tests/llm_translation/test_vertex_key.json litellm/proxy/migrations/0_init/migration.sql litellm/proxy/db/migrations/0_init/migration.sql litellm/proxy/db/migrations/* -litellm/proxy/migrations/* \ No newline at end of file +litellm/proxy/migrations/*config.yaml diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index a3f91fbacc..9a01c15233 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -160,7 +160,7 @@ def _build_vertex_schema(parameters: dict): # * https://github.com/pydantic/pydantic/issues/1270 # * https://stackoverflow.com/a/58841311 # * https://github.com/pydantic/pydantic/discussions/4872 - convert_to_nullable(parameters) + convert_anyof_null_to_nullable(parameters) add_object_type(parameters) # Postprocessing # 4. Suppress unnecessary title generation: @@ -211,34 +211,39 @@ def unpack_defs(schema, defs): continue -def convert_to_nullable(schema): - anyof = schema.pop("anyOf", None) +def convert_anyof_null_to_nullable(schema): + """ Converts null objects within anyOf by removing them and adding nullable to all remaining objects """ + anyof = schema.get("anyOf", None) if anyof is not None: - if len(anyof) != 2: + contains_null = False + for atype in anyof: + if atype == {"type": "null"}: + # remove null type + anyof.remove(atype) + contains_null = True + + if len(anyof) == 0: + # Edge case: response schema with only null type present is invalid in Vertex AI raise ValueError( - "Invalid input: Type Unions are not supported, except for `Optional` types. " - "Please provide an `Optional` type or a non-Union type." + "Invalid input: AnyOf schema with only null type is not supported. " + "Please provide a non-null type." ) - a, b = anyof - if a == {"type": "null"}: - schema.update(b) - elif b == {"type": "null"}: - schema.update(a) - else: - raise ValueError( - "Invalid input: Type Unions are not supported, except for `Optional` types. " - "Please provide an `Optional` type or a non-Union type." - ) - schema["nullable"] = True + + + if contains_null: + # set all types to nullable following guidance found here: https://cloud.google.com/vertex-ai/generative-ai/docs/samples/generativeaionvertexai-gemini-controlled-generation-response-schema-3#generativeaionvertexai_gemini_controlled_generation_response_schema_3-python + for atype in anyof: + atype["nullable"] = True + properties = schema.get("properties", None) if properties is not None: for name, value in properties.items(): - convert_to_nullable(value) + convert_anyof_null_to_nullable(value) items = schema.get("items", None) if items is not None: - convert_to_nullable(items) + convert_anyof_null_to_nullable(items) def add_object_type(schema): diff --git a/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index e89355443f..1c6ff65ce6 100644 --- a/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -12,6 +12,7 @@ import litellm from litellm.llms.vertex_ai.common_utils import ( get_vertex_location_from_url, get_vertex_project_id_from_url, + convert_anyof_null_to_nullable ) @@ -41,3 +42,72 @@ async def test_get_vertex_location_from_url(): url = "https://invalid-url.com" location = get_vertex_location_from_url(url) assert location is None + +def test_basic_anyof_conversion(): + """Test basic conversion of anyOf with 'null'.""" + schema = { + "type": "object", + "properties": { + "example": { + "anyOf": [ + {"type": "string"}, + {"type": "null"} + ] + } + } + } + + convert_anyof_null_to_nullable(schema) + + expected = { + "type": "object", + "properties": { + "example": { + "anyOf": [ + {"type": "string", "nullable": True} + ] + } + } + } + assert schema == expected + + +def test_nested_anyof_conversion(): + """Test nested conversion with 'anyOf' inside properties.""" + schema = { + "type": "object", + "properties": { + "outer": { + "type": "object", + "properties": { + "inner": { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "string"}, + {"type": "null"} + ] + } + } + } + } + } + + convert_anyof_null_to_nullable(schema) + + expected = { + "type": "object", + "properties": { + "outer": { + "type": "object", + "properties": { + "inner": { + "anyOf": [ + {"type": "array", "items": {"type": "string"}, "nullable": True}, + {"type": "string", "nullable": True} + ] + } + } + } + } + } + assert schema == expected \ No newline at end of file From 6e5d2b1ac77ef48fb4f65ef131799c8ca183bcf4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 23:14:44 -0700 Subject: [PATCH 019/208] handle failed db connections --- tests/proxy_unit_tests/test_auth_checks.py | 33 ---------------------- 1 file changed, 33 deletions(-) diff --git a/tests/proxy_unit_tests/test_auth_checks.py b/tests/proxy_unit_tests/test_auth_checks.py index 0eb1a38755..7695306c87 100644 --- a/tests/proxy_unit_tests/test_auth_checks.py +++ b/tests/proxy_unit_tests/test_auth_checks.py @@ -13,9 +13,6 @@ sys.path.insert( ) # Adds the parent directory to the system path import pytest, litellm import httpx -from litellm.proxy.auth.auth_checks import ( - _handle_failed_db_connection_for_get_key_object, -) from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.auth.auth_checks import get_end_user_object from litellm.caching.caching import DualCache @@ -78,36 +75,6 @@ async def test_get_end_user_object(customer_spend, customer_budget): ) -@pytest.mark.asyncio -async def test_handle_failed_db_connection(): - """ - Test cases: - 1. When allow_requests_on_db_unavailable=True -> return UserAPIKeyAuth - 2. When allow_requests_on_db_unavailable=False -> raise original error - """ - from litellm.proxy.proxy_server import general_settings, litellm_proxy_admin_name - - # Test case 1: allow_requests_on_db_unavailable=True - general_settings["allow_requests_on_db_unavailable"] = True - mock_error = httpx.ConnectError("Failed to connect to DB") - - result = await _handle_failed_db_connection_for_get_key_object(e=mock_error) - - assert isinstance(result, UserAPIKeyAuth) - assert result.key_name == "failed-to-connect-to-db" - assert result.token == "failed-to-connect-to-db" - assert result.user_id == litellm_proxy_admin_name - - # Test case 2: allow_requests_on_db_unavailable=False - general_settings["allow_requests_on_db_unavailable"] = False - - with pytest.raises(httpx.ConnectError) as exc_info: - await _handle_failed_db_connection_for_get_key_object(e=mock_error) - print("_handle_failed_db_connection_for_get_key_object got exception", exc_info) - - assert str(exc_info.value) == "Failed to connect to DB" - - @pytest.mark.parametrize( "model, expect_to_work", [ From efce84815adfbf3cd8301855e484c563c8a5e3ea Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 23:54:06 -0700 Subject: [PATCH 020/208] test_gemini_fine_tuned_model_request_consistency --- .../test_amazing_vertex_completion.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 25993d6d5b..7134c3536e 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -3327,3 +3327,96 @@ def test_signed_s3_url_with_format(): json_str = json.dumps(mock_client.call_args.kwargs["json"]) assert "image/jpeg" in json_str assert "image/png" not in json_str + + +def test_gemini_fine_tuned_model_request_consistency(): + """ + Assert the same transformation is applied to Fine tuned gemini 2.0 flash and gemini 2.0 flash + + - Request 1: Fine tuned: vertex_ai/ft-uuid with base_model: vertex_ai/gemini-2.0-flash-001 + - Request 2: vertex_ai/gemini-2.0-flash-001 + """ + litellm.set_verbose = True + load_vertex_ai_credentials() + from litellm.llms.custom_httpx.http_handler import HTTPHandler + from unittest.mock import patch, MagicMock + + # Set up the messages + messages = [ + { + "role": "system", + "content": "Your name is Litellm Bot, you are a helpful assistant", + }, + { + "role": "user", + "content": "Hello, what is your name and can you tell me the weather?", + }, + ] + + # Define tools + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + } + }, + "required": ["location"], + }, + }, + } + ] + + client = HTTPHandler(concurrent_limit=1) + + # First request + with patch.object(client, "post", new=MagicMock()) as mock_post_1: + try: + response_1 = completion( + model="vertex_ai/ft-uuid", + base_model="vertex_ai/gemini-2.0-flash-001", + messages=messages, + tools=tools, + tool_choice="auto", + client=client, + ) + + except Exception as e: + print(e) + + # Store the request body from the first call + first_request_body = mock_post_1.call_args.kwargs["json"] + print("first_request_body", first_request_body) + + # Second request + with patch.object(client, "post", new=MagicMock()) as mock_post_2: + try: + response_2 = completion( + model="vertex_ai/gemini-2.0-flash-001", + messages=messages, + tools=tools, + tool_choice="auto", + client=client, + ) + except Exception as e: + print(e) + + # Store the request body from the second call + second_request_body = mock_post_2.call_args.kwargs["json"] + print("second_request_body", second_request_body) + + # Get the diff between the two request bodies + # Convert dictionaries to formatted JSON strings + import json + + first_json = json.dumps(first_request_body, indent=2).splitlines() + second_json = json.dumps(second_request_body, indent=2).splitlines() + # Assert there is no difference between the request bodies + assert first_json == second_json, "Request bodies should be identical" From aa8ba9b8f295486b1c0bf744b733188d5f223fad Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 23:54:29 -0700 Subject: [PATCH 021/208] fix base_model in param mapping --- litellm/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/utils.py b/litellm/utils.py index dc97c4d898..ad9ba936ab 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2847,11 +2847,16 @@ def get_optional_params( # noqa: PLR0915 additional_drop_params=None, messages: Optional[List[AllMessageValues]] = None, thinking: Optional[AnthropicThinkingParam] = None, + base_model: Optional[str] = None, **kwargs, ): # retrieve all parameters passed to the function passed_params = locals().copy() special_params = passed_params.pop("kwargs") + + # Use `base_model` for paramter mapping if passed in by user + if base_model is not None: + model = base_model for k, v in special_params.items(): if k.startswith("aws_") and ( custom_llm_provider != "bedrock" and custom_llm_provider != "sagemaker" From 830ecbdb8c3485c5a7fbf26072ff8b40284ff262 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 25 Mar 2025 23:54:57 -0700 Subject: [PATCH 022/208] fix get_optional_params --- litellm/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/main.py b/litellm/main.py index 3d4152d634..5eb38a6bd1 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1100,6 +1100,7 @@ def completion( # type: ignore # noqa: PLR0915 user=user, # params to identify the model model=model, + base_model=base_model, custom_llm_provider=custom_llm_provider, response_format=response_format, seed=seed, From 2bef0481af0840de7a2b16e5f4b8120111658da4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 00:05:45 -0700 Subject: [PATCH 023/208] _transform_request_body --- .../litellm_core_utils/llm_request_utils.py | 123 +++++++++++------- .../llms/vertex_ai/gemini/transformation.py | 6 + litellm/utils.py | 14 +- 3 files changed, 88 insertions(+), 55 deletions(-) diff --git a/litellm/litellm_core_utils/llm_request_utils.py b/litellm/litellm_core_utils/llm_request_utils.py index 50dbdc5536..8cfa48e937 100644 --- a/litellm/litellm_core_utils/llm_request_utils.py +++ b/litellm/litellm_core_utils/llm_request_utils.py @@ -3,66 +3,89 @@ from typing import Dict, Optional import litellm -def _ensure_extra_body_is_safe(extra_body: Optional[Dict]) -> Optional[Dict]: - """ - Ensure that the extra_body sent in the request is safe, otherwise users will see this error +class LitellmCoreRequestUtils: - "Object of type TextPromptClient is not JSON serializable + @staticmethod + def _ensure_extra_body_is_safe(extra_body: Optional[Dict]) -> Optional[Dict]: + """ + Ensure that the extra_body sent in the request is safe, otherwise users will see this error + + "Object of type TextPromptClient is not JSON serializable - Relevant Issue: https://github.com/BerriAI/litellm/issues/4140 - """ - if extra_body is None: - return None + Relevant Issue: https://github.com/BerriAI/litellm/issues/4140 + """ + if extra_body is None: + return None + + if not isinstance(extra_body, dict): + return extra_body + + if "metadata" in extra_body and isinstance(extra_body["metadata"], dict): + if "prompt" in extra_body["metadata"]: + _prompt = extra_body["metadata"].get("prompt") + + # users can send Langfuse TextPromptClient objects, so we need to convert them to dicts + # Langfuse TextPromptClients have .__dict__ attribute + if _prompt is not None and hasattr(_prompt, "__dict__"): + extra_body["metadata"]["prompt"] = _prompt.__dict__ - if not isinstance(extra_body, dict): return extra_body - if "metadata" in extra_body and isinstance(extra_body["metadata"], dict): - if "prompt" in extra_body["metadata"]: - _prompt = extra_body["metadata"].get("prompt") + @staticmethod + def pick_cheapest_chat_models_from_llm_provider(custom_llm_provider: str, n=1): + """ + Pick the n cheapest chat models from the LLM provider. - # users can send Langfuse TextPromptClient objects, so we need to convert them to dicts - # Langfuse TextPromptClients have .__dict__ attribute - if _prompt is not None and hasattr(_prompt, "__dict__"): - extra_body["metadata"]["prompt"] = _prompt.__dict__ + Args: + custom_llm_provider (str): The name of the LLM provider. + n (int): The number of cheapest models to return. - return extra_body + Returns: + list[str]: A list of the n cheapest chat models. + """ + if custom_llm_provider not in litellm.models_by_provider: + return [] + known_models = litellm.models_by_provider.get(custom_llm_provider, []) + model_costs = [] -def pick_cheapest_chat_models_from_llm_provider(custom_llm_provider: str, n=1): - """ - Pick the n cheapest chat models from the LLM provider. - - Args: - custom_llm_provider (str): The name of the LLM provider. - n (int): The number of cheapest models to return. - - Returns: - list[str]: A list of the n cheapest chat models. - """ - if custom_llm_provider not in litellm.models_by_provider: - return [] - - known_models = litellm.models_by_provider.get(custom_llm_provider, []) - model_costs = [] - - for model in known_models: - try: - model_info = litellm.get_model_info( - model=model, custom_llm_provider=custom_llm_provider + for model in known_models: + try: + model_info = litellm.get_model_info( + model=model, custom_llm_provider=custom_llm_provider + ) + except Exception: + continue + if model_info.get("mode") != "chat": + continue + _cost = model_info.get("input_cost_per_token", 0) + model_info.get( + "output_cost_per_token", 0 ) - except Exception: - continue - if model_info.get("mode") != "chat": - continue - _cost = model_info.get("input_cost_per_token", 0) + model_info.get( - "output_cost_per_token", 0 - ) - model_costs.append((model, _cost)) + model_costs.append((model, _cost)) - # Sort by cost (ascending) - model_costs.sort(key=lambda x: x[1]) + # Sort by cost (ascending) + model_costs.sort(key=lambda x: x[1]) - # Return the top n cheapest models - return [model for model, _ in model_costs[:n]] + # Return the top n cheapest models + return [model for model, _ in model_costs[:n]] + + @staticmethod + def select_model_for_request_transformation( + model: str, + base_model: Optional[str] = None, + litellm_params: Optional[Dict] = None, + ) -> str: + """ + If `base_model` is passed in by user, use it for the request transformation + + Else, use the model passed in the request + """ + if base_model is not None: + return base_model + elif ( + litellm_params is not None and litellm_params.get("base_model") is not None + ): + return litellm_params["base_model"] + else: + return model diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index d6bafc7c60..5534ca03ce 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -12,6 +12,7 @@ from pydantic import BaseModel import litellm from litellm._logging import verbose_logger +from litellm.litellm_core_utils.llm_request_utils import LitellmCoreRequestUtils from litellm.litellm_core_utils.prompt_templates.factory import ( convert_to_anthropic_image_obj, convert_to_gemini_tool_call_invoke, @@ -284,6 +285,11 @@ def _transform_request_body( Common transformation logic across sync + async Gemini /generateContent calls. """ # Separate system prompt from rest of message + model = LitellmCoreRequestUtils.select_model_for_request_transformation( + model=model, + litellm_params=litellm_params, + ) + supports_system_message = get_supports_system_message( model=model, custom_llm_provider=custom_llm_provider ) diff --git a/litellm/utils.py b/litellm/utils.py index ad9ba936ab..bcf22e902c 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -84,7 +84,7 @@ from litellm.litellm_core_utils.get_llm_provider_logic import ( from litellm.litellm_core_utils.get_supported_openai_params import ( get_supported_openai_params, ) -from litellm.litellm_core_utils.llm_request_utils import _ensure_extra_body_is_safe +from litellm.litellm_core_utils.llm_request_utils import LitellmCoreRequestUtils from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( LiteLLMResponseObjectHandler, _handle_invalid_parallel_tool_calls, @@ -2855,8 +2855,10 @@ def get_optional_params( # noqa: PLR0915 special_params = passed_params.pop("kwargs") # Use `base_model` for paramter mapping if passed in by user - if base_model is not None: - model = base_model + model = LitellmCoreRequestUtils.select_model_for_request_transformation( + model=model, + base_model=base_model, + ) for k, v in special_params.items(): if k.startswith("aws_") and ( custom_llm_provider != "bedrock" and custom_llm_provider != "sagemaker" @@ -3738,8 +3740,10 @@ def get_optional_params( # noqa: PLR0915 **extra_body, } - optional_params["extra_body"] = _ensure_extra_body_is_safe( - extra_body=optional_params["extra_body"] + optional_params["extra_body"] = ( + LitellmCoreRequestUtils._ensure_extra_body_is_safe( + extra_body=optional_params["extra_body"] + ) ) else: # if user passed in non-default kwargs for specific providers/models, pass them along From 27c085cc56f91965c7393da0840cdea1492ead60 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 00:08:16 -0700 Subject: [PATCH 024/208] fix util vertex --- litellm/main.py | 10 +++++----- tests/litellm_utils_tests/test_utils.py | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 5eb38a6bd1..af0dc1f5b6 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -59,9 +59,7 @@ from litellm.litellm_core_utils.health_check_utils import ( _filter_model_params, ) from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj -from litellm.litellm_core_utils.llm_request_utils import ( - pick_cheapest_chat_models_from_llm_provider, -) +from litellm.litellm_core_utils.llm_request_utils import LitellmCoreRequestUtils from litellm.litellm_core_utils.mock_functions import ( mock_embedding, mock_image_generation, @@ -5424,8 +5422,10 @@ async def ahealth_check_wildcard_models( ) -> dict: # this is a wildcard model, we need to pick a random model from the provider - cheapest_models = pick_cheapest_chat_models_from_llm_provider( - custom_llm_provider=custom_llm_provider, n=3 + cheapest_models = ( + LitellmCoreRequestUtils.pick_cheapest_chat_models_from_llm_provider( + custom_llm_provider=custom_llm_provider, n=3 + ) ) if len(cheapest_models) == 0: raise Exception( diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index fea225e4a3..ef9bf30c13 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -1532,12 +1532,26 @@ def test_supports_vision_gemini(): def test_pick_cheapest_chat_model_from_llm_provider(): from litellm.litellm_core_utils.llm_request_utils import ( - pick_cheapest_chat_models_from_llm_provider, + LitellmCoreRequestUtils, ) - assert len(pick_cheapest_chat_models_from_llm_provider("openai", n=3)) == 3 + assert ( + len( + LitellmCoreRequestUtils.pick_cheapest_chat_models_from_llm_provider( + "openai", n=3 + ) + ) + == 3 + ) - assert len(pick_cheapest_chat_models_from_llm_provider("unknown", n=1)) == 0 + assert ( + len( + LitellmCoreRequestUtils.pick_cheapest_chat_models_from_llm_provider( + "unknown", n=1 + ) + ) + == 0 + ) def test_get_potential_model_names(): From fb31006cd847d921c9487c326c560dfd537e0f8d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 07:12:25 -0700 Subject: [PATCH 025/208] select_model_for_request_transformation --- litellm/main.py | 6 ++++-- litellm/utils.py | 6 ------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index af0dc1f5b6..8ebf66fd06 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1097,8 +1097,10 @@ def completion( # type: ignore # noqa: PLR0915 logit_bias=logit_bias, user=user, # params to identify the model - model=model, - base_model=base_model, + model=LitellmCoreRequestUtils.select_model_for_request_transformation( + model=model, + base_model=base_model, + ), custom_llm_provider=custom_llm_provider, response_format=response_format, seed=seed, diff --git a/litellm/utils.py b/litellm/utils.py index bcf22e902c..45fa9fa1fd 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2847,18 +2847,12 @@ def get_optional_params( # noqa: PLR0915 additional_drop_params=None, messages: Optional[List[AllMessageValues]] = None, thinking: Optional[AnthropicThinkingParam] = None, - base_model: Optional[str] = None, **kwargs, ): # retrieve all parameters passed to the function passed_params = locals().copy() special_params = passed_params.pop("kwargs") - # Use `base_model` for paramter mapping if passed in by user - model = LitellmCoreRequestUtils.select_model_for_request_transformation( - model=model, - base_model=base_model, - ) for k, v in special_params.items(): if k.startswith("aws_") and ( custom_llm_provider != "bedrock" and custom_llm_provider != "sagemaker" From bbe69a47a9fafc89db9b85a113fc3da9b73a5c4c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 10:53:23 -0700 Subject: [PATCH 026/208] _is_model_gemini_gemini_spec_model --- litellm/llms/vertex_ai/common_utils.py | 5 +++++ .../gemini/vertex_and_google_ai_studio_gemini.py | 13 ++++++++++++- litellm/main.py | 5 +---- .../local_testing/test_amazing_vertex_completion.py | 3 +-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index a3f91fbacc..d5ad330484 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -3,6 +3,7 @@ from typing import Dict, List, Literal, Optional, Tuple, Union import httpx +import litellm from litellm import supports_response_schema, supports_system_messages, verbose_logger from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.types.llms.vertex_ai import PartType @@ -28,6 +29,10 @@ def get_supports_system_message( supports_system_message = supports_system_messages( model=model, custom_llm_provider=_custom_llm_provider ) + + # Vertex Models called in the `/gemini` request/response format also support system messages + if litellm.VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): + supports_system_message = True except Exception as e: verbose_logger.warning( "Unable to identify if system message supported. Defaulting to 'False'. Received error message - {}\nAdd it here - https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json".format( diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 73215b4048..cbbbae5b34 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -207,7 +207,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "extra_headers", "seed", "logprobs", - "top_logprobs" # Added this to list of supported openAI params + "top_logprobs", # Added this to list of supported openAI params ] def map_tool_choice_values( @@ -419,6 +419,17 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "europe-west9", ] + @staticmethod + def _is_model_gemini_gemini_spec_model(model: Optional[str]) -> bool: + """ + Returns true if user is trying to call custom model in `/gemini` request/response format + """ + if model is None: + return False + if "gemini/" in model: + return True + return False + def get_flagged_finish_reasons(self) -> Dict[str, str]: """ Return Dictionary of finish reasons which indicate response was flagged diff --git a/litellm/main.py b/litellm/main.py index 8ebf66fd06..b96a054118 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1097,10 +1097,7 @@ def completion( # type: ignore # noqa: PLR0915 logit_bias=logit_bias, user=user, # params to identify the model - model=LitellmCoreRequestUtils.select_model_for_request_transformation( - model=model, - base_model=base_model, - ), + model=model, custom_llm_provider=custom_llm_provider, response_format=response_format, seed=seed, diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 7134c3536e..3687212e84 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -3380,8 +3380,7 @@ def test_gemini_fine_tuned_model_request_consistency(): with patch.object(client, "post", new=MagicMock()) as mock_post_1: try: response_1 = completion( - model="vertex_ai/ft-uuid", - base_model="vertex_ai/gemini-2.0-flash-001", + model="vertex_ai/gemini/ft-uuid", messages=messages, tools=tools, tool_choice="auto", From 3ee7962f9c24249bc1c3d9d8d5c24ee3de2f5108 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 10:55:56 -0700 Subject: [PATCH 027/208] fix llm request utils --- .../litellm_core_utils/llm_request_utils.py | 123 +++++++----------- 1 file changed, 50 insertions(+), 73 deletions(-) diff --git a/litellm/litellm_core_utils/llm_request_utils.py b/litellm/litellm_core_utils/llm_request_utils.py index 8cfa48e937..50dbdc5536 100644 --- a/litellm/litellm_core_utils/llm_request_utils.py +++ b/litellm/litellm_core_utils/llm_request_utils.py @@ -3,89 +3,66 @@ from typing import Dict, Optional import litellm -class LitellmCoreRequestUtils: +def _ensure_extra_body_is_safe(extra_body: Optional[Dict]) -> Optional[Dict]: + """ + Ensure that the extra_body sent in the request is safe, otherwise users will see this error - @staticmethod - def _ensure_extra_body_is_safe(extra_body: Optional[Dict]) -> Optional[Dict]: - """ - Ensure that the extra_body sent in the request is safe, otherwise users will see this error - - "Object of type TextPromptClient is not JSON serializable + "Object of type TextPromptClient is not JSON serializable - Relevant Issue: https://github.com/BerriAI/litellm/issues/4140 - """ - if extra_body is None: - return None - - if not isinstance(extra_body, dict): - return extra_body - - if "metadata" in extra_body and isinstance(extra_body["metadata"], dict): - if "prompt" in extra_body["metadata"]: - _prompt = extra_body["metadata"].get("prompt") - - # users can send Langfuse TextPromptClient objects, so we need to convert them to dicts - # Langfuse TextPromptClients have .__dict__ attribute - if _prompt is not None and hasattr(_prompt, "__dict__"): - extra_body["metadata"]["prompt"] = _prompt.__dict__ + Relevant Issue: https://github.com/BerriAI/litellm/issues/4140 + """ + if extra_body is None: + return None + if not isinstance(extra_body, dict): return extra_body - @staticmethod - def pick_cheapest_chat_models_from_llm_provider(custom_llm_provider: str, n=1): - """ - Pick the n cheapest chat models from the LLM provider. + if "metadata" in extra_body and isinstance(extra_body["metadata"], dict): + if "prompt" in extra_body["metadata"]: + _prompt = extra_body["metadata"].get("prompt") - Args: - custom_llm_provider (str): The name of the LLM provider. - n (int): The number of cheapest models to return. + # users can send Langfuse TextPromptClient objects, so we need to convert them to dicts + # Langfuse TextPromptClients have .__dict__ attribute + if _prompt is not None and hasattr(_prompt, "__dict__"): + extra_body["metadata"]["prompt"] = _prompt.__dict__ - Returns: - list[str]: A list of the n cheapest chat models. - """ - if custom_llm_provider not in litellm.models_by_provider: - return [] + return extra_body - known_models = litellm.models_by_provider.get(custom_llm_provider, []) - model_costs = [] - for model in known_models: - try: - model_info = litellm.get_model_info( - model=model, custom_llm_provider=custom_llm_provider - ) - except Exception: - continue - if model_info.get("mode") != "chat": - continue - _cost = model_info.get("input_cost_per_token", 0) + model_info.get( - "output_cost_per_token", 0 +def pick_cheapest_chat_models_from_llm_provider(custom_llm_provider: str, n=1): + """ + Pick the n cheapest chat models from the LLM provider. + + Args: + custom_llm_provider (str): The name of the LLM provider. + n (int): The number of cheapest models to return. + + Returns: + list[str]: A list of the n cheapest chat models. + """ + if custom_llm_provider not in litellm.models_by_provider: + return [] + + known_models = litellm.models_by_provider.get(custom_llm_provider, []) + model_costs = [] + + for model in known_models: + try: + model_info = litellm.get_model_info( + model=model, custom_llm_provider=custom_llm_provider ) - model_costs.append((model, _cost)) + except Exception: + continue + if model_info.get("mode") != "chat": + continue + _cost = model_info.get("input_cost_per_token", 0) + model_info.get( + "output_cost_per_token", 0 + ) + model_costs.append((model, _cost)) - # Sort by cost (ascending) - model_costs.sort(key=lambda x: x[1]) + # Sort by cost (ascending) + model_costs.sort(key=lambda x: x[1]) - # Return the top n cheapest models - return [model for model, _ in model_costs[:n]] - - @staticmethod - def select_model_for_request_transformation( - model: str, - base_model: Optional[str] = None, - litellm_params: Optional[Dict] = None, - ) -> str: - """ - If `base_model` is passed in by user, use it for the request transformation - - Else, use the model passed in the request - """ - if base_model is not None: - return base_model - elif ( - litellm_params is not None and litellm_params.get("base_model") is not None - ): - return litellm_params["base_model"] - else: - return model + # Return the top n cheapest models + return [model for model, _ in model_costs[:n]] From 8a72b67b18a9cd41599df07eb6ef0685a1dbaaf0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 10:57:08 -0700 Subject: [PATCH 028/208] undo code changes --- litellm/llms/vertex_ai/gemini/transformation.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 5534ca03ce..d6bafc7c60 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -12,7 +12,6 @@ from pydantic import BaseModel import litellm from litellm._logging import verbose_logger -from litellm.litellm_core_utils.llm_request_utils import LitellmCoreRequestUtils from litellm.litellm_core_utils.prompt_templates.factory import ( convert_to_anthropic_image_obj, convert_to_gemini_tool_call_invoke, @@ -285,11 +284,6 @@ def _transform_request_body( Common transformation logic across sync + async Gemini /generateContent calls. """ # Separate system prompt from rest of message - model = LitellmCoreRequestUtils.select_model_for_request_transformation( - model=model, - litellm_params=litellm_params, - ) - supports_system_message = get_supports_system_message( model=model, custom_llm_provider=custom_llm_provider ) From fee20250a60aca317850321cfb0fd26ea8e940d1 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 10:59:02 -0700 Subject: [PATCH 029/208] pick_cheapest_chat_models_from_llm_provider --- litellm/main.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index b96a054118..5e628975ef 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -59,7 +59,9 @@ from litellm.litellm_core_utils.health_check_utils import ( _filter_model_params, ) from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj -from litellm.litellm_core_utils.llm_request_utils import LitellmCoreRequestUtils +from litellm.litellm_core_utils.llm_request_utils import ( + pick_cheapest_chat_models_from_llm_provider, +) from litellm.litellm_core_utils.mock_functions import ( mock_embedding, mock_image_generation, @@ -2348,6 +2350,8 @@ def completion( # type: ignore # noqa: PLR0915 or litellm.api_key ) + api_base = api_base or litellm.api_base or get_secret("GEMINI_API_BASE") + new_params = deepcopy(optional_params) response = vertex_chat_completion.completion( # type: ignore model=model, @@ -2390,6 +2394,8 @@ def completion( # type: ignore # noqa: PLR0915 or get_secret("VERTEXAI_CREDENTIALS") ) + api_base = api_base or litellm.api_base or get_secret("VERTEXAI_API_BASE") + new_params = deepcopy(optional_params) if ( model.startswith("meta/") @@ -3655,6 +3661,8 @@ def embedding( # noqa: PLR0915 api_key or get_secret_str("GEMINI_API_KEY") or litellm.api_key ) + api_base = api_base or litellm.api_base or get_secret_str("GEMINI_API_BASE") + response = google_batch_embeddings.batch_embeddings( # type: ignore model=model, input=input, @@ -3669,6 +3677,8 @@ def embedding( # noqa: PLR0915 print_verbose=print_verbose, custom_llm_provider="gemini", api_key=gemini_api_key, + api_base=api_base, + client=client, ) elif custom_llm_provider == "vertex_ai": @@ -3693,6 +3703,13 @@ def embedding( # noqa: PLR0915 or get_secret_str("VERTEX_CREDENTIALS") ) + api_base = ( + api_base + or litellm.api_base + or get_secret_str("VERTEXAI_API_BASE") + or get_secret_str("VERTEX_API_BASE") + ) + if ( "image" in optional_params or "video" in optional_params @@ -3714,6 +3731,7 @@ def embedding( # noqa: PLR0915 print_verbose=print_verbose, custom_llm_provider="vertex_ai", client=client, + api_base=api_base, ) else: response = vertex_embedding.embedding( @@ -3731,6 +3749,8 @@ def embedding( # noqa: PLR0915 aembedding=aembedding, print_verbose=print_verbose, api_key=api_key, + api_base=api_base, + client=client, ) elif custom_llm_provider == "oobabooga": response = oobabooga.embedding( @@ -4693,6 +4713,14 @@ def image_generation( # noqa: PLR0915 or optional_params.pop("vertex_ai_credentials", None) or get_secret_str("VERTEXAI_CREDENTIALS") ) + + api_base = ( + api_base + or litellm.api_base + or get_secret_str("VERTEXAI_API_BASE") + or get_secret_str("VERTEX_API_BASE") + ) + model_response = vertex_image_generation.image_generation( model=model, prompt=prompt, @@ -4704,6 +4732,7 @@ def image_generation( # noqa: PLR0915 vertex_location=vertex_ai_location, vertex_credentials=vertex_credentials, aimg_generation=aimg_generation, + client=client, ) elif ( custom_llm_provider in litellm._custom_providers @@ -5421,10 +5450,8 @@ async def ahealth_check_wildcard_models( ) -> dict: # this is a wildcard model, we need to pick a random model from the provider - cheapest_models = ( - LitellmCoreRequestUtils.pick_cheapest_chat_models_from_llm_provider( - custom_llm_provider=custom_llm_provider, n=3 - ) + cheapest_models = pick_cheapest_chat_models_from_llm_provider( + custom_llm_provider=custom_llm_provider, n=3 ) if len(cheapest_models) == 0: raise Exception( From a2ae9be6a266b4f854c96983ec2d529aa52b8efe Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 11:09:29 -0700 Subject: [PATCH 030/208] undo changes to utils --- litellm/utils.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 45fa9fa1fd..dc97c4d898 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -84,7 +84,7 @@ from litellm.litellm_core_utils.get_llm_provider_logic import ( from litellm.litellm_core_utils.get_supported_openai_params import ( get_supported_openai_params, ) -from litellm.litellm_core_utils.llm_request_utils import LitellmCoreRequestUtils +from litellm.litellm_core_utils.llm_request_utils import _ensure_extra_body_is_safe from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( LiteLLMResponseObjectHandler, _handle_invalid_parallel_tool_calls, @@ -2852,7 +2852,6 @@ def get_optional_params( # noqa: PLR0915 # retrieve all parameters passed to the function passed_params = locals().copy() special_params = passed_params.pop("kwargs") - for k, v in special_params.items(): if k.startswith("aws_") and ( custom_llm_provider != "bedrock" and custom_llm_provider != "sagemaker" @@ -3734,10 +3733,8 @@ def get_optional_params( # noqa: PLR0915 **extra_body, } - optional_params["extra_body"] = ( - LitellmCoreRequestUtils._ensure_extra_body_is_safe( - extra_body=optional_params["extra_body"] - ) + optional_params["extra_body"] = _ensure_extra_body_is_safe( + extra_body=optional_params["extra_body"] ) else: # if user passed in non-default kwargs for specific providers/models, pass them along From 1822c45c0ec3d0ac200e273618cd2b95fa3c5962 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 11:13:21 -0700 Subject: [PATCH 031/208] test fix test_pick_cheapest_chat_model_from_llm_provider --- tests/litellm_utils_tests/test_utils.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index ef9bf30c13..fea225e4a3 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -1532,26 +1532,12 @@ def test_supports_vision_gemini(): def test_pick_cheapest_chat_model_from_llm_provider(): from litellm.litellm_core_utils.llm_request_utils import ( - LitellmCoreRequestUtils, + pick_cheapest_chat_models_from_llm_provider, ) - assert ( - len( - LitellmCoreRequestUtils.pick_cheapest_chat_models_from_llm_provider( - "openai", n=3 - ) - ) - == 3 - ) + assert len(pick_cheapest_chat_models_from_llm_provider("openai", n=3)) == 3 - assert ( - len( - LitellmCoreRequestUtils.pick_cheapest_chat_models_from_llm_provider( - "unknown", n=1 - ) - ) - == 0 - ) + assert len(pick_cheapest_chat_models_from_llm_provider("unknown", n=1)) == 0 def test_get_potential_model_names(): From 793a920caa8b8b3dcd7074be350a5e724bbab20e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 11:14:51 -0700 Subject: [PATCH 032/208] rename _is_model_gemini_spec_model --- litellm/llms/vertex_ai/common_utils.py | 2 +- .../llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index fb9f595fa8..bbaab31401 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -31,7 +31,7 @@ def get_supports_system_message( ) # Vertex Models called in the `/gemini` request/response format also support system messages - if litellm.VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): + if litellm.VertexGeminiConfig._is_model_gemini_spec_model(model): supports_system_message = True except Exception as e: verbose_logger.warning( diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index cbbbae5b34..fdb8c0e558 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -420,7 +420,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ] @staticmethod - def _is_model_gemini_gemini_spec_model(model: Optional[str]) -> bool: + def _is_model_gemini_spec_model(model: Optional[str]) -> bool: """ Returns true if user is trying to call custom model in `/gemini` request/response format """ From 93daf5cbac2af4cc632b47acd6f3c90cd8275b60 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 12:16:18 -0700 Subject: [PATCH 033/208] _get_model_name_from_gemini_spec_model --- litellm/llms/vertex_ai/common_utils.py | 4 +++- .../gemini/vertex_and_google_ai_studio_gemini.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index bbaab31401..b5bad6b858 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -31,7 +31,7 @@ def get_supports_system_message( ) # Vertex Models called in the `/gemini` request/response format also support system messages - if litellm.VertexGeminiConfig._is_model_gemini_spec_model(model): + if litellm.VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): supports_system_message = True except Exception as e: verbose_logger.warning( @@ -75,6 +75,8 @@ def _get_vertex_url( ) -> Tuple[str, str]: url: Optional[str] = None endpoint: Optional[str] = None + if litellm.VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): + model = litellm.VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) if mode == "chat": ### SET RUNTIME ENDPOINT ### endpoint = "generateContent" diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index fdb8c0e558..0e48c690ba 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -420,7 +420,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ] @staticmethod - def _is_model_gemini_spec_model(model: Optional[str]) -> bool: + def _is_model_gemini_gemini_spec_model(model: Optional[str]) -> bool: """ Returns true if user is trying to call custom model in `/gemini` request/response format """ @@ -430,6 +430,19 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return True return False + @staticmethod + def _get_model_name_from_gemini_spec_model(model: str) -> str: + """ + Returns the model name if model="vertex_ai/gemini/" + + Example: + - model = "gemini/1234567890" + - returns "1234567890" + """ + if "gemini/" in model: + return model.split("/")[-1] + return model + def get_flagged_finish_reasons(self) -> Dict[str, str]: """ Return Dictionary of finish reasons which indicate response was flagged From 12eb77d02d0b494ad288928d6ad070955dd322ac Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 12:24:49 -0700 Subject: [PATCH 034/208] docs litellm vertex ai ft models --- docs/my-website/docs/providers/vertex.md | 126 ++++++++++++----------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index 10ac13ecaf..b8633adc5e 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -1369,6 +1369,71 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ +## Gemini Pro +| Model Name | Function Call | +|------------------|--------------------------------------| +| gemini-pro | `completion('gemini-pro', messages)`, `completion('vertex_ai/gemini-pro', messages)` | + +## Fine-tuned Models + +Call fine-tuned Vertex AI Gemini models through LiteLLM. If you want to use LiteLLM to call a model in the `/gemini` request/response format, you can do so by setting `model="vertex_ai/gemini/{MODEL_ID}"`. This tells litellm that the request/response format follows the `gemini` model family format. + +| Property | Details | +|----------|---------| +| Provider Route | `vertex_ai/gemini/{MODEL_ID}` | +| Vertex Documentation | [Vertex AI - Fine-tuned Gemini Models](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-use-supervised-tuning#test_the_tuned_model_with_a_prompt)| +| Supported Operations | `/chat/completions`, `/completions`, `/embeddings`, `/images` | + + + + +```python showLineNumbers +import litellm +import os + +## set ENV variables +os.environ["VERTEXAI_PROJECT"] = "hardy-device-38811" +os.environ["VERTEXAI_LOCATION"] = "us-central1" + +response = litellm.completion( + model="vertex_ai/gemini/", # e.g. vertex_ai/4965075652664360960 + messages=[{ "content": "Hello, how are you?","role": "user"}], +) +``` + + + + +1. Add Vertex Credentials to your env + +```bash +!gcloud auth application-default login +``` + +2. Setup config.yaml + +```yaml +- model_name: finetuned-gemini + litellm_params: + model: vertex_ai/gemini/ + vertex_project: + vertex_location: +``` + +3. Test it! + +```bash +curl --location 'https://0.0.0.0:4000/v1/chat/completions' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: ' \ +--data '{"model": "finetuned-gemini" ,"messages":[{"role": "user", "content":[{"type": "text", "text": "hi"}]}]}' +``` + + + + + + ## Model Garden :::tip @@ -1479,67 +1544,6 @@ response = completion( -## Gemini Pro -| Model Name | Function Call | -|------------------|--------------------------------------| -| gemini-pro | `completion('gemini-pro', messages)`, `completion('vertex_ai/gemini-pro', messages)` | - -## Fine-tuned Models - -Fine tuned models on vertex have a numerical model/endpoint id. - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["VERTEXAI_PROJECT"] = "hardy-device-38811" -os.environ["VERTEXAI_LOCATION"] = "us-central1" - -response = completion( - model="vertex_ai/", # e.g. vertex_ai/4965075652664360960 - messages=[{ "content": "Hello, how are you?","role": "user"}], - base_model="vertex_ai/gemini-1.5-pro" # the base model - used for routing -) -``` - - - - -1. Add Vertex Credentials to your env - -```bash -!gcloud auth application-default login -``` - -2. Setup config.yaml - -```yaml -- model_name: finetuned-gemini - litellm_params: - model: vertex_ai/ - vertex_project: - vertex_location: - model_info: - base_model: vertex_ai/gemini-1.5-pro # IMPORTANT -``` - -3. Test it! - -```bash -curl --location 'https://0.0.0.0:4000/v1/chat/completions' \ ---header 'Content-Type: application/json' \ ---header 'Authorization: ' \ ---data '{"model": "finetuned-gemini" ,"messages":[{"role": "user", "content":[{"type": "text", "text": "hi"}]}]}' -``` - - - - - ## Gemini Pro Vision | Model Name | Function Call | From 82016eba0a02ffc3e6115d287651cdcec0b140b0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 13:55:38 -0700 Subject: [PATCH 035/208] docs vertex ft models --- docs/my-website/docs/providers/vertex.md | 34 +++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index b8633adc5e..ede3ae88d8 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -1385,7 +1385,7 @@ Call fine-tuned Vertex AI Gemini models through LiteLLM. If you want to use Lite | Supported Operations | `/chat/completions`, `/completions`, `/embeddings`, `/images` | - + ```python showLineNumbers import litellm @@ -1402,7 +1402,7 @@ response = litellm.completion( ``` - + 1. Add Vertex Credentials to your env @@ -1412,7 +1412,7 @@ response = litellm.completion( 2. Setup config.yaml -```yaml +```yaml showLineNumbers - model_name: finetuned-gemini litellm_params: model: vertex_ai/gemini/ @@ -1422,7 +1422,30 @@ response = litellm.completion( 3. Test it! -```bash + + + +```python showLineNumbers +from openai import OpenAI + +client = OpenAI( + api_key="your-litellm-key", + base_url="http://0.0.0.0:4000" +) + +response = client.chat.completions.create( + model="finetuned-gemini", + messages=[ + {"role": "user", "content": "hi"} + ] +) +print(response) +``` + + + + +```bash showLineNumbers curl --location 'https://0.0.0.0:4000/v1/chat/completions' \ --header 'Content-Type: application/json' \ --header 'Authorization: ' \ @@ -1432,6 +1455,9 @@ curl --location 'https://0.0.0.0:4000/v1/chat/completions' \ + + + ## Model Garden From e24a60189722fcefaee257ec158ca2b0d6a86681 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:03:42 -0700 Subject: [PATCH 036/208] docs verte ft models --- docs/my-website/docs/providers/vertex.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index ede3ae88d8..15c30d1222 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -1376,7 +1376,8 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ ## Fine-tuned Models -Call fine-tuned Vertex AI Gemini models through LiteLLM. If you want to use LiteLLM to call a model in the `/gemini` request/response format, you can do so by setting `model="vertex_ai/gemini/{MODEL_ID}"`. This tells litellm that the request/response format follows the `gemini` model family format. +You can call fine-tuned Vertex AI Gemini models through LiteLLM +``` | Property | Details | |----------|---------| @@ -1384,10 +1385,16 @@ Call fine-tuned Vertex AI Gemini models through LiteLLM. If you want to use Lite | Vertex Documentation | [Vertex AI - Fine-tuned Gemini Models](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-use-supervised-tuning#test_the_tuned_model_with_a_prompt)| | Supported Operations | `/chat/completions`, `/completions`, `/embeddings`, `/images` | +To use a model that follows the `/gemini` request/response format, simply set the model parameter as + +```python title="Model parameter for calling fine-tuned gemini models" +model="vertex_ai/gemini/" +``` + -```python showLineNumbers +```python showLineNumbers title="Example" import litellm import os @@ -1406,13 +1413,13 @@ response = litellm.completion( 1. Add Vertex Credentials to your env -```bash +```bash title="Authenticate to Vertex AI" !gcloud auth application-default login ``` 2. Setup config.yaml -```yaml showLineNumbers +```yaml showLineNumbers title="Add to litellm config" - model_name: finetuned-gemini litellm_params: model: vertex_ai/gemini/ @@ -1425,7 +1432,7 @@ response = litellm.completion( -```python showLineNumbers +```python showLineNumbers title="Example request" from openai import OpenAI client = OpenAI( @@ -1445,7 +1452,7 @@ print(response) -```bash showLineNumbers +```bash showLineNumbers title="Example request" curl --location 'https://0.0.0.0:4000/v1/chat/completions' \ --header 'Content-Type: application/json' \ --header 'Authorization: ' \ From 28ab8fdcccae5ece69b3addf6e41e22e3042f6ac Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:08:19 -0700 Subject: [PATCH 037/208] docs vertex ft model --- docs/my-website/docs/providers/vertex.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index 15c30d1222..38c18c7ad8 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -1403,7 +1403,7 @@ os.environ["VERTEXAI_PROJECT"] = "hardy-device-38811" os.environ["VERTEXAI_LOCATION"] = "us-central1" response = litellm.completion( - model="vertex_ai/gemini/", # e.g. vertex_ai/4965075652664360960 + model="vertex_ai/gemini/", # e.g. vertex_ai/gemini/4965075652664360960 messages=[{ "content": "Hello, how are you?","role": "user"}], ) ``` From 82faa49668ea674452365841041810c89de63221 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:09:27 -0700 Subject: [PATCH 038/208] doc fix Fine-tuned Models --- docs/my-website/docs/providers/vertex.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index 38c18c7ad8..f2489de769 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -1377,7 +1377,6 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ ## Fine-tuned Models You can call fine-tuned Vertex AI Gemini models through LiteLLM -``` | Property | Details | |----------|---------| From da9d84934821905ef5562add771e70021da128b8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:10:32 -0700 Subject: [PATCH 039/208] test_gemini_fine_tuned_model_request_consistency --- tests/local_testing/test_amazing_vertex_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 86f7e83dac..5d99ecb5cd 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -3334,7 +3334,7 @@ def test_gemini_fine_tuned_model_request_consistency(): """ Assert the same transformation is applied to Fine tuned gemini 2.0 flash and gemini 2.0 flash - - Request 1: Fine tuned: vertex_ai/ft-uuid with base_model: vertex_ai/gemini-2.0-flash-001 + - Request 1: Fine tuned: vertex_ai/gemini/ft-uuid - Request 2: vertex_ai/gemini-2.0-flash-001 """ litellm.set_verbose = True From 8eaf4c55c0d0f05d5533b9112a767410c9e2d6ac Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:18:11 -0700 Subject: [PATCH 040/208] test_gemini_fine_tuned_model_request_consistency --- litellm/llms/vertex_ai/common_utils.py | 4 ++-- .../vertex_and_google_ai_studio_gemini.py | 19 +++++++++++++++++++ .../test_amazing_vertex_completion.py | 8 ++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index b5bad6b858..2361939d81 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -75,8 +75,8 @@ def _get_vertex_url( ) -> Tuple[str, str]: url: Optional[str] = None endpoint: Optional[str] = None - if litellm.VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): - model = litellm.VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) + + model = litellm.VertexGeminiConfig.get_model_for_vertex_ai_url(model=model) if mode == "chat": ### SET RUNTIME ENDPOINT ### endpoint = "generateContent" diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 0e48c690ba..8aecfffd86 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -419,6 +419,25 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "europe-west9", ] + @staticmethod + def get_model_for_vertex_ai_url(model: str) -> str: + """ + Returns the model name to use in the request to Vertex AI + + Handles 2 cases: + 1. User passed `model="vertex_ai/gemini/ft-uuid"`, we need to return `ft-uuid` for the request to Vertex AI + 2. User passed `model="vertex_ai/gemini-2.0-flash-001"`, we need to return `gemini-2.0-flash-001` for the request to Vertex AI + + Args: + model (str): The model name to use in the request to Vertex AI + + Returns: + str: The model name to use in the request to Vertex AI + """ + if VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): + return VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) + return model + @staticmethod def _is_model_gemini_gemini_spec_model(model: Optional[str]) -> bool: """ diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 5d99ecb5cd..83c99d766b 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -3395,6 +3395,14 @@ def test_gemini_fine_tuned_model_request_consistency(): first_request_body = mock_post_1.call_args.kwargs["json"] print("first_request_body", first_request_body) + # Validate correct `model` is added to the request to Vertex AI + print("final URL=", mock_post_1.call_args.kwargs["url"]) + # Validate the request url + assert ( + "publishers/google/models/ft-uuid:generateContent" + in mock_post_1.call_args.kwargs["url"] + ) + # Second request with patch.object(client, "post", new=MagicMock()) as mock_post_2: try: From 72f08bc6eae994092479f155790db2e59c4903e6 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:21:35 -0700 Subject: [PATCH 041/208] unit tests for VertexGeminiConfig --- .../test_vertex_and_google_ai_studio.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py b/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py index 5e870b5af3..9fc9be7e0f 100644 --- a/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py +++ b/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py @@ -31,3 +31,41 @@ def test_top_logprobs(): ) assert v["responseLogprobs"] is non_default_params["logprobs"] assert v["logprobs"] is non_default_params["top_logprobs"] + + +def test_get_model_for_vertex_ai_url(): + # Test case 1: Regular model name + model = "gemini-pro" + result = VertexGeminiConfig.get_model_for_vertex_ai_url(model) + assert result == "gemini-pro" + + # Test case 2: Gemini spec model with UUID + model = "gemini/ft-uuid-123" + result = VertexGeminiConfig.get_model_for_vertex_ai_url(model) + assert result == "ft-uuid-123" + + +def test_is_model_gemini_gemini_spec_model(): + # Test case 1: None input + assert VertexGeminiConfig._is_model_gemini_gemini_spec_model(None) == False + + # Test case 2: Regular model name + assert VertexGeminiConfig._is_model_gemini_gemini_spec_model("gemini-pro") == False + + # Test case 3: Gemini spec model + assert ( + VertexGeminiConfig._is_model_gemini_gemini_spec_model("gemini/custom-model") + == True + ) + + +def test_get_model_name_from_gemini_spec_model(): + # Test case 1: Regular model name + model = "gemini-pro" + result = VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) + assert result == "gemini-pro" + + # Test case 2: Gemini spec model + model = "gemini/ft-uuid-123" + result = VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) + assert result == "ft-uuid-123" From c38b41f65b69cc6059dfe665e58324bef38508e8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:26:08 -0700 Subject: [PATCH 042/208] test_get_supports_system_message --- .../vertex_ai/test_vertex_ai_common_utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index e89355443f..b94d7495cb 100644 --- a/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -41,3 +41,21 @@ async def test_get_vertex_location_from_url(): url = "https://invalid-url.com" location = get_vertex_location_from_url(url) assert location is None + + +@pytest.mark.asyncio +async def test_get_supports_system_message(): + """Test get_supports_system_message with different models""" + from litellm.llms.vertex_ai.common_utils import get_supports_system_message + + # fine-tuned vertex gemini models will specifiy they are in the /gemini spec format + result = get_supports_system_message( + model="gemini/1234567890", custom_llm_provider="vertex_ai" + ) + assert result == True + + # non-fine-tuned vertex gemini models will not specifiy they are in the /gemini spec format + result = get_supports_system_message( + model="random-model-name", custom_llm_provider="vertex_ai" + ) + assert result == False From 0aae9aa24ada456e3013c146414305c66e3db8d4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:28:26 -0700 Subject: [PATCH 043/208] rename _is_model_gemini_spec_model --- litellm/llms/vertex_ai/common_utils.py | 2 +- .../gemini/vertex_and_google_ai_studio_gemini.py | 4 ++-- .../gemini/test_vertex_and_google_ai_studio.py | 11 ++++------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 2361939d81..9c9db2f047 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -31,7 +31,7 @@ def get_supports_system_message( ) # Vertex Models called in the `/gemini` request/response format also support system messages - if litellm.VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): + if litellm.VertexGeminiConfig._is_model_gemini_spec_model(model): supports_system_message = True except Exception as e: verbose_logger.warning( diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 8aecfffd86..2931b1b3a8 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -434,12 +434,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): Returns: str: The model name to use in the request to Vertex AI """ - if VertexGeminiConfig._is_model_gemini_gemini_spec_model(model): + if VertexGeminiConfig._is_model_gemini_spec_model(model): return VertexGeminiConfig._get_model_name_from_gemini_spec_model(model) return model @staticmethod - def _is_model_gemini_gemini_spec_model(model: Optional[str]) -> bool: + def _is_model_gemini_spec_model(model: Optional[str]) -> bool: """ Returns true if user is trying to call custom model in `/gemini` request/response format """ diff --git a/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py b/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py index 9fc9be7e0f..18d965200c 100644 --- a/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py +++ b/tests/litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio.py @@ -45,18 +45,15 @@ def test_get_model_for_vertex_ai_url(): assert result == "ft-uuid-123" -def test_is_model_gemini_gemini_spec_model(): +def test_is_model_gemini_spec_model(): # Test case 1: None input - assert VertexGeminiConfig._is_model_gemini_gemini_spec_model(None) == False + assert VertexGeminiConfig._is_model_gemini_spec_model(None) == False # Test case 2: Regular model name - assert VertexGeminiConfig._is_model_gemini_gemini_spec_model("gemini-pro") == False + assert VertexGeminiConfig._is_model_gemini_spec_model("gemini-pro") == False # Test case 3: Gemini spec model - assert ( - VertexGeminiConfig._is_model_gemini_gemini_spec_model("gemini/custom-model") - == True - ) + assert VertexGeminiConfig._is_model_gemini_spec_model("gemini/custom-model") == True def test_get_model_name_from_gemini_spec_model(): From 1812ce4a54e5c15c1206667b812c7fde924ed0d2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:43:03 -0700 Subject: [PATCH 044/208] undo config.yml changes --- .circleci/config.yml | 49 +------------------------------------------- 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 43de7e69e8..5a058144f6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -710,44 +710,6 @@ jobs: paths: - llm_translation_coverage.xml - llm_translation_coverage - litellm_proxy_reliability_tests: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - - steps: - - checkout - - run: - name: Install Dependencies - command: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-cov==5.0.0" - pip install "pytest-asyncio==0.21.1" - pip install "respx==0.21.1" - # Run pytest and generate JUnit XML report - - run: - name: Setup Toxiproxy - command: | - python tests/proxy_reliability_tests/setup_toxi_proxy.py & - sleep 5 - - run: - name: Run tests - command: | - pwd - ls - python -m pytest -vv tests/proxy_reliability_tests --cov=litellm --cov-report=xml -x -s -v --junitxml=test-results/junit.xml --durations=5 - no_output_timeout: 120m - - run: - name: Rename the coverage files - command: | - mv coverage.xml litellm_proxy_reliability_tests_coverage.xml - mv .coverage litellm_proxy_reliability_tests_coverage mcp_testing: docker: - image: cimg/python:3.11 @@ -1968,7 +1930,7 @@ jobs: no_output_timeout: 120m # Clean up first container - + proxy_build_from_pip_tests: # Change from docker to machine executor machine: @@ -2536,13 +2498,6 @@ workflows: only: - main - /litellm_.*/ - - litellm_proxy_reliability_tests: - filters: - branches: - only: - - main - - /litellm_stability_.*/ - - litellm_stable_release_branch - litellm_assistants_api_testing: filters: branches: @@ -2684,7 +2639,6 @@ workflows: - caching_unit_tests - litellm_proxy_unit_testing - litellm_proxy_security_tests - - litellm_proxy_reliability_tests - langfuse_logging_unit_tests - local_testing - litellm_assistants_api_testing @@ -2750,7 +2704,6 @@ workflows: - e2e_ui_testing - litellm_proxy_unit_testing - litellm_proxy_security_tests - - litellm_proxy_reliability_tests - installing_litellm_on_python - installing_litellm_on_python_3_13 - proxy_logging_guardrails_model_info_tests From 4948673e3572b16f95057e881d219b598a6a9d04 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:51:33 -0700 Subject: [PATCH 045/208] fix test changes --- .../setup_toxi_proxy.py | 96 ------------------- .../test_configs/test_config.yaml | 9 -- .../test_using_litellm_db_outage.py | 76 --------------- 3 files changed, 181 deletions(-) delete mode 100644 tests/proxy_reliability_tests/setup_toxi_proxy.py delete mode 100644 tests/proxy_reliability_tests/test_configs/test_config.yaml delete mode 100644 tests/proxy_reliability_tests/test_using_litellm_db_outage.py diff --git a/tests/proxy_reliability_tests/setup_toxi_proxy.py b/tests/proxy_reliability_tests/setup_toxi_proxy.py deleted file mode 100644 index b44298aca5..0000000000 --- a/tests/proxy_reliability_tests/setup_toxi_proxy.py +++ /dev/null @@ -1,96 +0,0 @@ -import os -import platform -import subprocess -import time -import requests -import stat - - -def download_toxiproxy(): - # Determine system architecture and OS - system = platform.system().lower() - machine = platform.machine().lower() - - # Map architecture names - arch_map = { - "x86_64": "amd64", - "amd64": "amd64", - "arm64": "arm64", - "aarch64": "arm64", - } - - arch = arch_map.get(machine, machine) - - # Construct download URL (using latest version 2.5.0) - base_url = "https://github.com/Shopify/toxiproxy/releases/download/v2.5.0/" - if system == "linux": - filename = f"toxiproxy-server-linux-{arch}" - cli_filename = f"toxiproxy-cli-linux-{arch}" - elif system == "darwin": - filename = f"toxiproxy-server-darwin-{arch}" - cli_filename = f"toxiproxy-cli-darwin-{arch}" - else: - raise Exception("Unsupported operating system") - - # Download server - response = requests.get(f"{base_url}{filename}") - with open("toxiproxy-server", "wb") as f: - f.write(response.content) - - # Download CLI - response = requests.get(f"{base_url}{cli_filename}") - with open("toxiproxy-cli", "wb") as f: - f.write(response.content) - - # Make files executable - os.chmod("toxiproxy-server", stat.S_IRWXU) - os.chmod("toxiproxy-cli", stat.S_IRWXU) - - -def setup_toxiproxy(): - # Start toxiproxy-server - server_process = subprocess.Popen(["./toxiproxy-server"]) - - # Wait for server to start - time.sleep(2) - - # Create proxy - subprocess.run( - [ - "./toxiproxy-cli", - "create", - "-l", - "0.0.0.0:6666", - "-u", - "ep-dry-paper-a69g2y1q-pooler.us-west-2.aws.neon.tech:5432", - "postgres_proxy", - ] - ) - - return server_process - - -def main(): - try: - # Download ToxiProxy binaries - download_toxiproxy() - - # Setup ToxiProxy - server_process = setup_toxiproxy() - - print("ToxiProxy setup completed successfully!") - print("Proxy 'postgres_proxy' created and listening on 127.0.0.1:6666") - - # Keep the script running to maintain the proxy - try: - server_process.wait() - except KeyboardInterrupt: - server_process.terminate() - print("\nToxiProxy server stopped") - - except Exception as e: - print(f"Error: {e}") - - -if __name__ == "__main__": - main() diff --git a/tests/proxy_reliability_tests/test_configs/test_config.yaml b/tests/proxy_reliability_tests/test_configs/test_config.yaml deleted file mode 100644 index 4912a35f89..0000000000 --- a/tests/proxy_reliability_tests/test_configs/test_config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -model_list: - - model_name: fake-openai-endpoint - litellm_params: - model: openai/fake - api_key: fake-key - api_base: https://exampleopenaiendpoint-production.up.railway.app/ - -general_settings: - allow_requests_on_db_unavailable: True \ No newline at end of file diff --git a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py b/tests/proxy_reliability_tests/test_using_litellm_db_outage.py deleted file mode 100644 index ea9c7e3f3a..0000000000 --- a/tests/proxy_reliability_tests/test_using_litellm_db_outage.py +++ /dev/null @@ -1,76 +0,0 @@ -import asyncio -import os -import subprocess -import sys -import time -import traceback -import pytest - -sys.path.insert( - 0, os.path.abspath("../..") -) # Adds the parent directory to the system path - -import litellm - -import os -import subprocess -import time - -import pytest -import requests - -TEST_MASTER_KEY = "sk-1234" - - -def test_litellm_proxy_server_config_no_general_settings(): - # Install the litellm[proxy] package - # Start the server - try: - subprocess.run(["pip", "install", "litellm[proxy]"]) - filepath = os.path.dirname(os.path.abspath(__file__)) - config_fp = f"{filepath}/test_configs/test_config.yaml" - - # Set DATABASE_URL environment variable - os.environ["DATABASE_URL"] = os.getenv("TOXI_PROXY_DATABASE_URL") - os.environ["LITELLM_MASTER_KEY"] = TEST_MASTER_KEY - - server_process = subprocess.Popen( - [ - "python", - "-m", - "litellm.proxy.proxy_cli", - "--config", - config_fp, - ] - ) - - # Allow some time for the server to start - time.sleep(60) # Adjust the sleep time if necessary - - # Send a request to the /health/liveliness endpoint - response = requests.get("http://localhost:4000/health/liveliness") - - # Check if the response is successful - assert response.status_code == 200 - assert response.json() == "I'm alive!" - - # Test /chat/completions - response = requests.post( - "http://localhost:4000/chat/completions", - headers={"Authorization": "Bearer 1234567890"}, - json={ - "model": "test_openai_models", - "messages": [{"role": "user", "content": "Hello, how are you?"}], - }, - ) - - assert response.status_code == 200 - - except ImportError: - pytest.fail("Failed to import litellm.proxy_server") - except requests.ConnectionError: - pytest.fail("Failed to connect to the server") - finally: - # Shut down the server - server_process.terminate() - server_process.wait() From f8caebc54b28dda058ec42ea638d344a5662a784 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 14:55:40 -0700 Subject: [PATCH 046/208] is_database_connection_error --- litellm/proxy/_types.py | 4 ++++ litellm/proxy/auth/auth_exception_handler.py | 10 +++++++--- litellm/proxy/auth/user_api_key_auth.py | 9 ++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c0fb0750eb..104d1e7338 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2070,6 +2070,10 @@ class ProxyErrorTypes(str, enum.Enum): """ Object was over budget """ + no_db_connection = "no_db_connection" + """ + No database connection + """ token_not_found_in_db = "token_not_found_in_db" """ diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py index 5b314ef931..c1a546b569 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -144,6 +144,10 @@ class UserAPIKeyAuthExceptionHandler: """ import prisma - return isinstance(e, DB_CONNECTION_ERROR_TYPES) or isinstance( - e, prisma.errors.PrismaError - ) + if isinstance(e, DB_CONNECTION_ERROR_TYPES): + return True + if isinstance(e, prisma.errors.PrismaError): + return True + if isinstance(e, ProxyException) and e.type == ProxyErrorTypes.no_db_connection: + return True + return False diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 06fa560866..85ee76ff31 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -675,7 +675,12 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 if ( prisma_client is None ): # if both master key + user key submitted, and user key != master key, and no db connected, raise an error - raise Exception("No connected db.") + raise ProxyException( + message="No connected db.", + type=ProxyErrorTypes.no_db_connection, + code=400, + param=None, + ) ## check for cache hit (In-Memory Cache) _user_role = None @@ -1001,8 +1006,6 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 route=route, start_time=start_time, ) - else: - raise Exception() except Exception as e: return await UserAPIKeyAuthExceptionHandler._handle_authentication_error( e=e, From 8bd2081dec15c2255e295302687a1e1c4ce413e3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 15:41:40 -0700 Subject: [PATCH 047/208] fix get_key_object --- litellm/proxy/auth/auth_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 9e71147706..dc0ec116f5 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -996,7 +996,7 @@ async def get_key_object( if _valid_token is None: raise ProxyException( - message="Key doesn't exist in db. key={}. Create key via `/key/generate` call.".format( + message="Invalid proxy server token passed. key={}, not found in db. Create key via `/key/generate` call.".format( hashed_token ), type=ProxyErrorTypes.token_not_found_in_db, From ff33ed020cc72fd4fa5541f58d4974f731b11006 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 15:45:58 -0700 Subject: [PATCH 048/208] fix auth checks --- litellm/proxy/auth/auth_checks.py | 3 --- litellm/proxy/auth/user_api_key_auth.py | 1 - 2 files changed, 4 deletions(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index dc0ec116f5..7f75272bbd 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -11,7 +11,6 @@ Run checks for: import asyncio import re import time -import traceback from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, cast from fastapi import Request, status @@ -23,7 +22,6 @@ from litellm.caching.caching import DualCache from litellm.caching.dual_cache import LimitedSizeOrderedDict from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.proxy._types import ( - DB_CONNECTION_ERROR_TYPES, RBAC_ROLES, CallInfo, LiteLLM_EndUserTable, @@ -45,7 +43,6 @@ from litellm.proxy.auth.route_checks import RouteChecks from litellm.proxy.route_llm_request import route_request from litellm.proxy.utils import PrismaClient, ProxyLogging, log_db_metrics from litellm.router import Router -from litellm.types.services import ServiceTypes from .auth_checks_organization import organization_role_based_access_check diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 85ee76ff31..b58353bf05 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -39,7 +39,6 @@ from litellm.proxy.auth.auth_checks import ( ) from litellm.proxy.auth.auth_exception_handler import UserAPIKeyAuthExceptionHandler from litellm.proxy.auth.auth_utils import ( - _get_request_ip_address, get_end_user_id_from_request_body, get_request_route, is_pass_through_provider_route, From 23aa7f81b562b4cd96079e05733acb56b7c0bd1b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 15:53:33 -0700 Subject: [PATCH 049/208] fix ProxyException --- litellm/proxy/auth/auth_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 7f75272bbd..efbfe8d90c 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -993,7 +993,7 @@ async def get_key_object( if _valid_token is None: raise ProxyException( - message="Invalid proxy server token passed. key={}, not found in db. Create key via `/key/generate` call.".format( + message="Authentication Error, Invalid proxy server token passed. key={}, not found in db. Create key via `/key/generate` call.".format( hashed_token ), type=ProxyErrorTypes.token_not_found_in_db, From 132d3f7baa528020fad5b2e869f990558a00c8b6 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 26 Mar 2025 16:22:56 -0700 Subject: [PATCH 050/208] feat(prisma-migrations): add baseline db migration file (#9565) adds initial baseline db migration file enables future schema changes to be documented via .sql files --- ci_cd/baseline_db.py | 61 +++ .../20250326162113_baseline/migration.sql | 360 ++++++++++++++++++ deploy/migrations/migration_lock.toml | 1 + .../test_db_schema_migration.py | 62 +++ 4 files changed, 484 insertions(+) create mode 100644 ci_cd/baseline_db.py create mode 100644 deploy/migrations/20250326162113_baseline/migration.sql create mode 100644 deploy/migrations/migration_lock.toml create mode 100644 tests/proxy_unit_tests/test_db_schema_migration.py diff --git a/ci_cd/baseline_db.py b/ci_cd/baseline_db.py new file mode 100644 index 0000000000..52aa5430f5 --- /dev/null +++ b/ci_cd/baseline_db.py @@ -0,0 +1,61 @@ +import os +import subprocess +from pathlib import Path +from datetime import datetime + + +def create_baseline(): + """Create baseline migration in deploy/migrations""" + try: + # Get paths + root_dir = Path(__file__).parent.parent + deploy_dir = root_dir / "deploy" + migrations_dir = deploy_dir / "migrations" + schema_path = root_dir / "schema.prisma" + + # Create migrations directory + migrations_dir.mkdir(parents=True, exist_ok=True) + + # Create migration_lock.toml if it doesn't exist + lock_file = migrations_dir / "migration_lock.toml" + if not lock_file.exists(): + lock_file.write_text('provider = "postgresql"\n') + + # Create timestamp-based migration directory + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + migration_dir = migrations_dir / f"{timestamp}_baseline" + migration_dir.mkdir(parents=True, exist_ok=True) + + # Generate migration SQL + result = subprocess.run( + [ + "prisma", + "migrate", + "diff", + "--from-empty", + "--to-schema-datamodel", + str(schema_path), + "--script", + ], + capture_output=True, + text=True, + check=True, + ) + + # Write the SQL to migration.sql + migration_file = migration_dir / "migration.sql" + migration_file.write_text(result.stdout) + + print(f"Created baseline migration in {migration_dir}") + return True + + except subprocess.CalledProcessError as e: + print(f"Error running prisma command: {e.stderr}") + return False + except Exception as e: + print(f"Error creating baseline migration: {str(e)}") + return False + + +if __name__ == "__main__": + create_baseline() diff --git a/deploy/migrations/20250326162113_baseline/migration.sql b/deploy/migrations/20250326162113_baseline/migration.sql new file mode 100644 index 0000000000..fb8a44814f --- /dev/null +++ b/deploy/migrations/20250326162113_baseline/migration.sql @@ -0,0 +1,360 @@ +-- CreateTable +CREATE TABLE "LiteLLM_BudgetTable" ( + "budget_id" TEXT NOT NULL, + "max_budget" DOUBLE PRECISION, + "soft_budget" DOUBLE PRECISION, + "max_parallel_requests" INTEGER, + "tpm_limit" BIGINT, + "rpm_limit" BIGINT, + "model_max_budget" JSONB, + "budget_duration" TEXT, + "budget_reset_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_by" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_BudgetTable_pkey" PRIMARY KEY ("budget_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_CredentialsTable" ( + "credential_id" TEXT NOT NULL, + "credential_name" TEXT NOT NULL, + "credential_values" JSONB NOT NULL, + "credential_info" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_by" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_CredentialsTable_pkey" PRIMARY KEY ("credential_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_ProxyModelTable" ( + "model_id" TEXT NOT NULL, + "model_name" TEXT NOT NULL, + "litellm_params" JSONB NOT NULL, + "model_info" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_by" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_ProxyModelTable_pkey" PRIMARY KEY ("model_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_OrganizationTable" ( + "organization_id" TEXT NOT NULL, + "organization_alias" TEXT NOT NULL, + "budget_id" TEXT NOT NULL, + "metadata" JSONB NOT NULL DEFAULT '{}', + "models" TEXT[], + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "model_spend" JSONB NOT NULL DEFAULT '{}', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_by" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_OrganizationTable_pkey" PRIMARY KEY ("organization_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_ModelTable" ( + "id" SERIAL NOT NULL, + "aliases" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_by" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_ModelTable_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_TeamTable" ( + "team_id" TEXT NOT NULL, + "team_alias" TEXT, + "organization_id" TEXT, + "admins" TEXT[], + "members" TEXT[], + "members_with_roles" JSONB NOT NULL DEFAULT '{}', + "metadata" JSONB NOT NULL DEFAULT '{}', + "max_budget" DOUBLE PRECISION, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "models" TEXT[], + "max_parallel_requests" INTEGER, + "tpm_limit" BIGINT, + "rpm_limit" BIGINT, + "budget_duration" TEXT, + "budget_reset_at" TIMESTAMP(3), + "blocked" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "model_spend" JSONB NOT NULL DEFAULT '{}', + "model_max_budget" JSONB NOT NULL DEFAULT '{}', + "model_id" INTEGER, + + CONSTRAINT "LiteLLM_TeamTable_pkey" PRIMARY KEY ("team_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_UserTable" ( + "user_id" TEXT NOT NULL, + "user_alias" TEXT, + "team_id" TEXT, + "sso_user_id" TEXT, + "organization_id" TEXT, + "password" TEXT, + "teams" TEXT[] DEFAULT ARRAY[]::TEXT[], + "user_role" TEXT, + "max_budget" DOUBLE PRECISION, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "user_email" TEXT, + "models" TEXT[], + "metadata" JSONB NOT NULL DEFAULT '{}', + "max_parallel_requests" INTEGER, + "tpm_limit" BIGINT, + "rpm_limit" BIGINT, + "budget_duration" TEXT, + "budget_reset_at" TIMESTAMP(3), + "allowed_cache_controls" TEXT[] DEFAULT ARRAY[]::TEXT[], + "model_spend" JSONB NOT NULL DEFAULT '{}', + "model_max_budget" JSONB NOT NULL DEFAULT '{}', + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LiteLLM_UserTable_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_VerificationToken" ( + "token" TEXT NOT NULL, + "key_name" TEXT, + "key_alias" TEXT, + "soft_budget_cooldown" BOOLEAN NOT NULL DEFAULT false, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "expires" TIMESTAMP(3), + "models" TEXT[], + "aliases" JSONB NOT NULL DEFAULT '{}', + "config" JSONB NOT NULL DEFAULT '{}', + "user_id" TEXT, + "team_id" TEXT, + "permissions" JSONB NOT NULL DEFAULT '{}', + "max_parallel_requests" INTEGER, + "metadata" JSONB NOT NULL DEFAULT '{}', + "blocked" BOOLEAN, + "tpm_limit" BIGINT, + "rpm_limit" BIGINT, + "max_budget" DOUBLE PRECISION, + "budget_duration" TEXT, + "budget_reset_at" TIMESTAMP(3), + "allowed_cache_controls" TEXT[] DEFAULT ARRAY[]::TEXT[], + "model_spend" JSONB NOT NULL DEFAULT '{}', + "model_max_budget" JSONB NOT NULL DEFAULT '{}', + "budget_id" TEXT, + "organization_id" TEXT, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_by" TEXT, + + CONSTRAINT "LiteLLM_VerificationToken_pkey" PRIMARY KEY ("token") +); + +-- CreateTable +CREATE TABLE "LiteLLM_EndUserTable" ( + "user_id" TEXT NOT NULL, + "alias" TEXT, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "allowed_model_region" TEXT, + "default_model" TEXT, + "budget_id" TEXT, + "blocked" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "LiteLLM_EndUserTable_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_Config" ( + "param_name" TEXT NOT NULL, + "param_value" JSONB, + + CONSTRAINT "LiteLLM_Config_pkey" PRIMARY KEY ("param_name") +); + +-- CreateTable +CREATE TABLE "LiteLLM_SpendLogs" ( + "request_id" TEXT NOT NULL, + "call_type" TEXT NOT NULL, + "api_key" TEXT NOT NULL DEFAULT '', + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "total_tokens" INTEGER NOT NULL DEFAULT 0, + "prompt_tokens" INTEGER NOT NULL DEFAULT 0, + "completion_tokens" INTEGER NOT NULL DEFAULT 0, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "completionStartTime" TIMESTAMP(3), + "model" TEXT NOT NULL DEFAULT '', + "model_id" TEXT DEFAULT '', + "model_group" TEXT DEFAULT '', + "custom_llm_provider" TEXT DEFAULT '', + "api_base" TEXT DEFAULT '', + "user" TEXT DEFAULT '', + "metadata" JSONB DEFAULT '{}', + "cache_hit" TEXT DEFAULT '', + "cache_key" TEXT DEFAULT '', + "request_tags" JSONB DEFAULT '[]', + "team_id" TEXT, + "end_user" TEXT, + "requester_ip_address" TEXT, + "messages" JSONB DEFAULT '{}', + "response" JSONB DEFAULT '{}', + + CONSTRAINT "LiteLLM_SpendLogs_pkey" PRIMARY KEY ("request_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_ErrorLogs" ( + "request_id" TEXT NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "api_base" TEXT NOT NULL DEFAULT '', + "model_group" TEXT NOT NULL DEFAULT '', + "litellm_model_name" TEXT NOT NULL DEFAULT '', + "model_id" TEXT NOT NULL DEFAULT '', + "request_kwargs" JSONB NOT NULL DEFAULT '{}', + "exception_type" TEXT NOT NULL DEFAULT '', + "exception_string" TEXT NOT NULL DEFAULT '', + "status_code" TEXT NOT NULL DEFAULT '', + + CONSTRAINT "LiteLLM_ErrorLogs_pkey" PRIMARY KEY ("request_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_UserNotifications" ( + "request_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "models" TEXT[], + "justification" TEXT NOT NULL, + "status" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_UserNotifications_pkey" PRIMARY KEY ("request_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_TeamMembership" ( + "user_id" TEXT NOT NULL, + "team_id" TEXT NOT NULL, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "budget_id" TEXT, + + CONSTRAINT "LiteLLM_TeamMembership_pkey" PRIMARY KEY ("user_id","team_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_OrganizationMembership" ( + "user_id" TEXT NOT NULL, + "organization_id" TEXT NOT NULL, + "user_role" TEXT, + "spend" DOUBLE PRECISION DEFAULT 0.0, + "budget_id" TEXT, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LiteLLM_OrganizationMembership_pkey" PRIMARY KEY ("user_id","organization_id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_InvitationLink" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "is_accepted" BOOLEAN NOT NULL DEFAULT false, + "accepted_at" TIMESTAMP(3), + "expires_at" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL, + "created_by" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL, + "updated_by" TEXT NOT NULL, + + CONSTRAINT "LiteLLM_InvitationLink_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LiteLLM_AuditLog" ( + "id" TEXT NOT NULL, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "changed_by" TEXT NOT NULL DEFAULT '', + "changed_by_api_key" TEXT NOT NULL DEFAULT '', + "action" TEXT NOT NULL, + "table_name" TEXT NOT NULL, + "object_id" TEXT NOT NULL, + "before_value" JSONB, + "updated_values" JSONB, + + CONSTRAINT "LiteLLM_AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "LiteLLM_CredentialsTable_credential_name_key" ON "LiteLLM_CredentialsTable"("credential_name"); + +-- CreateIndex +CREATE UNIQUE INDEX "LiteLLM_TeamTable_model_id_key" ON "LiteLLM_TeamTable"("model_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "LiteLLM_UserTable_sso_user_id_key" ON "LiteLLM_UserTable"("sso_user_id"); + +-- CreateIndex +CREATE INDEX "LiteLLM_SpendLogs_startTime_idx" ON "LiteLLM_SpendLogs"("startTime"); + +-- CreateIndex +CREATE INDEX "LiteLLM_SpendLogs_end_user_idx" ON "LiteLLM_SpendLogs"("end_user"); + +-- CreateIndex +CREATE UNIQUE INDEX "LiteLLM_OrganizationMembership_user_id_organization_id_key" ON "LiteLLM_OrganizationMembership"("user_id", "organization_id"); + +-- AddForeignKey +ALTER TABLE "LiteLLM_OrganizationTable" ADD CONSTRAINT "LiteLLM_OrganizationTable_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "LiteLLM_BudgetTable"("budget_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_TeamTable" ADD CONSTRAINT "LiteLLM_TeamTable_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "LiteLLM_OrganizationTable"("organization_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_TeamTable" ADD CONSTRAINT "LiteLLM_TeamTable_model_id_fkey" FOREIGN KEY ("model_id") REFERENCES "LiteLLM_ModelTable"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_UserTable" ADD CONSTRAINT "LiteLLM_UserTable_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "LiteLLM_OrganizationTable"("organization_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_VerificationToken" ADD CONSTRAINT "LiteLLM_VerificationToken_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "LiteLLM_BudgetTable"("budget_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_VerificationToken" ADD CONSTRAINT "LiteLLM_VerificationToken_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "LiteLLM_OrganizationTable"("organization_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_EndUserTable" ADD CONSTRAINT "LiteLLM_EndUserTable_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "LiteLLM_BudgetTable"("budget_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_TeamMembership" ADD CONSTRAINT "LiteLLM_TeamMembership_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "LiteLLM_BudgetTable"("budget_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_OrganizationMembership" ADD CONSTRAINT "LiteLLM_OrganizationMembership_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "LiteLLM_UserTable"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_OrganizationMembership" ADD CONSTRAINT "LiteLLM_OrganizationMembership_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "LiteLLM_OrganizationTable"("organization_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_OrganizationMembership" ADD CONSTRAINT "LiteLLM_OrganizationMembership_budget_id_fkey" FOREIGN KEY ("budget_id") REFERENCES "LiteLLM_BudgetTable"("budget_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_InvitationLink" ADD CONSTRAINT "LiteLLM_InvitationLink_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "LiteLLM_UserTable"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_InvitationLink" ADD CONSTRAINT "LiteLLM_InvitationLink_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "LiteLLM_UserTable"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LiteLLM_InvitationLink" ADD CONSTRAINT "LiteLLM_InvitationLink_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "LiteLLM_UserTable"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + diff --git a/deploy/migrations/migration_lock.toml b/deploy/migrations/migration_lock.toml new file mode 100644 index 0000000000..2fe25d87cc --- /dev/null +++ b/deploy/migrations/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/tests/proxy_unit_tests/test_db_schema_migration.py b/tests/proxy_unit_tests/test_db_schema_migration.py new file mode 100644 index 0000000000..ea50688548 --- /dev/null +++ b/tests/proxy_unit_tests/test_db_schema_migration.py @@ -0,0 +1,62 @@ +import pytest +import os +import subprocess +from pathlib import Path +from pytest_postgresql import factories + +# Create postgresql fixture +postgresql_my_proc = factories.postgresql_proc(port=None) +postgresql_my = factories.postgresql("postgresql_my_proc") + + +@pytest.fixture(scope="function") +def schema_setup(postgresql_my): + """Fixture to provide a test postgres database""" + return postgresql_my + + +def test_schema_migration_check(schema_setup): + """Test to check if schema requires migration""" + # Set test database URL + test_db_url = f"postgresql://{schema_setup.info.user}:@{schema_setup.info.host}:{schema_setup.info.port}/{schema_setup.info.dbname}" + os.environ["DATABASE_URL"] = test_db_url + + deploy_dir = Path("./deploy") + migrations_dir = deploy_dir / "migrations" + + print(migrations_dir) + if not migrations_dir.exists() or not any(migrations_dir.iterdir()): + print("No existing migrations found - first migration needed") + pytest.fail("No existing migrations found - first migration needed") + + # If migrations exist, check for changes + result = subprocess.run( + ["prisma", "migrate", "status"], capture_output=True, text=True + ) + + status_output = result.stdout.lower() + needs_migration = any( + state in status_output for state in ["drift detected", "pending"] + ) + + if needs_migration: + print("Schema changes detected. New migration needed.") + # Show the differences + diff_result = subprocess.run( + [ + "prisma", + "migrate", + "diff", + "--from-migrations", + "--to-schema-datamodel", + "schema.prisma", + ], + capture_output=True, + text=True, + ) + print("Schema differences:") + print(diff_result.stdout) + else: + print("No schema changes detected. Migration not needed.") + + assert not needs_migration, "Schema changes detected - new migration required" From fb9abd40f30c72c777974d30674e0ecb9ae38218 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 26 Mar 2025 16:25:08 -0700 Subject: [PATCH 051/208] ci(config.yml): add pytest-postgres to ci/cd --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5a058144f6..91636c5625 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -555,6 +555,7 @@ jobs: pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" + pip install "pytest-postgresql==7.0.1" - save_cache: paths: - ./venv From 37265c8b1c1bb88f7f7de2afe07474aa0b5c7c03 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 26 Mar 2025 16:36:36 -0700 Subject: [PATCH 052/208] Add Daily User Spend Aggregate view - allows UI Usage tab to work > 1m rows (#9538) * ci: update github action * build(schema.prisma): enable daily user spend table allows storing aggregate view of user's daily spend * build(schema.prisma): add new daily user spend table * feat: working daily user spend tracking maintains an aggregate view for easier querying in high traffic * setup_google_dns * ci: update ci yaml --------- Co-authored-by: Ishaan Jaff --- litellm/proxy/_types.py | 12 ++ litellm/proxy/proxy_server.py | 3 + litellm/proxy/schema.prisma | 23 ++++ litellm/proxy/utils.py | 231 +++++++++++++++++++++++++++++++++- schema.prisma | 22 ++++ 5 files changed, 290 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 220a0d5ddb..ebe7d1e955 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2646,3 +2646,15 @@ class DefaultInternalUserParams(LiteLLMPydanticObjectBase): models: Optional[List[str]] = Field( default=None, description="Default list of models that new users can access" ) + + +class DailyUserSpendTransaction(TypedDict): + user_id: str + date: str + api_key: str + model: str + model_group: Optional[str] + custom_llm_provider: Optional[str] + prompt_tokens: int + completion_tokens: int + spend: float diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 568253735c..eefc0bdd06 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -558,6 +558,7 @@ async def proxy_startup_event(app: FastAPI): proxy_batch_write_at=proxy_batch_write_at, proxy_logging_obj=proxy_logging_obj, ) + ## [Optional] Initialize dd tracer ProxyStartupEvent._init_dd_tracer() @@ -914,6 +915,8 @@ def _set_spend_logs_payload( prisma_client.spend_log_transactions.append(payload) elif prisma_client is not None: prisma_client.spend_log_transactions.append(payload) + + prisma_client.add_spend_log_transaction_to_daily_user_transaction(payload.copy()) return prisma_client diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index e453e74b46..9269e89014 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -313,3 +313,26 @@ model LiteLLM_AuditLog { before_value Json? // value of the row updated_values Json? // value of the row after change } + + +// Track daily user spend metrics per model and key +model LiteLLM_DailyUserSpend { + id String @id @default(uuid()) + user_id String + date String + api_key String // Hashed API Token + model String // The specific model used + model_group String? // public model_name / model_group + custom_llm_provider String? // The LLM provider (e.g., "openai", "anthropic") + prompt_tokens Int @default(0) + completion_tokens Int @default(0) + spend Float @default(0.0) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@unique([user_id, date, api_key, model, custom_llm_provider]) + @@index([date]) + @@index([user_id]) + @@index([api_key]) + @@index([model]) +} diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 0e7ae45596..7f1ac814a8 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -10,13 +10,15 @@ 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, List, Literal, Optional, Union, overload +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload from litellm.proxy._types import ( DB_CONNECTION_ERROR_TYPES, CommonProxyErrors, + DailyUserSpendTransaction, ProxyErrorTypes, ProxyException, + SpendLogsPayload, ) from litellm.types.guardrails import GuardrailEventHooks @@ -1103,6 +1105,7 @@ class PrismaClient: team_member_list_transactons: dict = {} # key is ["team_id" + "user_id"] org_list_transactons: dict = {} spend_log_transactions: List = [] + daily_user_spend_transactions: Dict[str, DailyUserSpendTransaction] = {} def __init__( self, @@ -1140,6 +1143,61 @@ class PrismaClient: ) # Client to connect to Prisma db verbose_proxy_logger.debug("Success - Created Prisma Client") + def add_spend_log_transaction_to_daily_user_transaction( + self, payload: Union[dict, SpendLogsPayload] + ): + """ + Add a spend log transaction to the daily user transaction list + + Key = @@unique([user_id, date, api_key, model, custom_llm_provider]) ) + + 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 + + if isinstance(payload["startTime"], datetime): + start_time = payload["startTime"].isoformat() + date = start_time.split("T")[0] + elif isinstance(payload["startTime"], str): + date = payload["startTime"].split("T")[0] + else: + verbose_proxy_logger.debug( + f"Invalid start time: {payload['startTime']}, skipping from daily_user_spend_transactions" + ) + 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 + ] + daily_transaction["spend"] += payload["spend"] + daily_transaction["prompt_tokens"] += payload["prompt_tokens"] + daily_transaction["completion_tokens"] += payload["completion_tokens"] + else: + daily_transaction = DailyUserSpendTransaction( + user_id=payload["user"], + date=date, + api_key=payload["api_key"], + model=payload["model"], + model_group=payload["model_group"], + custom_llm_provider=payload["custom_llm_provider"], + prompt_tokens=payload["prompt_tokens"], + completion_tokens=payload["completion_tokens"], + spend=payload["spend"], + ) + + self.daily_user_spend_transactions[daily_transaction_key] = ( + daily_transaction + ) + except Exception as e: + raise e + def hash_token(self, token: str): # Hash the string using SHA-256 hashed_token = hashlib.sha256(token.encode()).hexdigest() @@ -2476,6 +2534,116 @@ class ProxyUpdateSpend: e=e, start_time=start_time, proxy_logging_obj=proxy_logging_obj ) + @staticmethod + async def update_daily_user_spend( + n_retry_times: int, + prisma_client: PrismaClient, + proxy_logging_obj: ProxyLogging, + ): + """ + Batch job to update LiteLLM_DailyUserSpend table using in-memory daily_spend_transactions + """ + BATCH_SIZE = ( + 100 # Number of aggregated records to update in each database operation + ) + start_time = time.time() + + try: + for i in range(n_retry_times + 1): + try: + # Get transactions to process + transactions_to_process = dict( + list(prisma_client.daily_user_spend_transactions.items())[ + :BATCH_SIZE + ] + ) + + if len(transactions_to_process) == 0: + verbose_proxy_logger.debug( + "No new transactions to process for daily spend update" + ) + break + + # Update DailyUserSpend table in batches + async with prisma_client.db.batch_() as batcher: + for _, transaction in transactions_to_process.items(): + user_id = transaction.get("user_id") + if not user_id: # Skip if no user_id + continue + + batcher.litellm_dailyuserspend.upsert( + where={ + "user_id_date_api_key_model_custom_llm_provider": { + "user_id": user_id, + "date": transaction["date"], + "api_key": transaction["api_key"], + "model": transaction["model"], + "custom_llm_provider": transaction.get( + "custom_llm_provider" + ), + } + }, + data={ + "create": { + "user_id": user_id, + "date": transaction["date"], + "api_key": transaction["api_key"], + "model": transaction["model"], + "model_group": transaction.get("model_group"), + "custom_llm_provider": transaction.get( + "custom_llm_provider" + ), + "prompt_tokens": transaction["prompt_tokens"], + "completion_tokens": transaction[ + "completion_tokens" + ], + "spend": transaction["spend"], + }, + "update": { + "prompt_tokens": { + "increment": transaction["prompt_tokens"] + }, + "completion_tokens": { + "increment": transaction[ + "completion_tokens" + ] + }, + "spend": {"increment": transaction["spend"]}, + }, + }, + ) + + verbose_proxy_logger.info( + f"Processed {len(transactions_to_process)} daily spend transactions in {time.time() - start_time:.2f}s" + ) + + # Remove processed transactions + for key in transactions_to_process.keys(): + prisma_client.daily_user_spend_transactions.pop(key, None) + + verbose_proxy_logger.debug( + f"Processed {len(transactions_to_process)} daily spend transactions in {time.time() - start_time:.2f}s" + ) + break + + except DB_CONNECTION_ERROR_TYPES as e: + if i >= n_retry_times: + _raise_failed_update_spend_exception( + e=e, + start_time=start_time, + proxy_logging_obj=proxy_logging_obj, + ) + await asyncio.sleep(2**i) # Exponential backoff + + except Exception as e: + # Remove processed transactions even if there was an error + if "transactions_to_process" in locals(): + for key in transactions_to_process.keys(): # type: ignore + prisma_client.daily_user_spend_transactions.pop(key, None) + _raise_failed_update_spend_exception( + e=e, start_time=start_time, proxy_logging_obj=proxy_logging_obj + ) + @staticmethod def disable_spend_updates() -> bool: """ @@ -2716,6 +2884,20 @@ async def update_spend( # noqa: PLR0915 db_writer_client=db_writer_client, ) + ### UPDATE DAILY USER SPEND ### + verbose_proxy_logger.debug( + "Daily User Spend transactions: {}".format( + len(prisma_client.daily_user_spend_transactions) + ) + ) + + if len(prisma_client.daily_user_spend_transactions) > 0: + await ProxyUpdateSpend.update_daily_user_spend( + n_retry_times=n_retry_times, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + ) + def _raise_failed_update_spend_exception( e: Exception, start_time: float, proxy_logging_obj: ProxyLogging @@ -2911,3 +3093,50 @@ def _premium_user_check(): "error": f"This feature is only available for LiteLLM Enterprise users. {CommonProxyErrors.not_premium_user.value}" }, ) + + +async def _update_daily_spend_batch(prisma_client, spend_aggregates): + """Helper function to update daily spend in batches""" + async with prisma_client.db.batch_() as batcher: + for ( + user_id, + date, + api_key, + model, + model_group, + provider, + ), metrics in spend_aggregates.items(): + if not user_id: # Skip if no user_id + continue + + batcher.litellm_dailyuserspend.upsert( + where={ + "user_id_date_api_key_model_custom_llm_provider": { + "user_id": user_id, + "date": date, + "api_key": api_key, + "model": model, + "custom_llm_provider": provider, + } + }, + data={ + "create": { + "user_id": user_id, + "date": date, + "api_key": api_key, + "model": model, + "model_group": model_group, + "custom_llm_provider": provider, + "prompt_tokens": metrics["prompt_tokens"], + "completion_tokens": metrics["completion_tokens"], + "spend": metrics["spend"], + }, + "update": { + "prompt_tokens": {"increment": metrics["prompt_tokens"]}, + "completion_tokens": { + "increment": metrics["completion_tokens"] + }, + "spend": {"increment": metrics["spend"]}, + }, + }, + ) diff --git a/schema.prisma b/schema.prisma index e453e74b46..3312b26354 100644 --- a/schema.prisma +++ b/schema.prisma @@ -313,3 +313,25 @@ model LiteLLM_AuditLog { before_value Json? // value of the row updated_values Json? // value of the row after change } + +// Track daily user spend metrics per model and key +model LiteLLM_DailyUserSpend { + id String @id @default(uuid()) + user_id String + date String + api_key String + model String + model_group String? + custom_llm_provider String? + prompt_tokens Int @default(0) + completion_tokens Int @default(0) + spend Float @default(0.0) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@unique([user_id, date, api_key, model, custom_llm_provider]) + @@index([date]) + @@index([user_id]) + @@index([api_key]) + @@index([model]) +} From d4adc9764bea476cd7fb45a0fbcc92aabf5e77e6 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 26 Mar 2025 16:59:50 -0700 Subject: [PATCH 053/208] test(test_db_schema_migration.py): ci/cd test to enforce schema migrations are documented in .sql files --- .../test_db_schema_migration.py | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/tests/proxy_unit_tests/test_db_schema_migration.py b/tests/proxy_unit_tests/test_db_schema_migration.py index ea50688548..77052124fb 100644 --- a/tests/proxy_unit_tests/test_db_schema_migration.py +++ b/tests/proxy_unit_tests/test_db_schema_migration.py @@ -3,6 +3,8 @@ import os import subprocess from pathlib import Path from pytest_postgresql import factories +import shutil +import tempfile # Create postgresql fixture postgresql_my_proc = factories.postgresql_proc(port=None) @@ -19,44 +21,63 @@ def test_schema_migration_check(schema_setup): """Test to check if schema requires migration""" # Set test database URL test_db_url = f"postgresql://{schema_setup.info.user}:@{schema_setup.info.host}:{schema_setup.info.port}/{schema_setup.info.dbname}" + # test_db_url = "postgresql://neondb_owner:npg_JiZPS0DAhRn4@ep-delicate-wave-a55cvbuc.us-east-2.aws.neon.tech/neondb?sslmode=require" os.environ["DATABASE_URL"] = test_db_url - deploy_dir = Path("./deploy") - migrations_dir = deploy_dir / "migrations" + deploy_dir = Path("../../deploy") + source_migrations_dir = deploy_dir / "migrations" + schema_path = Path("../../schema.prisma") - print(migrations_dir) - if not migrations_dir.exists() or not any(migrations_dir.iterdir()): - print("No existing migrations found - first migration needed") - pytest.fail("No existing migrations found - first migration needed") + # Create temporary migrations directory next to schema.prisma + temp_migrations_dir = schema_path.parent / "migrations" - # If migrations exist, check for changes - result = subprocess.run( - ["prisma", "migrate", "status"], capture_output=True, text=True - ) + try: + # Copy migrations to correct location + if temp_migrations_dir.exists(): + shutil.rmtree(temp_migrations_dir) + shutil.copytree(source_migrations_dir, temp_migrations_dir) - status_output = result.stdout.lower() - needs_migration = any( - state in status_output for state in ["drift detected", "pending"] - ) + if not temp_migrations_dir.exists() or not any(temp_migrations_dir.iterdir()): + print("No existing migrations found - first migration needed") + pytest.fail("No existing migrations found - first migration needed") - if needs_migration: - print("Schema changes detected. New migration needed.") - # Show the differences + # Apply all existing migrations + subprocess.run( + ["prisma", "migrate", "deploy", "--schema", str(schema_path)], check=True + ) + + # Compare current database state against schema diff_result = subprocess.run( [ "prisma", "migrate", "diff", - "--from-migrations", + "--from-url", + test_db_url, "--to-schema-datamodel", - "schema.prisma", + str(schema_path), + "--script", # Show the SQL diff + "--exit-code", # Return exit code 2 if there are differences ], capture_output=True, text=True, ) - print("Schema differences:") - print(diff_result.stdout) - else: - print("No schema changes detected. Migration not needed.") - assert not needs_migration, "Schema changes detected - new migration required" + print("Exit code:", diff_result.returncode) + print("Stdout:", diff_result.stdout) + print("Stderr:", diff_result.stderr) + + if diff_result.returncode == 2: + print("Schema changes detected. New migration needed.") + print("Schema differences:") + print(diff_result.stdout) + pytest.fail( + "Schema changes detected - new migration required. Run 'prisma migrate dev' to create new migration." + ) + else: + print("No schema changes detected. Migration not needed.") + + finally: + # Clean up: remove temporary migrations directory + if temp_migrations_dir.exists(): + shutil.rmtree(temp_migrations_dir) From 72c0ad419fb24fbafac9b095b2dcbd2475b43ffe Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 26 Mar 2025 17:11:25 -0700 Subject: [PATCH 054/208] build(migration.sql): add migration file for new dailyusertable documents prisma db changes --- ci_cd/run_migration.py | 96 +++++++++++++++++++ .../migration.sql | 33 +++++++ 2 files changed, 129 insertions(+) create mode 100644 ci_cd/run_migration.py create mode 100644 deploy/migrations/20250326171002_add_daily_user_table/migration.sql diff --git a/ci_cd/run_migration.py b/ci_cd/run_migration.py new file mode 100644 index 0000000000..69b4308e57 --- /dev/null +++ b/ci_cd/run_migration.py @@ -0,0 +1,96 @@ +import os +import subprocess +from pathlib import Path +from datetime import datetime +import testing.postgresql +import shutil + + +def create_migration(migration_name: str = None): + """ + Create a new migration SQL file in deploy/migrations directory by comparing + current database state with schema + + Args: + migration_name (str): Name for the migration + """ + try: + # Get paths + root_dir = Path(__file__).parent.parent + deploy_dir = root_dir / "deploy" + migrations_dir = deploy_dir / "migrations" + schema_path = root_dir / "schema.prisma" + + # Create temporary PostgreSQL database + with testing.postgresql.Postgresql() as postgresql: + db_url = postgresql.url() + + # Create temporary migrations directory next to schema.prisma + temp_migrations_dir = schema_path.parent / "migrations" + + try: + # Copy existing migrations to temp directory + if temp_migrations_dir.exists(): + shutil.rmtree(temp_migrations_dir) + shutil.copytree(migrations_dir, temp_migrations_dir) + + # Apply existing migrations to temp database + os.environ["DATABASE_URL"] = db_url + subprocess.run( + ["prisma", "migrate", "deploy", "--schema", str(schema_path)], + check=True, + ) + + # Generate diff between current database and schema + result = subprocess.run( + [ + "prisma", + "migrate", + "diff", + "--from-url", + db_url, + "--to-schema-datamodel", + str(schema_path), + "--script", + ], + capture_output=True, + text=True, + check=True, + ) + + if result.stdout.strip(): + # Generate timestamp and create migration directory + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + migration_name = migration_name or "unnamed_migration" + migration_dir = migrations_dir / f"{timestamp}_{migration_name}" + migration_dir.mkdir(parents=True, exist_ok=True) + + # Write the SQL to migration.sql + migration_file = migration_dir / "migration.sql" + migration_file.write_text(result.stdout) + + print(f"Created migration in {migration_dir}") + return True + else: + print("No schema changes detected. Migration not needed.") + return False + + finally: + # Clean up: remove temporary migrations directory + if temp_migrations_dir.exists(): + shutil.rmtree(temp_migrations_dir) + + except subprocess.CalledProcessError as e: + print(f"Error generating migration: {e.stderr}") + return False + except Exception as e: + print(f"Error creating migration: {str(e)}") + return False + + +if __name__ == "__main__": + # If running directly, can optionally pass migration name as argument + import sys + + migration_name = sys.argv[1] if len(sys.argv) > 1 else None + create_migration(migration_name) diff --git a/deploy/migrations/20250326171002_add_daily_user_table/migration.sql b/deploy/migrations/20250326171002_add_daily_user_table/migration.sql new file mode 100644 index 0000000000..3379d8e9fd --- /dev/null +++ b/deploy/migrations/20250326171002_add_daily_user_table/migration.sql @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE "LiteLLM_DailyUserSpend" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "date" TEXT NOT NULL, + "api_key" TEXT NOT NULL, + "model" TEXT NOT NULL, + "model_group" TEXT, + "custom_llm_provider" TEXT, + "prompt_tokens" INTEGER NOT NULL DEFAULT 0, + "completion_tokens" INTEGER NOT NULL DEFAULT 0, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LiteLLM_DailyUserSpend_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyUserSpend_date_idx" ON "LiteLLM_DailyUserSpend"("date"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyUserSpend_user_id_idx" ON "LiteLLM_DailyUserSpend"("user_id"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyUserSpend_api_key_idx" ON "LiteLLM_DailyUserSpend"("api_key"); + +-- CreateIndex +CREATE INDEX "LiteLLM_DailyUserSpend_model_idx" ON "LiteLLM_DailyUserSpend"("model"); + +-- CreateIndex +CREATE UNIQUE INDEX "LiteLLM_DailyUserSpend_user_id_date_api_key_model_custom_ll_key" ON "LiteLLM_DailyUserSpend"("user_id", "date", "api_key", "model", "custom_llm_provider"); + From 04490c99d71d3ff948300bbcac85c1e8e43c9f4a Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 26 Mar 2025 17:12:09 -0700 Subject: [PATCH 055/208] test: fix test --- tests/proxy_unit_tests/test_db_schema_migration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/proxy_unit_tests/test_db_schema_migration.py b/tests/proxy_unit_tests/test_db_schema_migration.py index 77052124fb..59367aa9cb 100644 --- a/tests/proxy_unit_tests/test_db_schema_migration.py +++ b/tests/proxy_unit_tests/test_db_schema_migration.py @@ -24,9 +24,9 @@ def test_schema_migration_check(schema_setup): # test_db_url = "postgresql://neondb_owner:npg_JiZPS0DAhRn4@ep-delicate-wave-a55cvbuc.us-east-2.aws.neon.tech/neondb?sslmode=require" os.environ["DATABASE_URL"] = test_db_url - deploy_dir = Path("../../deploy") + deploy_dir = Path("./deploy") source_migrations_dir = deploy_dir / "migrations" - schema_path = Path("../../schema.prisma") + schema_path = Path("./schema.prisma") # Create temporary migrations directory next to schema.prisma temp_migrations_dir = schema_path.parent / "migrations" From 4351c77253a8355fdee17844e91eed13ed7effc4 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 26 Mar 2025 17:26:25 -0700 Subject: [PATCH 056/208] Support Gemini audio token cost tracking + fix openai audio input token cost tracking (#9535) * fix(vertex_and_google_ai_studio_gemini.py): log gemini audio tokens in usage object enables accurate cost tracking * refactor(vertex_ai/cost_calculator.py): refactor 128k+ token cost calculation to only run if model info has it Google has moved away from this for gemini-2.0 models * refactor(vertex_ai/cost_calculator.py): migrate to usage object for more flexible data passthrough * fix(llm_cost_calc/utils.py): support audio token cost tracking in generic cost per token enables vertex ai cost tracking to work with audio tokens * fix(llm_cost_calc/utils.py): default to total prompt tokens if text tokens field not set * refactor(llm_cost_calc/utils.py): move openai cost tracking to generic cost per token more consistent behaviour across providers * test: add unit test for gemini audio token cost calculation * ci: bump ci config * test: fix test --- litellm/cost_calculator.py | 6 +- .../litellm_core_utils/llm_cost_calc/utils.py | 81 ++++++++++-- litellm/llms/openai/cost_calculation.py | 86 ++++++------- litellm/llms/vertex_ai/cost_calculator.py | 115 +++++++++++------- .../vertex_and_google_ai_studio_gemini.py | 18 ++- litellm/types/llms/vertex_ai.py | 6 + tests/litellm/test_cost_calculator.py | 39 ++++++ tests/local_testing/test_completion_cost.py | 19 +-- 8 files changed, 253 insertions(+), 117 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index f5731618a3..7b95c45ac7 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -275,15 +275,13 @@ def cost_per_token( # noqa: PLR0915 custom_llm_provider=custom_llm_provider, prompt_characters=prompt_characters, completion_characters=completion_characters, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + usage=usage_block, ) elif cost_router == "cost_per_token": return google_cost_per_token( model=model_without_prefix, custom_llm_provider=custom_llm_provider, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + usage=usage_block, ) elif custom_llm_provider == "anthropic": return anthropic_cost_per_token(model=model, usage=usage_block) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 7af3a26d2e..6b778344d7 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -1,7 +1,7 @@ # What is this? ## Helper utilities for cost_per_token() -from typing import Optional, Tuple +from typing import Optional, Tuple, cast import litellm from litellm import verbose_logger @@ -143,26 +143,50 @@ def generic_cost_per_token( ### Cost of processing (non-cache hit + cache hit) + Cost of cache-writing (cache writing) prompt_cost = 0.0 ### PROCESSING COST - non_cache_hit_tokens = usage.prompt_tokens + text_tokens = usage.prompt_tokens cache_hit_tokens = 0 - if usage.prompt_tokens_details and usage.prompt_tokens_details.cached_tokens: - cache_hit_tokens = usage.prompt_tokens_details.cached_tokens - non_cache_hit_tokens = non_cache_hit_tokens - cache_hit_tokens + audio_tokens = 0 + if usage.prompt_tokens_details: + cache_hit_tokens = ( + cast( + Optional[int], getattr(usage.prompt_tokens_details, "cached_tokens", 0) + ) + or 0 + ) + text_tokens = ( + cast( + Optional[int], getattr(usage.prompt_tokens_details, "text_tokens", None) + ) + or 0 # default to prompt tokens, if this field is not set + ) + audio_tokens = ( + cast(Optional[int], getattr(usage.prompt_tokens_details, "audio_tokens", 0)) + or 0 + ) + + ## EDGE CASE - text tokens not set inside PromptTokensDetails + if text_tokens == 0: + text_tokens = usage.prompt_tokens - cache_hit_tokens - audio_tokens prompt_base_cost = _get_prompt_token_base_cost(model_info=model_info, usage=usage) - prompt_cost = float(non_cache_hit_tokens) * prompt_base_cost + prompt_cost = float(text_tokens) * prompt_base_cost _cache_read_input_token_cost = model_info.get("cache_read_input_token_cost") + + ### CACHE READ COST if ( _cache_read_input_token_cost is not None - and usage.prompt_tokens_details - and usage.prompt_tokens_details.cached_tokens + and cache_hit_tokens is not None + and cache_hit_tokens > 0 ): - prompt_cost += ( - float(usage.prompt_tokens_details.cached_tokens) - * _cache_read_input_token_cost - ) + prompt_cost += float(cache_hit_tokens) * _cache_read_input_token_cost + + ### AUDIO COST + + audio_token_cost = model_info.get("input_cost_per_audio_token") + if audio_token_cost is not None and audio_tokens is not None and audio_tokens > 0: + prompt_cost += float(audio_tokens) * audio_token_cost ### CACHE WRITING COST _cache_creation_input_token_cost = model_info.get("cache_creation_input_token_cost") @@ -175,6 +199,37 @@ def generic_cost_per_token( completion_base_cost = _get_completion_token_base_cost( model_info=model_info, usage=usage ) - completion_cost = usage["completion_tokens"] * completion_base_cost + text_tokens = usage.completion_tokens + audio_tokens = 0 + if usage.completion_tokens_details is not None: + audio_tokens = ( + cast( + Optional[int], + getattr(usage.completion_tokens_details, "audio_tokens", 0), + ) + or 0 + ) + text_tokens = ( + cast( + Optional[int], + getattr(usage.completion_tokens_details, "text_tokens", None), + ) + or usage.completion_tokens # default to completion tokens, if this field is not set + ) + + ## TEXT COST + completion_cost = float(text_tokens) * completion_base_cost + + _output_cost_per_audio_token: Optional[float] = model_info.get( + "output_cost_per_audio_token" + ) + + ## AUDIO COST + if ( + _output_cost_per_audio_token is not None + and audio_tokens is not None + and audio_tokens > 0 + ): + completion_cost += float(audio_tokens) * _output_cost_per_audio_token return prompt_cost, completion_cost diff --git a/litellm/llms/openai/cost_calculation.py b/litellm/llms/openai/cost_calculation.py index 0c26fd7448..304c444e37 100644 --- a/litellm/llms/openai/cost_calculation.py +++ b/litellm/llms/openai/cost_calculation.py @@ -6,6 +6,7 @@ Helper util for handling openai-specific cost calculation from typing import Literal, Optional, Tuple from litellm._logging import verbose_logger +from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token from litellm.types.utils import CallTypes, Usage from litellm.utils import get_model_info @@ -28,52 +29,53 @@ def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]: Returns: Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd """ - ## GET MODEL INFO - model_info = get_model_info(model=model, custom_llm_provider="openai") ## CALCULATE INPUT COST - ### Non-cached text tokens - non_cached_text_tokens = usage.prompt_tokens - cached_tokens: Optional[int] = None - if usage.prompt_tokens_details and usage.prompt_tokens_details.cached_tokens: - cached_tokens = usage.prompt_tokens_details.cached_tokens - non_cached_text_tokens = non_cached_text_tokens - cached_tokens - prompt_cost: float = non_cached_text_tokens * model_info["input_cost_per_token"] - ## Prompt Caching cost calculation - if model_info.get("cache_read_input_token_cost") is not None and cached_tokens: - # Note: We read ._cache_read_input_tokens from the Usage - since cost_calculator.py standardizes the cache read tokens on usage._cache_read_input_tokens - prompt_cost += cached_tokens * ( - model_info.get("cache_read_input_token_cost", 0) or 0 - ) + return generic_cost_per_token( + model=model, usage=usage, custom_llm_provider="openai" + ) + # ### Non-cached text tokens + # non_cached_text_tokens = usage.prompt_tokens + # cached_tokens: Optional[int] = None + # if usage.prompt_tokens_details and usage.prompt_tokens_details.cached_tokens: + # cached_tokens = usage.prompt_tokens_details.cached_tokens + # non_cached_text_tokens = non_cached_text_tokens - cached_tokens + # prompt_cost: float = non_cached_text_tokens * model_info["input_cost_per_token"] + # ## Prompt Caching cost calculation + # if model_info.get("cache_read_input_token_cost") is not None and cached_tokens: + # # Note: We read ._cache_read_input_tokens from the Usage - since cost_calculator.py standardizes the cache read tokens on usage._cache_read_input_tokens + # prompt_cost += cached_tokens * ( + # model_info.get("cache_read_input_token_cost", 0) or 0 + # ) - _audio_tokens: Optional[int] = ( - usage.prompt_tokens_details.audio_tokens - if usage.prompt_tokens_details is not None - else None - ) - _audio_cost_per_token: Optional[float] = model_info.get( - "input_cost_per_audio_token" - ) - if _audio_tokens is not None and _audio_cost_per_token is not None: - audio_cost: float = _audio_tokens * _audio_cost_per_token - prompt_cost += audio_cost + # _audio_tokens: Optional[int] = ( + # usage.prompt_tokens_details.audio_tokens + # if usage.prompt_tokens_details is not None + # else None + # ) + # _audio_cost_per_token: Optional[float] = model_info.get( + # "input_cost_per_audio_token" + # ) + # if _audio_tokens is not None and _audio_cost_per_token is not None: + # audio_cost: float = _audio_tokens * _audio_cost_per_token + # prompt_cost += audio_cost - ## CALCULATE OUTPUT COST - completion_cost: float = ( - usage["completion_tokens"] * model_info["output_cost_per_token"] - ) - _output_cost_per_audio_token: Optional[float] = model_info.get( - "output_cost_per_audio_token" - ) - _output_audio_tokens: Optional[int] = ( - usage.completion_tokens_details.audio_tokens - if usage.completion_tokens_details is not None - else None - ) - if _output_cost_per_audio_token is not None and _output_audio_tokens is not None: - audio_cost = _output_audio_tokens * _output_cost_per_audio_token - completion_cost += audio_cost + # ## CALCULATE OUTPUT COST + # completion_cost: float = ( + # usage["completion_tokens"] * model_info["output_cost_per_token"] + # ) + # _output_cost_per_audio_token: Optional[float] = model_info.get( + # "output_cost_per_audio_token" + # ) + # _output_audio_tokens: Optional[int] = ( + # usage.completion_tokens_details.audio_tokens + # if usage.completion_tokens_details is not None + # else None + # ) + # if _output_cost_per_audio_token is not None and _output_audio_tokens is not None: + # audio_cost = _output_audio_tokens * _output_cost_per_audio_token + # completion_cost += audio_cost - return prompt_cost, completion_cost + # return prompt_cost, completion_cost def cost_per_second( diff --git a/litellm/llms/vertex_ai/cost_calculator.py b/litellm/llms/vertex_ai/cost_calculator.py index fd23886045..54efc62589 100644 --- a/litellm/llms/vertex_ai/cost_calculator.py +++ b/litellm/llms/vertex_ai/cost_calculator.py @@ -4,7 +4,11 @@ from typing import Literal, Optional, Tuple, Union import litellm from litellm import verbose_logger -from litellm.litellm_core_utils.llm_cost_calc.utils import _is_above_128k +from litellm.litellm_core_utils.llm_cost_calc.utils import ( + _is_above_128k, + generic_cost_per_token, +) +from litellm.types.utils import ModelInfo, Usage """ Gemini pricing covers: @@ -20,7 +24,7 @@ Vertex AI -> character based pricing Google AI Studio -> token based pricing """ -models_without_dynamic_pricing = ["gemini-1.0-pro", "gemini-pro"] +models_without_dynamic_pricing = ["gemini-1.0-pro", "gemini-pro", "gemini-2"] def cost_router( @@ -46,14 +50,15 @@ def cost_router( call_type == "embedding" or call_type == "aembedding" ): return "cost_per_token" + elif custom_llm_provider == "vertex_ai" and ("gemini-2" in model): + return "cost_per_token" return "cost_per_character" def cost_per_character( model: str, custom_llm_provider: str, - prompt_tokens: float, - completion_tokens: float, + usage: Usage, prompt_characters: Optional[float] = None, completion_characters: Optional[float] = None, ) -> Tuple[float, float]: @@ -86,8 +91,7 @@ def cost_per_character( prompt_cost, _ = cost_per_token( model=model, custom_llm_provider=custom_llm_provider, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + usage=usage, ) else: try: @@ -124,8 +128,7 @@ def cost_per_character( prompt_cost, _ = cost_per_token( model=model, custom_llm_provider=custom_llm_provider, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + usage=usage, ) ## CALCULATE OUTPUT COST @@ -133,10 +136,10 @@ def cost_per_character( _, completion_cost = cost_per_token( model=model, custom_llm_provider=custom_llm_provider, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + usage=usage, ) else: + completion_tokens = usage.completion_tokens try: if ( _is_above_128k(tokens=completion_characters * 4) # 1 token = 4 char @@ -172,18 +175,54 @@ def cost_per_character( _, completion_cost = cost_per_token( model=model, custom_llm_provider=custom_llm_provider, - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, + usage=usage, ) return prompt_cost, completion_cost +def _handle_128k_pricing( + model_info: ModelInfo, + usage: Usage, +) -> Tuple[float, float]: + ## CALCULATE INPUT COST + input_cost_per_token_above_128k_tokens = model_info.get( + "input_cost_per_token_above_128k_tokens" + ) + output_cost_per_token_above_128k_tokens = model_info.get( + "output_cost_per_token_above_128k_tokens" + ) + + prompt_tokens = usage.prompt_tokens + completion_tokens = usage.completion_tokens + + if ( + _is_above_128k(tokens=prompt_tokens) + and input_cost_per_token_above_128k_tokens is not None + ): + prompt_cost = prompt_tokens * input_cost_per_token_above_128k_tokens + else: + prompt_cost = prompt_tokens * model_info["input_cost_per_token"] + + ## CALCULATE OUTPUT COST + output_cost_per_token_above_128k_tokens = model_info.get( + "output_cost_per_token_above_128k_tokens" + ) + if ( + _is_above_128k(tokens=completion_tokens) + and output_cost_per_token_above_128k_tokens is not None + ): + completion_cost = completion_tokens * output_cost_per_token_above_128k_tokens + else: + completion_cost = completion_tokens * model_info["output_cost_per_token"] + + return prompt_cost, completion_cost + + def cost_per_token( model: str, custom_llm_provider: str, - prompt_tokens: float, - completion_tokens: float, + usage: Usage, ) -> Tuple[float, float]: """ Calculates the cost per token for a given model, prompt tokens, and completion tokens. @@ -205,38 +244,24 @@ def cost_per_token( model=model, custom_llm_provider=custom_llm_provider ) - ## CALCULATE INPUT COST + ## HANDLE 128k+ PRICING + input_cost_per_token_above_128k_tokens = model_info.get( + "input_cost_per_token_above_128k_tokens" + ) + output_cost_per_token_above_128k_tokens = model_info.get( + "output_cost_per_token_above_128k_tokens" + ) if ( - _is_above_128k(tokens=prompt_tokens) - and model not in models_without_dynamic_pricing + input_cost_per_token_above_128k_tokens is not None + or output_cost_per_token_above_128k_tokens is not None ): - assert ( - "input_cost_per_token_above_128k_tokens" in model_info - and model_info["input_cost_per_token_above_128k_tokens"] is not None - ), "model info for model={} does not have pricing for > 128k tokens\nmodel_info={}".format( - model, model_info + return _handle_128k_pricing( + model_info=model_info, + usage=usage, ) - prompt_cost = ( - prompt_tokens * model_info["input_cost_per_token_above_128k_tokens"] - ) - else: - prompt_cost = prompt_tokens * model_info["input_cost_per_token"] - ## CALCULATE OUTPUT COST - if ( - _is_above_128k(tokens=completion_tokens) - and model not in models_without_dynamic_pricing - ): - assert ( - "output_cost_per_token_above_128k_tokens" in model_info - and model_info["output_cost_per_token_above_128k_tokens"] is not None - ), "model info for model={} does not have pricing for > 128k tokens\nmodel_info={}".format( - model, model_info - ) - completion_cost = ( - completion_tokens * model_info["output_cost_per_token_above_128k_tokens"] - ) - else: - completion_cost = completion_tokens * model_info["output_cost_per_token"] - - return prompt_cost, completion_cost + return generic_cost_per_token( + model=model, + custom_llm_provider=custom_llm_provider, + usage=usage, + ) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 2931b1b3a8..90c66f69a3 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -643,16 +643,25 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): completion_response: GenerateContentResponseBody, ) -> Usage: cached_tokens: Optional[int] = None + audio_tokens: Optional[int] = None + text_tokens: Optional[int] = None prompt_tokens_details: Optional[PromptTokensDetailsWrapper] = None if "cachedContentTokenCount" in completion_response["usageMetadata"]: cached_tokens = completion_response["usageMetadata"][ "cachedContentTokenCount" ] + if "promptTokensDetails" in completion_response["usageMetadata"]: + for detail in completion_response["usageMetadata"]["promptTokensDetails"]: + if detail["modality"] == "AUDIO": + audio_tokens = detail["tokenCount"] + elif detail["modality"] == "TEXT": + text_tokens = detail["tokenCount"] - if cached_tokens is not None: - prompt_tokens_details = PromptTokensDetailsWrapper( - cached_tokens=cached_tokens, - ) + prompt_tokens_details = PromptTokensDetailsWrapper( + cached_tokens=cached_tokens, + audio_tokens=audio_tokens, + text_tokens=text_tokens, + ) ## GET USAGE ## usage = Usage( prompt_tokens=completion_response["usageMetadata"].get( @@ -791,6 +800,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): model_response.choices.append(choice) usage = self._calculate_usage(completion_response=completion_response) + setattr(model_response, "usage", usage) ## ADD GROUNDING METADATA ## diff --git a/litellm/types/llms/vertex_ai.py b/litellm/types/llms/vertex_ai.py index 7024909a34..ed6bcdb346 100644 --- a/litellm/types/llms/vertex_ai.py +++ b/litellm/types/llms/vertex_ai.py @@ -179,11 +179,17 @@ class TTL(TypedDict, total=False): nano: float +class PromptTokensDetails(TypedDict): + modality: Literal["TEXT", "AUDIO", "IMAGE", "VIDEO"] + tokenCount: int + + class UsageMetadata(TypedDict, total=False): promptTokenCount: int totalTokenCount: int candidatesTokenCount: int cachedContentTokenCount: int + promptTokensDetails: List[PromptTokensDetails] class CachedContent(TypedDict, total=False): diff --git a/tests/litellm/test_cost_calculator.py b/tests/litellm/test_cost_calculator.py index c0073e2c56..d4322f1e5e 100644 --- a/tests/litellm/test_cost_calculator.py +++ b/tests/litellm/test_cost_calculator.py @@ -12,7 +12,9 @@ from unittest.mock import MagicMock, patch from pydantic import BaseModel +import litellm from litellm.cost_calculator import response_cost_calculator +from litellm.types.utils import ModelResponse, PromptTokensDetailsWrapper, Usage def test_cost_calculator_with_response_cost_in_additional_headers(): @@ -32,3 +34,40 @@ def test_cost_calculator_with_response_cost_in_additional_headers(): ) assert result == 1000 + + +def test_cost_calculator_with_usage(): + from litellm import get_model_info + + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + usage = Usage( + prompt_tokens=100, + completion_tokens=100, + prompt_tokens_details=PromptTokensDetailsWrapper( + text_tokens=10, audio_tokens=90 + ), + ) + mr = ModelResponse(usage=usage, model="gemini-2.0-flash-001") + + result = response_cost_calculator( + response_object=mr, + model="", + custom_llm_provider="vertex_ai", + call_type="acompletion", + optional_params={}, + cache_hit=None, + base_model=None, + ) + + model_info = litellm.model_cost["gemini-2.0-flash-001"] + + expected_cost = ( + usage.prompt_tokens_details.audio_tokens + * model_info["input_cost_per_audio_token"] + + usage.prompt_tokens_details.text_tokens * model_info["input_cost_per_token"] + + usage.completion_tokens * model_info["output_cost_per_token"] + ) + + assert result == expected_cost, f"Got {result}, Expected {expected_cost}" diff --git a/tests/local_testing/test_completion_cost.py b/tests/local_testing/test_completion_cost.py index d4efade9e3..0548984f45 100644 --- a/tests/local_testing/test_completion_cost.py +++ b/tests/local_testing/test_completion_cost.py @@ -2454,6 +2454,14 @@ def test_completion_cost_params_gemini_3(): os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" litellm.model_cost = litellm.get_model_cost_map(url="") + usage = Usage( + completion_tokens=2, + prompt_tokens=3771, + total_tokens=3773, + completion_tokens_details=None, + prompt_tokens_details=None, + ) + response = ModelResponse( id="chatcmpl-61043504-4439-48be-9996-e29bdee24dc3", choices=[ @@ -2472,13 +2480,7 @@ def test_completion_cost_params_gemini_3(): model="gemini-1.5-flash", object="chat.completion", system_fingerprint=None, - usage=Usage( - completion_tokens=2, - prompt_tokens=3771, - total_tokens=3773, - completion_tokens_details=None, - prompt_tokens_details=None, - ), + usage=usage, vertex_ai_grounding_metadata=[], vertex_ai_safety_results=[ [ @@ -2501,10 +2503,9 @@ def test_completion_cost_params_gemini_3(): **{ "model": "gemini-1.5-flash", "custom_llm_provider": "vertex_ai", - "prompt_tokens": 3771, - "completion_tokens": 2, "prompt_characters": None, "completion_characters": 3, + "usage": usage, } ) From 15b1a8afb0c85191c0cc2229c2b2cd8be71cabef Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 18:27:39 -0700 Subject: [PATCH 057/208] test_is_database_connection_error_prisma_errors --- .../proxy/auth/test_auth_exception_handler.py | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/litellm/proxy/auth/test_auth_exception_handler.py diff --git a/tests/litellm/proxy/auth/test_auth_exception_handler.py b/tests/litellm/proxy/auth/test_auth_exception_handler.py new file mode 100644 index 0000000000..ca0e8c88c4 --- /dev/null +++ b/tests/litellm/proxy/auth/test_auth_exception_handler.py @@ -0,0 +1,161 @@ +import asyncio +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest +from fastapi import HTTPException, Request, status +from prisma import errors as prisma_errors +from prisma.errors import ( + ClientNotConnectedError, + DataError, + ForeignKeyViolationError, + HTTPClientClosedError, + MissingRequiredValueError, + PrismaError, + RawQueryError, + RecordNotFoundError, + TableNotFoundError, + UniqueViolationError, +) + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import ProxyErrorTypes, ProxyException +from litellm.proxy.auth.auth_exception_handler import UserAPIKeyAuthExceptionHandler + + +# Test is_database_connection_error method +@pytest.mark.parametrize( + "prisma_error", + [ + PrismaError(), + DataError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), + UniqueViolationError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + ForeignKeyViolationError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + MissingRequiredValueError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + RawQueryError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), + TableNotFoundError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + RecordNotFoundError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + HTTPClientClosedError(), + ClientNotConnectedError(), + ], +) +def test_is_database_connection_error_prisma_errors(prisma_error): + """ + Test that all Prisma errors are considered database connection errors + """ + handler = UserAPIKeyAuthExceptionHandler() + assert handler.is_database_connection_error(prisma_error) == True + + +def test_is_database_connection_generic_errors(): + """ + Test non-Prisma error cases for database connection checking + """ + handler = UserAPIKeyAuthExceptionHandler() + + # Test with ProxyException (DB connection) + db_proxy_exception = ProxyException( + message="DB Connection Error", + type=ProxyErrorTypes.no_db_connection, + param="test-param", + ) + assert handler.is_database_connection_error(db_proxy_exception) == True + + # Test with non-DB error + regular_exception = Exception("Regular error") + assert handler.is_database_connection_error(regular_exception) == False + + +# Test should_allow_request_on_db_unavailable method +@patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": True}, +) +def test_should_allow_request_on_db_unavailable_true(): + handler = UserAPIKeyAuthExceptionHandler() + assert handler.should_allow_request_on_db_unavailable() == True + + +@patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": False}, +) +def test_should_allow_request_on_db_unavailable_false(): + handler = UserAPIKeyAuthExceptionHandler() + assert handler.should_allow_request_on_db_unavailable() == False + + +# Test _handle_authentication_error method +@pytest.mark.asyncio +async def test_handle_authentication_error_db_unavailable(): + handler = UserAPIKeyAuthExceptionHandler() + + # Mock request and other dependencies + mock_request = MagicMock() + mock_request_data = {} + mock_route = "/test" + mock_span = None + mock_api_key = "test-key" + + # Test with DB connection error when requests are allowed + with patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": True}, + ): + db_error = prisma_errors.PrismaError() + result = await handler._handle_authentication_error( + db_error, + mock_request, + mock_request_data, + mock_route, + mock_span, + mock_api_key, + ) + assert result.key_name == "failed-to-connect-to-db" + assert result.token == "failed-to-connect-to-db" + + +@pytest.mark.asyncio +async def test_handle_authentication_error_budget_exceeded(): + handler = UserAPIKeyAuthExceptionHandler() + + # Mock request and other dependencies + mock_request = MagicMock() + mock_request_data = {} + mock_route = "/test" + mock_span = None + mock_api_key = "test-key" + + # Test with budget exceeded error + with pytest.raises(ProxyException) as exc_info: + from litellm.exceptions import BudgetExceededError + + budget_error = BudgetExceededError( + message="Budget exceeded", current_cost=100, max_budget=100 + ) + await handler._handle_authentication_error( + budget_error, + mock_request, + mock_request_data, + mock_route, + mock_span, + mock_api_key, + ) + + assert exc_info.value.type == ProxyErrorTypes.budget_exceeded From 5242c5fbabd149c8812c8152ab2e64929c4778e8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 18:28:28 -0700 Subject: [PATCH 058/208] test - auth exception handler --- .../proxy/auth/test_auth_exception_handler.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/litellm/proxy/auth/test_auth_exception_handler.py b/tests/litellm/proxy/auth/test_auth_exception_handler.py index ca0e8c88c4..b44de86e05 100644 --- a/tests/litellm/proxy/auth/test_auth_exception_handler.py +++ b/tests/litellm/proxy/auth/test_auth_exception_handler.py @@ -101,9 +101,33 @@ def test_should_allow_request_on_db_unavailable_false(): assert handler.should_allow_request_on_db_unavailable() == False -# Test _handle_authentication_error method @pytest.mark.asyncio -async def test_handle_authentication_error_db_unavailable(): +@pytest.mark.parametrize( + "prisma_error", + [ + PrismaError(), + DataError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), + UniqueViolationError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + ForeignKeyViolationError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + MissingRequiredValueError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + RawQueryError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), + TableNotFoundError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + RecordNotFoundError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + HTTPClientClosedError(), + ClientNotConnectedError(), + ], +) +async def test_handle_authentication_error_db_unavailable(prisma_error): handler = UserAPIKeyAuthExceptionHandler() # Mock request and other dependencies @@ -118,9 +142,8 @@ async def test_handle_authentication_error_db_unavailable(): "litellm.proxy.proxy_server.general_settings", {"allow_requests_on_db_unavailable": True}, ): - db_error = prisma_errors.PrismaError() result = await handler._handle_authentication_error( - db_error, + prisma_error, mock_request, mock_request_data, mock_route, From 763f853a9f9f6e678d966e08df91ccec3a76009d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 18:32:58 -0700 Subject: [PATCH 059/208] docs fix --- docs/my-website/docs/providers/bedrock.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/my-website/docs/providers/bedrock.md b/docs/my-website/docs/providers/bedrock.md index 4f88fdb39b..ed65e14b8b 100644 --- a/docs/my-website/docs/providers/bedrock.md +++ b/docs/my-website/docs/providers/bedrock.md @@ -1776,6 +1776,7 @@ response = completion( ) ``` + 1. Setup config.yaml @@ -1820,11 +1821,13 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \ ``` + ### SSO Login (AWS Profile) - Set `AWS_PROFILE` environment variable - Make bedrock completion call + ```python import os from litellm import completion @@ -1940,9 +1943,6 @@ curl -L -X POST 'http://0.0.0.0:4000/v1/images/generations' \ "colorGuidedGenerationParams":{"colors":["#FFFFFF"]} }' ``` - - - | Model Name | Function Call | |-------------------------|---------------------------------------------| From 485aa87e6505e3749d0a15a0c335233af5c02136 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 18:48:18 -0700 Subject: [PATCH 060/208] allow_requests_on_db_unavailable --- docs/my-website/docs/proxy/config_settings.md | 2 +- docs/my-website/docs/proxy/prod.md | 110 +++--------------- 2 files changed, 18 insertions(+), 94 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 0093464d93..4a62184df7 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -160,7 +160,7 @@ general_settings: | database_url | string | The URL for the database connection [Set up Virtual Keys](virtual_keys) | | database_connection_pool_limit | integer | The limit for database connection pool [Setting DB Connection Pool limit](#configure-db-pool-limits--connection-timeouts) | | database_connection_timeout | integer | The timeout for database connections in seconds [Setting DB Connection Pool limit, timeout](#configure-db-pool-limits--connection-timeouts) | -| allow_requests_on_db_unavailable | boolean | If true, allows requests to succeed even if DB is unreachable. **Only use this if running LiteLLM in your VPC** This will allow requests to work even when LiteLLM cannot connect to the DB to verify a Virtual Key | +| allow_requests_on_db_unavailable | boolean | If true, allows requests to succeed even if DB is unreachable. **Only use this if running LiteLLM in your VPC** This will allow requests to work even when LiteLLM cannot connect to the DB to verify a Virtual Key [Doc on graceful db unavailability](prod#5-if-running-litellm-on-vpc-gracefully-handle-db-unavailability) | | custom_auth | string | Write your own custom authentication logic [Doc Custom Auth](virtual_keys#custom-auth) | | max_parallel_requests | integer | The max parallel requests allowed per deployment | | global_max_parallel_requests | integer | The max parallel requests allowed on the proxy overall | diff --git a/docs/my-website/docs/proxy/prod.md b/docs/my-website/docs/proxy/prod.md index d3ba2d6224..314300f2a0 100644 --- a/docs/my-website/docs/proxy/prod.md +++ b/docs/my-website/docs/proxy/prod.md @@ -94,15 +94,29 @@ This disables the load_dotenv() functionality, which will automatically load you ## 5. If running LiteLLM on VPC, gracefully handle DB unavailability -This will allow LiteLLM to continue to process requests even if the DB is unavailable. This is better handling for DB unavailability. +When running LiteLLM on a VPC (and inaccessible from the public internet), you can enable graceful degradation so that request processing continues even if the database is temporarily unavailable. + **WARNING: Only do this if you're running LiteLLM on VPC, that cannot be accessed from the public internet.** -```yaml +#### Configuration + +```yaml showLineNumbers title="litellm config.yaml" general_settings: allow_requests_on_db_unavailable: True ``` +#### Expected Behavior + +When `allow_requests_on_db_unavailable` is set to `true`, LiteLLM will handle errors as follows: + +| Type of Error | Expected Behavior | Details | +|---------------|-------------------|----------------| +| Prisma Errors | ✅ Request will be allowed | Covers issues like DB connection resets or rejections from the DB via Prisma, the ORM used by LiteLLM. | +| Httpx Errors | ✅ Request will be allowed | Occurs when the database is unreachable, allowing the request to proceed despite the DB outage. | +| LiteLLM Budget Errors or Model Errors | ❌ Request will be blocked | Triggered when the DB is reachable but the authentication token is invalid, lacks access, or exceeds budget limits. | + + ## 6. Disable spend_logs & error_logs if not using the LiteLLM UI By default, LiteLLM writes several types of logs to the database: @@ -182,94 +196,4 @@ You should only see the following level of details in logs on the proxy server # INFO: 192.168.2.205:11774 - "POST /chat/completions HTTP/1.1" 200 OK # INFO: 192.168.2.205:34717 - "POST /chat/completions HTTP/1.1" 200 OK # INFO: 192.168.2.205:29734 - "POST /chat/completions HTTP/1.1" 200 OK -``` - - -### Machine Specifications to Deploy LiteLLM - -| Service | Spec | CPUs | Memory | Architecture | Version| -| --- | --- | --- | --- | --- | --- | -| Server | `t2.small`. | `1vCPUs` | `8GB` | `x86` | -| Redis Cache | - | - | - | - | 7.0+ Redis Engine| - - -### Reference Kubernetes Deployment YAML - -Reference Kubernetes `deployment.yaml` that was load tested by us - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: litellm-deployment -spec: - replicas: 3 - selector: - matchLabels: - app: litellm - template: - metadata: - labels: - app: litellm - spec: - containers: - - name: litellm-container - image: ghcr.io/berriai/litellm:main-latest - imagePullPolicy: Always - env: - - name: AZURE_API_KEY - value: "d6******" - - name: AZURE_API_BASE - value: "https://ope******" - - name: LITELLM_MASTER_KEY - value: "sk-1234" - - name: DATABASE_URL - value: "po**********" - args: - - "--config" - - "/app/proxy_config.yaml" # Update the path to mount the config file - volumeMounts: # Define volume mount for proxy_config.yaml - - name: config-volume - mountPath: /app - readOnly: true - livenessProbe: - httpGet: - path: /health/liveliness - port: 4000 - initialDelaySeconds: 120 - periodSeconds: 15 - successThreshold: 1 - failureThreshold: 3 - timeoutSeconds: 10 - readinessProbe: - httpGet: - path: /health/readiness - port: 4000 - initialDelaySeconds: 120 - periodSeconds: 15 - successThreshold: 1 - failureThreshold: 3 - timeoutSeconds: 10 - volumes: # Define volume to mount proxy_config.yaml - - name: config-volume - configMap: - name: litellm-config - -``` - - -Reference Kubernetes `service.yaml` that was load tested by us -```yaml -apiVersion: v1 -kind: Service -metadata: - name: litellm-service -spec: - selector: - app: litellm - ports: - - protocol: TCP - port: 4000 - targetPort: 4000 - type: LoadBalancer -``` +``` \ No newline at end of file From 7142b0b61060292b5a353da65eb95185d281fb4d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 19:22:24 -0700 Subject: [PATCH 061/208] refactor PrismaDBExceptionHandler --- litellm/proxy/auth/auth_exception_handler.py | 38 +++----------------- litellm/proxy/db/exception_handler.py | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 litellm/proxy/db/exception_handler.py diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py index c1a546b569..05797381c6 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -9,13 +9,9 @@ from fastapi import HTTPException, Request, status import litellm from litellm._logging import verbose_proxy_logger -from litellm.proxy._types import ( - DB_CONNECTION_ERROR_TYPES, - ProxyErrorTypes, - ProxyException, - UserAPIKeyAuth, -) +from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth from litellm.proxy.auth.auth_utils import _get_request_ip_address +from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.types.services import ServiceTypes if TYPE_CHECKING: @@ -58,8 +54,8 @@ class UserAPIKeyAuthExceptionHandler: ) if ( - UserAPIKeyAuthExceptionHandler.should_allow_request_on_db_unavailable() - and UserAPIKeyAuthExceptionHandler.is_database_connection_error(e) + PrismaDBExceptionHandler.should_allow_request_on_db_unavailable() + and PrismaDBExceptionHandler.is_database_connection_error(e) ): # log this as a DB failure on prometheus proxy_logging_obj.service_logging_obj.service_failure_hook( @@ -125,29 +121,3 @@ class UserAPIKeyAuthExceptionHandler: param=getattr(e, "param", "None"), code=status.HTTP_401_UNAUTHORIZED, ) - - @staticmethod - def should_allow_request_on_db_unavailable() -> bool: - """ - Returns True if the request should be allowed to proceed despite the DB connection error - """ - from litellm.proxy.proxy_server import general_settings - - if general_settings.get("allow_requests_on_db_unavailable", False) is True: - return True - return False - - @staticmethod - def is_database_connection_error(e: Exception) -> bool: - """ - Returns True if the exception is from a database outage / connection error - """ - import prisma - - if isinstance(e, DB_CONNECTION_ERROR_TYPES): - return True - if isinstance(e, prisma.errors.PrismaError): - return True - if isinstance(e, ProxyException) and e.type == ProxyErrorTypes.no_db_connection: - return True - return False diff --git a/litellm/proxy/db/exception_handler.py b/litellm/proxy/db/exception_handler.py new file mode 100644 index 0000000000..315811103d --- /dev/null +++ b/litellm/proxy/db/exception_handler.py @@ -0,0 +1,37 @@ +from litellm.proxy._types import ( + DB_CONNECTION_ERROR_TYPES, + ProxyErrorTypes, + ProxyException, +) + + +class PrismaDBExceptionHandler: + """ + Class to handle DB Exceptions or Connection Errors + """ + + @staticmethod + def should_allow_request_on_db_unavailable() -> bool: + """ + Returns True if the request should be allowed to proceed despite the DB connection error + """ + from litellm.proxy.proxy_server import general_settings + + if general_settings.get("allow_requests_on_db_unavailable", False) is True: + return True + return False + + @staticmethod + def is_database_connection_error(e: Exception) -> bool: + """ + Returns True if the exception is from a database outage / connection error + """ + import prisma + + if isinstance(e, DB_CONNECTION_ERROR_TYPES): + return True + if isinstance(e, prisma.errors.PrismaError): + return True + if isinstance(e, ProxyException) and e.type == ProxyErrorTypes.no_db_connection: + return True + return False From 497570b2a6e26905d7a7564dbb6bf92b6fc0b343 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 19:49:51 -0700 Subject: [PATCH 062/208] bug fix - allow pods to startup when DB is unavailable --- litellm/proxy/db/exception_handler.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/db/exception_handler.py b/litellm/proxy/db/exception_handler.py index 315811103d..db73f9e9c9 100644 --- a/litellm/proxy/db/exception_handler.py +++ b/litellm/proxy/db/exception_handler.py @@ -1,8 +1,11 @@ +from typing import Union + from litellm.proxy._types import ( DB_CONNECTION_ERROR_TYPES, ProxyErrorTypes, ProxyException, ) +from litellm.secret_managers.main import str_to_bool class PrismaDBExceptionHandler: @@ -17,7 +20,12 @@ class PrismaDBExceptionHandler: """ from litellm.proxy.proxy_server import general_settings - if general_settings.get("allow_requests_on_db_unavailable", False) is True: + _allow_requests_on_db_unavailable: Union[bool, str] = general_settings.get( + "allow_requests_on_db_unavailable", False + ) + if isinstance(_allow_requests_on_db_unavailable, bool): + return _allow_requests_on_db_unavailable + if str_to_bool(_allow_requests_on_db_unavailable) is True: return True return False @@ -35,3 +43,19 @@ class PrismaDBExceptionHandler: if isinstance(e, ProxyException) and e.type == ProxyErrorTypes.no_db_connection: return True return False + + @staticmethod + def handle_db_exception(e: Exception): + """ + Primary handler for `allow_requests_on_db_unavailable` flag. Decides whether to raise a DB Exception or not based on the flag. + + - If exception is a DB Connection Error, and `allow_requests_on_db_unavailable` is True, + - Do not raise an exception, return None + - Else, raise the exception + """ + if ( + PrismaDBExceptionHandler.is_database_connection_error(e) + and PrismaDBExceptionHandler.should_allow_request_on_db_unavailable() + ): + return None + raise e From 88ef97b9d1d9b2ecad5b9258ff980930ff694033 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 19:50:57 -0700 Subject: [PATCH 063/208] allow proxy to startup on DB unavailable --- .../health_endpoints/_health_endpoints.py | 24 ++++--- litellm/proxy/proxy_server.py | 68 ++++++++++--------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 34e7d34bbf..9de845397a 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -20,6 +20,7 @@ from litellm.proxy._types import ( WebhookEvent, ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.proxy.health_check import ( _clean_endpoint_data, _update_litellm_params_for_health_check, @@ -381,20 +382,23 @@ async def _db_health_readiness_check(): global db_health_cache # Note - Intentionally don't try/except this so it raises an exception when it fails + try: + # if timedelta is less than 2 minutes return DB Status + time_diff = datetime.now() - db_health_cache["last_updated"] + if db_health_cache["status"] != "unknown" and time_diff < timedelta(minutes=2): + return db_health_cache - # if timedelta is less than 2 minutes return DB Status - time_diff = datetime.now() - db_health_cache["last_updated"] - if db_health_cache["status"] != "unknown" and time_diff < timedelta(minutes=2): + if prisma_client is None: + db_health_cache = {"status": "disconnected", "last_updated": datetime.now()} + return db_health_cache + + await prisma_client.health_check() + db_health_cache = {"status": "connected", "last_updated": datetime.now()} return db_health_cache - - if prisma_client is None: - db_health_cache = {"status": "disconnected", "last_updated": datetime.now()} + except Exception as e: + PrismaDBExceptionHandler.handle_db_exception(e) return db_health_cache - await prisma_client.health_check() - db_health_cache = {"status": "connected", "last_updated": datetime.now()} - return db_health_cache - @router.get( "/settings", diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index eefc0bdd06..0e04e9f213 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -176,6 +176,7 @@ from litellm.proxy.common_utils.proxy_state import ProxyState from litellm.proxy.common_utils.reset_budget_job import ResetBudgetJob from litellm.proxy.common_utils.swagger_utils import ERROR_RESPONSES from litellm.proxy.credential_endpoints.endpoints import router as credential_router +from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.proxy.fine_tuning_endpoints.endpoints import router as fine_tuning_router from litellm.proxy.fine_tuning_endpoints.endpoints import set_fine_tuning_config from litellm.proxy.guardrails.guardrail_endpoints import router as guardrails_router @@ -456,15 +457,6 @@ async def proxy_startup_event(app: FastAPI): ### LOAD MASTER KEY ### # check if master key set in environment - load from there master_key = get_secret("LITELLM_MASTER_KEY", None) # type: ignore - # check if DATABASE_URL in environment - load from there - if prisma_client is None: - _db_url: Optional[str] = get_secret("DATABASE_URL", None) # type: ignore - prisma_client = await ProxyStartupEvent._setup_prisma_client( - database_url=_db_url, - proxy_logging_obj=proxy_logging_obj, - user_api_key_cache=user_api_key_cache, - ) - ## CHECK PREMIUM USER verbose_proxy_logger.debug( "litellm.proxy.proxy_server.py::startup() - CHECKING PREMIUM USER - {}".format( @@ -527,6 +519,15 @@ async def proxy_startup_event(app: FastAPI): redis_usage_cache=redis_usage_cache, ) + # check if DATABASE_URL in environment - load from there + if prisma_client is None: + _db_url: Optional[str] = get_secret("DATABASE_URL", None) # type: ignore + prisma_client = await ProxyStartupEvent._setup_prisma_client( + database_url=_db_url, + proxy_logging_obj=proxy_logging_obj, + user_api_key_cache=user_api_key_cache, + ) + ## JWT AUTH ## ProxyStartupEvent._initialize_jwt_auth( general_settings=general_settings, @@ -3362,33 +3363,36 @@ class ProxyStartupEvent: - Sets up prisma client - Adds necessary views to proxy """ - prisma_client: Optional[PrismaClient] = None - if database_url is not None: - try: - prisma_client = PrismaClient( - database_url=database_url, proxy_logging_obj=proxy_logging_obj - ) - except Exception as e: - raise e + try: + prisma_client: Optional[PrismaClient] = None + if database_url is not None: + try: + prisma_client = PrismaClient( + database_url=database_url, proxy_logging_obj=proxy_logging_obj + ) + except Exception as e: + raise e - await prisma_client.connect() + await prisma_client.connect() - ## Add necessary views to proxy ## - asyncio.create_task( - prisma_client.check_view_exists() - ) # check if all necessary views exist. Don't block execution + ## Add necessary views to proxy ## + asyncio.create_task( + prisma_client.check_view_exists() + ) # check if all necessary views exist. Don't block execution - asyncio.create_task( - prisma_client._set_spend_logs_row_count_in_proxy_state() - ) # set the spend logs row count in proxy state. Don't block execution + asyncio.create_task( + prisma_client._set_spend_logs_row_count_in_proxy_state() + ) # set the spend logs row count in proxy state. Don't block execution - # run a health check to ensure the DB is ready - if ( - get_secret_bool("DISABLE_PRISMA_HEALTH_CHECK_ON_STARTUP", False) - is not True - ): - await prisma_client.health_check() - return prisma_client + # run a health check to ensure the DB is ready + if ( + get_secret_bool("DISABLE_PRISMA_HEALTH_CHECK_ON_STARTUP", False) + is not True + ): + await prisma_client.health_check() + return prisma_client + except Exception as e: + PrismaDBExceptionHandler.handle_db_exception(e) @classmethod def _init_dd_tracer(cls): From 15c04da73599316e4fa929787fd272fed49b5cda Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 20:00:10 -0700 Subject: [PATCH 064/208] refactor tests --- litellm/proxy/proxy_server.py | 1 + .../proxy/auth/test_auth_exception_handler.py | 72 ------------ .../proxy/db/test_exception_handler.py | 109 ++++++++++++++++++ 3 files changed, 110 insertions(+), 72 deletions(-) create mode 100644 tests/litellm/proxy/db/test_exception_handler.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0e04e9f213..59b4bc8fe3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3393,6 +3393,7 @@ class ProxyStartupEvent: return prisma_client except Exception as e: PrismaDBExceptionHandler.handle_db_exception(e) + return None @classmethod def _init_dd_tracer(cls): diff --git a/tests/litellm/proxy/auth/test_auth_exception_handler.py b/tests/litellm/proxy/auth/test_auth_exception_handler.py index b44de86e05..224bf24b57 100644 --- a/tests/litellm/proxy/auth/test_auth_exception_handler.py +++ b/tests/litellm/proxy/auth/test_auth_exception_handler.py @@ -29,78 +29,6 @@ from litellm.proxy._types import ProxyErrorTypes, ProxyException from litellm.proxy.auth.auth_exception_handler import UserAPIKeyAuthExceptionHandler -# Test is_database_connection_error method -@pytest.mark.parametrize( - "prisma_error", - [ - PrismaError(), - DataError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), - UniqueViolationError( - data={"user_facing_error": {"meta": {"table": "test_table"}}} - ), - ForeignKeyViolationError( - data={"user_facing_error": {"meta": {"table": "test_table"}}} - ), - MissingRequiredValueError( - data={"user_facing_error": {"meta": {"table": "test_table"}}} - ), - RawQueryError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), - TableNotFoundError( - data={"user_facing_error": {"meta": {"table": "test_table"}}} - ), - RecordNotFoundError( - data={"user_facing_error": {"meta": {"table": "test_table"}}} - ), - HTTPClientClosedError(), - ClientNotConnectedError(), - ], -) -def test_is_database_connection_error_prisma_errors(prisma_error): - """ - Test that all Prisma errors are considered database connection errors - """ - handler = UserAPIKeyAuthExceptionHandler() - assert handler.is_database_connection_error(prisma_error) == True - - -def test_is_database_connection_generic_errors(): - """ - Test non-Prisma error cases for database connection checking - """ - handler = UserAPIKeyAuthExceptionHandler() - - # Test with ProxyException (DB connection) - db_proxy_exception = ProxyException( - message="DB Connection Error", - type=ProxyErrorTypes.no_db_connection, - param="test-param", - ) - assert handler.is_database_connection_error(db_proxy_exception) == True - - # Test with non-DB error - regular_exception = Exception("Regular error") - assert handler.is_database_connection_error(regular_exception) == False - - -# Test should_allow_request_on_db_unavailable method -@patch( - "litellm.proxy.proxy_server.general_settings", - {"allow_requests_on_db_unavailable": True}, -) -def test_should_allow_request_on_db_unavailable_true(): - handler = UserAPIKeyAuthExceptionHandler() - assert handler.should_allow_request_on_db_unavailable() == True - - -@patch( - "litellm.proxy.proxy_server.general_settings", - {"allow_requests_on_db_unavailable": False}, -) -def test_should_allow_request_on_db_unavailable_false(): - handler = UserAPIKeyAuthExceptionHandler() - assert handler.should_allow_request_on_db_unavailable() == False - - @pytest.mark.asyncio @pytest.mark.parametrize( "prisma_error", diff --git a/tests/litellm/proxy/db/test_exception_handler.py b/tests/litellm/proxy/db/test_exception_handler.py new file mode 100644 index 0000000000..04f17ff19b --- /dev/null +++ b/tests/litellm/proxy/db/test_exception_handler.py @@ -0,0 +1,109 @@ +import asyncio +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest +from fastapi import HTTPException, Request, status +from prisma import errors as prisma_errors +from prisma.errors import ( + ClientNotConnectedError, + DataError, + ForeignKeyViolationError, + HTTPClientClosedError, + MissingRequiredValueError, + PrismaError, + RawQueryError, + RecordNotFoundError, + TableNotFoundError, + UniqueViolationError, +) + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import ProxyErrorTypes, ProxyException +from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler + + +# Test is_database_connection_error method +@pytest.mark.parametrize( + "prisma_error", + [ + PrismaError(), + DataError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), + UniqueViolationError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + ForeignKeyViolationError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + MissingRequiredValueError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + RawQueryError(data={"user_facing_error": {"meta": {"table": "test_table"}}}), + TableNotFoundError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + RecordNotFoundError( + data={"user_facing_error": {"meta": {"table": "test_table"}}} + ), + HTTPClientClosedError(), + ClientNotConnectedError(), + ], +) +def test_is_database_connection_error_prisma_errors(prisma_error): + """ + Test that all Prisma errors are considered database connection errors + """ + assert PrismaDBExceptionHandler.is_database_connection_error(prisma_error) == True + + +def test_is_database_connection_generic_errors(): + """ + Test non-Prisma error cases for database connection checking + """ + assert ( + PrismaDBExceptionHandler.is_database_connection_error( + Exception("Regular error") + ) + == False + ) + + # Test with ProxyException (DB connection) + db_proxy_exception = ProxyException( + message="DB Connection Error", + type=ProxyErrorTypes.no_db_connection, + param="test-param", + ) + assert ( + PrismaDBExceptionHandler.is_database_connection_error(db_proxy_exception) + == True + ) + + # Test with non-DB error + regular_exception = Exception("Regular error") + assert ( + PrismaDBExceptionHandler.is_database_connection_error(regular_exception) + == False + ) + + +# Test should_allow_request_on_db_unavailable method +@patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": True}, +) +def test_should_allow_request_on_db_unavailable_true(): + assert PrismaDBExceptionHandler.should_allow_request_on_db_unavailable() == True + + +@patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": False}, +) +def test_should_allow_request_on_db_unavailable_false(): + assert PrismaDBExceptionHandler.should_allow_request_on_db_unavailable() == False From 427580eff512fbdafec90157d443c4d1de63332d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 20:35:27 -0700 Subject: [PATCH 065/208] fix _setup_prisma_client --- litellm/proxy/db/exception_handler.py | 5 +++-- litellm/proxy/proxy_server.py | 17 ++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/litellm/proxy/db/exception_handler.py b/litellm/proxy/db/exception_handler.py index db73f9e9c9..79229f1e1b 100644 --- a/litellm/proxy/db/exception_handler.py +++ b/litellm/proxy/db/exception_handler.py @@ -18,9 +18,10 @@ class PrismaDBExceptionHandler: """ Returns True if the request should be allowed to proceed despite the DB connection error """ - from litellm.proxy.proxy_server import general_settings + from litellm.proxy.proxy_server import proxy_config - _allow_requests_on_db_unavailable: Union[bool, str] = general_settings.get( + _general_settings = proxy_config.config + _allow_requests_on_db_unavailable: Union[bool, str] = _general_settings.get( "allow_requests_on_db_unavailable", False ) if isinstance(_allow_requests_on_db_unavailable, bool): diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 59b4bc8fe3..67d88b9d32 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -457,6 +457,14 @@ async def proxy_startup_event(app: FastAPI): ### LOAD MASTER KEY ### # check if master key set in environment - load from there master_key = get_secret("LITELLM_MASTER_KEY", None) # type: ignore + # check if DATABASE_URL in environment - load from there + if prisma_client is None: + _db_url: Optional[str] = get_secret("DATABASE_URL", None) # type: ignore + prisma_client = await ProxyStartupEvent._setup_prisma_client( + database_url=_db_url, + proxy_logging_obj=proxy_logging_obj, + user_api_key_cache=user_api_key_cache, + ) ## CHECK PREMIUM USER verbose_proxy_logger.debug( "litellm.proxy.proxy_server.py::startup() - CHECKING PREMIUM USER - {}".format( @@ -519,15 +527,6 @@ async def proxy_startup_event(app: FastAPI): redis_usage_cache=redis_usage_cache, ) - # check if DATABASE_URL in environment - load from there - if prisma_client is None: - _db_url: Optional[str] = get_secret("DATABASE_URL", None) # type: ignore - prisma_client = await ProxyStartupEvent._setup_prisma_client( - database_url=_db_url, - proxy_logging_obj=proxy_logging_obj, - user_api_key_cache=user_api_key_cache, - ) - ## JWT AUTH ## ProxyStartupEvent._initialize_jwt_auth( general_settings=general_settings, From 34e58be36dcf16d7d139b0b51601f6be18df0a24 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 20:42:01 -0700 Subject: [PATCH 066/208] fix order of _setup_prisma_client --- litellm/proxy/db/exception_handler.py | 5 ++--- litellm/proxy/proxy_server.py | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/litellm/proxy/db/exception_handler.py b/litellm/proxy/db/exception_handler.py index 79229f1e1b..db73f9e9c9 100644 --- a/litellm/proxy/db/exception_handler.py +++ b/litellm/proxy/db/exception_handler.py @@ -18,10 +18,9 @@ class PrismaDBExceptionHandler: """ Returns True if the request should be allowed to proceed despite the DB connection error """ - from litellm.proxy.proxy_server import proxy_config + from litellm.proxy.proxy_server import general_settings - _general_settings = proxy_config.config - _allow_requests_on_db_unavailable: Union[bool, str] = _general_settings.get( + _allow_requests_on_db_unavailable: Union[bool, str] = general_settings.get( "allow_requests_on_db_unavailable", False ) if isinstance(_allow_requests_on_db_unavailable, bool): diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 67d88b9d32..c2044ac1d6 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -454,17 +454,6 @@ async def proxy_startup_event(app: FastAPI): import json init_verbose_loggers() - ### LOAD MASTER KEY ### - # check if master key set in environment - load from there - master_key = get_secret("LITELLM_MASTER_KEY", None) # type: ignore - # check if DATABASE_URL in environment - load from there - if prisma_client is None: - _db_url: Optional[str] = get_secret("DATABASE_URL", None) # type: ignore - prisma_client = await ProxyStartupEvent._setup_prisma_client( - database_url=_db_url, - proxy_logging_obj=proxy_logging_obj, - user_api_key_cache=user_api_key_cache, - ) ## CHECK PREMIUM USER verbose_proxy_logger.debug( "litellm.proxy.proxy_server.py::startup() - CHECKING PREMIUM USER - {}".format( @@ -521,6 +510,18 @@ async def proxy_startup_event(app: FastAPI): if isinstance(worker_config, dict): await initialize(**worker_config) + ### LOAD MASTER KEY ### + # check if master key set in environment - load from there + master_key = get_secret("LITELLM_MASTER_KEY", None) # type: ignore + # check if DATABASE_URL in environment - load from there + if prisma_client is None: + _db_url: Optional[str] = get_secret("DATABASE_URL", None) # type: ignore + prisma_client = await ProxyStartupEvent._setup_prisma_client( + database_url=_db_url, + proxy_logging_obj=proxy_logging_obj, + user_api_key_cache=user_api_key_cache, + ) + ProxyStartupEvent._initialize_startup_logging( llm_router=llm_router, proxy_logging_obj=proxy_logging_obj, From 87f0201f8400bee3550e1c82befccfc637fc4436 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 20:44:25 -0700 Subject: [PATCH 067/208] test_handle_db_exception_with_connection_error --- .../proxy/db/test_exception_handler.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/litellm/proxy/db/test_exception_handler.py b/tests/litellm/proxy/db/test_exception_handler.py index 04f17ff19b..e68c9b6a99 100644 --- a/tests/litellm/proxy/db/test_exception_handler.py +++ b/tests/litellm/proxy/db/test_exception_handler.py @@ -24,6 +24,7 @@ sys.path.insert( 0, os.path.abspath("../../..") ) # Adds the parent directory to the system path +import litellm from litellm._logging import verbose_proxy_logger from litellm.proxy._types import ProxyErrorTypes, ProxyException from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler @@ -107,3 +108,41 @@ def test_should_allow_request_on_db_unavailable_true(): ) def test_should_allow_request_on_db_unavailable_false(): assert PrismaDBExceptionHandler.should_allow_request_on_db_unavailable() == False + + +@patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": True}, +) +def test_handle_db_exception_with_connection_error(): + """ + Test that DB connection errors are handled gracefully when allow_requests_on_db_unavailable is True + """ + db_error = ClientNotConnectedError() + result = PrismaDBExceptionHandler.handle_db_exception(db_error) + assert result is None + + +@patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": False}, +) +def test_handle_db_exception_raises_error(): + """ + Test that DB connection errors are raised when allow_requests_on_db_unavailable is False + """ + db_error = ClientNotConnectedError() + with pytest.raises(ClientNotConnectedError): + PrismaDBExceptionHandler.handle_db_exception(db_error) + + +def test_handle_db_exception_with_non_db_error(): + """ + Test that non-DB errors are always raised regardless of allow_requests_on_db_unavailable setting + """ + regular_error = litellm.BudgetExceededError( + current_cost=10, + max_budget=10, + ) + with pytest.raises(litellm.BudgetExceededError): + PrismaDBExceptionHandler.handle_db_exception(regular_error) From b6506f7bda5bb2935c97e6ca2b75e306f2c7111f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 20:56:39 -0700 Subject: [PATCH 068/208] test_db_health_readiness_check_with_prisma_error --- .../health_endpoints/test_health_endpoints.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/litellm/proxy/health_endpoints/test_health_endpoints.py diff --git a/tests/litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/litellm/proxy/health_endpoints/test_health_endpoints.py new file mode 100644 index 0000000000..e2dd429357 --- /dev/null +++ b/tests/litellm/proxy/health_endpoints/test_health_endpoints.py @@ -0,0 +1,101 @@ +import asyncio +import json +import os +import sys +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +import pytest +from prisma.errors import ClientNotConnectedError, HTTPClientClosedError, PrismaError + +from litellm.proxy._types import ProxyErrorTypes, ProxyException +from litellm.proxy.health_endpoints._health_endpoints import ( + _db_health_readiness_check, + db_health_cache, +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "prisma_error", + [ + PrismaError(), + ClientNotConnectedError(), + HTTPClientClosedError(), + ], +) +async def test_db_health_readiness_check_with_prisma_error(prisma_error): + """ + Test that when prisma_client.health_check() raises a PrismaError and + allow_requests_on_db_unavailable is True, the function should not raise an error + and return the cached health status. + """ + # Mock the prisma client + mock_prisma_client = MagicMock() + mock_prisma_client.health_check.side_effect = prisma_error + + # Reset the health cache to a known state + global db_health_cache + db_health_cache = { + "status": "unknown", + "last_updated": datetime.now() - timedelta(minutes=5), + } + + # Patch the imports and general_settings + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": True}, + ): + + # Call the function + result = await _db_health_readiness_check() + + # Verify that the function called health_check + mock_prisma_client.health_check.assert_called_once() + + # Verify that the function returned the cache + assert result is not None + assert result["status"] == "unknown" # Should retain the status from the cache + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "prisma_error", + [ + PrismaError(), + ClientNotConnectedError(), + HTTPClientClosedError(), + ], +) +async def test_db_health_readiness_check_with_error_and_flag_off(prisma_error): + """ + Test that when prisma_client.health_check() raises a DB error but + allow_requests_on_db_unavailable is False, the exception should be raised. + """ + # Mock the prisma client + mock_prisma_client = MagicMock() + mock_prisma_client.health_check.side_effect = prisma_error + + # Reset the health cache + global db_health_cache + db_health_cache = { + "status": "unknown", + "last_updated": datetime.now() - timedelta(minutes=5), + } + + # Patch the imports and general_settings where the flag is False + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.general_settings", + {"allow_requests_on_db_unavailable": False}, + ): + + # The function should raise the exception + with pytest.raises(Exception) as excinfo: + await _db_health_readiness_check() + + # Verify that the raised exception is the same + assert excinfo.value == prisma_error From 05c38049feebd8cc113a361b3834331910dcb6d8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 26 Mar 2025 21:04:36 -0700 Subject: [PATCH 069/208] docs prod.md --- docs/my-website/docs/proxy/prod.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/my-website/docs/proxy/prod.md b/docs/my-website/docs/proxy/prod.md index 314300f2a0..1c1cbeedb4 100644 --- a/docs/my-website/docs/proxy/prod.md +++ b/docs/my-website/docs/proxy/prod.md @@ -114,6 +114,8 @@ When `allow_requests_on_db_unavailable` is set to `true`, LiteLLM will handle er |---------------|-------------------|----------------| | Prisma Errors | ✅ Request will be allowed | Covers issues like DB connection resets or rejections from the DB via Prisma, the ORM used by LiteLLM. | | Httpx Errors | ✅ Request will be allowed | Occurs when the database is unreachable, allowing the request to proceed despite the DB outage. | +| Pod Startup Behavior | ✅ Pods start regardless | LiteLLM Pods will start even if the database is down or unreachable, ensuring higher uptime guarantees for deployments. | +| Health/Readiness Check | ✅ Always returns 200 OK | The /health/readiness endpoint returns a 200 OK status to ensure that pods remain operational even when the database is unavailable. | LiteLLM Budget Errors or Model Errors | ❌ Request will be blocked | Triggered when the DB is reachable but the authentication token is invalid, lacks access, or exceeds budget limits. | From 109add7946ced31850466d38405b7b78af7f6bfd Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 26 Mar 2025 23:04:24 -0700 Subject: [PATCH 070/208] build(model_prices_and_context_window.json): add gemini multimodal embedding cost --- .../model_prices_and_context_window_backup.json | 17 +++++++++++++++++ model_prices_and_context_window.json | 17 +++++++++++++++++ tests/local_testing/test_get_model_info.py | 4 ++++ 3 files changed, 38 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 06f7238c01..b8e32a24ce 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -5373,6 +5373,23 @@ "mode": "embedding", "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" }, + "multimodalembedding": { + "max_tokens": 2048, + "max_input_tokens": 2048, + "output_vector_size": 768, + "input_cost_per_character": 0.0000002, + "input_cost_per_image": 0.0001, + "input_cost_per_video_per_second": 0.0005, + "input_cost_per_video_per_second_above_8s_interval": 0.0010, + "input_cost_per_video_per_second_above_15s_interval": 0.0020, + "input_cost_per_token": 0.0000008, + "output_cost_per_token": 0, + "litellm_provider": "vertex_ai-embedding-models", + "mode": "embedding", + "supported_endpoints": ["/v1/embeddings"], + "supported_modalities": ["text", "image", "video"], + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, "text-embedding-large-exp-03-07": { "max_tokens": 8192, "max_input_tokens": 8192, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 06f7238c01..b8e32a24ce 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -5373,6 +5373,23 @@ "mode": "embedding", "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" }, + "multimodalembedding": { + "max_tokens": 2048, + "max_input_tokens": 2048, + "output_vector_size": 768, + "input_cost_per_character": 0.0000002, + "input_cost_per_image": 0.0001, + "input_cost_per_video_per_second": 0.0005, + "input_cost_per_video_per_second_above_8s_interval": 0.0010, + "input_cost_per_video_per_second_above_15s_interval": 0.0020, + "input_cost_per_token": 0.0000008, + "output_cost_per_token": 0, + "litellm_provider": "vertex_ai-embedding-models", + "mode": "embedding", + "supported_endpoints": ["/v1/embeddings"], + "supported_modalities": ["text", "image", "video"], + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, "text-embedding-large-exp-03-07": { "max_tokens": 8192, "max_input_tokens": 8192, diff --git a/tests/local_testing/test_get_model_info.py b/tests/local_testing/test_get_model_info.py index 1a0f6d7a8d..27b9b1a2b6 100644 --- a/tests/local_testing/test_get_model_info.py +++ b/tests/local_testing/test_get_model_info.py @@ -441,6 +441,10 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "input_cost_per_token_batches": {"type": "number"}, "input_cost_per_token_cache_hit": {"type": "number"}, "input_cost_per_video_per_second": {"type": "number"}, + "input_cost_per_video_per_second_above_8s_interval": {"type": "number"}, + "input_cost_per_video_per_second_above_15s_interval": { + "type": "number" + }, "input_cost_per_video_per_second_above_128k_tokens": {"type": "number"}, "input_dbu_cost_per_token": {"type": "number"}, "litellm_provider": {"type": "string"}, From c0845fec1fa4a21758b405930d7568d4336d687f Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 26 Mar 2025 23:10:25 -0700 Subject: [PATCH 071/208] Add OpenAI gpt-4o-transcribe support (#9517) * refactor: introduce new transformation config for gpt-4o-transcribe models * refactor: expose new transformation configs for audio transcription * ci: fix config yml * feat(openai/transcriptions): support provider config transformation on openai audio transcriptions allows gpt-4o and whisper audio transformation to work as expected * refactor: migrate fireworks ai + deepgram to new transform request pattern * feat(openai/): working support for gpt-4o-audio-transcribe * build(model_prices_and_context_window.json): add gpt-4o-transcribe to model cost map * build(model_prices_and_context_window.json): specify what endpoints are supported for `/audio/transcriptions` * fix(get_supported_openai_params.py): fix return * refactor(deepgram/): migrate unit test to deepgram handler * refactor: cleanup unused imports * fix(get_supported_openai_params.py): fix linting error * test: update test --- litellm/__init__.py | 6 ++ .../get_supported_openai_params.py | 16 +++ .../audio_transcription/transformation.py | 16 ++- litellm/llms/custom_httpx/llm_http_handler.py | 67 ++++--------- .../audio_transcription/transformation.py | 52 +++++++++- .../audio_transcription/transformation.py | 19 +--- .../transcriptions/gpt_transformation.py | 34 +++++++ litellm/llms/openai/transcriptions/handler.py | 26 +++-- .../transcriptions/whisper_transformation.py | 97 +++++++++++++++++++ litellm/main.py | 10 ++ ...odel_prices_and_context_window_backup.json | 25 ++++- litellm/proxy/_new_secret_config.yaml | 4 + litellm/types/llms/openai.py | 7 +- litellm/utils.py | 5 + model_prices_and_context_window.json | 25 ++++- ...eek_audio_transcription_transformation.py} | 32 +++--- tests/litellm_utils_tests/test_utils.py | 10 ++ tests/llm_translation/test_openai.py | 11 +++ tests/local_testing/test_get_model_info.py | 2 + tests/local_testing/test_whisper.py | 30 ++++++ 20 files changed, 402 insertions(+), 92 deletions(-) create mode 100644 litellm/llms/openai/transcriptions/gpt_transformation.py create mode 100644 litellm/llms/openai/transcriptions/whisper_transformation.py rename tests/litellm/llms/{custom_httpx/test_handle_audio_file.py => deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py} (60%) diff --git a/litellm/__init__.py b/litellm/__init__.py index a59484b035..8cdde24a6a 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -950,6 +950,12 @@ openaiOSeriesConfig = OpenAIOSeriesConfig() from .llms.openai.chat.gpt_transformation import ( OpenAIGPTConfig, ) +from .llms.openai.transcriptions.whisper_transformation import ( + OpenAIWhisperAudioTranscriptionConfig, +) +from .llms.openai.transcriptions.gpt_transformation import ( + OpenAIGPTAudioTranscriptionConfig, +) openAIGPTConfig = OpenAIGPTConfig() from .llms.openai.chat.gpt_audio_transformation import ( diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 3d4f8cef6f..ccbdb331fd 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -79,6 +79,22 @@ def get_supported_openai_params( # noqa: PLR0915 elif custom_llm_provider == "maritalk": return litellm.MaritalkConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "openai": + if request_type == "transcription": + transcription_provider_config = ( + litellm.ProviderConfigManager.get_provider_audio_transcription_config( + model=model, provider=LlmProviders.OPENAI + ) + ) + if isinstance( + transcription_provider_config, litellm.OpenAIGPTAudioTranscriptionConfig + ): + return transcription_provider_config.get_supported_openai_params( + model=model + ) + else: + raise ValueError( + f"Unsupported provider config: {transcription_provider_config} for model: {model}" + ) return litellm.OpenAIConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "azure": if litellm.AzureOpenAIO1Config().is_o_series_model(model=model): diff --git a/litellm/llms/base_llm/audio_transcription/transformation.py b/litellm/llms/base_llm/audio_transcription/transformation.py index e550c574e2..d48edb041c 100644 --- a/litellm/llms/base_llm/audio_transcription/transformation.py +++ b/litellm/llms/base_llm/audio_transcription/transformation.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Union import httpx @@ -8,7 +8,7 @@ from litellm.types.llms.openai import ( AllMessageValues, OpenAIAudioTranscriptionOptionalParams, ) -from litellm.types.utils import ModelResponse +from litellm.types.utils import FileTypes, ModelResponse if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -42,6 +42,18 @@ class BaseAudioTranscriptionConfig(BaseConfig, ABC): """ return api_base or "" + @abstractmethod + def transform_audio_transcription_request( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + ) -> Union[dict, bytes]: + raise NotImplementedError( + "AudioTranscriptionConfig needs a request transformation for audio transcription models" + ) + def transform_request( self, model: str, diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 00caf55207..872626c747 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -1,4 +1,3 @@ -import io import json from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Tuple, Union @@ -8,6 +7,9 @@ import litellm import litellm.litellm_core_utils import litellm.types import litellm.types.utils +from litellm.llms.base_llm.audio_transcription.transformation import ( + BaseAudioTranscriptionConfig, +) from litellm.llms.base_llm.chat.transformation import BaseConfig from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig @@ -852,54 +854,12 @@ class BaseLLMHTTPHandler: request_data=request_data, ) - def handle_audio_file(self, audio_file: FileTypes) -> bytes: - """ - Processes the audio file input based on its type and returns the binary data. - - Args: - audio_file: Can be a file path (str), a tuple (filename, file_content), or binary data (bytes). - - Returns: - The binary data of the audio file. - """ - binary_data: bytes # Explicitly declare the type - - # Handle the audio file based on type - if isinstance(audio_file, str): - # If it's a file path - with open(audio_file, "rb") as f: - binary_data = f.read() # `f.read()` always returns `bytes` - elif isinstance(audio_file, tuple): - # Handle tuple case - _, file_content = audio_file[:2] - if isinstance(file_content, str): - with open(file_content, "rb") as f: - binary_data = f.read() # `f.read()` always returns `bytes` - elif isinstance(file_content, bytes): - binary_data = file_content - else: - raise TypeError( - f"Unexpected type in tuple: {type(file_content)}. Expected str or bytes." - ) - elif isinstance(audio_file, bytes): - # Assume it's already binary data - binary_data = audio_file - elif isinstance(audio_file, io.BufferedReader) or isinstance( - audio_file, io.BytesIO - ): - # Handle file-like objects - binary_data = audio_file.read() - - else: - raise TypeError(f"Unsupported type for audio_file: {type(audio_file)}") - - return binary_data - def audio_transcriptions( self, model: str, audio_file: FileTypes, optional_params: dict, + litellm_params: dict, model_response: TranscriptionResponse, timeout: float, max_retries: int, @@ -910,11 +870,8 @@ class BaseLLMHTTPHandler: client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, atranscription: bool = False, headers: dict = {}, - litellm_params: dict = {}, + provider_config: Optional[BaseAudioTranscriptionConfig] = None, ) -> TranscriptionResponse: - provider_config = ProviderConfigManager.get_provider_audio_transcription_config( - model=model, provider=litellm.LlmProviders(custom_llm_provider) - ) if provider_config is None: raise ValueError( f"No provider config found for model: {model} and provider: {custom_llm_provider}" @@ -938,7 +895,18 @@ class BaseLLMHTTPHandler: ) # Handle the audio file based on type - binary_data = self.handle_audio_file(audio_file) + data = provider_config.transform_audio_transcription_request( + model=model, + audio_file=audio_file, + optional_params=optional_params, + litellm_params=litellm_params, + ) + binary_data: Optional[bytes] = None + json_data: Optional[dict] = None + if isinstance(data, bytes): + binary_data = data + else: + json_data = data try: # Make the POST request @@ -946,6 +914,7 @@ class BaseLLMHTTPHandler: url=complete_url, headers=headers, content=binary_data, + json=json_data, timeout=timeout, ) except Exception as e: diff --git a/litellm/llms/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index 06296736ea..90720a77f7 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -2,6 +2,7 @@ Translates from OpenAI's `/v1/audio/transcriptions` to Deepgram's `/v1/listen` """ +import io from typing import List, Optional, Union from httpx import Headers, Response @@ -12,7 +13,7 @@ from litellm.types.llms.openai import ( AllMessageValues, OpenAIAudioTranscriptionOptionalParams, ) -from litellm.types.utils import TranscriptionResponse +from litellm.types.utils import FileTypes, TranscriptionResponse from ...base_llm.audio_transcription.transformation import ( BaseAudioTranscriptionConfig, @@ -47,6 +48,55 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig): message=error_message, status_code=status_code, headers=headers ) + def transform_audio_transcription_request( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + ) -> Union[dict, bytes]: + """ + Processes the audio file input based on its type and returns the binary data. + + Args: + audio_file: Can be a file path (str), a tuple (filename, file_content), or binary data (bytes). + + Returns: + The binary data of the audio file. + """ + binary_data: bytes # Explicitly declare the type + + # Handle the audio file based on type + if isinstance(audio_file, str): + # If it's a file path + with open(audio_file, "rb") as f: + binary_data = f.read() # `f.read()` always returns `bytes` + elif isinstance(audio_file, tuple): + # Handle tuple case + _, file_content = audio_file[:2] + if isinstance(file_content, str): + with open(file_content, "rb") as f: + binary_data = f.read() # `f.read()` always returns `bytes` + elif isinstance(file_content, bytes): + binary_data = file_content + else: + raise TypeError( + f"Unexpected type in tuple: {type(file_content)}. Expected str or bytes." + ) + elif isinstance(audio_file, bytes): + # Assume it's already binary data + binary_data = audio_file + elif isinstance(audio_file, io.BufferedReader) or isinstance( + audio_file, io.BytesIO + ): + # Handle file-like objects + binary_data = audio_file.read() + + else: + raise TypeError(f"Unsupported type for audio_file: {type(audio_file)}") + + return binary_data + def transform_audio_transcription_response( self, model: str, diff --git a/litellm/llms/fireworks_ai/audio_transcription/transformation.py b/litellm/llms/fireworks_ai/audio_transcription/transformation.py index 8f35705299..00bb5f2679 100644 --- a/litellm/llms/fireworks_ai/audio_transcription/transformation.py +++ b/litellm/llms/fireworks_ai/audio_transcription/transformation.py @@ -2,27 +2,16 @@ from typing import List from litellm.types.llms.openai import OpenAIAudioTranscriptionOptionalParams -from ...base_llm.audio_transcription.transformation import BaseAudioTranscriptionConfig +from ...openai.transcriptions.whisper_transformation import ( + OpenAIWhisperAudioTranscriptionConfig, +) from ..common_utils import FireworksAIMixin class FireworksAIAudioTranscriptionConfig( - FireworksAIMixin, BaseAudioTranscriptionConfig + FireworksAIMixin, OpenAIWhisperAudioTranscriptionConfig ): def get_supported_openai_params( self, model: str ) -> List[OpenAIAudioTranscriptionOptionalParams]: return ["language", "prompt", "response_format", "timestamp_granularities"] - - def map_openai_params( - self, - non_default_params: dict, - optional_params: dict, - model: str, - drop_params: bool, - ) -> dict: - supported_params = self.get_supported_openai_params(model) - for k, v in non_default_params.items(): - if k in supported_params: - optional_params[k] = v - return optional_params diff --git a/litellm/llms/openai/transcriptions/gpt_transformation.py b/litellm/llms/openai/transcriptions/gpt_transformation.py new file mode 100644 index 0000000000..796e10f515 --- /dev/null +++ b/litellm/llms/openai/transcriptions/gpt_transformation.py @@ -0,0 +1,34 @@ +from typing import List + +from litellm.types.llms.openai import OpenAIAudioTranscriptionOptionalParams +from litellm.types.utils import FileTypes + +from .whisper_transformation import OpenAIWhisperAudioTranscriptionConfig + + +class OpenAIGPTAudioTranscriptionConfig(OpenAIWhisperAudioTranscriptionConfig): + def get_supported_openai_params( + self, model: str + ) -> List[OpenAIAudioTranscriptionOptionalParams]: + """ + Get the supported OpenAI params for the `gpt-4o-transcribe` models + """ + return [ + "language", + "prompt", + "response_format", + "temperature", + "include", + ] + + def transform_audio_transcription_request( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + ) -> dict: + """ + Transform the audio transcription request + """ + return {"model": model, "file": audio_file, **optional_params} diff --git a/litellm/llms/openai/transcriptions/handler.py b/litellm/llms/openai/transcriptions/handler.py index d9dd3c123b..78a913cbf3 100644 --- a/litellm/llms/openai/transcriptions/handler.py +++ b/litellm/llms/openai/transcriptions/handler.py @@ -7,6 +7,9 @@ from pydantic import BaseModel import litellm from litellm.litellm_core_utils.audio_utils.utils import get_audio_file_name from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.audio_transcription.transformation import ( + BaseAudioTranscriptionConfig, +) from litellm.types.utils import FileTypes from litellm.utils import ( TranscriptionResponse, @@ -75,6 +78,7 @@ class OpenAIAudioTranscription(OpenAIChatCompletion): model: str, audio_file: FileTypes, optional_params: dict, + litellm_params: dict, model_response: TranscriptionResponse, timeout: float, max_retries: int, @@ -83,16 +87,24 @@ class OpenAIAudioTranscription(OpenAIChatCompletion): api_base: Optional[str], client=None, atranscription: bool = False, + provider_config: Optional[BaseAudioTranscriptionConfig] = None, ) -> TranscriptionResponse: - data = {"model": model, "file": audio_file, **optional_params} - - if "response_format" not in data or ( - data["response_format"] == "text" or data["response_format"] == "json" - ): - data["response_format"] = ( - "verbose_json" # ensures 'duration' is received - used for cost calculation + """ + Handle audio transcription request + """ + if provider_config is not None: + data = provider_config.transform_audio_transcription_request( + model=model, + audio_file=audio_file, + optional_params=optional_params, + litellm_params=litellm_params, ) + if isinstance(data, bytes): + raise ValueError("OpenAI transformation route requires a dict") + else: + data = {"model": model, "file": audio_file, **optional_params} + if atranscription is True: return self.async_audio_transcriptions( # type: ignore audio_file=audio_file, diff --git a/litellm/llms/openai/transcriptions/whisper_transformation.py b/litellm/llms/openai/transcriptions/whisper_transformation.py new file mode 100644 index 0000000000..5a7d6481a8 --- /dev/null +++ b/litellm/llms/openai/transcriptions/whisper_transformation.py @@ -0,0 +1,97 @@ +from typing import List, Optional, Union + +from httpx import Headers + +from litellm.llms.base_llm.audio_transcription.transformation import ( + BaseAudioTranscriptionConfig, +) +from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ( + AllMessageValues, + OpenAIAudioTranscriptionOptionalParams, +) +from litellm.types.utils import FileTypes + +from ..common_utils import OpenAIError + + +class OpenAIWhisperAudioTranscriptionConfig(BaseAudioTranscriptionConfig): + def get_supported_openai_params( + self, model: str + ) -> List[OpenAIAudioTranscriptionOptionalParams]: + """ + Get the supported OpenAI params for the `whisper-1` models + """ + return [ + "language", + "prompt", + "response_format", + "temperature", + "timestamp_granularities", + ] + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + """ + Map the OpenAI params to the Whisper params + """ + supported_params = self.get_supported_openai_params(model) + for k, v in non_default_params.items(): + if k in supported_params: + optional_params[k] = v + return optional_params + + def validate_environment( + self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + api_key = api_key or get_secret_str("OPENAI_API_KEY") + + auth_header = { + "Authorization": f"Bearer {api_key}", + } + + headers.update(auth_header) + return headers + + def transform_audio_transcription_request( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + litellm_params: dict, + ) -> dict: + """ + Transform the audio transcription request + """ + + data = {"model": model, "file": audio_file, **optional_params} + + if "response_format" not in data or ( + data["response_format"] == "text" or data["response_format"] == "json" + ): + data["response_format"] = ( + "verbose_json" # ensures 'duration' is received - used for cost calculation + ) + + return data + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, Headers] + ) -> BaseLLMException: + return OpenAIError( + status_code=status_code, + message=error_message, + headers=headers, + ) diff --git a/litellm/main.py b/litellm/main.py index 94e19aab0c..b2732c165c 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -5095,6 +5095,12 @@ def transcription( response: Optional[ Union[TranscriptionResponse, Coroutine[Any, Any, TranscriptionResponse]] ] = None + + provider_config = ProviderConfigManager.get_provider_audio_transcription_config( + model=model, + provider=LlmProviders(custom_llm_provider), + ) + if custom_llm_provider == "azure": # azure configs api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") @@ -5161,12 +5167,15 @@ def transcription( max_retries=max_retries, api_base=api_base, api_key=api_key, + provider_config=provider_config, + litellm_params=litellm_params_dict, ) elif custom_llm_provider == "deepgram": response = base_llm_http_handler.audio_transcriptions( model=model, audio_file=file, optional_params=optional_params, + litellm_params=litellm_params_dict, model_response=model_response, atranscription=atranscription, client=( @@ -5185,6 +5194,7 @@ def transcription( api_key=api_key, custom_llm_provider="deepgram", headers={}, + provider_config=provider_config, ) if response is None: raise ValueError("Unmapped provider passed in. Unable to get the response.") diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b8e32a24ce..da780f8353 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -1176,21 +1176,40 @@ "output_cost_per_pixel": 0.0, "litellm_provider": "openai" }, + "gpt-4o-transcribe": { + "mode": "audio_transcription", + "input_cost_per_token": 0.0000025, + "input_cost_per_audio_token": 0.000006, + "output_cost_per_token": 0.00001, + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/transcriptions"] + }, + "gpt-4o-mini-transcribe": { + "mode": "audio_transcription", + "input_cost_per_token": 0.00000125, + "input_cost_per_audio_token": 0.000003, + "output_cost_per_token": 0.000005, + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/transcriptions"] + }, "whisper-1": { "mode": "audio_transcription", "input_cost_per_second": 0.0001, "output_cost_per_second": 0.0001, - "litellm_provider": "openai" + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/transcriptions"] }, "tts-1": { "mode": "audio_speech", "input_cost_per_character": 0.000015, - "litellm_provider": "openai" + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/speech"] }, "tts-1-hd": { "mode": "audio_speech", "input_cost_per_character": 0.000030, - "litellm_provider": "openai" + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/speech"] }, "azure/gpt-4o-mini-realtime-preview-2024-12-17": { "max_tokens": 4096, diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index 09db9f10ee..b9a5765ee4 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -9,6 +9,10 @@ model_list: litellm_params: model: gpt-4o-mini api_key: os.environ/OPENAI_API_KEY + - model_name: "openai/*" + litellm_params: + model: openai/* + api_key: os.environ/OPENAI_API_KEY - model_name: "bedrock-nova" litellm_params: model: us.amazon.nova-pro-v1:0 diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 19899648f5..1c5552637c 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -779,7 +779,12 @@ class LiteLLMFineTuningJobCreate(FineTuningJobCreate): AllEmbeddingInputValues = Union[str, List[str], List[int], List[List[int]]] OpenAIAudioTranscriptionOptionalParams = Literal[ - "language", "prompt", "temperature", "response_format", "timestamp_granularities" + "language", + "prompt", + "temperature", + "response_format", + "timestamp_granularities", + "include", ] diff --git a/litellm/utils.py b/litellm/utils.py index dc97c4d898..3fcb4a803a 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6364,6 +6364,11 @@ class ProviderConfigManager: return litellm.FireworksAIAudioTranscriptionConfig() elif litellm.LlmProviders.DEEPGRAM == provider: return litellm.DeepgramAudioTranscriptionConfig() + elif litellm.LlmProviders.OPENAI == provider: + if "gpt-4o" in model: + return litellm.OpenAIGPTAudioTranscriptionConfig() + else: + return litellm.OpenAIWhisperAudioTranscriptionConfig() return None @staticmethod diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b8e32a24ce..da780f8353 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -1176,21 +1176,40 @@ "output_cost_per_pixel": 0.0, "litellm_provider": "openai" }, + "gpt-4o-transcribe": { + "mode": "audio_transcription", + "input_cost_per_token": 0.0000025, + "input_cost_per_audio_token": 0.000006, + "output_cost_per_token": 0.00001, + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/transcriptions"] + }, + "gpt-4o-mini-transcribe": { + "mode": "audio_transcription", + "input_cost_per_token": 0.00000125, + "input_cost_per_audio_token": 0.000003, + "output_cost_per_token": 0.000005, + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/transcriptions"] + }, "whisper-1": { "mode": "audio_transcription", "input_cost_per_second": 0.0001, "output_cost_per_second": 0.0001, - "litellm_provider": "openai" + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/transcriptions"] }, "tts-1": { "mode": "audio_speech", "input_cost_per_character": 0.000015, - "litellm_provider": "openai" + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/speech"] }, "tts-1-hd": { "mode": "audio_speech", "input_cost_per_character": 0.000030, - "litellm_provider": "openai" + "litellm_provider": "openai", + "supported_endpoints": ["/v1/audio/speech"] }, "azure/gpt-4o-mini-realtime-preview-2024-12-17": { "max_tokens": 4096, diff --git a/tests/litellm/llms/custom_httpx/test_handle_audio_file.py b/tests/litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py similarity index 60% rename from tests/litellm/llms/custom_httpx/test_handle_audio_file.py rename to tests/litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py index 682803788b..ea035db119 100644 --- a/tests/litellm/llms/custom_httpx/test_handle_audio_file.py +++ b/tests/litellm/llms/deepgram/audio_transcription/test_deepseek_audio_transcription_transformation.py @@ -1,47 +1,57 @@ - -import os import io +import os import pathlib import sys import pytest - sys.path.insert( - 0, os.path.abspath("../../../..") + 0, os.path.abspath("../../../../..") ) # Adds the parent directory to the system path -import litellm -from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler +import litellm +from litellm.llms.deepgram.audio_transcription.transformation import ( + DeepgramAudioTranscriptionConfig, +) + @pytest.fixture def test_bytes(): - return b'litellm', b'litellm' + return b"litellm", b"litellm" + @pytest.fixture def test_io_bytes(test_bytes): return io.BytesIO(test_bytes[0]), test_bytes[1] + @pytest.fixture def test_file(): pwd = os.path.dirname(os.path.realpath(__file__)) pwd_path = pathlib.Path(pwd) - test_root = pwd_path.parents[2] + test_root = pwd_path.parents[3] + print(f"test_root: {test_root}") file_path = os.path.join(test_root, "gettysburg.wav") f = open(file_path, "rb") content = f.read() f.seek(0) return f, content + @pytest.mark.parametrize( "fixture_name", [ "test_bytes", "test_io_bytes", "test_file", - ] + ], ) def test_audio_file_handling(fixture_name, request): - handler = BaseLLMHTTPHandler() + handler = DeepgramAudioTranscriptionConfig() (audio_file, expected_output) = request.getfixturevalue(fixture_name) - assert expected_output == handler.handle_audio_file(audio_file) \ No newline at end of file + assert expected_output == handler.transform_audio_transcription_request( + model="deepseek-audio-transcription", + audio_file=audio_file, + optional_params={}, + litellm_params={}, + ) diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index fea225e4a3..535861ce1a 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -2074,3 +2074,13 @@ def test_delta_object(): assert delta.role == "user" assert not hasattr(delta, "thinking_blocks") assert not hasattr(delta, "reasoning_content") + + +def test_get_provider_audio_transcription_config(): + from litellm.utils import ProviderConfigManager + from litellm.types.utils import LlmProviders + + for provider in LlmProviders: + config = ProviderConfigManager.get_provider_audio_transcription_config( + model="whisper-1", provider=provider + ) diff --git a/tests/llm_translation/test_openai.py b/tests/llm_translation/test_openai.py index 633ff76467..676019b86f 100644 --- a/tests/llm_translation/test_openai.py +++ b/tests/llm_translation/test_openai.py @@ -22,6 +22,7 @@ from litellm.types.llms.openai import ( ChatCompletionAnnotation, ChatCompletionAnnotationURLCitation, ) +from base_audio_transcription_unit_tests import BaseLLMAudioTranscriptionTest def test_openai_prediction_param(): @@ -458,3 +459,13 @@ def test_openai_web_search_streaming(): # Assert this request has at-least one web search annotation assert test_openai_web_search is not None validate_web_search_annotations(test_openai_web_search) + + +class TestOpenAIGPT4OAudioTranscription(BaseLLMAudioTranscriptionTest): + def get_base_audio_transcription_call_args(self) -> dict: + return { + "model": "openai/gpt-4o-transcribe", + } + + def get_custom_llm_provider(self) -> litellm.LlmProviders: + return litellm.LlmProviders.OPENAI diff --git a/tests/local_testing/test_get_model_info.py b/tests/local_testing/test_get_model_info.py index 27b9b1a2b6..7677f90671 100644 --- a/tests/local_testing/test_get_model_info.py +++ b/tests/local_testing/test_get_model_info.py @@ -520,6 +520,8 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "/v1/images/variations", "/v1/images/edits", "/v1/batch", + "/v1/audio/transcriptions", + "/v1/audio/speech", ], }, }, diff --git a/tests/local_testing/test_whisper.py b/tests/local_testing/test_whisper.py index 5bda5456a4..bca7ae2add 100644 --- a/tests/local_testing/test_whisper.py +++ b/tests/local_testing/test_whisper.py @@ -134,3 +134,33 @@ async def test_whisper_log_pre_call(): file=audio_file, ) mock_log_pre_call.assert_called_once() + + +@pytest.mark.asyncio +async def test_whisper_log_pre_call(): + from litellm.litellm_core_utils.litellm_logging import Logging + from datetime import datetime + from unittest.mock import patch, MagicMock + from litellm.integrations.custom_logger import CustomLogger + + custom_logger = CustomLogger() + + litellm.callbacks = [custom_logger] + + with patch.object(custom_logger, "log_pre_api_call") as mock_log_pre_call: + await litellm.atranscription( + model="whisper-1", + file=audio_file, + ) + mock_log_pre_call.assert_called_once() + + +@pytest.mark.asyncio +async def test_gpt_4o_transcribe(): + from litellm.litellm_core_utils.litellm_logging import Logging + from datetime import datetime + from unittest.mock import patch, MagicMock + + await litellm.atranscription( + model="openai/gpt-4o-transcribe", file=audio_file, response_format="json" + ) From 6ba31e1d84d1de51f91667decc11424baac29642 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Wed, 26 Mar 2025 23:11:15 -0700 Subject: [PATCH 072/208] Allow viewing keyinfo on request logs (#9568) * feat(view_logs.tsx): show model id + api base in request logs easier debugging * fix(index.tsx): fix length of api base easier viewing * build(ui/): initial commit allowing user to click into key from request logs allows easier debugging of 'what key is this? --- ui/litellm-dashboard/src/app/page.tsx | 3 +- .../src/components/networking.tsx | 6 +- .../src/components/view_logs/columns.tsx | 12 +++- .../src/components/view_logs/index.tsx | 61 ++++++++++++++++--- .../src/components/view_logs/table.tsx | 1 + 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 876a6b7c1c..9b9ec0c9c0 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation"; import { jwtDecode } from "jwt-decode"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { defaultOrg } from "@/components/common_components/default_org"; -import { Team } from "@/components/key_team_helpers/key_list"; +import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; import Navbar from "@/components/navbar"; import UserDashboard from "@/components/user_dashboard"; import ModelDashboard from "@/components/model_dashboard"; @@ -344,6 +344,7 @@ export default function CreateKeyPage() { userRole={userRole} token={token} accessToken={accessToken} + allTeams={teams as Team[] ?? []} /> ) : ( { try { + console.log("entering keyInfoV1Call"); let url = proxyBaseUrl ? `${proxyBaseUrl}/key/info` : `/key/info`; url = `${url}?key=${key}`; // Add key as query parameter @@ -2384,13 +2385,16 @@ export const keyInfoV1Call = async (accessToken: string, key: string) => { // Remove body since this is a GET request }); + console.log("response", response); + if (!response.ok) { const errorData = await response.text(); handleError(errorData); - throw new Error("Network response was not ok"); + message.error("Failed to fetch key info - " + errorData); } const data = await response.json(); + console.log("data", data); return data; } catch (error) { console.error("Failed to fetch key info:", error); diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 980124ff3b..98f5bfbc9d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -6,12 +6,14 @@ import { CountryCell } from "./country_cell"; import { getProviderLogoAndName } from "../provider_info_helpers"; import { Tooltip } from "antd"; import { TimeCell } from "./time_cell"; +import { Button } from "@tremor/react"; export type LogEntry = { request_id: string; api_key: string; team_id: string; model: string; + model_id: string; api_base?: string; call_type: string; spend: number; @@ -30,6 +32,7 @@ export type LogEntry = { requester_ip_address?: string; messages: string | any[] | Record; response: string | any[] | Record; + onKeyHashClick?: (keyHash: string) => void; }; export const columns: ColumnDef[] = [ @@ -140,9 +143,16 @@ export const columns: ColumnDef[] = [ accessorKey: "metadata.user_api_key", cell: (info: any) => { const value = String(info.getValue() || "-"); + const onKeyHashClick = info.row.original.onKeyHashClick; + return ( - {value} + onKeyHashClick?.(value)} + > + {value} + ); }, diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index e5df2bbe95..fe82ac4ad8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { useState, useRef, useEffect } from "react"; import { useQueryClient } from "@tanstack/react-query"; -import { uiSpendLogsCall } from "../networking"; +import { uiSpendLogsCall, keyInfoV1Call } from "../networking"; import { DataTable } from "./table"; import { columns, LogEntry } from "./columns"; import { Row } from "@tanstack/react-table"; @@ -12,12 +12,15 @@ import { RequestResponsePanel } from "./columns"; import { ErrorViewer } from './ErrorViewer'; import { internalUserRoles } from "../../utils/roles"; import { ConfigInfoMessage } from './ConfigInfoMessage'; - +import { Tooltip } from "antd"; +import { KeyResponse, Team } from "../key_team_helpers/key_list"; +import KeyInfoView from "../key_info_view"; interface SpendLogsTableProps { accessToken: string | null; token: string | null; userRole: string | null; userID: string | null; + allTeams: Team[]; } interface PaginatedResponse { @@ -38,6 +41,7 @@ export default function SpendLogsTable({ token, userRole, userID, + allTeams, }: SpendLogsTableProps) { const [searchTerm, setSearchTerm] = useState(""); const [showFilters, setShowFilters] = useState(false); @@ -63,14 +67,34 @@ export default function SpendLogsTable({ const [tempKeyHash, setTempKeyHash] = useState(""); const [selectedTeamId, setSelectedTeamId] = useState(""); const [selectedKeyHash, setSelectedKeyHash] = useState(""); + const [selectedKeyInfo, setSelectedKeyInfo] = useState(null); + const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState(null); const [selectedFilter, setSelectedFilter] = useState("Team ID"); const [filterByCurrentUser, setFilterByCurrentUser] = useState( userRole && internalUserRoles.includes(userRole) ); + const [expandedRequestId, setExpandedRequestId] = useState(null); const queryClient = useQueryClient(); + useEffect(() => { + const fetchKeyInfo = async () => { + if (selectedKeyIdInfoView && accessToken) { + const keyData = await keyInfoV1Call(accessToken, selectedKeyIdInfoView); + console.log("keyData", keyData); + + const keyResponse: KeyResponse = { + ...keyData["info"], + "token": selectedKeyIdInfoView, + "api_key": selectedKeyIdInfoView, + }; + setSelectedKeyInfo(keyResponse); + } + }; + fetchKeyInfo(); + }, [selectedKeyIdInfoView, accessToken]); + // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -206,7 +230,11 @@ export default function SpendLogsTable({ // No need for additional filtering since we're now handling this in the API call return matchesSearch; - }) || []; + + }).map(log => ({ + ...log, + onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash) + })) || []; // Add this function to handle manual refresh const handleRefresh = () => { @@ -242,7 +270,10 @@ export default function SpendLogsTable({

Request Logs

- + {selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( + setSelectedKeyIdInfoView(null)} /> + ) : ( + <>
@@ -594,6 +625,8 @@ export default function SpendLogsTable({ expandedRequestId={expandedRequestId} />
+ + )}
); } @@ -671,18 +704,25 @@ function RequestViewer({ row }: { row: Row }) { Model: {row.original.model}
+
+ Model ID: + {row.original.model_id} +
Provider: {row.original.custom_llm_provider || "-"}
+
+ API Base: + + {row.original.api_base || "-"} + +
Start Time: {row.original.startTime}
-
- End Time: - {row.original.endTime} -
+
@@ -712,6 +752,11 @@ function RequestViewer({ row }: { row: Row }) { }`}> {(row.original.metadata?.status || "Success").toLowerCase() !== "failure" ? "Success" : "Failure"} + +
+
+ End Time: + {row.original.endTime}
diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index af9bc5afd2..ed728de074 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -25,6 +25,7 @@ interface DataTableProps { isLoading?: boolean; expandedRequestId?: string | null; onRowExpand?: (requestId: string | null) => void; + setSelectedKeyIdInfoView?: (keyId: string | null) => void; } export function DataTable({ From 3ca34a181cae83b8b3c73e5937f07e36509a7f35 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 26 Mar 2025 23:23:16 -0700 Subject: [PATCH 073/208] docs(openai.md): add gpt-4o-transcribe to docs --- docs/my-website/docs/providers/openai.md | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/my-website/docs/providers/openai.md b/docs/my-website/docs/providers/openai.md index 794f3da647..9ab9061aaa 100644 --- a/docs/my-website/docs/providers/openai.md +++ b/docs/my-website/docs/providers/openai.md @@ -325,6 +325,74 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \ | fine tuned `gpt-3.5-turbo-0613` | `response = completion(model="ft:gpt-3.5-turbo-0613", messages=messages)` | +## OpenAI Audio Transcription + +LiteLLM supports OpenAI Audio Transcription endpoint. + +Supported models: + +| Model Name | Function Call | +|---------------------------|-----------------------------------------------------------------| +| `whisper-1` | `response = completion(model="whisper-1", file=audio_file)` | +| `gpt-4o-transcribe` | `response = completion(model="gpt-4o-transcribe", file=audio_file)` | +| `gpt-4o-mini-transcribe` | `response = completion(model="gpt-4o-mini-transcribe", file=audio_file)` | + + + + +```python +from litellm import transcription +import os + +# set api keys +os.environ["OPENAI_API_KEY"] = "" +audio_file = open("/path/to/audio.mp3", "rb") + +response = transcription(model="gpt-4o-transcribe", file=audio_file) + +print(f"response: {response}") +``` + + + + +1. Setup config.yaml + +```yaml +model_list: +- model_name: gpt-4o-transcribe + litellm_params: + model: gpt-4o-transcribe + api_key: os.environ/OPENAI_API_KEY + model_info: + mode: audio_transcription + +general_settings: + master_key: sk-1234 +``` + +2. Start the proxy + +```bash +litellm --config config.yaml +``` + +3. Test it! + +```bash +curl --location 'http://0.0.0.0:8000/v1/audio/transcriptions' \ +--header 'Authorization: Bearer sk-1234' \ +--form 'file=@"/Users/krrishdholakia/Downloads/gettysburg.wav"' \ +--form 'model="gpt-4o-transcribe"' +``` + + + + + + + + ## Advanced ### Getting OpenAI API Response Headers From 63c9f5937388fc9f87e9298e18f281d2b01d0f41 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Thu, 27 Mar 2025 12:06:31 -0700 Subject: [PATCH 074/208] Allow team admins to add/update/delete models on UI + show api base and model id on request logs (#9572) * feat(view_logs.tsx): show model id + api base in request logs easier debugging * fix(index.tsx): fix length of api base easier viewing * refactor(leftnav.tsx): show models tab to team admin * feat(model_dashboard.tsx): add explainer for what the 'models' page is for team admin helps them understand how they can use it * feat(model_management_endpoints.py): restrict model add by team to just team admin allow team admin to add models via non-team keys (e.g. ui token) * test(test_add_update_models.py): update unit testing for new behaviour * fix(model_dashboard.tsx): show user the models * feat(proxy_server.py): add new query param 'user_models_only' to `/v2/model/info` Allows user to retrieve just the models they've added Used in UI to show internal users just the models they've added * feat(model_dashboard.tsx): allow team admins to view their own models * fix: allow ui user to fetch model cost map * feat(add_model_tab.tsx): require team admins to specify team when onboarding models * fix(_types.py): add `/v1/model/info` to info route `/model/info` was already there * fix(model_info_view.tsx): allow user to edit a model they created * fix(model_management_endpoints.py): allow team admin to update team model * feat(model_managament_endpoints.py): allow team admin to delete team models * fix(model_management_endpoints.py): don't require team id to be set when adding a model * fix(proxy_server.py): fix linting error * fix: fix ui linting error * fix(model_management_endpoints.py): ensure consistent auth checks on all model calls * test: remove old test - function no longer exists in same form * test: add updated mock testing --- litellm/proxy/_types.py | 4 + .../model_management_endpoints.py | 170 ++++++++++++--- litellm/proxy/proxy_server.py | 51 ++++- .../test_model_management_endpoints.py | 196 ++++++++++++++++++ tests/local_testing/test_add_update_models.py | 43 ---- .../components/add_model/add_model_tab.tsx | 25 +++ .../add_model/advanced_settings.tsx | 7 - .../src/components/leftnav.tsx | 2 +- .../src/components/model_dashboard.tsx | 103 +++++---- .../src/components/model_info_view.tsx | 12 +- .../src/components/networking.tsx | 14 +- 11 files changed, 483 insertions(+), 144 deletions(-) create mode 100644 tests/litellm/proxy/management_endpoints/test_model_management_endpoints.py diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index e6294baab8..6e242ddacb 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -292,6 +292,7 @@ class LiteLLMRoutes(enum.Enum): "/team/available", "/user/info", "/model/info", + "/v1/model/info", "/v2/model/info", "/v2/key/info", "/model_group/info", @@ -386,6 +387,7 @@ class LiteLLMRoutes(enum.Enum): "/global/predict/spend/logs", "/global/activity", "/health/services", + "/get/litellm_model_cost_map", ] + info_routes internal_user_routes = [ @@ -412,6 +414,8 @@ class LiteLLMRoutes(enum.Enum): "/team/member_add", "/team/member_delete", "/model/new", + "/model/update", + "/model/delete", ] # routes that manage their own allowed/disallowed logic ## Org Admin Routes ## diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 2a2b7eae24..f83abe77be 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -13,7 +13,7 @@ model/{model_id}/update - PATCH endpoint for model update. import asyncio import json import uuid -from typing import Optional, cast +from typing import Literal, Optional, Union, cast from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel @@ -23,6 +23,7 @@ from litellm.constants import LITELLM_PROXY_ADMIN_NAME from litellm.proxy._types import ( CommonProxyErrors, LiteLLM_ProxyModelTable, + LiteLLM_TeamTable, LitellmTableNames, LitellmUserRoles, ModelInfoDelete, @@ -35,6 +36,7 @@ from litellm.proxy._types import ( ) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper +from litellm.proxy.management_endpoints.common_utils import _is_user_team_admin from litellm.proxy.management_endpoints.team_endpoints import ( team_model_add, update_team, @@ -317,22 +319,111 @@ async def _add_team_model_to_db( return model_response -def check_if_team_id_matches_key( - team_id: Optional[str], user_api_key_dict: UserAPIKeyAuth -) -> bool: - can_make_call = True - if ( - user_api_key_dict.user_role - and user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN - ): +class ModelManagementAuthChecks: + """ + Common auth checks for model management endpoints + """ + + @staticmethod + def can_user_make_team_model_call( + team_id: str, + user_api_key_dict: UserAPIKeyAuth, + team_obj: Optional[LiteLLM_TeamTable] = None, + premium_user: bool = False, + ) -> Literal[True]: + if premium_user is False: + raise HTTPException( + status_code=403, + detail={"error": CommonProxyErrors.not_premium_user.value}, + ) + if ( + user_api_key_dict.user_role + and user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN + ): + return True + elif team_obj is None or not _is_user_team_admin( + user_api_key_dict=user_api_key_dict, team_obj=team_obj + ): + raise HTTPException( + status_code=403, + detail={ + "error": "Team ID={} does not match the API key's team ID={}, OR you are not the admin for this team. Check `/user/info` to verify your team admin status.".format( + team_id, user_api_key_dict.team_id + ) + }, + ) + return True + + @staticmethod + async def allow_team_model_action( + model_params: Union[Deployment, updateDeployment], + user_api_key_dict: UserAPIKeyAuth, + prisma_client: PrismaClient, + premium_user: bool, + ) -> Literal[True]: + if model_params.model_info is None or model_params.model_info.team_id is None: + return True + if model_params.model_info.team_id is not None and premium_user is not True: + raise HTTPException( + status_code=403, + detail={"error": CommonProxyErrors.not_premium_user.value}, + ) + + _existing_team_row = await prisma_client.db.litellm_teamtable.find_unique( + where={"team_id": model_params.model_info.team_id} + ) + + if _existing_team_row is None: + raise HTTPException( + status_code=400, + detail={ + "error": "Team id={} does not exist in db".format( + model_params.model_info.team_id + ) + }, + ) + existing_team_row = LiteLLM_TeamTable(**_existing_team_row.model_dump()) + + ModelManagementAuthChecks.can_user_make_team_model_call( + team_id=model_params.model_info.team_id, + user_api_key_dict=user_api_key_dict, + team_obj=existing_team_row, + premium_user=premium_user, + ) + return True + + @staticmethod + async def can_user_make_model_call( + model_params: Union[Deployment, updateDeployment], + user_api_key_dict: UserAPIKeyAuth, + prisma_client: PrismaClient, + premium_user: bool, + ) -> Literal[True]: + + ## Check team model auth + if ( + model_params.model_info is not None + and model_params.model_info.team_id is not None + ): + return ModelManagementAuthChecks.can_user_make_team_model_call( + team_id=model_params.model_info.team_id, + user_api_key_dict=user_api_key_dict, + premium_user=premium_user, + ) + ## Check non-team model auth + elif user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: + raise HTTPException( + status_code=403, + detail={ + "error": "User does not have permission to make this model call. Your role={}. You can only make model calls if you are a PROXY_ADMIN or if you are a team admin, by specifying a team_id in the model_info.".format( + user_api_key_dict.user_role + ) + }, + ) + else: + return True + return True - if team_id is None: - if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: - can_make_call = False - else: - if user_api_key_dict.team_id != team_id: - can_make_call = False - return can_make_call #### [BETA] - This is a beta endpoint, format might change based on user feedback. - https://github.com/BerriAI/litellm/issues/964 @@ -358,6 +449,7 @@ async def delete_model( from litellm.proxy.proxy_server import ( llm_router, + premium_user, prisma_client, store_model_in_db, ) @@ -370,6 +462,23 @@ async def delete_model( }, ) + model_in_db = await prisma_client.db.litellm_proxymodeltable.find_unique( + where={"model_id": model_info.id} + ) + if model_in_db is None: + raise HTTPException( + status_code=400, + detail={"error": f"Model with id={model_info.id} not found in db"}, + ) + + model_params = Deployment(**model_in_db.model_dump()) + await ModelManagementAuthChecks.can_user_make_model_call( + model_params=model_params, + user_api_key_dict=user_api_key_dict, + prisma_client=prisma_client, + premium_user=premium_user, + ) + # update DB if store_model_in_db is True: """ @@ -464,19 +573,13 @@ async def add_new_model( }, ) - if model_params.model_info.team_id is not None and premium_user is not True: - raise HTTPException( - status_code=403, - detail={"error": CommonProxyErrors.not_premium_user.value}, - ) - - if not check_if_team_id_matches_key( - team_id=model_params.model_info.team_id, user_api_key_dict=user_api_key_dict - ): - raise HTTPException( - status_code=403, - detail={"error": "Team ID does not match the API key's team ID"}, - ) + ## Auth check + await ModelManagementAuthChecks.can_user_make_model_call( + model_params=model_params, + user_api_key_dict=user_api_key_dict, + prisma_client=prisma_client, + premium_user=premium_user, + ) model_response: Optional[LiteLLM_ProxyModelTable] = None # update DB @@ -593,6 +696,7 @@ async def update_model( from litellm.proxy.proxy_server import ( LITELLM_PROXY_ADMIN_NAME, llm_router, + premium_user, prisma_client, store_model_in_db, ) @@ -606,6 +710,14 @@ async def update_model( "error": "No DB Connected. Here's how to do it - https://docs.litellm.ai/docs/proxy/virtual_keys" }, ) + + await ModelManagementAuthChecks.can_user_make_model_call( + model_params=model_params, + user_api_key_dict=user_api_key_dict, + prisma_client=prisma_client, + premium_user=premium_user, + ) + # update DB if store_model_in_db is True: _model_id = None diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c2044ac1d6..5d7e92fd73 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -215,7 +215,6 @@ from litellm.proxy.management_endpoints.key_management_endpoints import ( from litellm.proxy.management_endpoints.model_management_endpoints import ( _add_model_to_db, _add_team_model_to_db, - check_if_team_id_matches_key, ) from litellm.proxy.management_endpoints.model_management_endpoints import ( router as model_management_router, @@ -5494,9 +5493,40 @@ async def transform_request(request: TransformRequestBody): return return_raw_request(endpoint=request.call_type, kwargs=request.request_body) +async def _check_if_model_is_user_added( + models: List[Dict], + user_api_key_dict: UserAPIKeyAuth, + prisma_client: Optional[PrismaClient], +) -> List[Dict]: + """ + Check if model is in db + + Check if db model is 'created_by' == user_api_key_dict.user_id + + Only return models that match + """ + if prisma_client is None: + raise HTTPException( + status_code=500, + detail={"error": CommonProxyErrors.db_not_connected_error.value}, + ) + filtered_models = [] + for model in models: + id = model.get("model_info", {}).get("id", None) + if id is None: + continue + db_model = await prisma_client.db.litellm_proxymodeltable.find_unique( + where={"model_id": id} + ) + if db_model is not None: + if db_model.created_by == user_api_key_dict.user_id: + filtered_models.append(model) + return filtered_models + + @router.get( "/v2/model/info", - description="v2 - returns all the models set on the config.yaml, shows 'user_access' = True if the user has access to the model. Provides more info about each model in /models, including config.yaml descriptions (except api key and api base)", + description="v2 - returns models available to the user based on their API key permissions. Shows model info from config.yaml (except api key and api base). Filter to just user-added models with ?user_models_only=true", tags=["model management"], dependencies=[Depends(user_api_key_auth)], include_in_schema=False, @@ -5506,6 +5536,9 @@ async def model_info_v2( model: Optional[str] = fastapi.Query( None, description="Specify the model name (optional)" ), + user_models_only: Optional[bool] = fastapi.Query( + False, description="Only return models added by this user" + ), debug: Optional[bool] = False, ): """ @@ -5536,6 +5569,20 @@ async def model_info_v2( if model is not None: all_models = [m for m in all_models if m["model_name"] == model] + if user_models_only is True: + """ + Check if model is in db + + Check if db model is 'created_by' == user_api_key_dict.user_id + + Only return models that match + """ + all_models = await _check_if_model_is_user_added( + models=all_models, + user_api_key_dict=user_api_key_dict, + prisma_client=prisma_client, + ) + # fill in model info based on config.yaml and litellm model_prices_and_context_window.json for _model in all_models: # provided model_info in config.yaml diff --git a/tests/litellm/proxy/management_endpoints/test_model_management_endpoints.py b/tests/litellm/proxy/management_endpoints/test_model_management_endpoints.py new file mode 100644 index 0000000000..49e3150e61 --- /dev/null +++ b/tests/litellm/proxy/management_endpoints/test_model_management_endpoints.py @@ -0,0 +1,196 @@ +import json +import os +import sys +from typing import Optional + +import pytest +from fastapi.testclient import TestClient + +sys.path.insert( + 0, os.path.abspath("../../../..") +) # Adds the parent directory to the system path +from litellm.proxy._types import ( + LiteLLM_TeamTable, + LitellmUserRoles, + Member, + UserAPIKeyAuth, +) +from litellm.proxy.management_endpoints.model_management_endpoints import ( + ModelManagementAuthChecks, +) +from litellm.proxy.utils import PrismaClient +from litellm.types.router import Deployment, LiteLLM_Params, updateDeployment + + +class MockPrismaClient: + def __init__(self, team_exists: bool = True): + self.team_exists = team_exists + self.db = self + + async def find_unique(self, where): + if self.team_exists: + return LiteLLM_TeamTable( + team_id=where["team_id"], + team_alias="test_team", + members_with_roles=[Member(user_id="test_user", role="admin")], + ) + return None + + @property + def litellm_teamtable(self): + return self + + +class TestModelManagementAuthChecks: + def setup_method(self): + """Setup test cases""" + self.admin_user = UserAPIKeyAuth( + user_id="test_admin", user_role=LitellmUserRoles.PROXY_ADMIN + ) + + self.normal_user = UserAPIKeyAuth( + user_id="test_user", user_role=LitellmUserRoles.INTERNAL_USER + ) + + self.team_admin_user = UserAPIKeyAuth( + user_id="test_user", + team_id="test_team", + user_role=LitellmUserRoles.INTERNAL_USER, + ) + + @pytest.mark.asyncio + async def test_can_user_make_team_model_call_admin_success(self): + """Test that admin users can make team model calls""" + result = ModelManagementAuthChecks.can_user_make_team_model_call( + team_id="test_team", user_api_key_dict=self.admin_user, premium_user=True + ) + assert result is True + + @pytest.mark.asyncio + async def test_can_user_make_team_model_call_non_premium_fails(self): + """Test that non-premium users cannot make team model calls""" + with pytest.raises(Exception) as exc_info: + ModelManagementAuthChecks.can_user_make_team_model_call( + team_id="test_team", + user_api_key_dict=self.admin_user, + premium_user=False, + ) + assert "403" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_can_user_make_team_model_call_team_admin_success(self): + """Test that team admins can make calls for their team""" + team_obj = LiteLLM_TeamTable( + team_id="test_team", + team_alias="test_team", + members_with_roles=[ + Member(user_id=self.team_admin_user.user_id, role="admin") + ], + ) + + result = ModelManagementAuthChecks.can_user_make_team_model_call( + team_id="test_team", + user_api_key_dict=self.team_admin_user, + team_obj=team_obj, + premium_user=True, + ) + assert result is True + + @pytest.mark.asyncio + async def test_allow_team_model_action_success(self): + """Test successful team model action""" + model_params = Deployment( + model_name="test_model", + litellm_params=LiteLLM_Params(model="test_model", team_id="test_team"), + model_info={"team_id": "test_team"}, + ) + prisma_client = MockPrismaClient(team_exists=True) + + result = await ModelManagementAuthChecks.allow_team_model_action( + model_params=model_params, + user_api_key_dict=self.admin_user, + prisma_client=prisma_client, + premium_user=True, + ) + assert result is True + + @pytest.mark.asyncio + async def test_allow_team_model_action_non_premium_fails(self): + """Test team model action fails for non-premium users""" + model_params = Deployment( + model_name="test_model", + litellm_params=LiteLLM_Params(model="test_model", team_id="test_team"), + model_info={"team_id": "test_team"}, + ) + prisma_client = MockPrismaClient(team_exists=True) + + with pytest.raises(Exception) as exc_info: + await ModelManagementAuthChecks.allow_team_model_action( + model_params=model_params, + user_api_key_dict=self.admin_user, + prisma_client=prisma_client, + premium_user=False, + ) + assert "403" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_allow_team_model_action_nonexistent_team_fails(self): + """Test team model action fails for non-existent team""" + model_params = Deployment( + model_name="test_model", + litellm_params=LiteLLM_Params( + model="test_model", + ), + model_info={"team_id": "nonexistent_team"}, + ) + prisma_client = MockPrismaClient(team_exists=False) + + with pytest.raises(Exception) as exc_info: + await ModelManagementAuthChecks.allow_team_model_action( + model_params=model_params, + user_api_key_dict=self.admin_user, + prisma_client=prisma_client, + premium_user=True, + ) + assert "400" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_can_user_make_model_call_admin_success(self): + """Test that admin users can make any model call""" + model_params = Deployment( + model_name="test_model", + litellm_params=LiteLLM_Params( + model="test_model", + ), + model_info={"team_id": "test_team"}, + ) + prisma_client = MockPrismaClient() + + result = await ModelManagementAuthChecks.can_user_make_model_call( + model_params=model_params, + user_api_key_dict=self.admin_user, + prisma_client=prisma_client, + premium_user=True, + ) + assert result is True + + @pytest.mark.asyncio + async def test_can_user_make_model_call_normal_user_fails(self): + """Test that normal users cannot make model calls""" + model_params = Deployment( + model_name="test_model", + litellm_params=LiteLLM_Params( + model="test_model", + ), + model_info={"team_id": "test_team"}, + ) + prisma_client = MockPrismaClient() + + with pytest.raises(Exception) as exc_info: + await ModelManagementAuthChecks.can_user_make_model_call( + model_params=model_params, + user_api_key_dict=self.normal_user, + prisma_client=prisma_client, + premium_user=True, + ) + assert "403" in str(exc_info.value) diff --git a/tests/local_testing/test_add_update_models.py b/tests/local_testing/test_add_update_models.py index 1ea714d9b7..163687c1c2 100644 --- a/tests/local_testing/test_add_update_models.py +++ b/tests/local_testing/test_add_update_models.py @@ -110,49 +110,6 @@ async def test_add_new_model(prisma_client): assert _new_model_in_db is not None -@pytest.mark.parametrize( - "team_id, key_team_id, user_role, expected_result", - [ - ("1234", "1234", LitellmUserRoles.PROXY_ADMIN.value, True), - ( - "1234", - "1235", - LitellmUserRoles.PROXY_ADMIN.value, - True, - ), # proxy admin can add models for any team - (None, "1234", LitellmUserRoles.PROXY_ADMIN.value, True), - (None, None, LitellmUserRoles.PROXY_ADMIN.value, True), - ( - "1234", - "1234", - LitellmUserRoles.INTERNAL_USER.value, - True, - ), # internal users can add models for their team - ("1234", "1235", LitellmUserRoles.INTERNAL_USER.value, False), - (None, "1234", LitellmUserRoles.INTERNAL_USER.value, False), - ( - None, - None, - LitellmUserRoles.INTERNAL_USER.value, - False, - ), # internal users cannot add models by default - ], -) -def test_can_add_model(team_id, key_team_id, user_role, expected_result): - from litellm.proxy.proxy_server import check_if_team_id_matches_key - - args = { - "team_id": team_id, - "user_api_key_dict": UserAPIKeyAuth( - user_role=user_role, - api_key="sk-1234", - team_id=key_team_id, - ), - } - - assert check_if_team_id_matches_key(**args) is expected_result - - @pytest.mark.asyncio @pytest.mark.skip(reason="new feature, tests passing locally") async def test_add_update_model(prisma_client): diff --git a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx index e2115e8467..1e0a2316df 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx @@ -13,6 +13,8 @@ import ConnectionErrorDisplay from "./model_connection_test"; import { TEST_MODES } from "./add_model_modes"; import { Row, Col } from "antd"; import { Text, TextInput } from "@tremor/react"; +import TeamDropdown from "../common_components/team_dropdown"; +import { all_admin_roles } from "@/utils/roles"; interface AddModelTabProps { form: FormInstance; @@ -28,6 +30,7 @@ interface AddModelTabProps { teams: Team[] | null; credentials: CredentialItem[]; accessToken: string; + userRole: string; } const { Title, Link } = Typography; @@ -46,6 +49,7 @@ const AddModelTab: React.FC = ({ teams, credentials, accessToken, + userRole, }) => { // State for test mode and connection testing const [testMode, setTestMode] = useState("chat"); @@ -64,6 +68,8 @@ const AddModelTab: React.FC = ({ setIsResultModalVisible(true); }; + const isAdmin = all_admin_roles.includes(userRole); + return ( <> Add new model @@ -217,6 +223,25 @@ const AddModelTab: React.FC = ({ ); }} +
+
+ Team Settings +
+
+ + + = ({
- - - }, { key: "3", page: "llm-playground", label: "Test Key", icon: , roles: rolesWithWriteAccess }, - { key: "2", page: "models", label: "Models", icon: , roles: all_admin_roles }, + { key: "2", page: "models", label: "Models", icon: , roles: rolesWithWriteAccess }, { key: "4", page: "usage", label: "Usage", icon: }, { key: "6", page: "teams", label: "Teams", icon: }, { key: "17", page: "organizations", label: "Organizations", icon: , roles: all_admin_roles }, diff --git a/ui/litellm-dashboard/src/components/model_dashboard.tsx b/ui/litellm-dashboard/src/components/model_dashboard.tsx index 1e80e2250e..493f9ba426 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard.tsx @@ -111,6 +111,7 @@ import ModelInfoView from "./model_info_view"; import AddModelTab from "./add_model/add_model_tab"; import { ModelDataTable } from "./model_dashboard/table"; import { columns } from "./model_dashboard/columns"; +import { all_admin_roles } from "@/utils/roles"; interface ModelDashboardProps { accessToken: string | null; @@ -479,9 +480,6 @@ const ModelDashboard: React.FC = ({ } const fetchData = async () => { try { - const _providerSettings = await modelSettingsCall(accessToken); - setProviderSettings(_providerSettings); - // Replace with your actual API call for model data const modelDataResponse = await modelInfoCall( accessToken, @@ -490,6 +488,12 @@ const ModelDashboard: React.FC = ({ ); console.log("Model data response:", modelDataResponse.data); setModelData(modelDataResponse); + const _providerSettings = await modelSettingsCall(accessToken); + if (_providerSettings) { + setProviderSettings(_providerSettings); + } + + // loop through modelDataResponse and get all`model_name` values let all_model_groups: Set = new Set(); @@ -1001,7 +1005,7 @@ const ModelDashboard: React.FC = ({ ); let dynamicProviderForm: ProviderSettings | undefined = undefined; - if (providerKey) { + if (providerKey && providerSettings) { dynamicProviderForm = providerSettings.find( (provider) => provider.name === provider_map[providerKey] ); @@ -1055,16 +1059,17 @@ const ModelDashboard: React.FC = ({ /> ) : ( +
- All Models + {all_admin_roles.includes(userRole) ? All Models : Your Models} Add Model - LLM Credentials - + {all_admin_roles.includes(userRole) && LLM Credentials} + {all_admin_roles.includes(userRole) &&
/health Models
-
- Model Analytics - Model Retry Settings +
} + {all_admin_roles.includes(userRole) && Model Analytics} + {all_admin_roles.includes(userRole) && Model Retry Settings}
@@ -1081,26 +1086,25 @@ const ModelDashboard: React.FC = ({
- -
- Filter by Public Model Name +
+ {/* Left side - Title and description */} +
+ Model Management + {!all_admin_roles.includes(userRole) && + Add models for teams you are an admin for. + } +
+ + {/* Right side - Filter */} +
+ Filter by Public Model Name:
- - selectedModelGroup === "all" || - model.model_name === selectedModelGroup || - !selectedModelGroup - )} - isLoading={false} // Add loading state if needed - /> - - handleEditModelSubmit(data, accessToken, setEditModalVisible, setSelectedModel)} +
+ + selectedModelGroup === "all" || + model.model_name === selectedModelGroup || + !selectedModelGroup + )} + isLoading={false} // Add loading state if needed /> @@ -1153,6 +1151,7 @@ const ModelDashboard: React.FC = ({ teams={teams} credentials={credentialsList} accessToken={accessToken} + userRole={userRole} /> diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 2d05e185c1..7a1c9a7abc 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -58,7 +58,7 @@ export default function ModelInfoView({ const [isEditing, setIsEditing] = useState(false); const [existingCredential, setExistingCredential] = useState(null); - const canEditModel = userRole === "Admin"; + const canEditModel = userRole === "Admin" || modelData.model_info.created_by === userID; const isAdmin = userRole === "Admin"; const usingExistingCredential = modelData.litellm_params?.litellm_credential_name != null && modelData.litellm_params?.litellm_credential_name != undefined; @@ -209,8 +209,8 @@ export default function ModelInfoView({ Public Model Name: {getDisplayModelName(modelData)} {modelData.model_info.id}
- {isAdmin && ( -
+
+ {isAdmin && ( Re-use Credentials + )} + {canEditModel && ( Delete Model -
- )} + )} +
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index cf0b9ec5dc..fbe0374e34 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1,6 +1,7 @@ /** * Helper file for calls being made to proxy */ +import { all_admin_roles } from "@/utils/roles"; import { message } from "antd"; const isLocal = process.env.NODE_ENV === "development"; @@ -131,9 +132,9 @@ export const modelCreateCall = async ( }); if (!response.ok) { - const errorData = await response.json(); + const errorData = await response.text(); const errorMsg = - errorData.error?.message?.error || + errorData|| "Network response was not ok"; message.error(errorMsg); throw new Error(errorMsg); @@ -183,9 +184,8 @@ export const modelSettingsCall = async (accessToken: String) => { //message.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key - } catch (error) { - console.error("Failed to get callbacks:", error); - throw error; + } catch (error: any) { + console.error("Failed to get model settings:", error); } }; @@ -1213,8 +1213,12 @@ export const modelInfoCall = async ( * Get all models on proxy */ try { + console.log("modelInfoCall:", accessToken, userID, userRole); let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/model/info` : `/v2/model/info`; + if (!all_admin_roles.includes(userRole as string)) { // only show users models they've added + url += `?user_models_only=true`; + } //message.info("Requesting model data"); const response = await fetch(url, { method: "GET", From f1fa0044cb1ee6b88f3bdfb199ddd018e6d39e54 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 27 Mar 2025 12:07:17 -0700 Subject: [PATCH 075/208] =?UTF-8?q?bump:=20version=201.64.1=20=E2=86=92=20?= =?UTF-8?q?1.65.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7aed9c3e3c..c3b0fd3224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm" -version = "1.64.1" +version = "1.65.0" description = "Library to easily interface with LLM API providers" authors = ["BerriAI"] license = "MIT" @@ -105,7 +105,7 @@ requires = ["poetry-core", "wheel"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "1.64.1" +version = "1.65.0" version_files = [ "pyproject.toml:^version" ] From f1f40eba3f06074e2d747ac54158410b0a323df8 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 27 Mar 2025 12:14:29 -0700 Subject: [PATCH 076/208] docs(index.md): document new team model flow --- .../img/release_notes/team_model_add.png | Bin 0 -> 71649 bytes .../my-website/release_notes/v1.65.0/index.md | 34 ++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 docs/my-website/img/release_notes/team_model_add.png create mode 100644 docs/my-website/release_notes/v1.65.0/index.md diff --git a/docs/my-website/img/release_notes/team_model_add.png b/docs/my-website/img/release_notes/team_model_add.png new file mode 100644 index 0000000000000000000000000000000000000000..f548469846b6c9de11d81629be55bdc8a721c5ac GIT binary patch literal 71649 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 b?(X+($AgE?;q~@$|BMXGZewr$_4NM$R6v%K literal 0 HcmV?d00001 diff --git a/docs/my-website/release_notes/v1.65.0/index.md b/docs/my-website/release_notes/v1.65.0/index.md new file mode 100644 index 0000000000..46525ea55f --- /dev/null +++ b/docs/my-website/release_notes/v1.65.0/index.md @@ -0,0 +1,34 @@ +--- +title: v1.65.0 - Team Model Add - update +slug: v1.65.0 +date: 2025-03-28T10:00:00 +authors: + - name: Krrish Dholakia + title: CEO, LiteLLM + url: https://www.linkedin.com/in/krish-d/ + image_url: https://media.licdn.com/dms/image/v2/D4D03AQGrlsJ3aqpHmQ/profile-displayphoto-shrink_400_400/B4DZSAzgP7HYAg-/0/1737327772964?e=1743638400&v=beta&t=39KOXMUFedvukiWWVPHf3qI45fuQD7lNglICwN31DrI + - name: Ishaan Jaffer + title: CTO, LiteLLM + url: https://www.linkedin.com/in/reffajnaahsi/ + image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg +tags: [management endpoints, team models, ui] +hide_table_of_contents: false +--- + +import Image from '@theme/IdealImage'; + +v1.65.0 updates the `/model/new` endpoint to prevent non-team admins from creating team models. + +This means that only proxy admins or team admins can create team models. + +## Additional Changes + +- Allows team admins to call `/model/update` to update team models. +- Allows team admins to call `/model/delete` to delete team models. +- Introduces new `user_models_only` param to `/v2/model/info` - only return models added by this user. + + +These changes enable team admins to add and manage models for their team on the LiteLLM UI + API. + + + \ No newline at end of file From 11838e1c3bd93025a2d591b068d18df1849ceb44 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Thu, 27 Mar 2025 14:50:41 -0700 Subject: [PATCH 077/208] Litellm fix db testing (#9593) * ci: fix test * test: safely change db url * fix: print db url * test: remove delenv --- .circleci/config.yml | 6 ++++++ tests/proxy_unit_tests/test_db_schema_migration.py | 4 ++-- tests/proxy_unit_tests/test_key_generate_prisma.py | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 91636c5625..e1488a9083 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -500,6 +500,12 @@ jobs: working_directory: ~/project steps: - checkout + - run: + name: Install PostgreSQL + command: | + sudo apt-get update + sudo apt-get install postgresql postgresql-contrib + echo 'export PATH=/usr/lib/postgresql/*/bin:$PATH' >> $BASH_ENV - setup_google_dns - run: name: Show git commit hash diff --git a/tests/proxy_unit_tests/test_db_schema_migration.py b/tests/proxy_unit_tests/test_db_schema_migration.py index 59367aa9cb..47125e86a7 100644 --- a/tests/proxy_unit_tests/test_db_schema_migration.py +++ b/tests/proxy_unit_tests/test_db_schema_migration.py @@ -17,12 +17,12 @@ def schema_setup(postgresql_my): return postgresql_my -def test_schema_migration_check(schema_setup): +def test_aaaasschema_migration_check(schema_setup, monkeypatch): """Test to check if schema requires migration""" # Set test database URL test_db_url = f"postgresql://{schema_setup.info.user}:@{schema_setup.info.host}:{schema_setup.info.port}/{schema_setup.info.dbname}" # test_db_url = "postgresql://neondb_owner:npg_JiZPS0DAhRn4@ep-delicate-wave-a55cvbuc.us-east-2.aws.neon.tech/neondb?sslmode=require" - os.environ["DATABASE_URL"] = test_db_url + monkeypatch.setenv("DATABASE_URL", test_db_url) deploy_dir = Path("./deploy") source_migrations_dir = deploy_dir / "migrations" diff --git a/tests/proxy_unit_tests/test_key_generate_prisma.py b/tests/proxy_unit_tests/test_key_generate_prisma.py index f922b2c27f..bbfa5b1c11 100644 --- a/tests/proxy_unit_tests/test_key_generate_prisma.py +++ b/tests/proxy_unit_tests/test_key_generate_prisma.py @@ -3286,6 +3286,7 @@ async def test_aadmin_only_routes(prisma_client): only an admin should be able to access admin only routes """ litellm.set_verbose = True + print(f"os.getenv('DATABASE_URL')={os.getenv('DATABASE_URL')}") setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) setattr(litellm.proxy.proxy_server, "master_key", "sk-1234") await litellm.proxy.proxy_server.prisma_client.connect() @@ -3882,6 +3883,7 @@ async def test_get_paginated_teams(prisma_client): @pytest.mark.asyncio +@pytest.mark.flaky(reruns=3) @pytest.mark.parametrize("entity_type", ["key", "user", "team"]) async def test_reset_budget_job(prisma_client, entity_type): """ From 21e3b764f570bdf23ebca9afda4875f83503f643 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 27 Mar 2025 16:31:23 -0700 Subject: [PATCH 078/208] use DBSpendUpdateWriter class for managing DB spend updates --- litellm/proxy/db/db_spend_update_writer.py | 233 ++++++++++++++++++ .../proxy/hooks/proxy_track_cost_callback.py | 8 +- litellm/proxy/proxy_server.py | 221 +---------------- .../hooks/test_proxy_track_cost_callback.py | 3 +- .../test_unit_test_proxy_hooks.py | 13 +- 5 files changed, 255 insertions(+), 223 deletions(-) create mode 100644 litellm/proxy/db/db_spend_update_writer.py diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py new file mode 100644 index 0000000000..d5f2ef5228 --- /dev/null +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -0,0 +1,233 @@ +import asyncio +import os +import traceback +from datetime import datetime +from typing import Any, Optional, Union + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import LiteLLM_UserTable, SpendLogsPayload +from litellm.proxy.proxy_server import hash_token +from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload +from litellm.proxy.utils import PrismaClient, ProxyUpdateSpend + + +class DBSpendUpdateWriter: + + @staticmethod + async def update_database( # noqa: PLR0915 + # LiteLLM management object fields + token: Optional[str], + user_id: Optional[str], + end_user_id: Optional[str], + team_id: Optional[str], + org_id: Optional[str], + # Completion object fields + kwargs: Optional[dict], + completion_response: Optional[Union[litellm.ModelResponse, Any, Exception]], + start_time: Optional[datetime], + end_time: Optional[datetime], + response_cost: Optional[float], + ): + from litellm.proxy.proxy_server import ( + disable_spend_logs, + litellm_proxy_budget_name, + prisma_client, + user_api_key_cache, + ) + + try: + verbose_proxy_logger.debug( + f"Enters prisma db call, response_cost: {response_cost}, token: {token}; user_id: {user_id}; team_id: {team_id}" + ) + if ProxyUpdateSpend.disable_spend_updates() is True: + return + if token is not None and isinstance(token, str) and token.startswith("sk-"): + hashed_token = hash_token(token=token) + else: + hashed_token = token + + ### UPDATE USER SPEND ### + async def _update_user_db(): + """ + - Update that user's row + - Update litellm-proxy-budget row (global proxy spend) + """ + ## if an end-user is passed in, do an upsert - we can't guarantee they already exist in db + existing_user_obj = await user_api_key_cache.async_get_cache( + key=user_id + ) + if existing_user_obj is not None and isinstance( + existing_user_obj, dict + ): + existing_user_obj = LiteLLM_UserTable(**existing_user_obj) + try: + if prisma_client is not None: # update + user_ids = [user_id] + if ( + litellm.max_budget > 0 + ): # track global proxy budget, if user set max budget + user_ids.append(litellm_proxy_budget_name) + ### KEY CHANGE ### + for _id in user_ids: + if _id is not None: + prisma_client.user_list_transactons[_id] = ( + response_cost + + prisma_client.user_list_transactons.get(_id, 0) + ) + if end_user_id is not None: + prisma_client.end_user_list_transactons[end_user_id] = ( + response_cost + + prisma_client.end_user_list_transactons.get( + end_user_id, 0 + ) + ) + except Exception as e: + verbose_proxy_logger.info( + "\033[91m" + + f"Update User DB call failed to execute {str(e)}\n{traceback.format_exc()}" + ) + + ### UPDATE KEY SPEND ### + async def _update_key_db(): + try: + verbose_proxy_logger.debug( + f"adding spend to key db. Response cost: {response_cost}. Token: {hashed_token}." + ) + if hashed_token is None: + return + if prisma_client is not None: + prisma_client.key_list_transactons[hashed_token] = ( + response_cost + + prisma_client.key_list_transactons.get(hashed_token, 0) + ) + except Exception as e: + verbose_proxy_logger.exception( + f"Update Key DB Call failed to execute - {str(e)}" + ) + raise e + + ### UPDATE SPEND LOGS ### + async def _insert_spend_log_to_db(): + try: + if prisma_client: + payload = get_logging_payload( + kwargs=kwargs, + response_obj=completion_response, + start_time=start_time, + end_time=end_time, + ) + payload["spend"] = response_cost or 0.0 + DBSpendUpdateWriter._set_spend_logs_payload( + payload=payload, + spend_logs_url=os.getenv("SPEND_LOGS_URL"), + prisma_client=prisma_client, + ) + except Exception as e: + verbose_proxy_logger.debug( + f"Update Spend Logs DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + + ### UPDATE TEAM SPEND ### + async def _update_team_db(): + try: + verbose_proxy_logger.debug( + f"adding spend to team db. Response cost: {response_cost}. team_id: {team_id}." + ) + if team_id is None: + verbose_proxy_logger.debug( + "track_cost_callback: team_id is None. Not tracking spend for team" + ) + return + if prisma_client is not None: + prisma_client.team_list_transactons[team_id] = ( + response_cost + + prisma_client.team_list_transactons.get(team_id, 0) + ) + + try: + # Track spend of the team member within this team + # key is "team_id::::user_id::" + team_member_key = f"team_id::{team_id}::user_id::{user_id}" + prisma_client.team_member_list_transactons[ + team_member_key + ] = ( + response_cost + + prisma_client.team_member_list_transactons.get( + team_member_key, 0 + ) + ) + except Exception: + pass + except Exception as e: + verbose_proxy_logger.info( + f"Update Team DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + + ### UPDATE ORG SPEND ### + async def _update_org_db(): + try: + verbose_proxy_logger.debug( + "adding spend to org db. Response cost: {}. org_id: {}.".format( + response_cost, org_id + ) + ) + if org_id is None: + verbose_proxy_logger.debug( + "track_cost_callback: org_id is None. Not tracking spend for org" + ) + return + if prisma_client is not None: + prisma_client.org_list_transactons[org_id] = ( + response_cost + + prisma_client.org_list_transactons.get(org_id, 0) + ) + except Exception as e: + verbose_proxy_logger.info( + f"Update Org DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + + asyncio.create_task(_update_user_db()) + asyncio.create_task(_update_key_db()) + asyncio.create_task(_update_team_db()) + asyncio.create_task(_update_org_db()) + if disable_spend_logs is False: + await _insert_spend_log_to_db() + else: + verbose_proxy_logger.info( + "disable_spend_logs=True. Skipping writing spend logs to db. Other spend updates - Key/User/Team table will still occur." + ) + + verbose_proxy_logger.debug("Runs spend update on all tables") + except Exception: + verbose_proxy_logger.debug( + f"Error updating Prisma database: {traceback.format_exc()}" + ) + + @staticmethod + def _set_spend_logs_payload( + payload: Union[dict, SpendLogsPayload], + prisma_client: PrismaClient, + spend_logs_url: Optional[str] = None, + ) -> PrismaClient: + verbose_proxy_logger.info( + "Writing spend log to db - request_id: {}, spend: {}".format( + payload.get("request_id"), payload.get("spend") + ) + ) + if prisma_client is not None and spend_logs_url is not None: + if isinstance(payload["startTime"], datetime): + payload["startTime"] = payload["startTime"].isoformat() + if isinstance(payload["endTime"], datetime): + payload["endTime"] = payload["endTime"].isoformat() + prisma_client.spend_log_transactions.append(payload) + elif prisma_client is not None: + prisma_client.spend_log_transactions.append(payload) + + prisma_client.add_spend_log_transaction_to_daily_user_transaction( + payload.copy() + ) + return prisma_client diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index e8a947329d..f205b0146f 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -13,6 +13,7 @@ from litellm.litellm_core_utils.core_helpers import ( 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.db.db_spend_update_writer import DBSpendUpdateWriter from litellm.proxy.utils import ProxyUpdateSpend from litellm.types.utils import ( StandardLoggingPayload, @@ -33,8 +34,6 @@ class _ProxyDBLogger(CustomLogger): original_exception: Exception, user_api_key_dict: UserAPIKeyAuth, ): - from litellm.proxy.proxy_server import update_database - if _ProxyDBLogger._should_track_errors_in_db() is False: return @@ -67,7 +66,7 @@ class _ProxyDBLogger(CustomLogger): request_data.get("proxy_server_request") or {} ) request_data["litellm_params"]["metadata"] = existing_metadata - await update_database( + await DBSpendUpdateWriter.update_database( token=user_api_key_dict.api_key, response_cost=0.0, user_id=user_api_key_dict.user_id, @@ -94,7 +93,6 @@ class _ProxyDBLogger(CustomLogger): prisma_client, proxy_logging_obj, update_cache, - update_database, ) verbose_proxy_logger.debug("INSIDE _PROXY_track_cost_callback") @@ -138,7 +136,7 @@ class _ProxyDBLogger(CustomLogger): end_user_id=end_user_id, ): ## UPDATE DATABASE - await update_database( + await DBSpendUpdateWriter.update_database( token=user_api_key, response_cost=response_cost, user_id=user_id, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 5d7e92fd73..6a2da7d83b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -897,211 +897,6 @@ def cost_tracking(): litellm.logging_callback_manager.add_litellm_callback(_ProxyDBLogger()) -def _set_spend_logs_payload( - payload: Union[dict, SpendLogsPayload], - prisma_client: PrismaClient, - spend_logs_url: Optional[str] = None, -): - verbose_proxy_logger.info( - "Writing spend log to db - request_id: {}, spend: {}".format( - payload.get("request_id"), payload.get("spend") - ) - ) - if prisma_client is not None and spend_logs_url is not None: - if isinstance(payload["startTime"], datetime): - payload["startTime"] = payload["startTime"].isoformat() - if isinstance(payload["endTime"], datetime): - payload["endTime"] = payload["endTime"].isoformat() - prisma_client.spend_log_transactions.append(payload) - elif prisma_client is not None: - prisma_client.spend_log_transactions.append(payload) - - prisma_client.add_spend_log_transaction_to_daily_user_transaction(payload.copy()) - return prisma_client - - -async def update_database( # noqa: PLR0915 - token, - response_cost, - user_id=None, - end_user_id=None, - team_id=None, - kwargs=None, - completion_response=None, - start_time=None, - end_time=None, - org_id=None, -): - try: - global prisma_client - verbose_proxy_logger.debug( - f"Enters prisma db call, response_cost: {response_cost}, token: {token}; user_id: {user_id}; team_id: {team_id}" - ) - if ProxyUpdateSpend.disable_spend_updates() is True: - return - if token is not None and isinstance(token, str) and token.startswith("sk-"): - hashed_token = hash_token(token=token) - else: - hashed_token = token - - ### UPDATE USER SPEND ### - async def _update_user_db(): - """ - - Update that user's row - - Update litellm-proxy-budget row (global proxy spend) - """ - ## if an end-user is passed in, do an upsert - we can't guarantee they already exist in db - existing_user_obj = await user_api_key_cache.async_get_cache(key=user_id) - if existing_user_obj is not None and isinstance(existing_user_obj, dict): - existing_user_obj = LiteLLM_UserTable(**existing_user_obj) - try: - if prisma_client is not None: # update - user_ids = [user_id] - if ( - litellm.max_budget > 0 - ): # track global proxy budget, if user set max budget - user_ids.append(litellm_proxy_budget_name) - ### KEY CHANGE ### - for _id in user_ids: - if _id is not None: - prisma_client.user_list_transactons[_id] = ( - response_cost - + prisma_client.user_list_transactons.get(_id, 0) - ) - if end_user_id is not None: - prisma_client.end_user_list_transactons[end_user_id] = ( - response_cost - + prisma_client.end_user_list_transactons.get( - end_user_id, 0 - ) - ) - except Exception as e: - verbose_proxy_logger.info( - "\033[91m" - + f"Update User DB call failed to execute {str(e)}\n{traceback.format_exc()}" - ) - - ### UPDATE KEY SPEND ### - async def _update_key_db(): - try: - verbose_proxy_logger.debug( - f"adding spend to key db. Response cost: {response_cost}. Token: {hashed_token}." - ) - if hashed_token is None: - return - if prisma_client is not None: - prisma_client.key_list_transactons[hashed_token] = ( - response_cost - + prisma_client.key_list_transactons.get(hashed_token, 0) - ) - except Exception as e: - verbose_proxy_logger.exception( - f"Update Key DB Call failed to execute - {str(e)}" - ) - raise e - - ### UPDATE SPEND LOGS ### - async def _insert_spend_log_to_db(): - try: - global prisma_client - if prisma_client is not None: - # Helper to generate payload to log - payload = get_logging_payload( - kwargs=kwargs, - response_obj=completion_response, - start_time=start_time, - end_time=end_time, - ) - payload["spend"] = response_cost - prisma_client = _set_spend_logs_payload( - payload=payload, - spend_logs_url=os.getenv("SPEND_LOGS_URL"), - prisma_client=prisma_client, - ) - except Exception as e: - verbose_proxy_logger.debug( - f"Update Spend Logs DB failed to execute - {str(e)}\n{traceback.format_exc()}" - ) - raise e - - ### UPDATE TEAM SPEND ### - async def _update_team_db(): - try: - verbose_proxy_logger.debug( - f"adding spend to team db. Response cost: {response_cost}. team_id: {team_id}." - ) - if team_id is None: - verbose_proxy_logger.debug( - "track_cost_callback: team_id is None. Not tracking spend for team" - ) - return - if prisma_client is not None: - prisma_client.team_list_transactons[team_id] = ( - response_cost - + prisma_client.team_list_transactons.get(team_id, 0) - ) - - try: - # Track spend of the team member within this team - # key is "team_id::::user_id::" - team_member_key = f"team_id::{team_id}::user_id::{user_id}" - prisma_client.team_member_list_transactons[team_member_key] = ( - response_cost - + prisma_client.team_member_list_transactons.get( - team_member_key, 0 - ) - ) - except Exception: - pass - except Exception as e: - verbose_proxy_logger.info( - f"Update Team DB failed to execute - {str(e)}\n{traceback.format_exc()}" - ) - raise e - - ### UPDATE ORG SPEND ### - async def _update_org_db(): - try: - verbose_proxy_logger.debug( - "adding spend to org db. Response cost: {}. org_id: {}.".format( - response_cost, org_id - ) - ) - if org_id is None: - verbose_proxy_logger.debug( - "track_cost_callback: org_id is None. Not tracking spend for org" - ) - return - if prisma_client is not None: - prisma_client.org_list_transactons[org_id] = ( - response_cost - + prisma_client.org_list_transactons.get(org_id, 0) - ) - except Exception as e: - verbose_proxy_logger.info( - f"Update Org DB failed to execute - {str(e)}\n{traceback.format_exc()}" - ) - raise e - - asyncio.create_task(_update_user_db()) - asyncio.create_task(_update_key_db()) - asyncio.create_task(_update_team_db()) - asyncio.create_task(_update_org_db()) - # asyncio.create_task(_insert_spend_log_to_db()) - if disable_spend_logs is False: - await _insert_spend_log_to_db() - else: - verbose_proxy_logger.info( - "disable_spend_logs=True. Skipping writing spend logs to db. Other spend updates - Key/User/Team table will still occur." - ) - - verbose_proxy_logger.debug("Runs spend update on all tables") - except Exception: - verbose_proxy_logger.debug( - f"Error updating Prisma database: {traceback.format_exc()}" - ) - - async def update_cache( # noqa: PLR0915 token: Optional[str], user_id: Optional[str], @@ -3294,14 +3089,14 @@ class ProxyStartupEvent: prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj ) - ### GET STORED CREDENTIALS ### - scheduler.add_job( - proxy_config.get_credentials, - "interval", - seconds=10, - args=[prisma_client], - ) - await proxy_config.get_credentials(prisma_client=prisma_client) + ### GET STORED CREDENTIALS ### + scheduler.add_job( + proxy_config.get_credentials, + "interval", + seconds=1, + args=[prisma_client], + ) + await proxy_config.get_credentials(prisma_client=prisma_client) if ( proxy_logging_obj is not None and proxy_logging_obj.slack_alerting_instance.alerting is not None diff --git a/tests/litellm/proxy/hooks/test_proxy_track_cost_callback.py b/tests/litellm/proxy/hooks/test_proxy_track_cost_callback.py index 1e3b22ae2d..8850436329 100644 --- a/tests/litellm/proxy/hooks/test_proxy_track_cost_callback.py +++ b/tests/litellm/proxy/hooks/test_proxy_track_cost_callback.py @@ -47,7 +47,8 @@ async def test_async_post_call_failure_hook(): # Mock update_database function with patch( - "litellm.proxy.proxy_server.update_database", new_callable=AsyncMock + "litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter.update_database", + new_callable=AsyncMock, ) as mock_update_database: # Call the method await logger.async_post_call_failure_hook( diff --git a/tests/proxy_unit_tests/test_unit_test_proxy_hooks.py b/tests/proxy_unit_tests/test_unit_test_proxy_hooks.py index 535f5bf019..129be6d754 100644 --- a/tests/proxy_unit_tests/test_unit_test_proxy_hooks.py +++ b/tests/proxy_unit_tests/test_unit_test_proxy_hooks.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch, AsyncMock import pytest from fastapi import Request from litellm.proxy.utils import _get_redoc_url, _get_docs_url +from datetime import datetime sys.path.insert(0, os.path.abspath("../..")) import litellm @@ -22,16 +23,20 @@ async def test_disable_spend_logs(): with patch("litellm.proxy.proxy_server.disable_spend_logs", True), patch( "litellm.proxy.proxy_server.prisma_client", mock_prisma_client ): - from litellm.proxy.proxy_server import update_database + from litellm.proxy.db.db_spend_update_writer import DBSpendUpdateWriter # Call update_database with disable_spend_logs=True - await update_database( + await DBSpendUpdateWriter.update_database( token="fake-token", response_cost=0.1, user_id="user123", completion_response=None, - start_time="2024-01-01", - end_time="2024-01-01", + start_time=datetime.now(), + end_time=datetime.now(), + end_user_id="end_user_id", + team_id="team_id", + org_id="org_id", + kwargs={}, ) # Verify no spend logs were added assert len(mock_prisma_client.spend_log_transactions) == 0 From 072be44a54a5cd37162f0cd2d0fb166d1d670cad Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 27 Mar 2025 16:34:06 -0700 Subject: [PATCH 079/208] fix get_credentials job --- litellm/proxy/proxy_server.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6a2da7d83b..d7e62f98d0 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3089,14 +3089,14 @@ class ProxyStartupEvent: prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj ) - ### GET STORED CREDENTIALS ### - scheduler.add_job( - proxy_config.get_credentials, - "interval", - seconds=1, - args=[prisma_client], - ) - await proxy_config.get_credentials(prisma_client=prisma_client) + ### GET STORED CREDENTIALS ### + scheduler.add_job( + proxy_config.get_credentials, + "interval", + seconds=10, + args=[prisma_client], + ) + await proxy_config.get_credentials(prisma_client=prisma_client) if ( proxy_logging_obj is not None and proxy_logging_obj.slack_alerting_instance.alerting is not None From a0fd508de405040e22b00cbb043e8bde0afd5e75 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 27 Mar 2025 16:43:18 -0700 Subject: [PATCH 080/208] DBSpendUpdateWriter --- litellm/proxy/db/db_spend_update_writer.py | 3 +-- .../spend_tracking/test_spend_management_endpoints.py | 9 ++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index d5f2ef5228..af1d3294c5 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -7,9 +7,8 @@ from typing import Any, Optional, Union import litellm from litellm._logging import verbose_proxy_logger from litellm.proxy._types import LiteLLM_UserTable, SpendLogsPayload -from litellm.proxy.proxy_server import hash_token from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload -from litellm.proxy.utils import PrismaClient, ProxyUpdateSpend +from litellm.proxy.utils import PrismaClient, ProxyUpdateSpend, hash_token class DBSpendUpdateWriter: diff --git a/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py index a5ee9ddf70..e08bce432d 100644 --- a/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -416,7 +416,8 @@ class TestSpendLogsPayload: # litellm._turn_on_debug() with patch.object( - litellm.proxy.proxy_server, "_set_spend_logs_payload" + litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter, + "_set_spend_logs_payload", ) as mock_client, patch.object(litellm.proxy.proxy_server, "prisma_client"): response = await litellm.acompletion( model="gpt-4o", @@ -509,7 +510,8 @@ class TestSpendLogsPayload: client = AsyncHTTPHandler() with patch.object( - litellm.proxy.proxy_server, "_set_spend_logs_payload" + litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter, + "_set_spend_logs_payload", ) as mock_client, patch.object( litellm.proxy.proxy_server, "prisma_client" ), patch.object( @@ -604,7 +606,8 @@ class TestSpendLogsPayload: ) with patch.object( - litellm.proxy.proxy_server, "_set_spend_logs_payload" + litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter, + "_set_spend_logs_payload", ) as mock_client, patch.object( litellm.proxy.proxy_server, "prisma_client" ), patch.object( From 7995fd7c98bf6fe3a058edfb60c64b05fc330622 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 27 Mar 2025 16:44:46 -0700 Subject: [PATCH 081/208] fix DBSpendUpdateWriter --- litellm/proxy/db/db_spend_update_writer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index af1d3294c5..ecafdcc3df 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -1,3 +1,10 @@ +""" +Module responsible for + +1. Writing spend increments to either in memory list of transactions or to redis +2. Reading increments from redis or in memory list of transactions and committing them to db +""" + import asyncio import os import traceback @@ -12,6 +19,12 @@ from litellm.proxy.utils import PrismaClient, ProxyUpdateSpend, hash_token class DBSpendUpdateWriter: + """ + Module responsible for + + 1. Writing spend increments to either in memory list of transactions or to redis + 2. Reading increments from redis or in memory list of transactions and committing them to db + """ @staticmethod async def update_database( # noqa: PLR0915 From 403f2ef68dae3735fc61b5ada98b336ee5ab044c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 27 Mar 2025 16:57:12 -0700 Subject: [PATCH 082/208] use simple static methods for updating spend --- litellm/proxy/db/db_spend_update_writer.py | 349 ++++++++++++--------- 1 file changed, 201 insertions(+), 148 deletions(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index ecafdcc3df..37d709c0f1 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -13,6 +13,7 @@ from typing import Any, Optional, Union import litellm from litellm._logging import verbose_proxy_logger +from litellm.caching import DualCache from litellm.proxy._types import LiteLLM_UserTable, SpendLogsPayload from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload from litellm.proxy.utils import PrismaClient, ProxyUpdateSpend, hash_token @@ -27,7 +28,7 @@ class DBSpendUpdateWriter: """ @staticmethod - async def update_database( # noqa: PLR0915 + async def update_database( # LiteLLM management object fields token: Optional[str], user_id: Optional[str], @@ -59,155 +60,47 @@ class DBSpendUpdateWriter: else: hashed_token = token - ### UPDATE USER SPEND ### - async def _update_user_db(): - """ - - Update that user's row - - Update litellm-proxy-budget row (global proxy spend) - """ - ## if an end-user is passed in, do an upsert - we can't guarantee they already exist in db - existing_user_obj = await user_api_key_cache.async_get_cache( - key=user_id + asyncio.create_task( + DBSpendUpdateWriter._update_user_db( + response_cost=response_cost, + user_id=user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + litellm_proxy_budget_name=litellm_proxy_budget_name, + end_user_id=end_user_id, ) - if existing_user_obj is not None and isinstance( - existing_user_obj, dict - ): - existing_user_obj = LiteLLM_UserTable(**existing_user_obj) - try: - if prisma_client is not None: # update - user_ids = [user_id] - if ( - litellm.max_budget > 0 - ): # track global proxy budget, if user set max budget - user_ids.append(litellm_proxy_budget_name) - ### KEY CHANGE ### - for _id in user_ids: - if _id is not None: - prisma_client.user_list_transactons[_id] = ( - response_cost - + prisma_client.user_list_transactons.get(_id, 0) - ) - if end_user_id is not None: - prisma_client.end_user_list_transactons[end_user_id] = ( - response_cost - + prisma_client.end_user_list_transactons.get( - end_user_id, 0 - ) - ) - except Exception as e: - verbose_proxy_logger.info( - "\033[91m" - + f"Update User DB call failed to execute {str(e)}\n{traceback.format_exc()}" - ) - - ### UPDATE KEY SPEND ### - async def _update_key_db(): - try: - verbose_proxy_logger.debug( - f"adding spend to key db. Response cost: {response_cost}. Token: {hashed_token}." - ) - if hashed_token is None: - return - if prisma_client is not None: - prisma_client.key_list_transactons[hashed_token] = ( - response_cost - + prisma_client.key_list_transactons.get(hashed_token, 0) - ) - except Exception as e: - verbose_proxy_logger.exception( - f"Update Key DB Call failed to execute - {str(e)}" - ) - raise e - - ### UPDATE SPEND LOGS ### - async def _insert_spend_log_to_db(): - try: - if prisma_client: - payload = get_logging_payload( - kwargs=kwargs, - response_obj=completion_response, - start_time=start_time, - end_time=end_time, - ) - payload["spend"] = response_cost or 0.0 - DBSpendUpdateWriter._set_spend_logs_payload( - payload=payload, - spend_logs_url=os.getenv("SPEND_LOGS_URL"), - prisma_client=prisma_client, - ) - except Exception as e: - verbose_proxy_logger.debug( - f"Update Spend Logs DB failed to execute - {str(e)}\n{traceback.format_exc()}" - ) - raise e - - ### UPDATE TEAM SPEND ### - async def _update_team_db(): - try: - verbose_proxy_logger.debug( - f"adding spend to team db. Response cost: {response_cost}. team_id: {team_id}." - ) - if team_id is None: - verbose_proxy_logger.debug( - "track_cost_callback: team_id is None. Not tracking spend for team" - ) - return - if prisma_client is not None: - prisma_client.team_list_transactons[team_id] = ( - response_cost - + prisma_client.team_list_transactons.get(team_id, 0) - ) - - try: - # Track spend of the team member within this team - # key is "team_id::::user_id::" - team_member_key = f"team_id::{team_id}::user_id::{user_id}" - prisma_client.team_member_list_transactons[ - team_member_key - ] = ( - response_cost - + prisma_client.team_member_list_transactons.get( - team_member_key, 0 - ) - ) - except Exception: - pass - except Exception as e: - verbose_proxy_logger.info( - f"Update Team DB failed to execute - {str(e)}\n{traceback.format_exc()}" - ) - raise e - - ### UPDATE ORG SPEND ### - async def _update_org_db(): - try: - verbose_proxy_logger.debug( - "adding spend to org db. Response cost: {}. org_id: {}.".format( - response_cost, org_id - ) - ) - if org_id is None: - verbose_proxy_logger.debug( - "track_cost_callback: org_id is None. Not tracking spend for org" - ) - return - if prisma_client is not None: - prisma_client.org_list_transactons[org_id] = ( - response_cost - + prisma_client.org_list_transactons.get(org_id, 0) - ) - except Exception as e: - verbose_proxy_logger.info( - f"Update Org DB failed to execute - {str(e)}\n{traceback.format_exc()}" - ) - raise e - - asyncio.create_task(_update_user_db()) - asyncio.create_task(_update_key_db()) - asyncio.create_task(_update_team_db()) - asyncio.create_task(_update_org_db()) + ) + asyncio.create_task( + DBSpendUpdateWriter._update_key_db( + response_cost=response_cost, + hashed_token=hashed_token, + prisma_client=prisma_client, + ) + ) + asyncio.create_task( + DBSpendUpdateWriter._update_team_db( + response_cost=response_cost, + team_id=team_id, + user_id=user_id, + prisma_client=prisma_client, + ) + ) + asyncio.create_task( + DBSpendUpdateWriter._update_org_db( + response_cost=response_cost, + org_id=org_id, + prisma_client=prisma_client, + ) + ) if disable_spend_logs is False: - await _insert_spend_log_to_db() + await DBSpendUpdateWriter._insert_spend_log_to_db( + kwargs=kwargs, + completion_response=completion_response, + start_time=start_time, + end_time=end_time, + response_cost=response_cost, + prisma_client=prisma_client, + ) else: verbose_proxy_logger.info( "disable_spend_logs=True. Skipping writing spend logs to db. Other spend updates - Key/User/Team table will still occur." @@ -219,6 +112,166 @@ class DBSpendUpdateWriter: f"Error updating Prisma database: {traceback.format_exc()}" ) + @staticmethod + async def _update_key_db( + response_cost: Optional[float], + hashed_token: Optional[str], + prisma_client: Optional[PrismaClient], + ): + try: + verbose_proxy_logger.debug( + f"adding spend to key db. Response cost: {response_cost}. Token: {hashed_token}." + ) + if hashed_token is None: + return + if prisma_client is not None: + prisma_client.key_list_transactons[hashed_token] = ( + response_cost + + prisma_client.key_list_transactons.get(hashed_token, 0) + ) + except Exception as e: + verbose_proxy_logger.exception( + f"Update Key DB Call failed to execute - {str(e)}" + ) + raise e + + @staticmethod + async def _update_user_db( + response_cost: Optional[float], + user_id: Optional[str], + prisma_client: Optional[PrismaClient], + user_api_key_cache: DualCache, + litellm_proxy_budget_name: Optional[str], + end_user_id: Optional[str] = None, + ): + """ + - Update that user's row + - Update litellm-proxy-budget row (global proxy spend) + """ + ## if an end-user is passed in, do an upsert - we can't guarantee they already exist in db + existing_user_obj = await user_api_key_cache.async_get_cache(key=user_id) + if existing_user_obj is not None and isinstance(existing_user_obj, dict): + existing_user_obj = LiteLLM_UserTable(**existing_user_obj) + try: + if prisma_client is not None: # update + user_ids = [user_id] + if ( + litellm.max_budget > 0 + ): # track global proxy budget, if user set max budget + user_ids.append(litellm_proxy_budget_name) + ### KEY CHANGE ### + for _id in user_ids: + if _id is not None: + prisma_client.user_list_transactons[_id] = ( + response_cost + + prisma_client.user_list_transactons.get(_id, 0) + ) + if end_user_id is not None: + prisma_client.end_user_list_transactons[end_user_id] = ( + response_cost + + prisma_client.end_user_list_transactons.get(end_user_id, 0) + ) + except Exception as e: + verbose_proxy_logger.info( + "\033[91m" + + f"Update User DB call failed to execute {str(e)}\n{traceback.format_exc()}" + ) + + @staticmethod + async def _update_team_db( + response_cost: Optional[float], + team_id: Optional[str], + user_id: Optional[str], + prisma_client: Optional[PrismaClient], + ): + try: + verbose_proxy_logger.debug( + f"adding spend to team db. Response cost: {response_cost}. team_id: {team_id}." + ) + if team_id is None: + verbose_proxy_logger.debug( + "track_cost_callback: team_id is None. Not tracking spend for team" + ) + return + if prisma_client is not None: + prisma_client.team_list_transactons[team_id] = ( + response_cost + prisma_client.team_list_transactons.get(team_id, 0) + ) + + try: + # Track spend of the team member within this team + # key is "team_id::::user_id::" + team_member_key = f"team_id::{team_id}::user_id::{user_id}" + prisma_client.team_member_list_transactons[team_member_key] = ( + response_cost + + prisma_client.team_member_list_transactons.get( + team_member_key, 0 + ) + ) + except Exception: + pass + except Exception as e: + verbose_proxy_logger.info( + f"Update Team DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + + @staticmethod + async def _update_org_db( + response_cost: Optional[float], + org_id: Optional[str], + prisma_client: Optional[PrismaClient], + ): + try: + verbose_proxy_logger.debug( + "adding spend to org db. Response cost: {}. org_id: {}.".format( + response_cost, org_id + ) + ) + if org_id is None: + verbose_proxy_logger.debug( + "track_cost_callback: org_id is None. Not tracking spend for org" + ) + return + if prisma_client is not None: + prisma_client.org_list_transactons[org_id] = ( + response_cost + prisma_client.org_list_transactons.get(org_id, 0) + ) + except Exception as e: + verbose_proxy_logger.info( + f"Update Org DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + + @staticmethod + async def _insert_spend_log_to_db( + kwargs: Optional[dict], + completion_response: Optional[Union[litellm.ModelResponse, Any, Exception]], + start_time: Optional[datetime], + end_time: Optional[datetime], + response_cost: Optional[float], + prisma_client: Optional[PrismaClient], + ): + try: + if prisma_client: + payload = get_logging_payload( + kwargs=kwargs, + response_obj=completion_response, + start_time=start_time, + end_time=end_time, + ) + payload["spend"] = response_cost or 0.0 + DBSpendUpdateWriter._set_spend_logs_payload( + payload=payload, + spend_logs_url=os.getenv("SPEND_LOGS_URL"), + prisma_client=prisma_client, + ) + except Exception as e: + verbose_proxy_logger.debug( + f"Update Spend Logs DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + @staticmethod def _set_spend_logs_payload( payload: Union[dict, SpendLogsPayload], From b721b2b4acfb84e4b5b73e488a33562b12759c75 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 27 Mar 2025 17:04:50 -0700 Subject: [PATCH 083/208] use DBSpendUpdateWriter common function for --- litellm/proxy/_types.py | 15 +++ litellm/proxy/db/db_spend_update_writer.py | 142 ++++++++++++++------- 2 files changed, 111 insertions(+), 46 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 6e242ddacb..17b4acd138 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -144,6 +144,21 @@ class LitellmTableNames(str, enum.Enum): PROXY_MODEL_TABLE_NAME = "LiteLLM_ProxyModelTable" +class Litellm_EntityType(enum.Enum): + """ + Enum for types of entities on litellm + + This enum allows specifying the type of entity that is being tracked in the database. + """ + + KEY = "key" + USER = "user" + END_USER = "end_user" + TEAM = "team" + TEAM_MEMBER = "team_member" + ORGANIZATION = "organization" + + def hash_token(token: str): import hashlib diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 37d709c0f1..34e8eae173 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -14,7 +14,7 @@ from typing import Any, Optional, Union import litellm from litellm._logging import verbose_proxy_logger from litellm.caching import DualCache -from litellm.proxy._types import LiteLLM_UserTable, SpendLogsPayload +from litellm.proxy._types import Litellm_EntityType, LiteLLM_UserTable, SpendLogsPayload from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload from litellm.proxy.utils import PrismaClient, ProxyUpdateSpend, hash_token @@ -112,6 +112,52 @@ class DBSpendUpdateWriter: f"Error updating Prisma database: {traceback.format_exc()}" ) + @staticmethod + async def _update_transaction_list( + response_cost: Optional[float], + entity_id: Optional[str], + transaction_list: dict, + entity_type: Litellm_EntityType, + debug_msg: Optional[str] = None, + ) -> bool: + """ + Common helper method to update a transaction list for an entity + + Args: + response_cost: The cost to add + entity_id: The ID of the entity to update + transaction_list: The transaction list dictionary to update + entity_type: The type of entity (from EntityType enum) + debug_msg: Optional custom debug message + + Returns: + bool: True if update happened, False otherwise + """ + try: + if debug_msg: + verbose_proxy_logger.debug(debug_msg) + else: + verbose_proxy_logger.debug( + f"adding spend to {entity_type.value} db. Response cost: {response_cost}. {entity_type.value}_id: {entity_id}." + ) + + if entity_id is None: + verbose_proxy_logger.debug( + f"track_cost_callback: {entity_type.value}_id is None. Not tracking spend for {entity_type.value}" + ) + return False + + transaction_list[entity_id] = response_cost + transaction_list.get( + entity_id, 0 + ) + return True + + except Exception as e: + verbose_proxy_logger.info( + f"Update {entity_type.value.capitalize()} DB failed to execute - {str(e)}\n{traceback.format_exc()}" + ) + raise e + @staticmethod async def _update_key_db( response_cost: Optional[float], @@ -119,16 +165,16 @@ class DBSpendUpdateWriter: prisma_client: Optional[PrismaClient], ): try: - verbose_proxy_logger.debug( - f"adding spend to key db. Response cost: {response_cost}. Token: {hashed_token}." - ) - if hashed_token is None: + if hashed_token is None or prisma_client is None: return - if prisma_client is not None: - prisma_client.key_list_transactons[hashed_token] = ( - response_cost - + prisma_client.key_list_transactons.get(hashed_token, 0) - ) + + await DBSpendUpdateWriter._update_transaction_list( + response_cost=response_cost, + entity_id=hashed_token, + transaction_list=prisma_client.key_list_transactons, + entity_type=Litellm_EntityType.KEY, + debug_msg=f"adding spend to key db. Response cost: {response_cost}. Token: {hashed_token}.", + ) except Exception as e: verbose_proxy_logger.exception( f"Update Key DB Call failed to execute - {str(e)}" @@ -159,17 +205,22 @@ class DBSpendUpdateWriter: litellm.max_budget > 0 ): # track global proxy budget, if user set max budget user_ids.append(litellm_proxy_budget_name) - ### KEY CHANGE ### + for _id in user_ids: if _id is not None: - prisma_client.user_list_transactons[_id] = ( - response_cost - + prisma_client.user_list_transactons.get(_id, 0) + await DBSpendUpdateWriter._update_transaction_list( + response_cost=response_cost, + entity_id=_id, + transaction_list=prisma_client.user_list_transactons, + entity_type=Litellm_EntityType.USER, ) + if end_user_id is not None: - prisma_client.end_user_list_transactons[end_user_id] = ( - response_cost - + prisma_client.end_user_list_transactons.get(end_user_id, 0) + await DBSpendUpdateWriter._update_transaction_list( + response_cost=response_cost, + entity_id=end_user_id, + transaction_list=prisma_client.end_user_list_transactons, + entity_type=Litellm_EntityType.END_USER, ) except Exception as e: verbose_proxy_logger.info( @@ -185,31 +236,32 @@ class DBSpendUpdateWriter: prisma_client: Optional[PrismaClient], ): try: - verbose_proxy_logger.debug( - f"adding spend to team db. Response cost: {response_cost}. team_id: {team_id}." - ) - if team_id is None: + if team_id is None or prisma_client is None: verbose_proxy_logger.debug( - "track_cost_callback: team_id is None. Not tracking spend for team" + "track_cost_callback: team_id is None or prisma_client is None. Not tracking spend for team" ) return - if prisma_client is not None: - prisma_client.team_list_transactons[team_id] = ( - response_cost + prisma_client.team_list_transactons.get(team_id, 0) - ) - try: - # Track spend of the team member within this team + await DBSpendUpdateWriter._update_transaction_list( + response_cost=response_cost, + entity_id=team_id, + transaction_list=prisma_client.team_list_transactons, + entity_type=Litellm_EntityType.TEAM, + ) + + try: + # Track spend of the team member within this team + if user_id is not None: # key is "team_id::::user_id::" team_member_key = f"team_id::{team_id}::user_id::{user_id}" - prisma_client.team_member_list_transactons[team_member_key] = ( - response_cost - + prisma_client.team_member_list_transactons.get( - team_member_key, 0 - ) + await DBSpendUpdateWriter._update_transaction_list( + response_cost=response_cost, + entity_id=team_member_key, + transaction_list=prisma_client.team_member_list_transactons, + entity_type=Litellm_EntityType.TEAM_MEMBER, ) - except Exception: - pass + except Exception: + pass except Exception as e: verbose_proxy_logger.info( f"Update Team DB failed to execute - {str(e)}\n{traceback.format_exc()}" @@ -223,20 +275,18 @@ class DBSpendUpdateWriter: prisma_client: Optional[PrismaClient], ): try: - verbose_proxy_logger.debug( - "adding spend to org db. Response cost: {}. org_id: {}.".format( - response_cost, org_id - ) - ) - if org_id is None: + if org_id is None or prisma_client is None: verbose_proxy_logger.debug( - "track_cost_callback: org_id is None. Not tracking spend for org" + "track_cost_callback: org_id is None or prisma_client is None. Not tracking spend for org" ) return - if prisma_client is not None: - prisma_client.org_list_transactons[org_id] = ( - response_cost + prisma_client.org_list_transactons.get(org_id, 0) - ) + + await DBSpendUpdateWriter._update_transaction_list( + response_cost=response_cost, + entity_id=org_id, + transaction_list=prisma_client.org_list_transactons, + entity_type=Litellm_EntityType.ORGANIZATION, + ) except Exception as e: verbose_proxy_logger.info( f"Update Org DB failed to execute - {str(e)}\n{traceback.format_exc()}" From fb83567a037837aa380bdc98ed3627c831868191 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Thu, 27 Mar 2025 17:15:25 -0700 Subject: [PATCH 084/208] Litellm new UI build (#9601) * build: new ui build * build: new ui build * fix(proxy_server.py): only show user models their key can access on `/models` * fix(model_management_endpoints.py): ensure team admin can add models * test: update unit testing to reflect changes * fix(model_dashboard.tsx): fix sizing on models page * build: fix ui --- .../static/chunks/250-601568e45a5ffece.js | 1 + .../static/chunks/250-a75ee9d79f1140b0.js | 1 - .../static/chunks/394-48a36e9c9b2cb488.js | 4 +- .../chunks/app/layout-429ad74a94df7643.js | 1 + .../chunks/app/layout-af8319e6c59a08da.js | 1 - .../app/model_hub/page-cde2fb783e81a6c1.js | 2 +- .../app/onboarding/page-1ffe69692e4b2037.js | 1 - .../app/onboarding/page-5110f2c6a3c9a2f4.js | 1 + .../chunks/app/page-75d771fb848b47a8.js | 1 - .../chunks/app/page-e21d4be3d6c3c16e.js | 1 + ...80647d.js => main-app-4f7318ae681a6d94.js} | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 .../out/assets/logos/anthropic.svg | 5 + .../out/assets/logos/assemblyai_small.png | Bin 0 -> 414 bytes .../_experimental/out/assets/logos/aws.svg | 34 +++++++ .../out/assets/logos/bedrock.svg | 1 + .../out/assets/logos/cerebras.svg | 89 ++++++++++++++++++ .../_experimental/out/assets/logos/cohere.svg | 1 + .../out/assets/logos/databricks.svg | 1 + .../out/assets/logos/deepseek.svg | 25 +++++ .../out/assets/logos/fireworks.svg | 1 + .../_experimental/out/assets/logos/google.svg | 2 + .../_experimental/out/assets/logos/groq.svg | 3 + .../out/assets/logos/microsoft_azure.svg | 72 ++++++++++++++ .../out/assets/logos/mistral.svg | 1 + .../_experimental/out/assets/logos/ollama.svg | 7 ++ .../out/assets/logos/openai_small.svg | 5 + .../out/assets/logos/openrouter.svg | 39 ++++++++ .../out/assets/logos/perplexity-ai.svg | 16 ++++ .../out/assets/logos/sambanova.svg | 1 + .../out/assets/logos/togetherai.svg | 14 +++ .../_experimental/out/assets/logos/xai.svg | 28 ++++++ litellm/proxy/_experimental/out/index.html | 2 +- litellm/proxy/_experimental/out/index.txt | 4 +- litellm/proxy/_experimental/out/model_hub.txt | 4 +- .../proxy/_experimental/out/onboarding.txt | 4 +- litellm/proxy/_new_secret_config.yaml | 11 +-- .../model_management_endpoints.py | 15 +++ litellm/proxy/proxy_server.py | 12 ++- .../test_model_management_endpoints.py | 13 ++- ui/litellm-dashboard/out/404.html | 2 +- .../static/chunks/250-601568e45a5ffece.js | 1 + .../static/chunks/250-a75ee9d79f1140b0.js | 1 - .../static/chunks/394-48a36e9c9b2cb488.js | 4 +- .../chunks/app/layout-429ad74a94df7643.js | 1 + .../chunks/app/layout-af8319e6c59a08da.js | 1 - .../app/model_hub/page-cde2fb783e81a6c1.js | 2 +- .../app/onboarding/page-1ffe69692e4b2037.js | 1 - .../app/onboarding/page-5110f2c6a3c9a2f4.js | 1 + .../chunks/app/page-75d771fb848b47a8.js | 1 - .../chunks/app/page-e21d4be3d6c3c16e.js | 1 + ...80647d.js => main-app-4f7318ae681a6d94.js} | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 .../out/assets/logos/anthropic.svg | 5 + .../out/assets/logos/assemblyai_small.png | Bin 0 -> 414 bytes ui/litellm-dashboard/out/assets/logos/aws.svg | 34 +++++++ .../out/assets/logos/bedrock.svg | 1 + .../out/assets/logos/cerebras.svg | 89 ++++++++++++++++++ .../out/assets/logos/cohere.svg | 1 + .../out/assets/logos/databricks.svg | 1 + .../out/assets/logos/deepseek.svg | 25 +++++ .../out/assets/logos/fireworks.svg | 1 + .../out/assets/logos/google.svg | 2 + .../out/assets/logos/groq.svg | 3 + .../out/assets/logos/microsoft_azure.svg | 72 ++++++++++++++ .../out/assets/logos/mistral.svg | 1 + .../out/assets/logos/ollama.svg | 7 ++ .../out/assets/logos/openai_small.svg | 5 + .../out/assets/logos/openrouter.svg | 39 ++++++++ .../out/assets/logos/perplexity-ai.svg | 16 ++++ .../out/assets/logos/sambanova.svg | 1 + .../out/assets/logos/togetherai.svg | 14 +++ ui/litellm-dashboard/out/assets/logos/xai.svg | 28 ++++++ ui/litellm-dashboard/out/index.html | 2 +- ui/litellm-dashboard/out/index.txt | 4 +- ui/litellm-dashboard/out/model_hub.html | 2 +- ui/litellm-dashboard/out/model_hub.txt | 4 +- ui/litellm-dashboard/out/onboarding.html | 2 +- ui/litellm-dashboard/out/onboarding.txt | 4 +- .../src/components/model_dashboard.tsx | 14 ++- 82 files changed, 770 insertions(+), 51 deletions(-) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/250-601568e45a5ffece.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/250-a75ee9d79f1140b0.js rename ui/litellm-dashboard/out/_next/static/chunks/394-0222ddf4d701e0b4.js => litellm/proxy/_experimental/out/_next/static/chunks/394-48a36e9c9b2cb488.js (66%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/layout-429ad74a94df7643.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/layout-af8319e6c59a08da.js rename ui/litellm-dashboard/out/_next/static/chunks/app/model_hub/page-068a441595bd0fc3.js => litellm/proxy/_experimental/out/_next/static/chunks/app/model_hub/page-cde2fb783e81a6c1.js (60%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/onboarding/page-1ffe69692e4b2037.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/onboarding/page-5110f2c6a3c9a2f4.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/page-75d771fb848b47a8.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/page-e21d4be3d6c3c16e.js rename litellm/proxy/_experimental/out/_next/static/chunks/{main-app-475d6efe4080647d.js => main-app-4f7318ae681a6d94.js} (54%) rename litellm/proxy/_experimental/out/_next/static/{9yIyUkG6nV2cO0gn7kJ-Q => soi--ciJeUE6G2Fk4NMBG}/_buildManifest.js (100%) rename litellm/proxy/_experimental/out/_next/static/{9yIyUkG6nV2cO0gn7kJ-Q => soi--ciJeUE6G2Fk4NMBG}/_ssgManifest.js (100%) create mode 100644 litellm/proxy/_experimental/out/assets/logos/anthropic.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/assemblyai_small.png create mode 100644 litellm/proxy/_experimental/out/assets/logos/aws.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/bedrock.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/cerebras.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/cohere.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/databricks.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/deepseek.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/fireworks.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/google.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/groq.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/microsoft_azure.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/mistral.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/ollama.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/openai_small.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/openrouter.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/perplexity-ai.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/sambanova.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/togetherai.svg create mode 100644 litellm/proxy/_experimental/out/assets/logos/xai.svg create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/250-601568e45a5ffece.js delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/250-a75ee9d79f1140b0.js rename litellm/proxy/_experimental/out/_next/static/chunks/394-0222ddf4d701e0b4.js => ui/litellm-dashboard/out/_next/static/chunks/394-48a36e9c9b2cb488.js (66%) create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/layout-429ad74a94df7643.js delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/layout-af8319e6c59a08da.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/model_hub/page-068a441595bd0fc3.js => ui/litellm-dashboard/out/_next/static/chunks/app/model_hub/page-cde2fb783e81a6c1.js (60%) delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/onboarding/page-1ffe69692e4b2037.js create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/onboarding/page-5110f2c6a3c9a2f4.js delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/page-75d771fb848b47a8.js create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/page-e21d4be3d6c3c16e.js rename ui/litellm-dashboard/out/_next/static/chunks/{main-app-475d6efe4080647d.js => main-app-4f7318ae681a6d94.js} (54%) rename ui/litellm-dashboard/out/_next/static/{9yIyUkG6nV2cO0gn7kJ-Q => soi--ciJeUE6G2Fk4NMBG}/_buildManifest.js (100%) rename ui/litellm-dashboard/out/_next/static/{9yIyUkG6nV2cO0gn7kJ-Q => soi--ciJeUE6G2Fk4NMBG}/_ssgManifest.js (100%) create mode 100644 ui/litellm-dashboard/out/assets/logos/anthropic.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/assemblyai_small.png create mode 100644 ui/litellm-dashboard/out/assets/logos/aws.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/bedrock.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/cerebras.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/cohere.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/databricks.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/deepseek.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/fireworks.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/google.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/groq.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/microsoft_azure.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/mistral.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/ollama.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/openai_small.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/openrouter.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/perplexity-ai.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/sambanova.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/togetherai.svg create mode 100644 ui/litellm-dashboard/out/assets/logos/xai.svg diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/250-601568e45a5ffece.js b/litellm/proxy/_experimental/out/_next/static/chunks/250-601568e45a5ffece.js new file mode 100644 index 0000000000..e6ec4d178e --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/250-601568e45a5ffece.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[250],{19250:function(e,t,o){o.d(t,{$I:function(){return M},AZ:function(){return U},Au:function(){return ep},BL:function(){return eI},Br:function(){return F},E9:function(){return eV},EG:function(){return eD},EY:function(){return eM},Eb:function(){return N},FC:function(){return ea},Gh:function(){return eb},H1:function(){return v},H2:function(){return n},Hx:function(){return eu},I1:function(){return j},It:function(){return x},J$:function(){return W},K8:function(){return d},K_:function(){return eH},LY:function(){return ev},Lp:function(){return eB},N3:function(){return eT},N8:function(){return $},NL:function(){return eQ},NV:function(){return f},Nc:function(){return eC},O3:function(){return eJ},OD:function(){return ef},OU:function(){return es},Of:function(){return b},Og:function(){return y},Ov:function(){return E},PT:function(){return D},Qg:function(){return eE},RQ:function(){return _},Rg:function(){return X},Sb:function(){return eP},So:function(){return Q},Tj:function(){return eq},VA:function(){return G},Vt:function(){return eU},W_:function(){return J},X:function(){return ee},XO:function(){return k},Xd:function(){return em},Xm:function(){return S},YU:function(){return eR},Zr:function(){return m},a6:function(){return B},ao:function(){return eZ},b1:function(){return ec},cq:function(){return A},cu:function(){return eS},eH:function(){return H},eZ:function(){return eN},fP:function(){return K},g:function(){return eX},gX:function(){return ej},h3:function(){return er},hT:function(){return ek},hy:function(){return u},ix:function(){return L},j2:function(){return et},jA:function(){return eL},jE:function(){return eA},kK:function(){return p},kn:function(){return Z},lP:function(){return h},lg:function(){return eg},mR:function(){return Y},m_:function(){return I},mp:function(){return ez},n$:function(){return eh},nd:function(){return eY},o6:function(){return q},oC:function(){return e_},pf:function(){return eG},qI:function(){return g},qk:function(){return e$},qm:function(){return w},r6:function(){return O},rs:function(){return C},s0:function(){return R},sN:function(){return ex},t$:function(){return P},t0:function(){return ey},t3:function(){return eK},tN:function(){return en},u5:function(){return eo},um:function(){return eF},v9:function(){return ew},vh:function(){return eO},wX:function(){return T},wd:function(){return el},xA:function(){return ed},zg:function(){return ei}});var r=o(20347),a=o(41021);let n=null;console.log=function(){};let c=0,s=e=>new Promise(t=>setTimeout(t,e)),l=async e=>{let t=Date.now();t-c>6e4?(e.includes("Authentication Error - Expired Key")&&(a.ZP.info("UI Session Expired. Logging out."),c=t,await s(3e3),document.cookie="token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;",window.location.href="/"),c=t):console.log("Error suppressed to prevent spam:",e)},i="Authorization";function d(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"Authorization";console.log("setGlobalLitellmHeaderName: ".concat(e)),i=e}let h=async()=>{let e=n?"".concat(n,"/openapi.json"):"/openapi.json",t=await fetch(e);return await t.json()},w=async e=>{try{let t=n?"".concat(n,"/get/litellm_model_cost_map"):"/get/litellm_model_cost_map",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}}),r=await o.json();return console.log("received litellm model cost data: ".concat(r)),r}catch(e){throw console.error("Failed to get model cost map:",e),e}},p=async(e,t)=>{try{let o=n?"".concat(n,"/model/new"):"/model/new",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text()||"Network response was not ok";throw a.ZP.error(e),Error(e)}let c=await r.json();return console.log("API Response:",c),a.ZP.destroy(),a.ZP.success("Model ".concat(t.model_name," created successfully"),2),c}catch(e){throw console.error("Failed to create key:",e),e}},u=async e=>{try{let t=n?"".concat(n,"/model/settings"):"/model/settings",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){console.error("Failed to get model settings:",e)}},y=async(e,t)=>{console.log("model_id in model delete call: ".concat(t));try{let o=n?"".concat(n,"/model/delete"):"/model/delete",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({id:t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},f=async(e,t)=>{if(console.log("budget_id in budget delete call: ".concat(t)),null!=e)try{let o=n?"".concat(n,"/budget/delete"):"/budget/delete",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({id:t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},m=async(e,t)=>{try{console.log("Form Values in budgetCreateCall:",t),console.log("Form Values after check:",t);let o=n?"".concat(n,"/budget/new"):"/budget/new",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},g=async(e,t)=>{try{console.log("Form Values in budgetUpdateCall:",t),console.log("Form Values after check:",t);let o=n?"".concat(n,"/budget/update"):"/budget/update",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},k=async(e,t)=>{try{let o=n?"".concat(n,"/invitation/new"):"/invitation/new",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},_=async e=>{try{let t=n?"".concat(n,"/alerting/settings"):"/alerting/settings",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},T=async(e,t,o)=>{try{if(console.log("Form Values in keyCreateCall:",o),o.description&&(o.metadata||(o.metadata={}),o.metadata.description=o.description,delete o.description,o.metadata=JSON.stringify(o.metadata)),o.metadata){console.log("formValues.metadata:",o.metadata);try{o.metadata=JSON.parse(o.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",o);let r=n?"".concat(n,"/key/generate"):"/key/generate",a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error(e)}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},E=async(e,t,o)=>{try{if(console.log("Form Values in keyCreateCall:",o),o.description&&(o.metadata||(o.metadata={}),o.metadata.description=o.description,delete o.description,o.metadata=JSON.stringify(o.metadata)),o.metadata){console.log("formValues.metadata:",o.metadata);try{o.metadata=JSON.parse(o.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",o);let r=n?"".concat(n,"/user/new"):"/user/new",a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error(e)}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},j=async(e,t)=>{try{let o=n?"".concat(n,"/key/delete"):"/key/delete";console.log("in keyDeleteCall:",t);let r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({keys:[t]})});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log(a),a}catch(e){throw console.error("Failed to create key:",e),e}},N=async(e,t)=>{try{let o=n?"".concat(n,"/user/delete"):"/user/delete";console.log("in userDeleteCall:",t);let r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_ids:t})});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log(a),a}catch(e){throw console.error("Failed to delete user(s):",e),e}},C=async(e,t)=>{try{let o=n?"".concat(n,"/team/delete"):"/team/delete";console.log("in teamDeleteCall:",t);let r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_ids:[t]})});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log(a),a}catch(e){throw console.error("Failed to delete key:",e),e}},b=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;try{let a=n?"".concat(n,"/user/list"):"/user/list";console.log("in userListCall");let c=new URLSearchParams;if(t&&t.length>0){let e=t.join(",");c.append("user_ids",e)}o&&c.append("page",o.toString()),r&&c.append("page_size",r.toString());let s=c.toString();s&&(a+="?".concat(s));let d=await fetch(a,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!d.ok){let e=await d.text();throw l(e),Error("Network response was not ok")}let h=await d.json();return console.log("/user/list API Response:",h),h}catch(e){throw console.error("Failed to create key:",e),e}},F=async function(e,t,o){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],a=arguments.length>4?arguments[4]:void 0,c=arguments.length>5?arguments[5]:void 0;try{let s;if(r){s=n?"".concat(n,"/user/list"):"/user/list";let e=new URLSearchParams;null!=a&&e.append("page",a.toString()),null!=c&&e.append("page_size",c.toString()),s+="?".concat(e.toString())}else s=n?"".concat(n,"/user/info"):"/user/info","Admin"===o||"Admin Viewer"===o||t&&(s+="?user_id=".concat(t));console.log("Requesting user data from:",s);let d=await fetch(s,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!d.ok){let e=await d.text();throw l(e),Error("Network response was not ok")}let h=await d.json();return console.log("API Response:",h),h}catch(e){throw console.error("Failed to fetch user data:",e),e}},S=async(e,t)=>{try{let o=n?"".concat(n,"/team/info"):"/team/info";t&&(o="".concat(o,"?team_id=").concat(t)),console.log("in teamInfoCall");let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},x=async function(e,t){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;try{let r=n?"".concat(n,"/team/list"):"/team/list";console.log("in teamInfoCall");let a=new URLSearchParams;o&&a.append("user_id",o.toString()),t&&a.append("organization_id",t.toString());let c=a.toString();c&&(r+="?".concat(c));let s=await fetch(r,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!s.ok){let e=await s.text();throw l(e),Error("Network response was not ok")}let d=await s.json();return console.log("/team/list API Response:",d),d}catch(e){throw console.error("Failed to create key:",e),e}},B=async e=>{try{let t=n?"".concat(n,"/team/available"):"/team/available";console.log("in availableTeamListCall");let o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log("/team/available_teams API Response:",r),r}catch(e){throw e}},O=async e=>{try{let t=n?"".concat(n,"/organization/list"):"/organization/list",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},P=async(e,t)=>{try{let o=n?"".concat(n,"/organization/info"):"/organization/info";t&&(o="".concat(o,"?organization_id=").concat(t)),console.log("in teamInfoCall");let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},v=async(e,t)=>{try{if(console.log("Form Values in organizationCreateCall:",t),t.metadata){console.log("formValues.metadata:",t.metadata);try{t.metadata=JSON.parse(t.metadata)}catch(e){throw console.error("Failed to parse metadata:",e),Error("Failed to parse metadata: "+e)}}let o=n?"".concat(n,"/organization/new"):"/organization/new",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},G=async(e,t)=>{try{console.log("Form Values in organizationUpdateCall:",t);let o=n?"".concat(n,"/organization/update"):"/organization/update",r=await fetch(o,{method:"PATCH",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("Update Team Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},A=async(e,t)=>{try{let o=n?"".concat(n,"/organization/delete"):"/organization/delete",r=await fetch(o,{method:"DELETE",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_ids:[t]})});if(!r.ok){let e=await r.text();throw l(e),Error("Error deleting organization: ".concat(e))}return await r.json()}catch(e){throw console.error("Failed to delete organization:",e),e}},J=async e=>{try{let t=n?"".concat(n,"/onboarding/get_token"):"/onboarding/get_token";t+="?invite_link=".concat(e);let o=await fetch(t,{method:"GET",headers:{"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},I=async(e,t,o,r)=>{let a=n?"".concat(n,"/onboarding/claim_token"):"/onboarding/claim_token";try{let n=await fetch(a,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({invitation_link:t,user_id:o,password:r})});if(!n.ok){let e=await n.text();throw l(e),Error("Network response was not ok")}let c=await n.json();return console.log(c),c}catch(e){throw console.error("Failed to delete key:",e),e}},R=async(e,t,o)=>{try{let r=n?"".concat(n,"/key/").concat(t,"/regenerate"):"/key/".concat(t,"/regenerate"),a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify(o)});if(!a.ok){let e=await a.text();throw l(e),Error("Network response was not ok")}let c=await a.json();return console.log("Regenerate key Response:",c),c}catch(e){throw console.error("Failed to regenerate key:",e),e}},z=!1,V=null,U=async(e,t,o)=>{try{console.log("modelInfoCall:",e,t,o);let c=n?"".concat(n,"/v2/model/info"):"/v2/model/info";r.ZL.includes(o)||(c+="?user_models_only=true");let s=await fetch(c,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!s.ok){let e=await s.text();throw e+="error shown=".concat(z),z||(e.includes("No model list passed")&&(e="No Models Exist. Click Add Model to get started."),a.ZP.info(e,10),z=!0,V&&clearTimeout(V),V=setTimeout(()=>{z=!1},1e4)),Error("Network response was not ok")}let l=await s.json();return console.log("modelInfoCall:",l),l}catch(e){throw console.error("Failed to create key:",e),e}},L=async(e,t)=>{try{let o=n?"".concat(n,"/v1/model/info"):"/v1/model/info";o+="?litellm_model_id=".concat(t);let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok)throw await r.text(),Error("Network response was not ok");let a=await r.json();return console.log("modelInfoV1Call:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},Z=async e=>{try{let t=n?"".concat(n,"/model_group/info"):"/model_group/info",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log("modelHubCall:",r),r}catch(e){throw console.error("Failed to create key:",e),e}},D=async e=>{try{let t=n?"".concat(n,"/get/allowed_ips"):"/get/allowed_ips",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw Error("Network response was not ok: ".concat(e))}let r=await o.json();return console.log("getAllowedIPs:",r),r.data}catch(e){throw console.error("Failed to get allowed IPs:",e),e}},H=async(e,t)=>{try{let o=n?"".concat(n,"/add/allowed_ip"):"/add/allowed_ip",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({ip:t})});if(!r.ok){let e=await r.text();throw Error("Network response was not ok: ".concat(e))}let a=await r.json();return console.log("addAllowedIP:",a),a}catch(e){throw console.error("Failed to add allowed IP:",e),e}},M=async(e,t)=>{try{let o=n?"".concat(n,"/delete/allowed_ip"):"/delete/allowed_ip",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({ip:t})});if(!r.ok){let e=await r.text();throw Error("Network response was not ok: ".concat(e))}let a=await r.json();return console.log("deleteAllowedIP:",a),a}catch(e){throw console.error("Failed to delete allowed IP:",e),e}},q=async(e,t,o,r,a,c,s,d)=>{try{let t=n?"".concat(n,"/model/metrics"):"/model/metrics";r&&(t="".concat(t,"?_selected_model_group=").concat(r,"&startTime=").concat(a,"&endTime=").concat(c,"&api_key=").concat(s,"&customer=").concat(d));let o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},X=async(e,t,o,r)=>{try{let a=n?"".concat(n,"/model/streaming_metrics"):"/model/streaming_metrics";t&&(a="".concat(a,"?_selected_model_group=").concat(t,"&startTime=").concat(o,"&endTime=").concat(r));let c=await fetch(a,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!c.ok){let e=await c.text();throw l(e),Error("Network response was not ok")}return await c.json()}catch(e){throw console.error("Failed to create key:",e),e}},K=async(e,t,o,r,a,c,s,d)=>{try{let t=n?"".concat(n,"/model/metrics/slow_responses"):"/model/metrics/slow_responses";r&&(t="".concat(t,"?_selected_model_group=").concat(r,"&startTime=").concat(a,"&endTime=").concat(c,"&api_key=").concat(s,"&customer=").concat(d));let o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},$=async(e,t,o,r,a,c,s,d)=>{try{let t=n?"".concat(n,"/model/metrics/exceptions"):"/model/metrics/exceptions";r&&(t="".concat(t,"?_selected_model_group=").concat(r,"&startTime=").concat(a,"&endTime=").concat(c,"&api_key=").concat(s,"&customer=").concat(d));let o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},Q=async function(e,t,o){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],a=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;console.log("in /models calls, globalLitellmHeaderName",i);try{let t=n?"".concat(n,"/models"):"/models",o=new URLSearchParams;!0===r&&o.append("return_wildcard_routes","True"),a&&o.append("team_id",a.toString()),o.toString()&&(t+="?".concat(o.toString()));let c=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!c.ok){let e=await c.text();throw l(e),Error("Network response was not ok")}return await c.json()}catch(e){throw console.error("Failed to create key:",e),e}},Y=async e=>{try{let t=n?"".concat(n,"/global/spend/teams"):"/global/spend/teams";console.log("in teamSpendLogsCall:",t);let o=await fetch("".concat(t),{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},W=async(e,t,o,r)=>{try{let a=n?"".concat(n,"/global/spend/tags"):"/global/spend/tags";t&&o&&(a="".concat(a,"?start_date=").concat(t,"&end_date=").concat(o)),r&&(a+="".concat(a,"&tags=").concat(r.join(","))),console.log("in tagsSpendLogsCall:",a);let c=await fetch("".concat(a),{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to create key:",e),e}},ee=async e=>{try{let t=n?"".concat(n,"/global/spend/all_tag_names"):"/global/spend/all_tag_names";console.log("in global/spend/all_tag_names call",t);let o=await fetch("".concat(t),{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},et=async e=>{try{let t=n?"".concat(n,"/global/all_end_users"):"/global/all_end_users";console.log("in global/all_end_users call",t);let o=await fetch("".concat(t),{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},eo=async(e,t)=>{try{let o=n?"".concat(n,"/user/filter/ui"):"/user/filter/ui";t.get("user_email")&&(o+="?user_email=".concat(t.get("user_email"))),t.get("user_id")&&(o+="?user_id=".concat(t.get("user_id")));let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to create key:",e),e}},er=async(e,t,o,r,a,c,s,d,h)=>{try{let w=n?"".concat(n,"/spend/logs/ui"):"/spend/logs/ui",p=new URLSearchParams;t&&p.append("api_key",t),o&&p.append("team_id",o),r&&p.append("request_id",r),a&&p.append("start_date",a),c&&p.append("end_date",c),s&&p.append("page",s.toString()),d&&p.append("page_size",d.toString()),h&&p.append("user_id",h);let u=p.toString();u&&(w+="?".concat(u));let y=await fetch(w,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!y.ok){let e=await y.text();throw l(e),Error("Network response was not ok")}let f=await y.json();return console.log("Spend Logs Response:",f),f}catch(e){throw console.error("Failed to fetch spend logs:",e),e}},ea=async e=>{try{let t=n?"".concat(n,"/global/spend/logs"):"/global/spend/logs",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},en=async e=>{try{let t=n?"".concat(n,"/global/spend/keys?limit=5"):"/global/spend/keys?limit=5",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},ec=async(e,t,o,r)=>{try{let a=n?"".concat(n,"/global/spend/end_users"):"/global/spend/end_users",c="";c=t?JSON.stringify({api_key:t,startTime:o,endTime:r}):JSON.stringify({startTime:o,endTime:r});let s={method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:c},d=await fetch(a,s);if(!d.ok){let e=await d.text();throw l(e),Error("Network response was not ok")}let h=await d.json();return console.log(h),h}catch(e){throw console.error("Failed to create key:",e),e}},es=async(e,t,o,r)=>{try{let a=n?"".concat(n,"/global/spend/provider"):"/global/spend/provider";o&&r&&(a+="?start_date=".concat(o,"&end_date=").concat(r)),t&&(a+="&api_key=".concat(t));let c={method:"GET",headers:{[i]:"Bearer ".concat(e)}},s=await fetch(a,c);if(!s.ok){let e=await s.text();throw l(e),Error("Network response was not ok")}let d=await s.json();return console.log(d),d}catch(e){throw console.error("Failed to fetch spend data:",e),e}},el=async(e,t,o)=>{try{let r=n?"".concat(n,"/global/activity"):"/global/activity";t&&o&&(r+="?start_date=".concat(t,"&end_date=").concat(o));let a={method:"GET",headers:{[i]:"Bearer ".concat(e)}},c=await fetch(r,a);if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ei=async(e,t,o)=>{try{let r=n?"".concat(n,"/global/activity/cache_hits"):"/global/activity/cache_hits";t&&o&&(r+="?start_date=".concat(t,"&end_date=").concat(o));let a={method:"GET",headers:{[i]:"Bearer ".concat(e)}},c=await fetch(r,a);if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ed=async(e,t,o)=>{try{let r=n?"".concat(n,"/global/activity/model"):"/global/activity/model";t&&o&&(r+="?start_date=".concat(t,"&end_date=").concat(o));let a={method:"GET",headers:{[i]:"Bearer ".concat(e)}},c=await fetch(r,a);if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to fetch spend data:",e),e}},eh=async(e,t,o,r)=>{try{let a=n?"".concat(n,"/global/activity/exceptions"):"/global/activity/exceptions";t&&o&&(a+="?start_date=".concat(t,"&end_date=").concat(o)),r&&(a+="&model_group=".concat(r));let c={method:"GET",headers:{[i]:"Bearer ".concat(e)}},s=await fetch(a,c);if(!s.ok)throw await s.text(),Error("Network response was not ok");let l=await s.json();return console.log(l),l}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ew=async(e,t,o,r)=>{try{let a=n?"".concat(n,"/global/activity/exceptions/deployment"):"/global/activity/exceptions/deployment";t&&o&&(a+="?start_date=".concat(t,"&end_date=").concat(o)),r&&(a+="&model_group=".concat(r));let c={method:"GET",headers:{[i]:"Bearer ".concat(e)}},s=await fetch(a,c);if(!s.ok)throw await s.text(),Error("Network response was not ok");let l=await s.json();return console.log(l),l}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ep=async e=>{try{let t=n?"".concat(n,"/global/spend/models?limit=5"):"/global/spend/models?limit=5",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},eu=async(e,t,o)=>{try{console.log("Sending model connection test request:",JSON.stringify(t));let a=n?"".concat(n,"/health/test_connection"):"/health/test_connection",c=await fetch(a,{method:"POST",headers:{"Content-Type":"application/json",[i]:"Bearer ".concat(e)},body:JSON.stringify({litellm_params:t,mode:o})}),s=c.headers.get("content-type");if(!s||!s.includes("application/json")){let e=await c.text();throw console.error("Received non-JSON response:",e),Error("Received non-JSON response (".concat(c.status,": ").concat(c.statusText,"). Check network tab for details."))}let l=await c.json();if(!c.ok||"error"===l.status){if("error"===l.status);else{var r;return{status:"error",message:(null===(r=l.error)||void 0===r?void 0:r.message)||"Connection test failed: ".concat(c.status," ").concat(c.statusText)}}}return l}catch(e){throw console.error("Model connection test error:",e),e}},ey=async(e,t)=>{try{console.log("entering keyInfoV1Call");let o=n?"".concat(n,"/key/info"):"/key/info";o="".concat(o,"?key=").concat(t);let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(console.log("response",r),!r.ok){let e=await r.text();l(e),a.ZP.error("Failed to fetch key info - "+e)}let c=await r.json();return console.log("data",c),c}catch(e){throw console.error("Failed to fetch key info:",e),e}},ef=async(e,t,o,r,a)=>{try{let c=n?"".concat(n,"/key/list"):"/key/list";console.log("in keyListCall");let s=new URLSearchParams;o&&s.append("team_id",o.toString()),t&&s.append("organization_id",t.toString()),r&&s.append("page",r.toString()),a&&s.append("size",a.toString()),s.append("return_full_object","true"),s.append("include_team_keys","true");let d=s.toString();d&&(c+="?".concat(d));let h=await fetch(c,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!h.ok){let e=await h.text();throw l(e),Error("Network response was not ok")}let w=await h.json();return console.log("/team/list API Response:",w),w}catch(e){throw console.error("Failed to create key:",e),e}},em=async(e,t)=>{try{let o=n?"".concat(n,"/user/get_users?role=").concat(t):"/user/get_users?role=".concat(t);console.log("in userGetAllUsersCall:",o);let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log(a),a}catch(e){throw console.error("Failed to get requested models:",e),e}},eg=async e=>{try{let t=n?"".concat(n,"/user/available_roles"):"/user/available_roles",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log("response from user/available_role",r),r}catch(e){throw e}},ek=async(e,t)=>{try{if(console.log("Form Values in teamCreateCall:",t),t.metadata){console.log("formValues.metadata:",t.metadata);try{t.metadata=JSON.parse(t.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}let o=n?"".concat(n,"/team/new"):"/team/new",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},e_=async(e,t)=>{try{if(console.log("Form Values in credentialCreateCall:",t),t.metadata){console.log("formValues.metadata:",t.metadata);try{t.metadata=JSON.parse(t.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}let o=n?"".concat(n,"/credentials"):"/credentials",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("API Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},eT=async e=>{try{let t=n?"".concat(n,"/credentials"):"/credentials";console.log("in credentialListCall");let o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log("/credentials API Response:",r),r}catch(e){throw console.error("Failed to create key:",e),e}},eE=async(e,t,o)=>{try{let r=n?"".concat(n,"/credentials"):"/credentials";t?r+="/by_name/".concat(t):o&&(r+="/by_model/".concat(o)),console.log("in credentialListCall");let a=await fetch(r,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!a.ok){let e=await a.text();throw l(e),Error("Network response was not ok")}let c=await a.json();return console.log("/credentials API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},ej=async(e,t)=>{try{let o=n?"".concat(n,"/credentials/").concat(t):"/credentials/".concat(t);console.log("in credentialDeleteCall:",t);let r=await fetch(o,{method:"DELETE",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let a=await r.json();return console.log(a),a}catch(e){throw console.error("Failed to delete key:",e),e}},eN=async(e,t,o)=>{try{if(console.log("Form Values in credentialUpdateCall:",o),o.metadata){console.log("formValues.metadata:",o.metadata);try{o.metadata=JSON.parse(o.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}let r=n?"".concat(n,"/credentials/").concat(t):"/credentials/".concat(t),a=await fetch(r,{method:"PATCH",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eC=async(e,t)=>{try{if(console.log("Form Values in keyUpdateCall:",t),t.model_tpm_limit){console.log("formValues.model_tpm_limit:",t.model_tpm_limit);try{t.model_tpm_limit=JSON.parse(t.model_tpm_limit)}catch(e){throw Error("Failed to parse model_tpm_limit: "+e)}}if(t.model_rpm_limit){console.log("formValues.model_rpm_limit:",t.model_rpm_limit);try{t.model_rpm_limit=JSON.parse(t.model_rpm_limit)}catch(e){throw Error("Failed to parse model_rpm_limit: "+e)}}let o=n?"".concat(n,"/key/update"):"/key/update",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("Update key Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},eb=async(e,t)=>{try{console.log("Form Values in teamUpateCall:",t);let o=n?"".concat(n,"/team/update"):"/team/update",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("Update Team Response:",a),a}catch(e){throw console.error("Failed to create key:",e),e}},eF=async(e,t)=>{try{console.log("Form Values in modelUpateCall:",t);let o=n?"".concat(n,"/model/update"):"/model/update",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),console.error("Error update from the server:",e),Error("Network response was not ok")}let a=await r.json();return console.log("Update model Response:",a),a}catch(e){throw console.error("Failed to update model:",e),e}},eS=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=n?"".concat(n,"/team/member_add"):"/team/member_add",a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_id:t,member:o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},ex=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=n?"".concat(n,"/team/member_update"):"/team/member_update",a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_id:t,role:o.role,user_id:o.user_id})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eB=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=n?"".concat(n,"/team/member_delete"):"/team/member_delete",a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_id:t,...void 0!==o.user_email&&{user_email:o.user_email},...void 0!==o.user_id&&{user_id:o.user_id}})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eO=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=n?"".concat(n,"/organization/member_add"):"/organization/member_add",a=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_id:t,member:o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error(e)}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create organization member:",e),e}},eP=async(e,t,o)=>{try{console.log("Form Values in organizationMemberDeleteCall:",o);let r=n?"".concat(n,"/organization/member_delete"):"/organization/member_delete",a=await fetch(r,{method:"DELETE",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_id:t,user_id:o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to delete organization member:",e),e}},ev=async(e,t,o)=>{try{console.log("Form Values in organizationMemberUpdateCall:",o);let r=n?"".concat(n,"/organization/member_update"):"/organization/member_update",a=await fetch(r,{method:"PATCH",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_id:t,...o})});if(!a.ok){let e=await a.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await a.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to update organization member:",e),e}},eG=async(e,t,o)=>{try{console.log("Form Values in userUpdateUserCall:",t);let r=n?"".concat(n,"/user/update"):"/user/update",a={...t};null!==o&&(a.user_role=o),a=JSON.stringify(a);let c=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:a});if(!c.ok){let e=await c.text();throw l(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let s=await c.json();return console.log("API Response:",s),s}catch(e){throw console.error("Failed to create key:",e),e}},eA=async(e,t)=>{try{let o=n?"".concat(n,"/health/services?service=").concat(t):"/health/services?service=".concat(t);console.log("Checking Slack Budget Alerts service health");let r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error(e)}let c=await r.json();return a.ZP.success("Test request to ".concat(t," made - check logs/alerts on ").concat(t," to verify")),c}catch(e){throw console.error("Failed to perform health check:",e),e}},eJ=async e=>{try{let t=n?"".concat(n,"/budget/list"):"/budget/list",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eI=async(e,t,o)=>{try{let t=n?"".concat(n,"/get/config/callbacks"):"/get/config/callbacks",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eR=async e=>{try{let t=n?"".concat(n,"/config/list?config_type=general_settings"):"/config/list?config_type=general_settings",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},ez=async e=>{try{let t=n?"".concat(n,"/config/pass_through_endpoint"):"/config/pass_through_endpoint",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eV=async(e,t)=>{try{let o=n?"".concat(n,"/config/field/info?field_name=").concat(t):"/config/field/info?field_name=".concat(t),r=await fetch(o,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok)throw await r.text(),Error("Network response was not ok");return await r.json()}catch(e){throw console.error("Failed to set callbacks:",e),e}},eU=async(e,t)=>{try{let o=n?"".concat(n,"/config/pass_through_endpoint"):"/config/pass_through_endpoint",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to set callbacks:",e),e}},eL=async(e,t,o)=>{try{let r=n?"".concat(n,"/config/field/update"):"/config/field/update",c=await fetch(r,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({field_name:t,field_value:o,config_type:"general_settings"})});if(!c.ok){let e=await c.text();throw l(e),Error("Network response was not ok")}let s=await c.json();return a.ZP.success("Successfully updated value!"),s}catch(e){throw console.error("Failed to set callbacks:",e),e}},eZ=async(e,t)=>{try{let o=n?"".concat(n,"/config/field/delete"):"/config/field/delete",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({field_name:t,config_type:"general_settings"})});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let c=await r.json();return a.ZP.success("Field reset on proxy"),c}catch(e){throw console.error("Failed to get callbacks:",e),e}},eD=async(e,t)=>{try{let o=n?"".concat(n,"/config/pass_through_endpoint?endpoint_id=").concat(t):"/config/pass_through_endpoint".concat(t),r=await fetch(o,{method:"DELETE",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eH=async(e,t)=>{try{let o=n?"".concat(n,"/config/update"):"/config/update",r=await fetch(o,{method:"POST",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to set callbacks:",e),e}},eM=async e=>{try{let t=n?"".concat(n,"/health"):"/health",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to call /health:",e),e}},eq=async e=>{try{let t=n?"".concat(n,"/cache/ping"):"/cache/ping",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error(e)}return await o.json()}catch(e){throw console.error("Failed to call /cache/ping:",e),e}},eX=async e=>{try{let t=n?"".concat(n,"/sso/get/ui_settings"):"/sso/get/ui_settings",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eK=async e=>{try{let t=n?"".concat(n,"/guardrails/list"):"/guardrails/list",o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log("Guardrails list response:",r),r}catch(e){throw console.error("Failed to fetch guardrails list:",e),e}},e$=async(e,t,o)=>{try{let r=n?"".concat(n,"/spend/logs/ui/").concat(t,"?start_date=").concat(encodeURIComponent(o)):"/spend/logs/ui/".concat(t,"?start_date=").concat(encodeURIComponent(o));console.log("Fetching log details from:",r);let a=await fetch(r,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!a.ok){let e=await a.text();throw l(e),Error("Network response was not ok")}let c=await a.json();return console.log("Fetched log details:",c),c}catch(e){throw console.error("Failed to fetch log details:",e),e}},eQ=async e=>{try{let t=n?"".concat(n,"/get/internal_user_settings"):"/get/internal_user_settings";console.log("Fetching SSO settings from:",t);let o=await fetch(t,{method:"GET",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw l(e),Error("Network response was not ok")}let r=await o.json();return console.log("Fetched SSO settings:",r),r}catch(e){throw console.error("Failed to fetch SSO settings:",e),e}},eY=async(e,t)=>{try{let o=n?"".concat(n,"/update/internal_user_settings"):"/update/internal_user_settings";console.log("Updating internal user settings:",t);let r=await fetch(o,{method:"PATCH",headers:{[i]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){let e=await r.text();throw l(e),Error("Network response was not ok")}let c=await r.json();return console.log("Updated internal user settings:",c),a.ZP.success("Internal user settings updated successfully"),c}catch(e){throw console.error("Failed to update internal user settings:",e),e}}},20347:function(e,t,o){o.d(t,{LQ:function(){return n},ZL:function(){return r},lo:function(){return a}});let r=["Admin","Admin Viewer","proxy_admin","proxy_admin_viewer","org_admin"],a=["Internal User","Internal Viewer"],n=["Internal User","Admin"]}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/250-a75ee9d79f1140b0.js b/litellm/proxy/_experimental/out/_next/static/chunks/250-a75ee9d79f1140b0.js deleted file mode 100644 index 882840d12b..0000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/250-a75ee9d79f1140b0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[250],{19250:function(e,t,o){o.d(t,{$I:function(){return H},AZ:function(){return V},Au:function(){return ew},BL:function(){return eA},Br:function(){return b},E9:function(){return ez},EG:function(){return eZ},EY:function(){return eH},Eb:function(){return j},FC:function(){return er},Gh:function(){return eC},H1:function(){return P},H2:function(){return a},Hx:function(){return ep},I1:function(){return E},It:function(){return S},J$:function(){return Q},K8:function(){return i},K_:function(){return eD},LY:function(){return eP},Lp:function(){return ex},N3:function(){return e_},N8:function(){return K},NL:function(){return e$},NV:function(){return y},Nc:function(){return eN},O3:function(){return eJ},OD:function(){return ey},OU:function(){return ec},Of:function(){return C},Og:function(){return u},Ov:function(){return T},PT:function(){return Z},Qg:function(){return eT},RQ:function(){return k},Rg:function(){return q},Sb:function(){return eO},So:function(){return $},Tj:function(){return eM},VA:function(){return v},Vt:function(){return eV},W_:function(){return J},X:function(){return W},XO:function(){return g},Xd:function(){return ef},Xm:function(){return F},YU:function(){return eR},Zr:function(){return f},a6:function(){return x},ao:function(){return eL},b1:function(){return en},cq:function(){return G},cu:function(){return eF},eH:function(){return D},eZ:function(){return ej},fP:function(){return X},g:function(){return eq},gX:function(){return eE},h3:function(){return eo},hT:function(){return eg},hy:function(){return p},ix:function(){return U},j2:function(){return ee},jA:function(){return eU},jE:function(){return eG},kK:function(){return w},kn:function(){return L},lP:function(){return d},lg:function(){return em},mR:function(){return Y},m_:function(){return A},mp:function(){return eI},n$:function(){return ed},nd:function(){return eY},o6:function(){return M},oC:function(){return ek},pf:function(){return ev},qI:function(){return m},qk:function(){return eK},qm:function(){return h},r6:function(){return B},rs:function(){return N},s0:function(){return R},sN:function(){return eS},t$:function(){return O},t0:function(){return eu},t3:function(){return eX},tN:function(){return ea},u5:function(){return et},um:function(){return eb},v9:function(){return eh},vh:function(){return eB},wX:function(){return _},wd:function(){return es},xA:function(){return ei},zg:function(){return el}});var r=o(41021);let a=null;console.log=function(){};let n=0,c=e=>new Promise(t=>setTimeout(t,e)),s=async e=>{let t=Date.now();t-n>6e4?(e.includes("Authentication Error - Expired Key")&&(r.ZP.info("UI Session Expired. Logging out."),n=t,await c(3e3),document.cookie="token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;",window.location.href="/"),n=t):console.log("Error suppressed to prevent spam:",e)},l="Authorization";function i(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"Authorization";console.log("setGlobalLitellmHeaderName: ".concat(e)),l=e}let d=async()=>{let e=a?"".concat(a,"/openapi.json"):"/openapi.json",t=await fetch(e);return await t.json()},h=async e=>{try{let t=a?"".concat(a,"/get/litellm_model_cost_map"):"/get/litellm_model_cost_map",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}}),r=await o.json();return console.log("received litellm model cost data: ".concat(r)),r}catch(e){throw console.error("Failed to get model cost map:",e),e}},w=async(e,t)=>{try{let c=a?"".concat(a,"/model/new"):"/model/new",s=await fetch(c,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!s.ok){var o,n;let e=await s.json(),t=(null===(n=e.error)||void 0===n?void 0:null===(o=n.message)||void 0===o?void 0:o.error)||"Network response was not ok";throw r.ZP.error(t),Error(t)}let i=await s.json();return console.log("API Response:",i),r.ZP.destroy(),r.ZP.success("Model ".concat(t.model_name," created successfully"),2),i}catch(e){throw console.error("Failed to create key:",e),e}},p=async e=>{try{let t=a?"".concat(a,"/model/settings"):"/model/settings",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},u=async(e,t)=>{console.log("model_id in model delete call: ".concat(t));try{let o=a?"".concat(a,"/model/delete"):"/model/delete",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({id:t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},y=async(e,t)=>{if(console.log("budget_id in budget delete call: ".concat(t)),null!=e)try{let o=a?"".concat(a,"/budget/delete"):"/budget/delete",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({id:t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},f=async(e,t)=>{try{console.log("Form Values in budgetCreateCall:",t),console.log("Form Values after check:",t);let o=a?"".concat(a,"/budget/new"):"/budget/new",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},m=async(e,t)=>{try{console.log("Form Values in budgetUpdateCall:",t),console.log("Form Values after check:",t);let o=a?"".concat(a,"/budget/update"):"/budget/update",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},g=async(e,t)=>{try{let o=a?"".concat(a,"/invitation/new"):"/invitation/new",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},k=async e=>{try{let t=a?"".concat(a,"/alerting/settings"):"/alerting/settings",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},_=async(e,t,o)=>{try{if(console.log("Form Values in keyCreateCall:",o),o.description&&(o.metadata||(o.metadata={}),o.metadata.description=o.description,delete o.description,o.metadata=JSON.stringify(o.metadata)),o.metadata){console.log("formValues.metadata:",o.metadata);try{o.metadata=JSON.parse(o.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",o);let r=a?"".concat(a,"/key/generate"):"/key/generate",n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error(e)}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},T=async(e,t,o)=>{try{if(console.log("Form Values in keyCreateCall:",o),o.description&&(o.metadata||(o.metadata={}),o.metadata.description=o.description,delete o.description,o.metadata=JSON.stringify(o.metadata)),o.metadata){console.log("formValues.metadata:",o.metadata);try{o.metadata=JSON.parse(o.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}console.log("Form Values after check:",o);let r=a?"".concat(a,"/user/new"):"/user/new",n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_id:t,...o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error(e)}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},E=async(e,t)=>{try{let o=a?"".concat(a,"/key/delete"):"/key/delete";console.log("in keyDeleteCall:",t);let r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({keys:[t]})});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log(n),n}catch(e){throw console.error("Failed to create key:",e),e}},j=async(e,t)=>{try{let o=a?"".concat(a,"/user/delete"):"/user/delete";console.log("in userDeleteCall:",t);let r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({user_ids:t})});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log(n),n}catch(e){throw console.error("Failed to delete user(s):",e),e}},N=async(e,t)=>{try{let o=a?"".concat(a,"/team/delete"):"/team/delete";console.log("in teamDeleteCall:",t);let r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_ids:[t]})});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log(n),n}catch(e){throw console.error("Failed to delete key:",e),e}},C=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;try{let n=a?"".concat(a,"/user/list"):"/user/list";console.log("in userListCall");let c=new URLSearchParams;if(t&&t.length>0){let e=t.join(",");c.append("user_ids",e)}o&&c.append("page",o.toString()),r&&c.append("page_size",r.toString());let i=c.toString();i&&(n+="?".concat(i));let d=await fetch(n,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!d.ok){let e=await d.text();throw s(e),Error("Network response was not ok")}let h=await d.json();return console.log("/user/list API Response:",h),h}catch(e){throw console.error("Failed to create key:",e),e}},b=async function(e,t,o){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],n=arguments.length>4?arguments[4]:void 0,c=arguments.length>5?arguments[5]:void 0;try{let i;if(r){i=a?"".concat(a,"/user/list"):"/user/list";let e=new URLSearchParams;null!=n&&e.append("page",n.toString()),null!=c&&e.append("page_size",c.toString()),i+="?".concat(e.toString())}else i=a?"".concat(a,"/user/info"):"/user/info","Admin"===o||"Admin Viewer"===o||t&&(i+="?user_id=".concat(t));console.log("Requesting user data from:",i);let d=await fetch(i,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!d.ok){let e=await d.text();throw s(e),Error("Network response was not ok")}let h=await d.json();return console.log("API Response:",h),h}catch(e){throw console.error("Failed to fetch user data:",e),e}},F=async(e,t)=>{try{let o=a?"".concat(a,"/team/info"):"/team/info";t&&(o="".concat(o,"?team_id=").concat(t)),console.log("in teamInfoCall");let r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},S=async function(e,t){let o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;try{let r=a?"".concat(a,"/team/list"):"/team/list";console.log("in teamInfoCall");let n=new URLSearchParams;o&&n.append("user_id",o.toString()),t&&n.append("organization_id",t.toString());let c=n.toString();c&&(r+="?".concat(c));let i=await fetch(r,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!i.ok){let e=await i.text();throw s(e),Error("Network response was not ok")}let d=await i.json();return console.log("/team/list API Response:",d),d}catch(e){throw console.error("Failed to create key:",e),e}},x=async e=>{try{let t=a?"".concat(a,"/team/available"):"/team/available";console.log("in availableTeamListCall");let o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log("/team/available_teams API Response:",r),r}catch(e){throw e}},B=async e=>{try{let t=a?"".concat(a,"/organization/list"):"/organization/list",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},O=async(e,t)=>{try{let o=a?"".concat(a,"/organization/info"):"/organization/info";t&&(o="".concat(o,"?organization_id=").concat(t)),console.log("in teamInfoCall");let r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},P=async(e,t)=>{try{if(console.log("Form Values in organizationCreateCall:",t),t.metadata){console.log("formValues.metadata:",t.metadata);try{t.metadata=JSON.parse(t.metadata)}catch(e){throw console.error("Failed to parse metadata:",e),Error("Failed to parse metadata: "+e)}}let o=a?"".concat(a,"/organization/new"):"/organization/new",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},v=async(e,t)=>{try{console.log("Form Values in organizationUpdateCall:",t);let o=a?"".concat(a,"/organization/update"):"/organization/update",r=await fetch(o,{method:"PATCH",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("Update Team Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},G=async(e,t)=>{try{let o=a?"".concat(a,"/organization/delete"):"/organization/delete",r=await fetch(o,{method:"DELETE",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_ids:[t]})});if(!r.ok){let e=await r.text();throw s(e),Error("Error deleting organization: ".concat(e))}return await r.json()}catch(e){throw console.error("Failed to delete organization:",e),e}},J=async e=>{try{let t=a?"".concat(a,"/onboarding/get_token"):"/onboarding/get_token";t+="?invite_link=".concat(e);let o=await fetch(t,{method:"GET",headers:{"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},A=async(e,t,o,r)=>{let n=a?"".concat(a,"/onboarding/claim_token"):"/onboarding/claim_token";try{let a=await fetch(n,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({invitation_link:t,user_id:o,password:r})});if(!a.ok){let e=await a.text();throw s(e),Error("Network response was not ok")}let c=await a.json();return console.log(c),c}catch(e){throw console.error("Failed to delete key:",e),e}},R=async(e,t,o)=>{try{let r=a?"".concat(a,"/key/").concat(t,"/regenerate"):"/key/".concat(t,"/regenerate"),n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify(o)});if(!n.ok){let e=await n.text();throw s(e),Error("Network response was not ok")}let c=await n.json();return console.log("Regenerate key Response:",c),c}catch(e){throw console.error("Failed to regenerate key:",e),e}},I=!1,z=null,V=async(e,t,o)=>{try{let t=a?"".concat(a,"/v2/model/info"):"/v2/model/info",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw e+="error shown=".concat(I),I||(e.includes("No model list passed")&&(e="No Models Exist. Click Add Model to get started."),r.ZP.info(e,10),I=!0,z&&clearTimeout(z),z=setTimeout(()=>{I=!1},1e4)),Error("Network response was not ok")}let n=await o.json();return console.log("modelInfoCall:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},U=async(e,t)=>{try{let o=a?"".concat(a,"/v1/model/info"):"/v1/model/info";o+="?litellm_model_id=".concat(t);let r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok)throw await r.text(),Error("Network response was not ok");let n=await r.json();return console.log("modelInfoV1Call:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},L=async e=>{try{let t=a?"".concat(a,"/model_group/info"):"/model_group/info",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log("modelHubCall:",r),r}catch(e){throw console.error("Failed to create key:",e),e}},Z=async e=>{try{let t=a?"".concat(a,"/get/allowed_ips"):"/get/allowed_ips",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw Error("Network response was not ok: ".concat(e))}let r=await o.json();return console.log("getAllowedIPs:",r),r.data}catch(e){throw console.error("Failed to get allowed IPs:",e),e}},D=async(e,t)=>{try{let o=a?"".concat(a,"/add/allowed_ip"):"/add/allowed_ip",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({ip:t})});if(!r.ok){let e=await r.text();throw Error("Network response was not ok: ".concat(e))}let n=await r.json();return console.log("addAllowedIP:",n),n}catch(e){throw console.error("Failed to add allowed IP:",e),e}},H=async(e,t)=>{try{let o=a?"".concat(a,"/delete/allowed_ip"):"/delete/allowed_ip",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({ip:t})});if(!r.ok){let e=await r.text();throw Error("Network response was not ok: ".concat(e))}let n=await r.json();return console.log("deleteAllowedIP:",n),n}catch(e){throw console.error("Failed to delete allowed IP:",e),e}},M=async(e,t,o,r,n,c,i,d)=>{try{let t=a?"".concat(a,"/model/metrics"):"/model/metrics";r&&(t="".concat(t,"?_selected_model_group=").concat(r,"&startTime=").concat(n,"&endTime=").concat(c,"&api_key=").concat(i,"&customer=").concat(d));let o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},q=async(e,t,o,r)=>{try{let n=a?"".concat(a,"/model/streaming_metrics"):"/model/streaming_metrics";t&&(n="".concat(n,"?_selected_model_group=").concat(t,"&startTime=").concat(o,"&endTime=").concat(r));let c=await fetch(n,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!c.ok){let e=await c.text();throw s(e),Error("Network response was not ok")}return await c.json()}catch(e){throw console.error("Failed to create key:",e),e}},X=async(e,t,o,r,n,c,i,d)=>{try{let t=a?"".concat(a,"/model/metrics/slow_responses"):"/model/metrics/slow_responses";r&&(t="".concat(t,"?_selected_model_group=").concat(r,"&startTime=").concat(n,"&endTime=").concat(c,"&api_key=").concat(i,"&customer=").concat(d));let o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},K=async(e,t,o,r,n,c,i,d)=>{try{let t=a?"".concat(a,"/model/metrics/exceptions"):"/model/metrics/exceptions";r&&(t="".concat(t,"?_selected_model_group=").concat(r,"&startTime=").concat(n,"&endTime=").concat(c,"&api_key=").concat(i,"&customer=").concat(d));let o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to create key:",e),e}},$=async function(e,t,o){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],n=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;console.log("in /models calls, globalLitellmHeaderName",l);try{let t=a?"".concat(a,"/models"):"/models",o=new URLSearchParams;!0===r&&o.append("return_wildcard_routes","True"),n&&o.append("team_id",n.toString()),o.toString()&&(t+="?".concat(o.toString()));let c=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!c.ok){let e=await c.text();throw s(e),Error("Network response was not ok")}return await c.json()}catch(e){throw console.error("Failed to create key:",e),e}},Y=async e=>{try{let t=a?"".concat(a,"/global/spend/teams"):"/global/spend/teams";console.log("in teamSpendLogsCall:",t);let o=await fetch("".concat(t),{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},Q=async(e,t,o,r)=>{try{let n=a?"".concat(a,"/global/spend/tags"):"/global/spend/tags";t&&o&&(n="".concat(n,"?start_date=").concat(t,"&end_date=").concat(o)),r&&(n+="".concat(n,"&tags=").concat(r.join(","))),console.log("in tagsSpendLogsCall:",n);let c=await fetch("".concat(n),{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to create key:",e),e}},W=async e=>{try{let t=a?"".concat(a,"/global/spend/all_tag_names"):"/global/spend/all_tag_names";console.log("in global/spend/all_tag_names call",t);let o=await fetch("".concat(t),{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},ee=async e=>{try{let t=a?"".concat(a,"/global/all_end_users"):"/global/all_end_users";console.log("in global/all_end_users call",t);let o=await fetch("".concat(t),{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},et=async(e,t)=>{try{let o=a?"".concat(a,"/user/filter/ui"):"/user/filter/ui";t.get("user_email")&&(o+="?user_email=".concat(t.get("user_email"))),t.get("user_id")&&(o+="?user_id=".concat(t.get("user_id")));let r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to create key:",e),e}},eo=async(e,t,o,r,n,c,i,d,h)=>{try{let w=a?"".concat(a,"/spend/logs/ui"):"/spend/logs/ui",p=new URLSearchParams;t&&p.append("api_key",t),o&&p.append("team_id",o),r&&p.append("request_id",r),n&&p.append("start_date",n),c&&p.append("end_date",c),i&&p.append("page",i.toString()),d&&p.append("page_size",d.toString()),h&&p.append("user_id",h);let u=p.toString();u&&(w+="?".concat(u));let y=await fetch(w,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!y.ok){let e=await y.text();throw s(e),Error("Network response was not ok")}let f=await y.json();return console.log("Spend Logs Response:",f),f}catch(e){throw console.error("Failed to fetch spend logs:",e),e}},er=async e=>{try{let t=a?"".concat(a,"/global/spend/logs"):"/global/spend/logs",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},ea=async e=>{try{let t=a?"".concat(a,"/global/spend/keys?limit=5"):"/global/spend/keys?limit=5",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},en=async(e,t,o,r)=>{try{let n=a?"".concat(a,"/global/spend/end_users"):"/global/spend/end_users",c="";c=t?JSON.stringify({api_key:t,startTime:o,endTime:r}):JSON.stringify({startTime:o,endTime:r});let i={method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:c},d=await fetch(n,i);if(!d.ok){let e=await d.text();throw s(e),Error("Network response was not ok")}let h=await d.json();return console.log(h),h}catch(e){throw console.error("Failed to create key:",e),e}},ec=async(e,t,o,r)=>{try{let n=a?"".concat(a,"/global/spend/provider"):"/global/spend/provider";o&&r&&(n+="?start_date=".concat(o,"&end_date=").concat(r)),t&&(n+="&api_key=".concat(t));let c={method:"GET",headers:{[l]:"Bearer ".concat(e)}},i=await fetch(n,c);if(!i.ok){let e=await i.text();throw s(e),Error("Network response was not ok")}let d=await i.json();return console.log(d),d}catch(e){throw console.error("Failed to fetch spend data:",e),e}},es=async(e,t,o)=>{try{let r=a?"".concat(a,"/global/activity"):"/global/activity";t&&o&&(r+="?start_date=".concat(t,"&end_date=").concat(o));let n={method:"GET",headers:{[l]:"Bearer ".concat(e)}},c=await fetch(r,n);if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to fetch spend data:",e),e}},el=async(e,t,o)=>{try{let r=a?"".concat(a,"/global/activity/cache_hits"):"/global/activity/cache_hits";t&&o&&(r+="?start_date=".concat(t,"&end_date=").concat(o));let n={method:"GET",headers:{[l]:"Bearer ".concat(e)}},c=await fetch(r,n);if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ei=async(e,t,o)=>{try{let r=a?"".concat(a,"/global/activity/model"):"/global/activity/model";t&&o&&(r+="?start_date=".concat(t,"&end_date=").concat(o));let n={method:"GET",headers:{[l]:"Bearer ".concat(e)}},c=await fetch(r,n);if(!c.ok)throw await c.text(),Error("Network response was not ok");let s=await c.json();return console.log(s),s}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ed=async(e,t,o,r)=>{try{let n=a?"".concat(a,"/global/activity/exceptions"):"/global/activity/exceptions";t&&o&&(n+="?start_date=".concat(t,"&end_date=").concat(o)),r&&(n+="&model_group=".concat(r));let c={method:"GET",headers:{[l]:"Bearer ".concat(e)}},s=await fetch(n,c);if(!s.ok)throw await s.text(),Error("Network response was not ok");let i=await s.json();return console.log(i),i}catch(e){throw console.error("Failed to fetch spend data:",e),e}},eh=async(e,t,o,r)=>{try{let n=a?"".concat(a,"/global/activity/exceptions/deployment"):"/global/activity/exceptions/deployment";t&&o&&(n+="?start_date=".concat(t,"&end_date=").concat(o)),r&&(n+="&model_group=".concat(r));let c={method:"GET",headers:{[l]:"Bearer ".concat(e)}},s=await fetch(n,c);if(!s.ok)throw await s.text(),Error("Network response was not ok");let i=await s.json();return console.log(i),i}catch(e){throw console.error("Failed to fetch spend data:",e),e}},ew=async e=>{try{let t=a?"".concat(a,"/global/spend/models?limit=5"):"/global/spend/models?limit=5",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log(r),r}catch(e){throw console.error("Failed to create key:",e),e}},ep=async(e,t,o)=>{try{console.log("Sending model connection test request:",JSON.stringify(t));let n=a?"".concat(a,"/health/test_connection"):"/health/test_connection",c=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",[l]:"Bearer ".concat(e)},body:JSON.stringify({litellm_params:t,mode:o})}),s=c.headers.get("content-type");if(!s||!s.includes("application/json")){let e=await c.text();throw console.error("Received non-JSON response:",e),Error("Received non-JSON response (".concat(c.status,": ").concat(c.statusText,"). Check network tab for details."))}let i=await c.json();if(!c.ok||"error"===i.status){if("error"===i.status);else{var r;return{status:"error",message:(null===(r=i.error)||void 0===r?void 0:r.message)||"Connection test failed: ".concat(c.status," ").concat(c.statusText)}}}return i}catch(e){throw console.error("Model connection test error:",e),e}},eu=async(e,t)=>{try{let o=a?"".concat(a,"/key/info"):"/key/info";o="".concat(o,"?key=").concat(t);let r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to fetch key info:",e),e}},ey=async(e,t,o,r,n)=>{try{let c=a?"".concat(a,"/key/list"):"/key/list";console.log("in keyListCall");let i=new URLSearchParams;o&&i.append("team_id",o.toString()),t&&i.append("organization_id",t.toString()),r&&i.append("page",r.toString()),n&&i.append("size",n.toString()),i.append("return_full_object","true"),i.append("include_team_keys","true");let d=i.toString();d&&(c+="?".concat(d));let h=await fetch(c,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!h.ok){let e=await h.text();throw s(e),Error("Network response was not ok")}let w=await h.json();return console.log("/team/list API Response:",w),w}catch(e){throw console.error("Failed to create key:",e),e}},ef=async(e,t)=>{try{let o=a?"".concat(a,"/user/get_users?role=").concat(t):"/user/get_users?role=".concat(t);console.log("in userGetAllUsersCall:",o);let r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log(n),n}catch(e){throw console.error("Failed to get requested models:",e),e}},em=async e=>{try{let t=a?"".concat(a,"/user/available_roles"):"/user/available_roles",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");let r=await o.json();return console.log("response from user/available_role",r),r}catch(e){throw e}},eg=async(e,t)=>{try{if(console.log("Form Values in teamCreateCall:",t),t.metadata){console.log("formValues.metadata:",t.metadata);try{t.metadata=JSON.parse(t.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}let o=a?"".concat(a,"/team/new"):"/team/new",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},ek=async(e,t)=>{try{if(console.log("Form Values in credentialCreateCall:",t),t.metadata){console.log("formValues.metadata:",t.metadata);try{t.metadata=JSON.parse(t.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}let o=a?"".concat(a,"/credentials"):"/credentials",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("API Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},e_=async e=>{try{let t=a?"".concat(a,"/credentials"):"/credentials";console.log("in credentialListCall");let o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log("/credentials API Response:",r),r}catch(e){throw console.error("Failed to create key:",e),e}},eT=async(e,t,o)=>{try{let r=a?"".concat(a,"/credentials"):"/credentials";t?r+="/by_name/".concat(t):o&&(r+="/by_model/".concat(o)),console.log("in credentialListCall");let n=await fetch(r,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!n.ok){let e=await n.text();throw s(e),Error("Network response was not ok")}let c=await n.json();return console.log("/credentials API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eE=async(e,t)=>{try{let o=a?"".concat(a,"/credentials/").concat(t):"/credentials/".concat(t);console.log("in credentialDeleteCall:",t);let r=await fetch(o,{method:"DELETE",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}let n=await r.json();return console.log(n),n}catch(e){throw console.error("Failed to delete key:",e),e}},ej=async(e,t,o)=>{try{if(console.log("Form Values in credentialUpdateCall:",o),o.metadata){console.log("formValues.metadata:",o.metadata);try{o.metadata=JSON.parse(o.metadata)}catch(e){throw Error("Failed to parse metadata: "+e)}}let r=a?"".concat(a,"/credentials/").concat(t):"/credentials/".concat(t),n=await fetch(r,{method:"PATCH",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eN=async(e,t)=>{try{if(console.log("Form Values in keyUpdateCall:",t),t.model_tpm_limit){console.log("formValues.model_tpm_limit:",t.model_tpm_limit);try{t.model_tpm_limit=JSON.parse(t.model_tpm_limit)}catch(e){throw Error("Failed to parse model_tpm_limit: "+e)}}if(t.model_rpm_limit){console.log("formValues.model_rpm_limit:",t.model_rpm_limit);try{t.model_rpm_limit=JSON.parse(t.model_rpm_limit)}catch(e){throw Error("Failed to parse model_rpm_limit: "+e)}}let o=a?"".concat(a,"/key/update"):"/key/update",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("Update key Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},eC=async(e,t)=>{try{console.log("Form Values in teamUpateCall:",t);let o=a?"".concat(a,"/team/update"):"/team/update",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("Update Team Response:",n),n}catch(e){throw console.error("Failed to create key:",e),e}},eb=async(e,t)=>{try{console.log("Form Values in modelUpateCall:",t);let o=a?"".concat(a,"/model/update"):"/model/update",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),console.error("Error update from the server:",e),Error("Network response was not ok")}let n=await r.json();return console.log("Update model Response:",n),n}catch(e){throw console.error("Failed to update model:",e),e}},eF=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=a?"".concat(a,"/team/member_add"):"/team/member_add",n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_id:t,member:o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eS=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=a?"".concat(a,"/team/member_update"):"/team/member_update",n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_id:t,role:o.role,user_id:o.user_id})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},ex=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=a?"".concat(a,"/team/member_delete"):"/team/member_delete",n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({team_id:t,...void 0!==o.user_email&&{user_email:o.user_email},...void 0!==o.user_id&&{user_id:o.user_id}})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create key:",e),e}},eB=async(e,t,o)=>{try{console.log("Form Values in teamMemberAddCall:",o);let r=a?"".concat(a,"/organization/member_add"):"/organization/member_add",n=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_id:t,member:o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error(e)}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to create organization member:",e),e}},eO=async(e,t,o)=>{try{console.log("Form Values in organizationMemberDeleteCall:",o);let r=a?"".concat(a,"/organization/member_delete"):"/organization/member_delete",n=await fetch(r,{method:"DELETE",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_id:t,user_id:o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to delete organization member:",e),e}},eP=async(e,t,o)=>{try{console.log("Form Values in organizationMemberUpdateCall:",o);let r=a?"".concat(a,"/organization/member_update"):"/organization/member_update",n=await fetch(r,{method:"PATCH",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({organization_id:t,...o})});if(!n.ok){let e=await n.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let c=await n.json();return console.log("API Response:",c),c}catch(e){throw console.error("Failed to update organization member:",e),e}},ev=async(e,t,o)=>{try{console.log("Form Values in userUpdateUserCall:",t);let r=a?"".concat(a,"/user/update"):"/user/update",n={...t};null!==o&&(n.user_role=o),n=JSON.stringify(n);let c=await fetch(r,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:n});if(!c.ok){let e=await c.text();throw s(e),console.error("Error response from the server:",e),Error("Network response was not ok")}let i=await c.json();return console.log("API Response:",i),i}catch(e){throw console.error("Failed to create key:",e),e}},eG=async(e,t)=>{try{let o=a?"".concat(a,"/health/services?service=").concat(t):"/health/services?service=".concat(t);console.log("Checking Slack Budget Alerts service health");let n=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!n.ok){let e=await n.text();throw s(e),Error(e)}let c=await n.json();return r.ZP.success("Test request to ".concat(t," made - check logs/alerts on ").concat(t," to verify")),c}catch(e){throw console.error("Failed to perform health check:",e),e}},eJ=async e=>{try{let t=a?"".concat(a,"/budget/list"):"/budget/list",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eA=async(e,t,o)=>{try{let t=a?"".concat(a,"/get/config/callbacks"):"/get/config/callbacks",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eR=async e=>{try{let t=a?"".concat(a,"/config/list?config_type=general_settings"):"/config/list?config_type=general_settings",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eI=async e=>{try{let t=a?"".concat(a,"/config/pass_through_endpoint"):"/config/pass_through_endpoint",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},ez=async(e,t)=>{try{let o=a?"".concat(a,"/config/field/info?field_name=").concat(t):"/config/field/info?field_name=".concat(t),r=await fetch(o,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok)throw await r.text(),Error("Network response was not ok");return await r.json()}catch(e){throw console.error("Failed to set callbacks:",e),e}},eV=async(e,t)=>{try{let o=a?"".concat(a,"/config/pass_through_endpoint"):"/config/pass_through_endpoint",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to set callbacks:",e),e}},eU=async(e,t,o)=>{try{let n=a?"".concat(a,"/config/field/update"):"/config/field/update",c=await fetch(n,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({field_name:t,field_value:o,config_type:"general_settings"})});if(!c.ok){let e=await c.text();throw s(e),Error("Network response was not ok")}let i=await c.json();return r.ZP.success("Successfully updated value!"),i}catch(e){throw console.error("Failed to set callbacks:",e),e}},eL=async(e,t)=>{try{let o=a?"".concat(a,"/config/field/delete"):"/config/field/delete",n=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({field_name:t,config_type:"general_settings"})});if(!n.ok){let e=await n.text();throw s(e),Error("Network response was not ok")}let c=await n.json();return r.ZP.success("Field reset on proxy"),c}catch(e){throw console.error("Failed to get callbacks:",e),e}},eZ=async(e,t)=>{try{let o=a?"".concat(a,"/config/pass_through_endpoint?endpoint_id=").concat(t):"/config/pass_through_endpoint".concat(t),r=await fetch(o,{method:"DELETE",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eD=async(e,t)=>{try{let o=a?"".concat(a,"/config/update"):"/config/update",r=await fetch(o,{method:"POST",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify({...t})});if(!r.ok){let e=await r.text();throw s(e),Error("Network response was not ok")}return await r.json()}catch(e){throw console.error("Failed to set callbacks:",e),e}},eH=async e=>{try{let t=a?"".concat(a,"/health"):"/health",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}return await o.json()}catch(e){throw console.error("Failed to call /health:",e),e}},eM=async e=>{try{let t=a?"".concat(a,"/cache/ping"):"/cache/ping",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error(e)}return await o.json()}catch(e){throw console.error("Failed to call /cache/ping:",e),e}},eq=async e=>{try{let t=a?"".concat(a,"/sso/get/ui_settings"):"/sso/get/ui_settings",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok)throw await o.text(),Error("Network response was not ok");return await o.json()}catch(e){throw console.error("Failed to get callbacks:",e),e}},eX=async e=>{try{let t=a?"".concat(a,"/guardrails/list"):"/guardrails/list",o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log("Guardrails list response:",r),r}catch(e){throw console.error("Failed to fetch guardrails list:",e),e}},eK=async(e,t,o)=>{try{let r=a?"".concat(a,"/spend/logs/ui/").concat(t,"?start_date=").concat(encodeURIComponent(o)):"/spend/logs/ui/".concat(t,"?start_date=").concat(encodeURIComponent(o));console.log("Fetching log details from:",r);let n=await fetch(r,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!n.ok){let e=await n.text();throw s(e),Error("Network response was not ok")}let c=await n.json();return console.log("Fetched log details:",c),c}catch(e){throw console.error("Failed to fetch log details:",e),e}},e$=async e=>{try{let t=a?"".concat(a,"/get/internal_user_settings"):"/get/internal_user_settings";console.log("Fetching SSO settings from:",t);let o=await fetch(t,{method:"GET",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"}});if(!o.ok){let e=await o.text();throw s(e),Error("Network response was not ok")}let r=await o.json();return console.log("Fetched SSO settings:",r),r}catch(e){throw console.error("Failed to fetch SSO settings:",e),e}},eY=async(e,t)=>{try{let o=a?"".concat(a,"/update/internal_user_settings"):"/update/internal_user_settings";console.log("Updating internal user settings:",t);let n=await fetch(o,{method:"PATCH",headers:{[l]:"Bearer ".concat(e),"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok){let e=await n.text();throw s(e),Error("Network response was not ok")}let c=await n.json();return console.log("Updated internal user settings:",c),r.ZP.success("Internal user settings updated successfully"),c}catch(e){throw console.error("Failed to update internal user settings:",e),e}}}}]); \ No newline at end of file diff --git a/ui/litellm-dashboard/out/_next/static/chunks/394-0222ddf4d701e0b4.js b/litellm/proxy/_experimental/out/_next/static/chunks/394-48a36e9c9b2cb488.js similarity index 66% rename from ui/litellm-dashboard/out/_next/static/chunks/394-0222ddf4d701e0b4.js rename to litellm/proxy/_experimental/out/_next/static/chunks/394-48a36e9c9b2cb488.js index f6dfcd861e..fa8ff4bc05 100644 --- a/ui/litellm-dashboard/out/_next/static/chunks/394-0222ddf4d701e0b4.js +++ b/litellm/proxy/_experimental/out/_next/static/chunks/394-48a36e9c9b2cb488.js @@ -1,10 +1,10 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[394],{12660:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M917.7 148.8l-42.4-42.4c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-76.1 76.1a199.27 199.27 0 00-112.1-34.3c-51.2 0-102.4 19.5-141.5 58.6L432.3 308.7a8.03 8.03 0 000 11.3L704 591.7c1.6 1.6 3.6 2.3 5.7 2.3 2 0 4.1-.8 5.7-2.3l101.9-101.9c68.9-69 77-175.7 24.3-253.5l76.1-76.1c3.1-3.2 3.1-8.3 0-11.4zM769.1 441.7l-59.4 59.4-186.8-186.8 59.4-59.4c24.9-24.9 58.1-38.7 93.4-38.7 35.3 0 68.4 13.7 93.4 38.7 24.9 24.9 38.7 58.1 38.7 93.4 0 35.3-13.8 68.4-38.7 93.4zm-190.2 105a8.03 8.03 0 00-11.3 0L501 613.3 410.7 523l66.7-66.7c3.1-3.1 3.1-8.2 0-11.3L441 408.6a8.03 8.03 0 00-11.3 0L363 475.3l-43-43a7.85 7.85 0 00-5.7-2.3c-2 0-4.1.8-5.7 2.3L206.8 534.2c-68.9 69-77 175.7-24.3 253.5l-76.1 76.1a8.03 8.03 0 000 11.3l42.4 42.4c1.6 1.6 3.6 2.3 5.7 2.3s4.1-.8 5.7-2.3l76.1-76.1c33.7 22.9 72.9 34.3 112.1 34.3 51.2 0 102.4-19.5 141.5-58.6l101.9-101.9c3.1-3.1 3.1-8.2 0-11.3l-43-43 66.7-66.7c3.1-3.1 3.1-8.2 0-11.3l-36.6-36.2zM441.7 769.1a131.32 131.32 0 01-93.4 38.7c-35.3 0-68.4-13.7-93.4-38.7a131.32 131.32 0 01-38.7-93.4c0-35.3 13.7-68.4 38.7-93.4l59.4-59.4 186.8 186.8-59.4 59.4z"}}]},name:"api",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},88009:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},37527:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M894 462c30.9 0 43.8-39.7 18.7-58L530.8 126.2a31.81 31.81 0 00-37.6 0L111.3 404c-25.1 18.2-12.2 58 18.8 58H192v374h-72c-4.4 0-8 3.6-8 8v52c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-52c0-4.4-3.6-8-8-8h-72V462h62zM512 196.7l271.1 197.2H240.9L512 196.7zM264 462h117v374H264V462zm189 0h117v374H453V462zm307 374H642V462h118v374z"}}]},name:"bank",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},9775:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-600-80h56c4.4 0 8-3.6 8-8V560c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v144c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V384c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v320c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V462c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v242c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v400c0 4.4 3.6 8 8 8z"}}]},name:"bar-chart",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},68208:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M856 376H648V168c0-8.8-7.2-16-16-16H168c-8.8 0-16 7.2-16 16v464c0 8.8 7.2 16 16 16h208v208c0 8.8 7.2 16 16 16h464c8.8 0 16-7.2 16-16V392c0-8.8-7.2-16-16-16zm-480 16v188H220V220h360v156H392c-8.8 0-16 7.2-16 16zm204 52v136H444V444h136zm224 360H444V648h188c8.8 0 16-7.2 16-16V444h156v360z"}}]},name:"block",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},9738:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"}}]},name:"check",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},44625:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-600 72h560v208H232V136zm560 480H232V408h560v208zm0 272H232V680h560v208zM304 240a40 40 0 1080 0 40 40 0 10-80 0zm0 272a40 40 0 1080 0 40 40 0 10-80 0zm0 272a40 40 0 1080 0 40 40 0 10-80 0z"}}]},name:"database",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},70464:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"}}]},name:"down",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},73879:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"}}]},name:"download",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},39760:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"}}]},name:"ellipsis",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},41169:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 472a40 40 0 1080 0 40 40 0 10-80 0zm367 352.9L696.3 352V178H768v-68H256v68h71.7v174L145 824.9c-2.8 7.4-4.3 15.2-4.3 23.1 0 35.3 28.7 64 64 64h614.6c7.9 0 15.7-1.5 23.1-4.3 33-12.7 49.4-49.8 36.6-82.8zM395.7 364.7V180h232.6v184.7L719.2 600c-20.7-5.3-42.1-8-63.9-8-61.2 0-119.2 21.5-165.3 60a188.78 188.78 0 01-121.3 43.9c-32.7 0-64.1-8.3-91.8-23.7l118.8-307.5zM210.5 844l41.7-107.8c35.7 18.1 75.4 27.8 116.6 27.8 61.2 0 119.2-21.5 165.3-60 33.9-28.2 76.3-43.9 121.3-43.9 35 0 68.4 9.5 97.6 27.1L813.5 844h-603z"}}]},name:"experiment",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},6520:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"}}]},name:"eye",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},15424:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}},{tag:"path",attrs:{d:"M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"}}]},name:"info-circle",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},92403:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M608 112c-167.9 0-304 136.1-304 304 0 70.3 23.9 135 63.9 186.5l-41.1 41.1-62.3-62.3a8.15 8.15 0 00-11.4 0l-39.8 39.8a8.15 8.15 0 000 11.4l62.3 62.3-44.9 44.9-62.3-62.3a8.15 8.15 0 00-11.4 0l-39.8 39.8a8.15 8.15 0 000 11.4l62.3 62.3-65.3 65.3a8.03 8.03 0 000 11.3l42.3 42.3c3.1 3.1 8.2 3.1 11.3 0l253.6-253.6A304.06 304.06 0 00608 720c167.9 0 304-136.1 304-304S775.9 112 608 112zm161.2 465.2C726.2 620.3 668.9 644 608 644c-60.9 0-118.2-23.7-161.2-66.8-43.1-43-66.8-100.3-66.8-161.2 0-60.9 23.7-118.2 66.8-161.2 43-43.1 100.3-66.8 161.2-66.8 60.9 0 118.2 23.7 161.2 66.8 43.1 43 66.8 100.3 66.8 161.2 0 60.9-23.7 118.2-66.8 161.2z"}}]},name:"key",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},15327:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"}}]},name:"left",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},48231:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM305.8 637.7c3.1 3.1 8.1 3.1 11.3 0l138.3-137.6L583 628.5c3.1 3.1 8.2 3.1 11.3 0l275.4-275.3c3.1-3.1 3.1-8.2 0-11.3l-39.6-39.6a8.03 8.03 0 00-11.3 0l-230 229.9L461.4 404a8.03 8.03 0 00-11.3 0L266.3 586.7a8.03 8.03 0 000 11.3l39.5 39.7z"}}]},name:"line-chart",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},40428:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M868 732h-70.3c-4.8 0-9.3 2.1-12.3 5.8-7 8.5-14.5 16.7-22.4 24.5a353.84 353.84 0 01-112.7 75.9A352.8 352.8 0 01512.4 866c-47.9 0-94.3-9.4-137.9-27.8a353.84 353.84 0 01-112.7-75.9 353.28 353.28 0 01-76-112.5C167.3 606.2 158 559.9 158 512s9.4-94.2 27.8-137.8c17.8-42.1 43.4-80 76-112.5s70.5-58.1 112.7-75.9c43.6-18.4 90-27.8 137.9-27.8 47.9 0 94.3 9.3 137.9 27.8 42.2 17.8 80.1 43.4 112.7 75.9 7.9 7.9 15.3 16.1 22.4 24.5 3 3.7 7.6 5.8 12.3 5.8H868c6.3 0 10.2-7 6.7-12.3C798 160.5 663.8 81.6 511.3 82 271.7 82.6 79.6 277.1 82 516.4 84.4 751.9 276.2 942 512.4 942c152.1 0 285.7-78.8 362.3-197.7 3.4-5.3-.4-12.3-6.7-12.3zm88.9-226.3L815 393.7c-5.3-4.2-13-.4-13 6.3v76H488c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h314v76c0 6.7 7.8 10.5 13 6.3l141.9-112a8 8 0 000-12.6z"}}]},name:"logout",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},45246:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M696 480H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"minus-circle",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},28595:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}},{tag:"path",attrs:{d:"M719.4 499.1l-296.1-215A15.9 15.9 0 00398 297v430c0 13.1 14.8 20.5 25.3 12.9l296.1-215a15.9 15.9 0 000-25.8zm-257.6 134V390.9L628.5 512 461.8 633.1z"}}]},name:"play-circle",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},96473:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"}},{tag:"path",attrs:{d:"M192 474h672q8 0 8 8v60q0 8-8 8H160q-8 0-8-8v-60q0-8 8-8z"}}]},name:"plus",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},57400:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"0 0 1024 1024",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64L128 192v384c0 212.1 171.9 384 384 384s384-171.9 384-384V192L512 64zm312 512c0 172.3-139.7 312-312 312S200 748.3 200 576V246l312-110 312 110v330z"}},{tag:"path",attrs:{d:"M378.4 475.1a35.91 35.91 0 00-50.9 0 35.91 35.91 0 000 50.9l129.4 129.4 2.1 2.1a33.98 33.98 0 0048.1 0L730.6 434a33.98 33.98 0 000-48.1l-2.8-2.8a33.98 33.98 0 00-48.1 0L483 579.7 378.4 475.1z"}}]},name:"safety",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},29436:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"}}]},name:"search",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},55322:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"}}]},name:"setting",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},41361:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M824.2 699.9a301.55 301.55 0 00-86.4-60.4C783.1 602.8 812 546.8 812 484c0-110.8-92.4-201.7-203.2-200-109.1 1.7-197 90.6-197 200 0 62.8 29 118.8 74.2 155.5a300.95 300.95 0 00-86.4 60.4C345 754.6 314 826.8 312 903.8a8 8 0 008 8.2h56c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5A226.62 226.62 0 01612 684c60.9 0 118.2 23.7 161.3 66.8C814.5 792 838 846.3 840 904.3c.1 4.3 3.7 7.7 8 7.7h56a8 8 0 008-8.2c-2-77-33-149.2-87.8-203.9zM612 612c-34.2 0-66.4-13.3-90.5-37.5a126.86 126.86 0 01-37.5-91.8c.3-32.8 13.4-64.5 36.3-88 24-24.6 56.1-38.3 90.4-38.7 33.9-.3 66.8 12.9 91 36.6 24.8 24.3 38.4 56.8 38.4 91.4 0 34.2-13.3 66.3-37.5 90.5A127.3 127.3 0 01612 612zM361.5 510.4c-.9-8.7-1.4-17.5-1.4-26.4 0-15.9 1.5-31.4 4.3-46.5.7-3.6-1.2-7.3-4.5-8.8-13.6-6.1-26.1-14.5-36.9-25.1a127.54 127.54 0 01-38.7-95.4c.9-32.1 13.8-62.6 36.3-85.6 24.7-25.3 57.9-39.1 93.2-38.7 31.9.3 62.7 12.6 86 34.4 7.9 7.4 14.7 15.6 20.4 24.4 2 3.1 5.9 4.4 9.3 3.2 17.6-6.1 36.2-10.4 55.3-12.4 5.6-.6 8.8-6.6 6.3-11.6-32.5-64.3-98.9-108.7-175.7-109.9-110.9-1.7-203.3 89.2-203.3 199.9 0 62.8 28.9 118.8 74.2 155.5-31.8 14.7-61.1 35-86.5 60.4-54.8 54.7-85.8 126.9-87.8 204a8 8 0 008 8.2h56.1c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5 29.4-29.4 65.4-49.8 104.7-59.7 3.9-1 6.5-4.7 6-8.7z"}}]},name:"team",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},3632:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 00-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"}}]},name:"upload",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},15883:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"}}]},name:"user",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},35291:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"},a=n(55015),l=o.forwardRef(function(e,t){return o.createElement(a.Z,(0,r.Z)({},e,{ref:t,icon:i}))})},58747:function(e,t,n){"use strict";n.d(t,{Z:function(){return i}});var r=n(5853),o=n(2265);let i=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),o.createElement("path",{d:"M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"}))}},4537:function(e,t,n){"use strict";n.d(t,{Z:function(){return i}});var r=n(5853),o=n(2265);let i=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),o.createElement("path",{d:"M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 10.5858L9.17157 7.75736L7.75736 9.17157L10.5858 12L7.75736 14.8284L9.17157 16.2426L12 13.4142L14.8284 16.2426L16.2426 14.8284L13.4142 12L16.2426 9.17157L14.8284 7.75736L12 10.5858Z"}))}},75105:function(e,t,n){"use strict";n.d(t,{Z:function(){return et}});var r=n(5853),o=n(2265),i=n(47625),a=n(93765),l=n(61994),c=n(59221),s=n(86757),u=n.n(s),d=n(95645),f=n.n(d),p=n(77571),h=n.n(p),m=n(82559),g=n.n(m),v=n(21652),y=n.n(v),b=n(57165),x=n(81889),w=n(9841),S=n(58772),k=n(34067),E=n(16630),C=n(85355),O=n(82944),j=["layout","type","stroke","connectNulls","isRange","ref"];function P(e){return(P="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function N(){return(N=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(i,j));return o.createElement(w.m,{clipPath:n?"url(#clipPath-".concat(r,")"):null},o.createElement(b.H,N({},(0,O.L6)(d,!0),{points:e,connectNulls:s,type:l,baseLine:t,layout:a,stroke:"none",className:"recharts-area-area"})),"none"!==c&&o.createElement(b.H,N({},(0,O.L6)(this.props,!1),{className:"recharts-area-curve",layout:a,type:l,connectNulls:s,fill:"none",points:e})),"none"!==c&&u&&o.createElement(b.H,N({},(0,O.L6)(this.props,!1),{className:"recharts-area-curve",layout:a,type:l,connectNulls:s,fill:"none",points:t})))}},{key:"renderAreaWithAnimation",value:function(e,t){var n=this,r=this.props,i=r.points,a=r.baseLine,l=r.isAnimationActive,s=r.animationBegin,u=r.animationDuration,d=r.animationEasing,f=r.animationId,p=this.state,m=p.prevPoints,v=p.prevBaseLine;return o.createElement(c.ZP,{begin:s,duration:u,isActive:l,easing:d,from:{t:0},to:{t:1},key:"area-".concat(f),onAnimationEnd:this.handleAnimationEnd,onAnimationStart:this.handleAnimationStart},function(r){var l=r.t;if(m){var c,s=m.length/i.length,u=i.map(function(e,t){var n=Math.floor(t*s);if(m[n]){var r=m[n],o=(0,E.k4)(r.x,e.x),i=(0,E.k4)(r.y,e.y);return I(I({},e),{},{x:o(l),y:i(l)})}return e});return c=(0,E.hj)(a)&&"number"==typeof a?(0,E.k4)(v,a)(l):h()(a)||g()(a)?(0,E.k4)(v,0)(l):a.map(function(e,t){var n=Math.floor(t*s);if(v[n]){var r=v[n],o=(0,E.k4)(r.x,e.x),i=(0,E.k4)(r.y,e.y);return I(I({},e),{},{x:o(l),y:i(l)})}return e}),n.renderAreaStatically(u,c,e,t)}return o.createElement(w.m,null,o.createElement("defs",null,o.createElement("clipPath",{id:"animationClipPath-".concat(t)},n.renderClipRect(l))),o.createElement(w.m,{clipPath:"url(#animationClipPath-".concat(t,")")},n.renderAreaStatically(i,a,e,t)))})}},{key:"renderArea",value:function(e,t){var n=this.props,r=n.points,o=n.baseLine,i=n.isAnimationActive,a=this.state,l=a.prevPoints,c=a.prevBaseLine,s=a.totalLength;return i&&r&&r.length&&(!l&&s>0||!y()(l,r)||!y()(c,o))?this.renderAreaWithAnimation(e,t):this.renderAreaStatically(r,o,e,t)}},{key:"render",value:function(){var e,t=this.props,n=t.hide,r=t.dot,i=t.points,a=t.className,c=t.top,s=t.left,u=t.xAxis,d=t.yAxis,f=t.width,p=t.height,m=t.isAnimationActive,g=t.id;if(n||!i||!i.length)return null;var v=this.state.isAnimationFinished,y=1===i.length,b=(0,l.Z)("recharts-area",a),x=u&&u.allowDataOverflow,k=d&&d.allowDataOverflow,E=x||k,C=h()(g)?this.id:g,j=null!==(e=(0,O.L6)(r,!1))&&void 0!==e?e:{r:3,strokeWidth:2},P=j.r,N=j.strokeWidth,M=((0,O.$k)(r)?r:{}).clipDot,I=void 0===M||M,R=2*(void 0===P?3:P)+(void 0===N?2:N);return o.createElement(w.m,{className:b},x||k?o.createElement("defs",null,o.createElement("clipPath",{id:"clipPath-".concat(C)},o.createElement("rect",{x:x?s:s-f/2,y:k?c:c-p/2,width:x?f:2*f,height:k?p:2*p})),!I&&o.createElement("clipPath",{id:"clipPath-dots-".concat(C)},o.createElement("rect",{x:s-R/2,y:c-R/2,width:f+R,height:p+R}))):null,y?null:this.renderArea(E,C),(r||y)&&this.renderDots(E,I,C),(!m||v)&&S.e.renderCallByParent(this.props,i))}}],r=[{key:"getDerivedStateFromProps",value:function(e,t){return e.animationId!==t.prevAnimationId?{prevAnimationId:e.animationId,curPoints:e.points,curBaseLine:e.baseLine,prevPoints:t.curPoints,prevBaseLine:t.curBaseLine}:e.points!==t.curPoints||e.baseLine!==t.curBaseLine?{curPoints:e.points,curBaseLine:e.baseLine}:null}}],n&&R(a.prototype,n),r&&R(a,r),Object.defineProperty(a,"prototype",{writable:!1}),a}(o.PureComponent);D(L,"displayName","Area"),D(L,"defaultProps",{stroke:"#3182bd",fill:"#3182bd",fillOpacity:.6,xAxisId:0,yAxisId:0,legendType:"line",connectNulls:!1,points:[],dot:!1,activeDot:!0,hide:!1,isAnimationActive:!k.x.isSsr,animationBegin:0,animationDuration:1500,animationEasing:"ease"}),D(L,"getBaseValue",function(e,t,n,r){var o=e.layout,i=e.baseValue,a=t.props.baseValue,l=null!=a?a:i;if((0,E.hj)(l)&&"number"==typeof l)return l;var c="horizontal"===o?r:n,s=c.scale.domain();if("number"===c.type){var u=Math.max(s[0],s[1]),d=Math.min(s[0],s[1]);return"dataMin"===l?d:"dataMax"===l?u:u<0?u:Math.max(Math.min(s[0],s[1]),0)}return"dataMin"===l?s[0]:"dataMax"===l?s[1]:s[0]}),D(L,"getComposedData",function(e){var t,n=e.props,r=e.item,o=e.xAxis,i=e.yAxis,a=e.xAxisTicks,l=e.yAxisTicks,c=e.bandSize,s=e.dataKey,u=e.stackedData,d=e.dataStartIndex,f=e.displayedData,p=e.offset,h=n.layout,m=u&&u.length,g=L.getBaseValue(n,r,o,i),v="horizontal"===h,y=!1,b=f.map(function(e,t){m?n=u[d+t]:Array.isArray(n=(0,C.F$)(e,s))?y=!0:n=[g,n];var n,r=null==n[1]||m&&null==(0,C.F$)(e,s);return v?{x:(0,C.Hv)({axis:o,ticks:a,bandSize:c,entry:e,index:t}),y:r?null:i.scale(n[1]),value:n,payload:e}:{x:r?null:o.scale(n[1]),y:(0,C.Hv)({axis:i,ticks:l,bandSize:c,entry:e,index:t}),value:n,payload:e}});return t=m||y?b.map(function(e){var t=Array.isArray(e.value)?e.value[0]:null;return v?{x:e.x,y:null!=t&&null!=e.y?i.scale(t):null}:{x:null!=t?o.scale(t):null,y:e.y}}):v?i.scale(g):o.scale(g),I({points:b,baseLine:t,layout:h,isRange:y},p)}),D(L,"renderDotItem",function(e,t){return o.isValidElement(e)?o.cloneElement(e,t):u()(e)?e(t):o.createElement(x.o,N({},t,{className:"recharts-area-dot"}))});var z=n(97059),B=n(62994),F=n(25311),H=(0,a.z)({chartName:"AreaChart",GraphicalChild:L,axisComponents:[{axisType:"xAxis",AxisComp:z.K},{axisType:"yAxis",AxisComp:B.B}],formatAxisMap:F.t9}),q=n(56940),W=n(8147),K=n(22190),V=n(54061),U=n(65278),G=n(98593),X=n(69448),$=n(32644),Y=n(7084),Q=n(26898),J=n(65954),ee=n(1153);let et=o.forwardRef((e,t)=>{let{data:n=[],categories:a=[],index:l,stack:c=!1,colors:s=Q.s,valueFormatter:u=ee.Cj,startEndOnly:d=!1,showXAxis:f=!0,showYAxis:p=!0,yAxisWidth:h=56,intervalType:m="equidistantPreserveStart",showAnimation:g=!1,animationDuration:v=900,showTooltip:y=!0,showLegend:b=!0,showGridLines:w=!0,showGradient:S=!0,autoMinValue:k=!1,curveType:E="linear",minValue:C,maxValue:O,connectNulls:j=!1,allowDecimals:P=!0,noDataText:N,className:M,onValueChange:I,enableLegendSlider:R=!1,customTooltip:T,rotateLabelX:A,tickGap:_=5}=e,D=(0,r._T)(e,["data","categories","index","stack","colors","valueFormatter","startEndOnly","showXAxis","showYAxis","yAxisWidth","intervalType","showAnimation","animationDuration","showTooltip","showLegend","showGridLines","showGradient","autoMinValue","curveType","minValue","maxValue","connectNulls","allowDecimals","noDataText","className","onValueChange","enableLegendSlider","customTooltip","rotateLabelX","tickGap"]),Z=(f||p)&&(!d||p)?20:0,[F,et]=(0,o.useState)(60),[en,er]=(0,o.useState)(void 0),[eo,ei]=(0,o.useState)(void 0),ea=(0,$.me)(a,s),el=(0,$.i4)(k,C,O),ec=!!I;function es(e){ec&&(e===eo&&!en||(0,$.FB)(n,e)&&en&&en.dataKey===e?(ei(void 0),null==I||I(null)):(ei(e),null==I||I({eventType:"category",categoryClicked:e})),er(void 0))}return o.createElement("div",Object.assign({ref:t,className:(0,J.q)("w-full h-80",M)},D),o.createElement(i.h,{className:"h-full w-full"},(null==n?void 0:n.length)?o.createElement(H,{data:n,onClick:ec&&(eo||en)?()=>{er(void 0),ei(void 0),null==I||I(null)}:void 0},w?o.createElement(q.q,{className:(0,J.q)("stroke-1","stroke-tremor-border","dark:stroke-dark-tremor-border"),horizontal:!0,vertical:!1}):null,o.createElement(z.K,{padding:{left:Z,right:Z},hide:!f,dataKey:l,tick:{transform:"translate(0, 6)"},ticks:d?[n[0][l],n[n.length-1][l]]:void 0,fill:"",stroke:"",className:(0,J.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),interval:d?"preserveStartEnd":m,tickLine:!1,axisLine:!1,minTickGap:_,angle:null==A?void 0:A.angle,dy:null==A?void 0:A.verticalShift,height:null==A?void 0:A.xAxisHeight}),o.createElement(B.B,{width:h,hide:!p,axisLine:!1,tickLine:!1,type:"number",domain:el,tick:{transform:"translate(-3, 0)"},fill:"",stroke:"",className:(0,J.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickFormatter:u,allowDecimals:P}),o.createElement(W.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,cursor:{stroke:"#d1d5db",strokeWidth:1},content:y?e=>{let{active:t,payload:n,label:r}=e;return T?o.createElement(T,{payload:null==n?void 0:n.map(e=>{var t;return Object.assign(Object.assign({},e),{color:null!==(t=ea.get(e.dataKey))&&void 0!==t?t:Y.fr.Gray})}),active:t,label:r}):o.createElement(G.ZP,{active:t,payload:n,label:r,valueFormatter:u,categoryColors:ea})}:o.createElement(o.Fragment,null),position:{y:0}}),b?o.createElement(K.D,{verticalAlign:"top",height:F,content:e=>{let{payload:t}=e;return(0,U.Z)({payload:t},ea,et,eo,ec?e=>es(e):void 0,R)}}):null,a.map(e=>{var t,n;return o.createElement("defs",{key:e},S?o.createElement("linearGradient",{className:(0,ee.bM)(null!==(t=ea.get(e))&&void 0!==t?t:Y.fr.Gray,Q.K.text).textColor,id:ea.get(e),x1:"0",y1:"0",x2:"0",y2:"1"},o.createElement("stop",{offset:"5%",stopColor:"currentColor",stopOpacity:en||eo&&eo!==e?.15:.4}),o.createElement("stop",{offset:"95%",stopColor:"currentColor",stopOpacity:0})):o.createElement("linearGradient",{className:(0,ee.bM)(null!==(n=ea.get(e))&&void 0!==n?n:Y.fr.Gray,Q.K.text).textColor,id:ea.get(e),x1:"0",y1:"0",x2:"0",y2:"1"},o.createElement("stop",{stopColor:"currentColor",stopOpacity:en||eo&&eo!==e?.1:.3})))}),a.map(e=>{var t;return o.createElement(L,{className:(0,ee.bM)(null!==(t=ea.get(e))&&void 0!==t?t:Y.fr.Gray,Q.K.text).strokeColor,strokeOpacity:en||eo&&eo!==e?.3:1,activeDot:e=>{var t;let{cx:r,cy:i,stroke:a,strokeLinecap:l,strokeLinejoin:c,strokeWidth:s,dataKey:u}=e;return o.createElement(x.o,{className:(0,J.q)("stroke-tremor-background dark:stroke-dark-tremor-background",I?"cursor-pointer":"",(0,ee.bM)(null!==(t=ea.get(u))&&void 0!==t?t:Y.fr.Gray,Q.K.text).fillColor),cx:r,cy:i,r:5,fill:"",stroke:a,strokeLinecap:l,strokeLinejoin:c,strokeWidth:s,onClick:(t,r)=>{r.stopPropagation(),ec&&(e.index===(null==en?void 0:en.index)&&e.dataKey===(null==en?void 0:en.dataKey)||(0,$.FB)(n,e.dataKey)&&eo&&eo===e.dataKey?(ei(void 0),er(void 0),null==I||I(null)):(ei(e.dataKey),er({index:e.index,dataKey:e.dataKey}),null==I||I(Object.assign({eventType:"dot",categoryClicked:e.dataKey},e.payload))))}})},dot:t=>{var r;let{stroke:i,strokeLinecap:a,strokeLinejoin:l,strokeWidth:c,cx:s,cy:u,dataKey:d,index:f}=t;return(0,$.FB)(n,e)&&!(en||eo&&eo!==e)||(null==en?void 0:en.index)===f&&(null==en?void 0:en.dataKey)===e?o.createElement(x.o,{key:f,cx:s,cy:u,r:5,stroke:i,fill:"",strokeLinecap:a,strokeLinejoin:l,strokeWidth:c,className:(0,J.q)("stroke-tremor-background dark:stroke-dark-tremor-background",I?"cursor-pointer":"",(0,ee.bM)(null!==(r=ea.get(d))&&void 0!==r?r:Y.fr.Gray,Q.K.text).fillColor)}):o.createElement(o.Fragment,{key:f})},key:e,name:e,type:E,dataKey:e,stroke:"",fill:"url(#".concat(ea.get(e),")"),strokeWidth:2,strokeLinejoin:"round",strokeLinecap:"round",isAnimationActive:g,animationDuration:v,stackId:c?"a":void 0,connectNulls:j})}),I?a.map(e=>o.createElement(V.x,{className:(0,J.q)("cursor-pointer"),strokeOpacity:0,key:e,name:e,type:E,dataKey:e,stroke:"transparent",fill:"transparent",legendType:"none",tooltipType:"none",strokeWidth:12,connectNulls:j,onClick:(e,t)=>{t.stopPropagation();let{name:n}=e;es(n)}})):null):o.createElement(X.Z,{noDataText:N})))});et.displayName="AreaChart"},40278:function(e,t,n){"use strict";n.d(t,{Z:function(){return k}});var r=n(5853),o=n(7084),i=n(26898),a=n(65954),l=n(1153),c=n(2265),s=n(47625),u=n(93765),d=n(31699),f=n(97059),p=n(62994),h=n(25311),m=(0,u.z)({chartName:"BarChart",GraphicalChild:d.$,defaultTooltipEventType:"axis",validateTooltipEventTypes:["axis","item"],axisComponents:[{axisType:"xAxis",AxisComp:f.K},{axisType:"yAxis",AxisComp:p.B}],formatAxisMap:h.t9}),g=n(56940),v=n(8147),y=n(22190),b=n(65278),x=n(98593),w=n(69448),S=n(32644);let k=c.forwardRef((e,t)=>{let{data:n=[],categories:u=[],index:h,colors:k=i.s,valueFormatter:E=l.Cj,layout:C="horizontal",stack:O=!1,relative:j=!1,startEndOnly:P=!1,animationDuration:N=900,showAnimation:M=!1,showXAxis:I=!0,showYAxis:R=!0,yAxisWidth:T=56,intervalType:A="equidistantPreserveStart",showTooltip:_=!0,showLegend:D=!0,showGridLines:Z=!0,autoMinValue:L=!1,minValue:z,maxValue:B,allowDecimals:F=!0,noDataText:H,onValueChange:q,enableLegendSlider:W=!1,customTooltip:K,rotateLabelX:V,tickGap:U=5,className:G}=e,X=(0,r._T)(e,["data","categories","index","colors","valueFormatter","layout","stack","relative","startEndOnly","animationDuration","showAnimation","showXAxis","showYAxis","yAxisWidth","intervalType","showTooltip","showLegend","showGridLines","autoMinValue","minValue","maxValue","allowDecimals","noDataText","onValueChange","enableLegendSlider","customTooltip","rotateLabelX","tickGap","className"]),$=I||R?20:0,[Y,Q]=(0,c.useState)(60),J=(0,S.me)(u,k),[ee,et]=c.useState(void 0),[en,er]=(0,c.useState)(void 0),eo=!!q;function ei(e,t,n){var r,o,i,a;n.stopPropagation(),q&&((0,S.vZ)(ee,Object.assign(Object.assign({},e.payload),{value:e.value}))?(er(void 0),et(void 0),null==q||q(null)):(er(null===(o=null===(r=e.tooltipPayload)||void 0===r?void 0:r[0])||void 0===o?void 0:o.dataKey),et(Object.assign(Object.assign({},e.payload),{value:e.value})),null==q||q(Object.assign({eventType:"bar",categoryClicked:null===(a=null===(i=e.tooltipPayload)||void 0===i?void 0:i[0])||void 0===a?void 0:a.dataKey},e.payload))))}let ea=(0,S.i4)(L,z,B);return c.createElement("div",Object.assign({ref:t,className:(0,a.q)("w-full h-80",G)},X),c.createElement(s.h,{className:"h-full w-full"},(null==n?void 0:n.length)?c.createElement(m,{data:n,stackOffset:O?"sign":j?"expand":"none",layout:"vertical"===C?"vertical":"horizontal",onClick:eo&&(en||ee)?()=>{et(void 0),er(void 0),null==q||q(null)}:void 0},Z?c.createElement(g.q,{className:(0,a.q)("stroke-1","stroke-tremor-border","dark:stroke-dark-tremor-border"),horizontal:"vertical"!==C,vertical:"vertical"===C}):null,"vertical"!==C?c.createElement(f.K,{padding:{left:$,right:$},hide:!I,dataKey:h,interval:P?"preserveStartEnd":A,tick:{transform:"translate(0, 6)"},ticks:P?[n[0][h],n[n.length-1][h]]:void 0,fill:"",stroke:"",className:(0,a.q)("mt-4 text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickLine:!1,axisLine:!1,angle:null==V?void 0:V.angle,dy:null==V?void 0:V.verticalShift,height:null==V?void 0:V.xAxisHeight,minTickGap:U}):c.createElement(f.K,{hide:!I,type:"number",tick:{transform:"translate(-3, 0)"},domain:ea,fill:"",stroke:"",className:(0,a.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickLine:!1,axisLine:!1,tickFormatter:E,minTickGap:U,allowDecimals:F,angle:null==V?void 0:V.angle,dy:null==V?void 0:V.verticalShift,height:null==V?void 0:V.xAxisHeight}),"vertical"!==C?c.createElement(p.B,{width:T,hide:!R,axisLine:!1,tickLine:!1,type:"number",domain:ea,tick:{transform:"translate(-3, 0)"},fill:"",stroke:"",className:(0,a.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickFormatter:j?e=>"".concat((100*e).toString()," %"):E,allowDecimals:F}):c.createElement(p.B,{width:T,hide:!R,dataKey:h,axisLine:!1,tickLine:!1,ticks:P?[n[0][h],n[n.length-1][h]]:void 0,type:"category",interval:"preserveStartEnd",tick:{transform:"translate(0, 6)"},fill:"",stroke:"",className:(0,a.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content")}),c.createElement(v.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,cursor:{fill:"#d1d5db",opacity:"0.15"},content:_?e=>{let{active:t,payload:n,label:r}=e;return K?c.createElement(K,{payload:null==n?void 0:n.map(e=>{var t;return Object.assign(Object.assign({},e),{color:null!==(t=J.get(e.dataKey))&&void 0!==t?t:o.fr.Gray})}),active:t,label:r}):c.createElement(x.ZP,{active:t,payload:n,label:r,valueFormatter:E,categoryColors:J})}:c.createElement(c.Fragment,null),position:{y:0}}),D?c.createElement(y.D,{verticalAlign:"top",height:Y,content:e=>{let{payload:t}=e;return(0,b.Z)({payload:t},J,Q,en,eo?e=>{eo&&(e!==en||ee?(er(e),null==q||q({eventType:"category",categoryClicked:e})):(er(void 0),null==q||q(null)),et(void 0))}:void 0,W)}}):null,u.map(e=>{var t;return c.createElement(d.$,{className:(0,a.q)((0,l.bM)(null!==(t=J.get(e))&&void 0!==t?t:o.fr.Gray,i.K.background).fillColor,q?"cursor-pointer":""),key:e,name:e,type:"linear",stackId:O||j?"a":void 0,dataKey:e,fill:"",isAnimationActive:M,animationDuration:N,shape:e=>((e,t,n,r)=>{let{fillOpacity:o,name:i,payload:a,value:l}=e,{x:s,width:u,y:d,height:f}=e;return"horizontal"===r&&f<0?(d+=f,f=Math.abs(f)):"vertical"===r&&u<0&&(s+=u,u=Math.abs(u)),c.createElement("rect",{x:s,y:d,width:u,height:f,opacity:t||n&&n!==i?(0,S.vZ)(t,Object.assign(Object.assign({},a),{value:l}))?o:.3:o})})(e,ee,en,C),onClick:ei})})):c.createElement(w.Z,{noDataText:H})))});k.displayName="BarChart"},14042:function(e,t,n){"use strict";n.d(t,{Z:function(){return ez}});var r=n(5853),o=n(7084),i=n(26898),a=n(65954),l=n(1153),c=n(2265),s=n(60474),u=n(47625),d=n(93765),f=n(86757),p=n.n(f),h=n(9841),m=n(81889),g=n(61994),v=n(82944),y=["points","className","baseLinePoints","connectNulls"];function b(){return(b=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=Array(t);n0&&void 0!==arguments[0]?arguments[0]:[],t=[[]];return e.forEach(function(e){S(e)?t[t.length-1].push(e):t[t.length-1].length>0&&t.push([])}),S(e[0])&&t[t.length-1].push(e[0]),t[t.length-1].length<=0&&(t=t.slice(0,-1)),t},E=function(e,t){var n=k(e);t&&(n=[n.reduce(function(e,t){return[].concat(x(e),x(t))},[])]);var r=n.map(function(e){return e.reduce(function(e,t,n){return"".concat(e).concat(0===n?"M":"L").concat(t.x,",").concat(t.y)},"")}).join("");return 1===n.length?"".concat(r,"Z"):r},C=function(e,t,n){var r=E(e,n);return"".concat("Z"===r.slice(-1)?r.slice(0,-1):r,"L").concat(E(t.reverse(),n).slice(1))},O=function(e){var t=e.points,n=e.className,r=e.baseLinePoints,o=e.connectNulls,i=function(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,y);if(!t||!t.length)return null;var a=(0,g.Z)("recharts-polygon",n);if(r&&r.length){var l=i.stroke&&"none"!==i.stroke,s=C(t,r,o);return c.createElement("g",{className:a},c.createElement("path",b({},(0,v.L6)(i,!0),{fill:"Z"===s.slice(-1)?i.fill:"none",stroke:"none",d:s})),l?c.createElement("path",b({},(0,v.L6)(i,!0),{fill:"none",d:E(t,o)})):null,l?c.createElement("path",b({},(0,v.L6)(i,!0),{fill:"none",d:E(r,o)})):null)}var u=E(t,o);return c.createElement("path",b({},(0,v.L6)(i,!0),{fill:"Z"===u.slice(-1)?i.fill:"none",className:a,d:u}))},j=n(58811),P=n(41637),N=n(39206);function M(e){return(M="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function I(){return(I=Object.assign?Object.assign.bind():function(e){for(var t=1;t1e-5?"outer"===t?"start":"end":n<-.00001?"outer"===t?"end":"start":"middle"}},{key:"renderAxisLine",value:function(){var e=this.props,t=e.cx,n=e.cy,r=e.radius,o=e.axisLine,i=e.axisLineType,a=T(T({},(0,v.L6)(this.props,!1)),{},{fill:"none"},(0,v.L6)(o,!1));if("circle"===i)return c.createElement(m.o,I({className:"recharts-polar-angle-axis-line"},a,{cx:t,cy:n,r:r}));var l=this.props.ticks.map(function(e){return(0,N.op)(t,n,r,e.coordinate)});return c.createElement(O,I({className:"recharts-polar-angle-axis-line"},a,{points:l}))}},{key:"renderTicks",value:function(){var e=this,t=this.props,n=t.ticks,r=t.tick,o=t.tickLine,a=t.tickFormatter,l=t.stroke,s=(0,v.L6)(this.props,!1),u=(0,v.L6)(r,!1),d=T(T({},s),{},{fill:"none"},(0,v.L6)(o,!1)),f=n.map(function(t,n){var f=e.getTickLineCoord(t),p=T(T(T({textAnchor:e.getTickTextAnchor(t)},s),{},{stroke:"none",fill:l},u),{},{index:n,payload:t,x:f.x2,y:f.y2});return c.createElement(h.m,I({className:"recharts-polar-angle-axis-tick",key:"tick-".concat(t.coordinate)},(0,P.bw)(e.props,t,n)),o&&c.createElement("line",I({className:"recharts-polar-angle-axis-tick-line"},d,f)),r&&i.renderTickItem(r,p,a?a(t.value,n):t.value))});return c.createElement(h.m,{className:"recharts-polar-angle-axis-ticks"},f)}},{key:"render",value:function(){var e=this.props,t=e.ticks,n=e.radius,r=e.axisLine;return!(n<=0)&&t&&t.length?c.createElement(h.m,{className:"recharts-polar-angle-axis"},r&&this.renderAxisLine(),this.renderTicks()):null}}],r=[{key:"renderTickItem",value:function(e,t,n){return c.isValidElement(e)?c.cloneElement(e,t):p()(e)?e(t):c.createElement(j.x,I({},t,{className:"recharts-polar-angle-axis-tick-value"}),n)}}],n&&A(i.prototype,n),r&&A(i,r),Object.defineProperty(i,"prototype",{writable:!1}),i}(c.PureComponent);Z(B,"displayName","PolarAngleAxis"),Z(B,"axisType","angleAxis"),Z(B,"defaultProps",{type:"category",angleAxisId:0,scale:"auto",cx:0,cy:0,orientation:"outer",axisLine:!0,tickLine:!0,tickSize:8,tick:!0,hide:!1,allowDuplicatedCategory:!0});var F=n(35802),H=n.n(F),q=n(37891),W=n.n(q),K=n(26680),V=["cx","cy","angle","ticks","axisLine"],U=["ticks","tick","angle","tickFormatter","stroke"];function G(e){return(G="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function X(){return(X=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function J(e,t){for(var n=0;n0?el()(e,"paddingAngle",0):0;if(n){var l=(0,eg.k4)(n.endAngle-n.startAngle,e.endAngle-e.startAngle),c=ek(ek({},e),{},{startAngle:i+a,endAngle:i+l(r)+a});o.push(c),i=c.endAngle}else{var s=e.endAngle,d=e.startAngle,f=(0,eg.k4)(0,s-d)(r),p=ek(ek({},e),{},{startAngle:i+a,endAngle:i+f+a});o.push(p),i=p.endAngle}}),c.createElement(h.m,null,e.renderSectorsStatically(o))})}},{key:"attachKeyboardHandlers",value:function(e){var t=this;e.onkeydown=function(e){if(!e.altKey)switch(e.key){case"ArrowLeft":var n=++t.state.sectorToFocus%t.sectorRefs.length;t.sectorRefs[n].focus(),t.setState({sectorToFocus:n});break;case"ArrowRight":var r=--t.state.sectorToFocus<0?t.sectorRefs.length-1:t.state.sectorToFocus%t.sectorRefs.length;t.sectorRefs[r].focus(),t.setState({sectorToFocus:r});break;case"Escape":t.sectorRefs[t.state.sectorToFocus].blur(),t.setState({sectorToFocus:0})}}}},{key:"renderSectors",value:function(){var e=this.props,t=e.sectors,n=e.isAnimationActive,r=this.state.prevSectors;return n&&t&&t.length&&(!r||!es()(r,t))?this.renderSectorsWithAnimation():this.renderSectorsStatically(t)}},{key:"componentDidMount",value:function(){this.pieRef&&this.attachKeyboardHandlers(this.pieRef)}},{key:"render",value:function(){var e=this,t=this.props,n=t.hide,r=t.sectors,o=t.className,i=t.label,a=t.cx,l=t.cy,s=t.innerRadius,u=t.outerRadius,d=t.isAnimationActive,f=this.state.isAnimationFinished;if(n||!r||!r.length||!(0,eg.hj)(a)||!(0,eg.hj)(l)||!(0,eg.hj)(s)||!(0,eg.hj)(u))return null;var p=(0,g.Z)("recharts-pie",o);return c.createElement(h.m,{tabIndex:this.props.rootTabIndex,className:p,ref:function(t){e.pieRef=t}},this.renderSectors(),i&&this.renderLabels(r),K._.renderCallByParent(this.props,null,!1),(!d||f)&&ep.e.renderCallByParent(this.props,r,!1))}}],r=[{key:"getDerivedStateFromProps",value:function(e,t){return t.prevIsAnimationActive!==e.isAnimationActive?{prevIsAnimationActive:e.isAnimationActive,prevAnimationId:e.animationId,curSectors:e.sectors,prevSectors:[],isAnimationFinished:!0}:e.isAnimationActive&&e.animationId!==t.prevAnimationId?{prevAnimationId:e.animationId,curSectors:e.sectors,prevSectors:t.curSectors,isAnimationFinished:!0}:e.sectors!==t.curSectors?{curSectors:e.sectors,isAnimationFinished:!0}:null}},{key:"getTextAnchor",value:function(e,t){return e>t?"start":e=360?x:x-1)*u,S=i.reduce(function(e,t){var n=(0,ev.F$)(t,b,0);return e+((0,eg.hj)(n)?n:0)},0);return S>0&&(t=i.map(function(e,t){var r,o=(0,ev.F$)(e,b,0),i=(0,ev.F$)(e,f,t),a=((0,eg.hj)(o)?o:0)/S,s=(r=t?n.endAngle+(0,eg.uY)(v)*u*(0!==o?1:0):c)+(0,eg.uY)(v)*((0!==o?m:0)+a*w),d=(r+s)/2,p=(g.innerRadius+g.outerRadius)/2,y=[{name:i,value:o,payload:e,dataKey:b,type:h}],x=(0,N.op)(g.cx,g.cy,p,d);return n=ek(ek(ek({percent:a,cornerRadius:l,name:i,tooltipPayload:y,midAngle:d,middleRadius:p,tooltipPosition:x},e),g),{},{value:(0,ev.F$)(e,b),startAngle:r,endAngle:s,payload:e,paddingAngle:(0,eg.uY)(v)*u})})),ek(ek({},g),{},{sectors:t,data:i})});var eI=(0,d.z)({chartName:"PieChart",GraphicalChild:eM,validateTooltipEventTypes:["item"],defaultTooltipEventType:"item",legendContent:"children",axisComponents:[{axisType:"angleAxis",AxisComp:B},{axisType:"radiusAxis",AxisComp:eo}],formatAxisMap:N.t9,defaultProps:{layout:"centric",startAngle:0,endAngle:360,cx:"50%",cy:"50%",innerRadius:0,outerRadius:"80%"}}),eR=n(8147),eT=n(69448),eA=n(98593);let e_=e=>{let{active:t,payload:n,valueFormatter:r}=e;if(t&&(null==n?void 0:n[0])){let e=null==n?void 0:n[0];return c.createElement(eA.$B,null,c.createElement("div",{className:(0,a.q)("px-4 py-2")},c.createElement(eA.zX,{value:r(e.value),name:e.name,color:e.payload.color})))}return null},eD=(e,t)=>e.map((e,n)=>{let r=ne||t((0,l.vP)(n.map(e=>e[r]))),eL=e=>{let{cx:t,cy:n,innerRadius:r,outerRadius:o,startAngle:i,endAngle:a,className:l}=e;return c.createElement("g",null,c.createElement(s.L,{cx:t,cy:n,innerRadius:r,outerRadius:o,startAngle:i,endAngle:a,className:l,fill:"",opacity:.3,style:{outline:"none"}}))},ez=c.forwardRef((e,t)=>{let{data:n=[],category:s="value",index:d="name",colors:f=i.s,variant:p="donut",valueFormatter:h=l.Cj,label:m,showLabel:g=!0,animationDuration:v=900,showAnimation:y=!1,showTooltip:b=!0,noDataText:x,onValueChange:w,customTooltip:S,className:k}=e,E=(0,r._T)(e,["data","category","index","colors","variant","valueFormatter","label","showLabel","animationDuration","showAnimation","showTooltip","noDataText","onValueChange","customTooltip","className"]),C="donut"==p,O=eZ(m,h,n,s),[j,P]=c.useState(void 0),N=!!w;return(0,c.useEffect)(()=>{let e=document.querySelectorAll(".recharts-pie-sector");e&&e.forEach(e=>{e.setAttribute("style","outline: none")})},[j]),c.createElement("div",Object.assign({ref:t,className:(0,a.q)("w-full h-40",k)},E),c.createElement(u.h,{className:"h-full w-full"},(null==n?void 0:n.length)?c.createElement(eI,{onClick:N&&j?()=>{P(void 0),null==w||w(null)}:void 0,margin:{top:0,left:0,right:0,bottom:0}},g&&C?c.createElement("text",{className:(0,a.q)("fill-tremor-content-emphasis","dark:fill-dark-tremor-content-emphasis"),x:"50%",y:"50%",textAnchor:"middle",dominantBaseline:"middle"},O):null,c.createElement(eM,{className:(0,a.q)("stroke-tremor-background dark:stroke-dark-tremor-background",w?"cursor-pointer":"cursor-default"),data:eD(n,f),cx:"50%",cy:"50%",startAngle:90,endAngle:-270,innerRadius:C?"75%":"0%",outerRadius:"100%",stroke:"",strokeLinejoin:"round",dataKey:s,nameKey:d,isAnimationActive:y,animationDuration:v,onClick:function(e,t,n){n.stopPropagation(),N&&(j===t?(P(void 0),null==w||w(null)):(P(t),null==w||w(Object.assign({eventType:"slice"},e.payload.payload))))},activeIndex:j,inactiveShape:eL,style:{outline:"none"}}),c.createElement(eR.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,content:b?e=>{var t;let{active:n,payload:r}=e;return S?c.createElement(S,{payload:null==r?void 0:r.map(e=>{var t,n,i;return Object.assign(Object.assign({},e),{color:null!==(i=null===(n=null===(t=null==r?void 0:r[0])||void 0===t?void 0:t.payload)||void 0===n?void 0:n.color)&&void 0!==i?i:o.fr.Gray})}),active:n,label:null===(t=null==r?void 0:r[0])||void 0===t?void 0:t.name}):c.createElement(e_,{active:n,payload:r,valueFormatter:h})}:c.createElement(c.Fragment,null)})):c.createElement(eT.Z,{noDataText:x})))});ez.displayName="DonutChart"},59664:function(e,t,n){"use strict";n.d(t,{Z:function(){return E}});var r=n(5853),o=n(2265),i=n(47625),a=n(93765),l=n(54061),c=n(97059),s=n(62994),u=n(25311),d=(0,a.z)({chartName:"LineChart",GraphicalChild:l.x,axisComponents:[{axisType:"xAxis",AxisComp:c.K},{axisType:"yAxis",AxisComp:s.B}],formatAxisMap:u.t9}),f=n(56940),p=n(8147),h=n(22190),m=n(81889),g=n(65278),v=n(98593),y=n(69448),b=n(32644),x=n(7084),w=n(26898),S=n(65954),k=n(1153);let E=o.forwardRef((e,t)=>{let{data:n=[],categories:a=[],index:u,colors:E=w.s,valueFormatter:C=k.Cj,startEndOnly:O=!1,showXAxis:j=!0,showYAxis:P=!0,yAxisWidth:N=56,intervalType:M="equidistantPreserveStart",animationDuration:I=900,showAnimation:R=!1,showTooltip:T=!0,showLegend:A=!0,showGridLines:_=!0,autoMinValue:D=!1,curveType:Z="linear",minValue:L,maxValue:z,connectNulls:B=!1,allowDecimals:F=!0,noDataText:H,className:q,onValueChange:W,enableLegendSlider:K=!1,customTooltip:V,rotateLabelX:U,tickGap:G=5}=e,X=(0,r._T)(e,["data","categories","index","colors","valueFormatter","startEndOnly","showXAxis","showYAxis","yAxisWidth","intervalType","animationDuration","showAnimation","showTooltip","showLegend","showGridLines","autoMinValue","curveType","minValue","maxValue","connectNulls","allowDecimals","noDataText","className","onValueChange","enableLegendSlider","customTooltip","rotateLabelX","tickGap"]),$=j||P?20:0,[Y,Q]=(0,o.useState)(60),[J,ee]=(0,o.useState)(void 0),[et,en]=(0,o.useState)(void 0),er=(0,b.me)(a,E),eo=(0,b.i4)(D,L,z),ei=!!W;function ea(e){ei&&(e===et&&!J||(0,b.FB)(n,e)&&J&&J.dataKey===e?(en(void 0),null==W||W(null)):(en(e),null==W||W({eventType:"category",categoryClicked:e})),ee(void 0))}return o.createElement("div",Object.assign({ref:t,className:(0,S.q)("w-full h-80",q)},X),o.createElement(i.h,{className:"h-full w-full"},(null==n?void 0:n.length)?o.createElement(d,{data:n,onClick:ei&&(et||J)?()=>{ee(void 0),en(void 0),null==W||W(null)}:void 0},_?o.createElement(f.q,{className:(0,S.q)("stroke-1","stroke-tremor-border","dark:stroke-dark-tremor-border"),horizontal:!0,vertical:!1}):null,o.createElement(c.K,{padding:{left:$,right:$},hide:!j,dataKey:u,interval:O?"preserveStartEnd":M,tick:{transform:"translate(0, 6)"},ticks:O?[n[0][u],n[n.length-1][u]]:void 0,fill:"",stroke:"",className:(0,S.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickLine:!1,axisLine:!1,minTickGap:G,angle:null==U?void 0:U.angle,dy:null==U?void 0:U.verticalShift,height:null==U?void 0:U.xAxisHeight}),o.createElement(s.B,{width:N,hide:!P,axisLine:!1,tickLine:!1,type:"number",domain:eo,tick:{transform:"translate(-3, 0)"},fill:"",stroke:"",className:(0,S.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickFormatter:C,allowDecimals:F}),o.createElement(p.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,cursor:{stroke:"#d1d5db",strokeWidth:1},content:T?e=>{let{active:t,payload:n,label:r}=e;return V?o.createElement(V,{payload:null==n?void 0:n.map(e=>{var t;return Object.assign(Object.assign({},e),{color:null!==(t=er.get(e.dataKey))&&void 0!==t?t:x.fr.Gray})}),active:t,label:r}):o.createElement(v.ZP,{active:t,payload:n,label:r,valueFormatter:C,categoryColors:er})}:o.createElement(o.Fragment,null),position:{y:0}}),A?o.createElement(h.D,{verticalAlign:"top",height:Y,content:e=>{let{payload:t}=e;return(0,g.Z)({payload:t},er,Q,et,ei?e=>ea(e):void 0,K)}}):null,a.map(e=>{var t;return o.createElement(l.x,{className:(0,S.q)((0,k.bM)(null!==(t=er.get(e))&&void 0!==t?t:x.fr.Gray,w.K.text).strokeColor),strokeOpacity:J||et&&et!==e?.3:1,activeDot:e=>{var t;let{cx:r,cy:i,stroke:a,strokeLinecap:l,strokeLinejoin:c,strokeWidth:s,dataKey:u}=e;return o.createElement(m.o,{className:(0,S.q)("stroke-tremor-background dark:stroke-dark-tremor-background",W?"cursor-pointer":"",(0,k.bM)(null!==(t=er.get(u))&&void 0!==t?t:x.fr.Gray,w.K.text).fillColor),cx:r,cy:i,r:5,fill:"",stroke:a,strokeLinecap:l,strokeLinejoin:c,strokeWidth:s,onClick:(t,r)=>{r.stopPropagation(),ei&&(e.index===(null==J?void 0:J.index)&&e.dataKey===(null==J?void 0:J.dataKey)||(0,b.FB)(n,e.dataKey)&&et&&et===e.dataKey?(en(void 0),ee(void 0),null==W||W(null)):(en(e.dataKey),ee({index:e.index,dataKey:e.dataKey}),null==W||W(Object.assign({eventType:"dot",categoryClicked:e.dataKey},e.payload))))}})},dot:t=>{var r;let{stroke:i,strokeLinecap:a,strokeLinejoin:l,strokeWidth:c,cx:s,cy:u,dataKey:d,index:f}=t;return(0,b.FB)(n,e)&&!(J||et&&et!==e)||(null==J?void 0:J.index)===f&&(null==J?void 0:J.dataKey)===e?o.createElement(m.o,{key:f,cx:s,cy:u,r:5,stroke:i,fill:"",strokeLinecap:a,strokeLinejoin:l,strokeWidth:c,className:(0,S.q)("stroke-tremor-background dark:stroke-dark-tremor-background",W?"cursor-pointer":"",(0,k.bM)(null!==(r=er.get(d))&&void 0!==r?r:x.fr.Gray,w.K.text).fillColor)}):o.createElement(o.Fragment,{key:f})},key:e,name:e,type:Z,dataKey:e,stroke:"",strokeWidth:2,strokeLinejoin:"round",strokeLinecap:"round",isAnimationActive:R,animationDuration:I,connectNulls:B})}),W?a.map(e=>o.createElement(l.x,{className:(0,S.q)("cursor-pointer"),strokeOpacity:0,key:e,name:e,type:Z,dataKey:e,stroke:"transparent",fill:"transparent",legendType:"none",tooltipType:"none",strokeWidth:12,connectNulls:B,onClick:(e,t)=>{t.stopPropagation();let{name:n}=e;ea(n)}})):null):o.createElement(y.Z,{noDataText:H})))});E.displayName="LineChart"},65278:function(e,t,n){"use strict";n.d(t,{Z:function(){return m}});var r=n(2265);let o=(e,t)=>{let[n,o]=(0,r.useState)(t);(0,r.useEffect)(()=>{let t=()=>{o(window.innerWidth),e()};return t(),window.addEventListener("resize",t),()=>window.removeEventListener("resize",t)},[e,n])};var i=n(5853),a=n(26898),l=n(65954),c=n(1153);let s=e=>{var t=(0,i._T)(e,[]);return r.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"}),r.createElement("path",{d:"M8 12L14 6V18L8 12Z"}))},u=e=>{var t=(0,i._T)(e,[]);return r.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"}),r.createElement("path",{d:"M16 12L10 18V6L16 12Z"}))},d=(0,c.fn)("Legend"),f=e=>{let{name:t,color:n,onClick:o,activeLegend:i}=e,s=!!o;return r.createElement("li",{className:(0,l.q)(d("legendItem"),"group inline-flex items-center px-2 py-0.5 rounded-tremor-small transition whitespace-nowrap",s?"cursor-pointer":"cursor-default","text-tremor-content",s?"hover:bg-tremor-background-subtle":"","dark:text-dark-tremor-content",s?"dark:hover:bg-dark-tremor-background-subtle":""),onClick:e=>{e.stopPropagation(),null==o||o(t,n)}},r.createElement("svg",{className:(0,l.q)("flex-none h-2 w-2 mr-1.5",(0,c.bM)(n,a.K.text).textColor,i&&i!==t?"opacity-40":"opacity-100"),fill:"currentColor",viewBox:"0 0 8 8"},r.createElement("circle",{cx:4,cy:4,r:4})),r.createElement("p",{className:(0,l.q)("whitespace-nowrap truncate text-tremor-default","text-tremor-content",s?"group-hover:text-tremor-content-emphasis":"","dark:text-dark-tremor-content",i&&i!==t?"opacity-40":"opacity-100",s?"dark:group-hover:text-dark-tremor-content-emphasis":"")},t))},p=e=>{let{icon:t,onClick:n,disabled:o}=e,[i,a]=r.useState(!1),c=r.useRef(null);return r.useEffect(()=>(i?c.current=setInterval(()=>{null==n||n()},300):clearInterval(c.current),()=>clearInterval(c.current)),[i,n]),(0,r.useEffect)(()=>{o&&(clearInterval(c.current),a(!1))},[o]),r.createElement("button",{type:"button",className:(0,l.q)(d("legendSliderButton"),"w-5 group inline-flex items-center truncate rounded-tremor-small transition",o?"cursor-not-allowed":"cursor-pointer",o?"text-tremor-content-subtle":"text-tremor-content hover:text-tremor-content-emphasis hover:bg-tremor-background-subtle",o?"dark:text-dark-tremor-subtle":"dark:text-dark-tremor dark:hover:text-tremor-content-emphasis dark:hover:bg-dark-tremor-background-subtle"),disabled:o,onClick:e=>{e.stopPropagation(),null==n||n()},onMouseDown:e=>{e.stopPropagation(),a(!0)},onMouseUp:e=>{e.stopPropagation(),a(!1)}},r.createElement(t,{className:"w-full"}))},h=r.forwardRef((e,t)=>{var n,o;let{categories:c,colors:h=a.s,className:m,onClickLegendItem:g,activeLegend:v,enableLegendSlider:y=!1}=e,b=(0,i._T)(e,["categories","colors","className","onClickLegendItem","activeLegend","enableLegendSlider"]),x=r.useRef(null),[w,S]=r.useState(null),[k,E]=r.useState(null),C=r.useRef(null),O=(0,r.useCallback)(()=>{let e=null==x?void 0:x.current;e&&S({left:e.scrollLeft>0,right:e.scrollWidth-e.clientWidth>e.scrollLeft})},[S]),j=(0,r.useCallback)(e=>{var t;let n=null==x?void 0:x.current,r=null!==(t=null==n?void 0:n.clientWidth)&&void 0!==t?t:0;n&&y&&(n.scrollTo({left:"left"===e?n.scrollLeft-r:n.scrollLeft+r,behavior:"smooth"}),setTimeout(()=>{O()},400))},[y,O]);r.useEffect(()=>{let e=e=>{"ArrowLeft"===e?j("left"):"ArrowRight"===e&&j("right")};return k?(e(k),C.current=setInterval(()=>{e(k)},300)):clearInterval(C.current),()=>clearInterval(C.current)},[k,j]);let P=e=>{e.stopPropagation(),"ArrowLeft"!==e.key&&"ArrowRight"!==e.key||(e.preventDefault(),E(e.key))},N=e=>{e.stopPropagation(),E(null)};return r.useEffect(()=>{let e=null==x?void 0:x.current;return y&&(O(),null==e||e.addEventListener("keydown",P),null==e||e.addEventListener("keyup",N)),()=>{null==e||e.removeEventListener("keydown",P),null==e||e.removeEventListener("keyup",N)}},[O,y]),r.createElement("ol",Object.assign({ref:t,className:(0,l.q)(d("root"),"relative overflow-hidden",m)},b),r.createElement("div",{ref:x,tabIndex:0,className:(0,l.q)("h-full flex",y?(null==w?void 0:w.right)||(null==w?void 0:w.left)?"pl-4 pr-12 items-center overflow-auto snap-mandatory [&::-webkit-scrollbar]:hidden [scrollbar-width:none]":"":"flex-wrap")},c.map((e,t)=>r.createElement(f,{key:"item-".concat(t),name:e,color:h[t],onClick:g,activeLegend:v}))),y&&((null==w?void 0:w.right)||(null==w?void 0:w.left))?r.createElement(r.Fragment,null,r.createElement("div",{className:(0,l.q)("from-tremor-background","dark:from-dark-tremor-background","absolute top-0 bottom-0 left-0 w-4 bg-gradient-to-r to-transparent pointer-events-none")}),r.createElement("div",{className:(0,l.q)("to-tremor-background","dark:to-dark-tremor-background","absolute top-0 bottom-0 right-10 w-4 bg-gradient-to-r from-transparent pointer-events-none")}),r.createElement("div",{className:(0,l.q)("bg-tremor-background","dark:bg-dark-tremor-background","absolute flex top-0 pr-1 bottom-0 right-0 items-center justify-center h-full")},r.createElement(p,{icon:s,onClick:()=>{E(null),j("left")},disabled:!(null==w?void 0:w.left)}),r.createElement(p,{icon:u,onClick:()=>{E(null),j("right")},disabled:!(null==w?void 0:w.right)}))):null)});h.displayName="Legend";let m=(e,t,n,i,a,l)=>{let{payload:c}=e,s=(0,r.useRef)(null);o(()=>{var e,t;n((t=null===(e=s.current)||void 0===e?void 0:e.clientHeight)?Number(t)+20:60)});let u=c.filter(e=>"none"!==e.type);return r.createElement("div",{ref:s,className:"flex items-center justify-end"},r.createElement(h,{categories:u.map(e=>e.value),colors:u.map(e=>t.get(e.value)),onClickLegendItem:a,activeLegend:i,enableLegendSlider:l}))}},98593:function(e,t,n){"use strict";n.d(t,{$B:function(){return c},ZP:function(){return u},zX:function(){return s}});var r=n(2265),o=n(7084),i=n(26898),a=n(65954),l=n(1153);let c=e=>{let{children:t}=e;return r.createElement("div",{className:(0,a.q)("rounded-tremor-default text-tremor-default border","bg-tremor-background shadow-tremor-dropdown border-tremor-border","dark:bg-dark-tremor-background dark:shadow-dark-tremor-dropdown dark:border-dark-tremor-border")},t)},s=e=>{let{value:t,name:n,color:o}=e;return r.createElement("div",{className:"flex items-center justify-between space-x-8"},r.createElement("div",{className:"flex items-center space-x-2"},r.createElement("span",{className:(0,a.q)("shrink-0 rounded-tremor-full border-2 h-3 w-3","border-tremor-background shadow-tremor-card","dark:border-dark-tremor-background dark:shadow-dark-tremor-card",(0,l.bM)(o,i.K.background).bgColor)}),r.createElement("p",{className:(0,a.q)("text-right whitespace-nowrap","text-tremor-content","dark:text-dark-tremor-content")},n)),r.createElement("p",{className:(0,a.q)("font-medium tabular-nums text-right whitespace-nowrap","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis")},t))},u=e=>{let{active:t,payload:n,label:i,categoryColors:l,valueFormatter:u}=e;if(t&&n){let e=n.filter(e=>"none"!==e.type);return r.createElement(c,null,r.createElement("div",{className:(0,a.q)("border-tremor-border border-b px-4 py-2","dark:border-dark-tremor-border")},r.createElement("p",{className:(0,a.q)("font-medium","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis")},i)),r.createElement("div",{className:(0,a.q)("px-4 py-2 space-y-1")},e.map((e,t)=>{var n;let{value:i,name:a}=e;return r.createElement(s,{key:"id-".concat(t),value:u(i),name:a,color:null!==(n=l.get(a))&&void 0!==n?n:o.fr.Blue})})))}return null}},69448:function(e,t,n){"use strict";n.d(t,{Z:function(){return f}});var r=n(65954),o=n(2265),i=n(5853);let a=(0,n(1153).fn)("Flex"),l={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"},c={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"},s={row:"flex-row",col:"flex-col","row-reverse":"flex-row-reverse","col-reverse":"flex-col-reverse"},u=o.forwardRef((e,t)=>{let{flexDirection:n="row",justifyContent:u="between",alignItems:d="center",children:f,className:p}=e,h=(0,i._T)(e,["flexDirection","justifyContent","alignItems","children","className"]);return o.createElement("div",Object.assign({ref:t,className:(0,r.q)(a("root"),"flex w-full",s[n],l[u],c[d],p)},h),f)});u.displayName="Flex";var d=n(84264);let f=e=>{let{noDataText:t="No data"}=e;return o.createElement(u,{alignItems:"center",justifyContent:"center",className:(0,r.q)("w-full h-full border border-dashed rounded-tremor-default","border-tremor-border","dark:border-dark-tremor-border")},o.createElement(d.Z,{className:(0,r.q)("text-tremor-content","dark:text-dark-tremor-content")},t))}},32644:function(e,t,n){"use strict";n.d(t,{FB:function(){return i},i4:function(){return o},me:function(){return r},vZ:function(){return function e(t,n){if(t===n)return!0;if("object"!=typeof t||"object"!=typeof n||null===t||null===n)return!1;let r=Object.keys(t),o=Object.keys(n);if(r.length!==o.length)return!1;for(let i of r)if(!o.includes(i)||!e(t[i],n[i]))return!1;return!0}}});let r=(e,t)=>{let n=new Map;return e.forEach((e,r)=>{n.set(e,t[r])}),n},o=(e,t,n)=>[e?"auto":null!=t?t:0,null!=n?n:"auto"];function i(e,t){let n=[];for(let r of e)if(Object.prototype.hasOwnProperty.call(r,t)&&(n.push(r[t]),n.length>1))return!1;return!0}},41649:function(e,t,n){"use strict";n.d(t,{Z:function(){return p}});var r=n(5853),o=n(2265),i=n(1526),a=n(7084),l=n(26898),c=n(65954),s=n(1153);let u={xs:{paddingX:"px-2",paddingY:"py-0.5",fontSize:"text-xs"},sm:{paddingX:"px-2.5",paddingY:"py-0.5",fontSize:"text-sm"},md:{paddingX:"px-3",paddingY:"py-0.5",fontSize:"text-md"},lg:{paddingX:"px-3.5",paddingY:"py-0.5",fontSize:"text-lg"},xl:{paddingX:"px-4",paddingY:"py-1",fontSize:"text-xl"}},d={xs:{height:"h-4",width:"w-4"},sm:{height:"h-4",width:"w-4"},md:{height:"h-4",width:"w-4"},lg:{height:"h-5",width:"w-5"},xl:{height:"h-6",width:"w-6"}},f=(0,s.fn)("Badge"),p=o.forwardRef((e,t)=>{let{color:n,icon:p,size:h=a.u8.SM,tooltip:m,className:g,children:v}=e,y=(0,r._T)(e,["color","icon","size","tooltip","className","children"]),b=p||null,{tooltipProps:x,getReferenceProps:w}=(0,i.l)();return o.createElement("span",Object.assign({ref:(0,s.lq)([t,x.refs.setReference]),className:(0,c.q)(f("root"),"w-max flex-shrink-0 inline-flex justify-center items-center cursor-default rounded-tremor-full",n?(0,c.q)((0,s.bM)(n,l.K.background).bgColor,(0,s.bM)(n,l.K.text).textColor,"bg-opacity-20 dark:bg-opacity-25"):(0,c.q)("bg-tremor-brand-muted text-tremor-brand-emphasis","dark:bg-dark-tremor-brand-muted dark:text-dark-tremor-brand-emphasis"),u[h].paddingX,u[h].paddingY,u[h].fontSize,g)},w,y),o.createElement(i.Z,Object.assign({text:m},x)),b?o.createElement(b,{className:(0,c.q)(f("icon"),"shrink-0 -ml-1 mr-1.5",d[h].height,d[h].width)}):null,o.createElement("p",{className:(0,c.q)(f("text"),"text-sm whitespace-nowrap")},v))});p.displayName="Badge"},47323:function(e,t,n){"use strict";n.d(t,{Z:function(){return m}});var r=n(5853),o=n(2265),i=n(1526),a=n(7084),l=n(65954),c=n(1153),s=n(26898);let u={xs:{paddingX:"px-1.5",paddingY:"py-1.5"},sm:{paddingX:"px-1.5",paddingY:"py-1.5"},md:{paddingX:"px-2",paddingY:"py-2"},lg:{paddingX:"px-2",paddingY:"py-2"},xl:{paddingX:"px-2.5",paddingY:"py-2.5"}},d={xs:{height:"h-3",width:"w-3"},sm:{height:"h-5",width:"w-5"},md:{height:"h-5",width:"w-5"},lg:{height:"h-7",width:"w-7"},xl:{height:"h-9",width:"w-9"}},f={simple:{rounded:"",border:"",ring:"",shadow:""},light:{rounded:"rounded-tremor-default",border:"",ring:"",shadow:""},shadow:{rounded:"rounded-tremor-default",border:"border",ring:"",shadow:"shadow-tremor-card dark:shadow-dark-tremor-card"},solid:{rounded:"rounded-tremor-default",border:"border-2",ring:"ring-1",shadow:""},outlined:{rounded:"rounded-tremor-default",border:"border",ring:"ring-2",shadow:""}},p=(e,t)=>{switch(e){case"simple":return{textColor:t?(0,c.bM)(t,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:"",borderColor:"",ringColor:""};case"light":return{textColor:t?(0,c.bM)(t,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:t?(0,l.q)((0,c.bM)(t,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-brand-muted dark:bg-dark-tremor-brand-muted",borderColor:"",ringColor:""};case"shadow":return{textColor:t?(0,c.bM)(t,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:t?(0,l.q)((0,c.bM)(t,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-background dark:bg-dark-tremor-background",borderColor:"border-tremor-border dark:border-dark-tremor-border",ringColor:""};case"solid":return{textColor:t?(0,c.bM)(t,s.K.text).textColor:"text-tremor-brand-inverted dark:text-dark-tremor-brand-inverted",bgColor:t?(0,l.q)((0,c.bM)(t,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-brand dark:bg-dark-tremor-brand",borderColor:"border-tremor-brand-inverted dark:border-dark-tremor-brand-inverted",ringColor:"ring-tremor-ring dark:ring-dark-tremor-ring"};case"outlined":return{textColor:t?(0,c.bM)(t,s.K.text).textColor:"text-tremor-brand dark:text-dark-tremor-brand",bgColor:t?(0,l.q)((0,c.bM)(t,s.K.background).bgColor,"bg-opacity-20"):"bg-tremor-background dark:bg-dark-tremor-background",borderColor:t?(0,c.bM)(t,s.K.ring).borderColor:"border-tremor-brand-subtle dark:border-dark-tremor-brand-subtle",ringColor:t?(0,l.q)((0,c.bM)(t,s.K.ring).ringColor,"ring-opacity-40"):"ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted"}}},h=(0,c.fn)("Icon"),m=o.forwardRef((e,t)=>{let{icon:n,variant:s="simple",tooltip:m,size:g=a.u8.SM,color:v,className:y}=e,b=(0,r._T)(e,["icon","variant","tooltip","size","color","className"]),x=p(s,v),{tooltipProps:w,getReferenceProps:S}=(0,i.l)();return o.createElement("span",Object.assign({ref:(0,c.lq)([t,w.refs.setReference]),className:(0,l.q)(h("root"),"inline-flex flex-shrink-0 items-center",x.bgColor,x.textColor,x.borderColor,x.ringColor,f[s].rounded,f[s].border,f[s].shadow,f[s].ring,u[g].paddingX,u[g].paddingY,y)},S,b),o.createElement(i.Z,Object.assign({text:m},w)),o.createElement(n,{className:(0,l.q)(h("icon"),"shrink-0",d[g].height,d[g].width)}))});m.displayName="Icon"},53003:function(e,t,n){"use strict";let r,o,i;n.d(t,{Z:function(){return nF}});var a,l,c,s,u=n(5853),d=n(2265),f=n(54887),p=n(13323),h=n(64518),m=n(96822),g=n(40293);function v(){for(var e=arguments.length,t=Array(e),n=0;n(0,g.r)(...t),[...t])}var y=n(72238),b=n(93689);let x=(0,d.createContext)(!1);var w=n(61424),S=n(27847);let k=d.Fragment,E=d.Fragment,C=(0,d.createContext)(null),O=(0,d.createContext)(null);Object.assign((0,S.yV)(function(e,t){var n;let r,o,i=(0,d.useRef)(null),a=(0,b.T)((0,b.h)(e=>{i.current=e}),t),l=v(i),c=function(e){let t=(0,d.useContext)(x),n=(0,d.useContext)(C),r=v(e),[o,i]=(0,d.useState)(()=>{if(!t&&null!==n||w.O.isServer)return null;let e=null==r?void 0:r.getElementById("headlessui-portal-root");if(e)return e;if(null===r)return null;let o=r.createElement("div");return o.setAttribute("id","headlessui-portal-root"),r.body.appendChild(o)});return(0,d.useEffect)(()=>{null!==o&&(null!=r&&r.body.contains(o)||null==r||r.body.appendChild(o))},[o,r]),(0,d.useEffect)(()=>{t||null!==n&&i(n.current)},[n,i,t]),o}(i),[s]=(0,d.useState)(()=>{var e;return w.O.isServer?null:null!=(e=null==l?void 0:l.createElement("div"))?e:null}),u=(0,d.useContext)(O),g=(0,y.H)();return(0,h.e)(()=>{!c||!s||c.contains(s)||(s.setAttribute("data-headlessui-portal",""),c.appendChild(s))},[c,s]),(0,h.e)(()=>{if(s&&u)return u.register(s)},[u,s]),n=()=>{var e;c&&s&&(s instanceof Node&&c.contains(s)&&c.removeChild(s),c.childNodes.length<=0&&(null==(e=c.parentElement)||e.removeChild(c)))},r=(0,p.z)(n),o=(0,d.useRef)(!1),(0,d.useEffect)(()=>(o.current=!1,()=>{o.current=!0,(0,m.Y)(()=>{o.current&&r()})}),[r]),g&&c&&s?(0,f.createPortal)((0,S.sY)({ourProps:{ref:a},theirProps:e,defaultTag:k,name:"Portal"}),s):null}),{Group:(0,S.yV)(function(e,t){let{target:n,...r}=e,o={ref:(0,b.T)(t)};return d.createElement(C.Provider,{value:n},(0,S.sY)({ourProps:o,theirProps:r,defaultTag:E,name:"Popover.Group"}))})});var j=n(31948),P=n(17684),N=n(98505),M=n(80004),I=n(38198),R=n(3141),T=((r=T||{})[r.Forwards=0]="Forwards",r[r.Backwards=1]="Backwards",r);function A(){let e=(0,d.useRef)(0);return(0,R.s)("keydown",t=>{"Tab"===t.key&&(e.current=t.shiftKey?1:0)},!0),e}var _=n(37863),D=n(47634),Z=n(37105),L=n(24536),z=n(37388),B=((o=B||{})[o.Open=0]="Open",o[o.Closed=1]="Closed",o),F=((i=F||{})[i.TogglePopover=0]="TogglePopover",i[i.ClosePopover=1]="ClosePopover",i[i.SetButton=2]="SetButton",i[i.SetButtonId=3]="SetButtonId",i[i.SetPanel=4]="SetPanel",i[i.SetPanelId=5]="SetPanelId",i);let H={0:e=>{let t={...e,popoverState:(0,L.E)(e.popoverState,{0:1,1:0})};return 0===t.popoverState&&(t.__demoMode=!1),t},1:e=>1===e.popoverState?e:{...e,popoverState:1},2:(e,t)=>e.button===t.button?e:{...e,button:t.button},3:(e,t)=>e.buttonId===t.buttonId?e:{...e,buttonId:t.buttonId},4:(e,t)=>e.panel===t.panel?e:{...e,panel:t.panel},5:(e,t)=>e.panelId===t.panelId?e:{...e,panelId:t.panelId}},q=(0,d.createContext)(null);function W(e){let t=(0,d.useContext)(q);if(null===t){let t=Error("<".concat(e," /> is missing a parent component."));throw Error.captureStackTrace&&Error.captureStackTrace(t,W),t}return t}q.displayName="PopoverContext";let K=(0,d.createContext)(null);function V(e){let t=(0,d.useContext)(K);if(null===t){let t=Error("<".concat(e," /> is missing a parent component."));throw Error.captureStackTrace&&Error.captureStackTrace(t,V),t}return t}K.displayName="PopoverAPIContext";let U=(0,d.createContext)(null);function G(){return(0,d.useContext)(U)}U.displayName="PopoverGroupContext";let X=(0,d.createContext)(null);function $(e,t){return(0,L.E)(t.type,H,e,t)}X.displayName="PopoverPanelContext";let Y=S.AN.RenderStrategy|S.AN.Static,Q=S.AN.RenderStrategy|S.AN.Static,J=Object.assign((0,S.yV)(function(e,t){var n,r,o,i;let a,l,c,s,u,f;let{__demoMode:h=!1,...m}=e,g=(0,d.useRef)(null),y=(0,b.T)(t,(0,b.h)(e=>{g.current=e})),x=(0,d.useRef)([]),w=(0,d.useReducer)($,{__demoMode:h,popoverState:h?0:1,buttons:x,button:null,buttonId:null,panel:null,panelId:null,beforePanelSentinel:(0,d.createRef)(),afterPanelSentinel:(0,d.createRef)()}),[{popoverState:k,button:E,buttonId:C,panel:P,panelId:M,beforePanelSentinel:R,afterPanelSentinel:T},A]=w,D=v(null!=(n=g.current)?n:E),z=(0,d.useMemo)(()=>{if(!E||!P)return!1;for(let e of document.querySelectorAll("body > *"))if(Number(null==e?void 0:e.contains(E))^Number(null==e?void 0:e.contains(P)))return!0;let e=(0,Z.GO)(),t=e.indexOf(E),n=(t+e.length-1)%e.length,r=(t+1)%e.length,o=e[n],i=e[r];return!P.contains(o)&&!P.contains(i)},[E,P]),B=(0,j.E)(C),F=(0,j.E)(M),H=(0,d.useMemo)(()=>({buttonId:B,panelId:F,close:()=>A({type:1})}),[B,F,A]),W=G(),V=null==W?void 0:W.registerPopover,U=(0,p.z)(()=>{var e;return null!=(e=null==W?void 0:W.isFocusWithinPopoverGroup())?e:(null==D?void 0:D.activeElement)&&((null==E?void 0:E.contains(D.activeElement))||(null==P?void 0:P.contains(D.activeElement)))});(0,d.useEffect)(()=>null==V?void 0:V(H),[V,H]);let[Y,Q]=(a=(0,d.useContext)(O),l=(0,d.useRef)([]),c=(0,p.z)(e=>(l.current.push(e),a&&a.register(e),()=>s(e))),s=(0,p.z)(e=>{let t=l.current.indexOf(e);-1!==t&&l.current.splice(t,1),a&&a.unregister(e)}),u=(0,d.useMemo)(()=>({register:c,unregister:s,portals:l}),[c,s,l]),[l,(0,d.useMemo)(()=>function(e){let{children:t}=e;return d.createElement(O.Provider,{value:u},t)},[u])]),J=function(){var e;let{defaultContainers:t=[],portals:n,mainTreeNodeRef:r}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=(0,d.useRef)(null!=(e=null==r?void 0:r.current)?e:null),i=v(o),a=(0,p.z)(()=>{var e,r,a;let l=[];for(let e of t)null!==e&&(e instanceof HTMLElement?l.push(e):"current"in e&&e.current instanceof HTMLElement&&l.push(e.current));if(null!=n&&n.current)for(let e of n.current)l.push(e);for(let t of null!=(e=null==i?void 0:i.querySelectorAll("html > *, body > *"))?e:[])t!==document.body&&t!==document.head&&t instanceof HTMLElement&&"headlessui-portal-root"!==t.id&&(t.contains(o.current)||t.contains(null==(a=null==(r=o.current)?void 0:r.getRootNode())?void 0:a.host)||l.some(e=>t.contains(e))||l.push(t));return l});return{resolveContainers:a,contains:(0,p.z)(e=>a().some(t=>t.contains(e))),mainTreeNodeRef:o,MainTreeNode:(0,d.useMemo)(()=>function(){return null!=r?null:d.createElement(I._,{features:I.A.Hidden,ref:o})},[o,r])}}({mainTreeNodeRef:null==W?void 0:W.mainTreeNodeRef,portals:Y,defaultContainers:[E,P]});r=null==D?void 0:D.defaultView,o="focus",i=e=>{var t,n,r,o;e.target!==window&&e.target instanceof HTMLElement&&0===k&&(U()||E&&P&&(J.contains(e.target)||null!=(n=null==(t=R.current)?void 0:t.contains)&&n.call(t,e.target)||null!=(o=null==(r=T.current)?void 0:r.contains)&&o.call(r,e.target)||A({type:1})))},f=(0,j.E)(i),(0,d.useEffect)(()=>{function e(e){f.current(e)}return(r=null!=r?r:window).addEventListener(o,e,!0),()=>r.removeEventListener(o,e,!0)},[r,o,!0]),(0,N.O)(J.resolveContainers,(e,t)=>{A({type:1}),(0,Z.sP)(t,Z.tJ.Loose)||(e.preventDefault(),null==E||E.focus())},0===k);let ee=(0,p.z)(e=>{A({type:1});let t=e?e instanceof HTMLElement?e:"current"in e&&e.current instanceof HTMLElement?e.current:E:E;null==t||t.focus()}),et=(0,d.useMemo)(()=>({close:ee,isPortalled:z}),[ee,z]),en=(0,d.useMemo)(()=>({open:0===k,close:ee}),[k,ee]);return d.createElement(X.Provider,{value:null},d.createElement(q.Provider,{value:w},d.createElement(K.Provider,{value:et},d.createElement(_.up,{value:(0,L.E)(k,{0:_.ZM.Open,1:_.ZM.Closed})},d.createElement(Q,null,(0,S.sY)({ourProps:{ref:y},theirProps:m,slot:en,defaultTag:"div",name:"Popover"}),d.createElement(J.MainTreeNode,null))))))}),{Button:(0,S.yV)(function(e,t){let n=(0,P.M)(),{id:r="headlessui-popover-button-".concat(n),...o}=e,[i,a]=W("Popover.Button"),{isPortalled:l}=V("Popover.Button"),c=(0,d.useRef)(null),s="headlessui-focus-sentinel-".concat((0,P.M)()),u=G(),f=null==u?void 0:u.closeOthers,h=null!==(0,d.useContext)(X);(0,d.useEffect)(()=>{if(!h)return a({type:3,buttonId:r}),()=>{a({type:3,buttonId:null})}},[h,r,a]);let[m]=(0,d.useState)(()=>Symbol()),g=(0,b.T)(c,t,h?null:e=>{if(e)i.buttons.current.push(m);else{let e=i.buttons.current.indexOf(m);-1!==e&&i.buttons.current.splice(e,1)}i.buttons.current.length>1&&console.warn("You are already using a but only 1 is supported."),e&&a({type:2,button:e})}),y=(0,b.T)(c,t),x=v(c),w=(0,p.z)(e=>{var t,n,r;if(h){if(1===i.popoverState)return;switch(e.key){case z.R.Space:case z.R.Enter:e.preventDefault(),null==(n=(t=e.target).click)||n.call(t),a({type:1}),null==(r=i.button)||r.focus()}}else switch(e.key){case z.R.Space:case z.R.Enter:e.preventDefault(),e.stopPropagation(),1===i.popoverState&&(null==f||f(i.buttonId)),a({type:0});break;case z.R.Escape:if(0!==i.popoverState)return null==f?void 0:f(i.buttonId);if(!c.current||null!=x&&x.activeElement&&!c.current.contains(x.activeElement))return;e.preventDefault(),e.stopPropagation(),a({type:1})}}),k=(0,p.z)(e=>{h||e.key===z.R.Space&&e.preventDefault()}),E=(0,p.z)(t=>{var n,r;(0,D.P)(t.currentTarget)||e.disabled||(h?(a({type:1}),null==(n=i.button)||n.focus()):(t.preventDefault(),t.stopPropagation(),1===i.popoverState&&(null==f||f(i.buttonId)),a({type:0}),null==(r=i.button)||r.focus()))}),C=(0,p.z)(e=>{e.preventDefault(),e.stopPropagation()}),O=0===i.popoverState,j=(0,d.useMemo)(()=>({open:O}),[O]),N=(0,M.f)(e,c),R=h?{ref:y,type:N,onKeyDown:w,onClick:E}:{ref:g,id:i.buttonId,type:N,"aria-expanded":0===i.popoverState,"aria-controls":i.panel?i.panelId:void 0,onKeyDown:w,onKeyUp:k,onClick:E,onMouseDown:C},_=A(),B=(0,p.z)(()=>{let e=i.panel;e&&(0,L.E)(_.current,{[T.Forwards]:()=>(0,Z.jA)(e,Z.TO.First),[T.Backwards]:()=>(0,Z.jA)(e,Z.TO.Last)})===Z.fE.Error&&(0,Z.jA)((0,Z.GO)().filter(e=>"true"!==e.dataset.headlessuiFocusGuard),(0,L.E)(_.current,{[T.Forwards]:Z.TO.Next,[T.Backwards]:Z.TO.Previous}),{relativeTo:i.button})});return d.createElement(d.Fragment,null,(0,S.sY)({ourProps:R,theirProps:o,slot:j,defaultTag:"button",name:"Popover.Button"}),O&&!h&&l&&d.createElement(I._,{id:s,features:I.A.Focusable,"data-headlessui-focus-guard":!0,as:"button",type:"button",onFocus:B}))}),Overlay:(0,S.yV)(function(e,t){let n=(0,P.M)(),{id:r="headlessui-popover-overlay-".concat(n),...o}=e,[{popoverState:i},a]=W("Popover.Overlay"),l=(0,b.T)(t),c=(0,_.oJ)(),s=null!==c?(c&_.ZM.Open)===_.ZM.Open:0===i,u=(0,p.z)(e=>{if((0,D.P)(e.currentTarget))return e.preventDefault();a({type:1})}),f=(0,d.useMemo)(()=>({open:0===i}),[i]);return(0,S.sY)({ourProps:{ref:l,id:r,"aria-hidden":!0,onClick:u},theirProps:o,slot:f,defaultTag:"div",features:Y,visible:s,name:"Popover.Overlay"})}),Panel:(0,S.yV)(function(e,t){let n=(0,P.M)(),{id:r="headlessui-popover-panel-".concat(n),focus:o=!1,...i}=e,[a,l]=W("Popover.Panel"),{close:c,isPortalled:s}=V("Popover.Panel"),u="headlessui-focus-sentinel-before-".concat((0,P.M)()),f="headlessui-focus-sentinel-after-".concat((0,P.M)()),m=(0,d.useRef)(null),g=(0,b.T)(m,t,e=>{l({type:4,panel:e})}),y=v(m),x=(0,S.Y2)();(0,h.e)(()=>(l({type:5,panelId:r}),()=>{l({type:5,panelId:null})}),[r,l]);let w=(0,_.oJ)(),k=null!==w?(w&_.ZM.Open)===_.ZM.Open:0===a.popoverState,E=(0,p.z)(e=>{var t;if(e.key===z.R.Escape){if(0!==a.popoverState||!m.current||null!=y&&y.activeElement&&!m.current.contains(y.activeElement))return;e.preventDefault(),e.stopPropagation(),l({type:1}),null==(t=a.button)||t.focus()}});(0,d.useEffect)(()=>{var t;e.static||1===a.popoverState&&(null==(t=e.unmount)||t)&&l({type:4,panel:null})},[a.popoverState,e.unmount,e.static,l]),(0,d.useEffect)(()=>{if(a.__demoMode||!o||0!==a.popoverState||!m.current)return;let e=null==y?void 0:y.activeElement;m.current.contains(e)||(0,Z.jA)(m.current,Z.TO.First)},[a.__demoMode,o,m,a.popoverState]);let C=(0,d.useMemo)(()=>({open:0===a.popoverState,close:c}),[a,c]),O={ref:g,id:r,onKeyDown:E,onBlur:o&&0===a.popoverState?e=>{var t,n,r,o,i;let c=e.relatedTarget;c&&m.current&&(null!=(t=m.current)&&t.contains(c)||(l({type:1}),(null!=(r=null==(n=a.beforePanelSentinel.current)?void 0:n.contains)&&r.call(n,c)||null!=(i=null==(o=a.afterPanelSentinel.current)?void 0:o.contains)&&i.call(o,c))&&c.focus({preventScroll:!0})))}:void 0,tabIndex:-1},j=A(),N=(0,p.z)(()=>{let e=m.current;e&&(0,L.E)(j.current,{[T.Forwards]:()=>{var t;(0,Z.jA)(e,Z.TO.First)===Z.fE.Error&&(null==(t=a.afterPanelSentinel.current)||t.focus())},[T.Backwards]:()=>{var e;null==(e=a.button)||e.focus({preventScroll:!0})}})}),M=(0,p.z)(()=>{let e=m.current;e&&(0,L.E)(j.current,{[T.Forwards]:()=>{var e;if(!a.button)return;let t=(0,Z.GO)(),n=t.indexOf(a.button),r=t.slice(0,n+1),o=[...t.slice(n+1),...r];for(let t of o.slice())if("true"===t.dataset.headlessuiFocusGuard||null!=(e=a.panel)&&e.contains(t)){let e=o.indexOf(t);-1!==e&&o.splice(e,1)}(0,Z.jA)(o,Z.TO.First,{sorted:!1})},[T.Backwards]:()=>{var t;(0,Z.jA)(e,Z.TO.Previous)===Z.fE.Error&&(null==(t=a.button)||t.focus())}})});return d.createElement(X.Provider,{value:r},k&&s&&d.createElement(I._,{id:u,ref:a.beforePanelSentinel,features:I.A.Focusable,"data-headlessui-focus-guard":!0,as:"button",type:"button",onFocus:N}),(0,S.sY)({mergeRefs:x,ourProps:O,theirProps:i,slot:C,defaultTag:"div",features:Q,visible:k,name:"Popover.Panel"}),k&&s&&d.createElement(I._,{id:f,ref:a.afterPanelSentinel,features:I.A.Focusable,"data-headlessui-focus-guard":!0,as:"button",type:"button",onFocus:M}))}),Group:(0,S.yV)(function(e,t){let n;let r=(0,d.useRef)(null),o=(0,b.T)(r,t),[i,a]=(0,d.useState)([]),l={mainTreeNodeRef:n=(0,d.useRef)(null),MainTreeNode:(0,d.useMemo)(()=>function(){return d.createElement(I._,{features:I.A.Hidden,ref:n})},[n])},c=(0,p.z)(e=>{a(t=>{let n=t.indexOf(e);if(-1!==n){let e=t.slice();return e.splice(n,1),e}return t})}),s=(0,p.z)(e=>(a(t=>[...t,e]),()=>c(e))),u=(0,p.z)(()=>{var e;let t=(0,g.r)(r);if(!t)return!1;let n=t.activeElement;return!!(null!=(e=r.current)&&e.contains(n))||i.some(e=>{var r,o;return(null==(r=t.getElementById(e.buttonId.current))?void 0:r.contains(n))||(null==(o=t.getElementById(e.panelId.current))?void 0:o.contains(n))})}),f=(0,p.z)(e=>{for(let t of i)t.buttonId.current!==e&&t.close()}),h=(0,d.useMemo)(()=>({registerPopover:s,unregisterPopover:c,isFocusWithinPopoverGroup:u,closeOthers:f,mainTreeNodeRef:l.mainTreeNodeRef}),[s,c,u,f,l.mainTreeNodeRef]),m=(0,d.useMemo)(()=>({}),[]);return d.createElement(U.Provider,{value:h},(0,S.sY)({ourProps:{ref:o},theirProps:e,slot:m,defaultTag:"div",name:"Popover.Group"}),d.createElement(l.MainTreeNode,null))})});var ee=n(33044),et=n(28517);let en=e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor"}),d.createElement("path",{fillRule:"evenodd",d:"M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z",clipRule:"evenodd"}))};var er=n(4537),eo=n(99735),ei=n(7656);function ea(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e);return t.setHours(0,0,0,0),t}function el(){return ea(Date.now())}function ec(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e);return t.setDate(1),t.setHours(0,0,0,0),t}var es=n(65954),eu=n(96398),ed=n(41154);function ef(e){var t,n;if((0,ei.Z)(1,arguments),e&&"function"==typeof e.forEach)t=e;else{if("object"!==(0,ed.Z)(e)||null===e)return new Date(NaN);t=Array.prototype.slice.call(e)}return t.forEach(function(e){var t=(0,eo.Z)(e);(void 0===n||nt||isNaN(t.getDate()))&&(n=t)}),n||new Date(NaN)}var eh=n(25721),em=n(47869);function eg(e,t){(0,ei.Z)(2,arguments);var n=(0,em.Z)(t);return(0,eh.Z)(e,-n)}var ev=n(55463);function ey(e,t){if((0,ei.Z)(2,arguments),!t||"object"!==(0,ed.Z)(t))return new Date(NaN);var n=t.years?(0,em.Z)(t.years):0,r=t.months?(0,em.Z)(t.months):0,o=t.weeks?(0,em.Z)(t.weeks):0,i=t.days?(0,em.Z)(t.days):0,a=t.hours?(0,em.Z)(t.hours):0,l=t.minutes?(0,em.Z)(t.minutes):0,c=t.seconds?(0,em.Z)(t.seconds):0;return new Date(eg(function(e,t){(0,ei.Z)(2,arguments);var n=(0,em.Z)(t);return(0,ev.Z)(e,-n)}(e,r+12*n),i+7*o).getTime()-1e3*(c+60*(l+60*a)))}function eb(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=new Date(0);return n.setFullYear(t.getFullYear(),0,1),n.setHours(0,0,0,0),n}function ex(e){return(0,ei.Z)(1,arguments),e instanceof Date||"object"===(0,ed.Z)(e)&&"[object Date]"===Object.prototype.toString.call(e)}function ew(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getUTCDay();return t.setUTCDate(t.getUTCDate()-((n<1?7:0)+n-1)),t.setUTCHours(0,0,0,0),t}function eS(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getUTCFullYear(),r=new Date(0);r.setUTCFullYear(n+1,0,4),r.setUTCHours(0,0,0,0);var o=ew(r),i=new Date(0);i.setUTCFullYear(n,0,4),i.setUTCHours(0,0,0,0);var a=ew(i);return t.getTime()>=o.getTime()?n+1:t.getTime()>=a.getTime()?n:n-1}var ek={};function eE(e,t){(0,ei.Z)(1,arguments);var n,r,o,i,a,l,c,s,u=(0,em.Z)(null!==(n=null!==(r=null!==(o=null!==(i=null==t?void 0:t.weekStartsOn)&&void 0!==i?i:null==t?void 0:null===(a=t.locale)||void 0===a?void 0:null===(l=a.options)||void 0===l?void 0:l.weekStartsOn)&&void 0!==o?o:ek.weekStartsOn)&&void 0!==r?r:null===(c=ek.locale)||void 0===c?void 0:null===(s=c.options)||void 0===s?void 0:s.weekStartsOn)&&void 0!==n?n:0);if(!(u>=0&&u<=6))throw RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=(0,eo.Z)(e),f=d.getUTCDay();return d.setUTCDate(d.getUTCDate()-((f=1&&f<=7))throw RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var p=new Date(0);p.setUTCFullYear(d+1,0,f),p.setUTCHours(0,0,0,0);var h=eE(p,t),m=new Date(0);m.setUTCFullYear(d,0,f),m.setUTCHours(0,0,0,0);var g=eE(m,t);return u.getTime()>=h.getTime()?d+1:u.getTime()>=g.getTime()?d:d-1}function eO(e,t){for(var n=Math.abs(e).toString();n.length0?n:1-n;return eO("yy"===t?r%100:r,t.length)},M:function(e,t){var n=e.getUTCMonth();return"M"===t?String(n+1):eO(n+1,2)},d:function(e,t){return eO(e.getUTCDate(),t.length)},h:function(e,t){return eO(e.getUTCHours()%12||12,t.length)},H:function(e,t){return eO(e.getUTCHours(),t.length)},m:function(e,t){return eO(e.getUTCMinutes(),t.length)},s:function(e,t){return eO(e.getUTCSeconds(),t.length)},S:function(e,t){var n=t.length;return eO(Math.floor(e.getUTCMilliseconds()*Math.pow(10,n-3)),t.length)}},eP={midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"};function eN(e,t){var n=e>0?"-":"+",r=Math.abs(e),o=Math.floor(r/60),i=r%60;return 0===i?n+String(o):n+String(o)+(t||"")+eO(i,2)}function eM(e,t){return e%60==0?(e>0?"-":"+")+eO(Math.abs(e)/60,2):eI(e,t)}function eI(e,t){var n=Math.abs(e);return(e>0?"-":"+")+eO(Math.floor(n/60),2)+(t||"")+eO(n%60,2)}var eR={G:function(e,t,n){var r=e.getUTCFullYear()>0?1:0;switch(t){case"G":case"GG":case"GGG":return n.era(r,{width:"abbreviated"});case"GGGGG":return n.era(r,{width:"narrow"});default:return n.era(r,{width:"wide"})}},y:function(e,t,n){if("yo"===t){var r=e.getUTCFullYear();return n.ordinalNumber(r>0?r:1-r,{unit:"year"})}return ej.y(e,t)},Y:function(e,t,n,r){var o=eC(e,r),i=o>0?o:1-o;return"YY"===t?eO(i%100,2):"Yo"===t?n.ordinalNumber(i,{unit:"year"}):eO(i,t.length)},R:function(e,t){return eO(eS(e),t.length)},u:function(e,t){return eO(e.getUTCFullYear(),t.length)},Q:function(e,t,n){var r=Math.ceil((e.getUTCMonth()+1)/3);switch(t){case"Q":return String(r);case"QQ":return eO(r,2);case"Qo":return n.ordinalNumber(r,{unit:"quarter"});case"QQQ":return n.quarter(r,{width:"abbreviated",context:"formatting"});case"QQQQQ":return n.quarter(r,{width:"narrow",context:"formatting"});default:return n.quarter(r,{width:"wide",context:"formatting"})}},q:function(e,t,n){var r=Math.ceil((e.getUTCMonth()+1)/3);switch(t){case"q":return String(r);case"qq":return eO(r,2);case"qo":return n.ordinalNumber(r,{unit:"quarter"});case"qqq":return n.quarter(r,{width:"abbreviated",context:"standalone"});case"qqqqq":return n.quarter(r,{width:"narrow",context:"standalone"});default:return n.quarter(r,{width:"wide",context:"standalone"})}},M:function(e,t,n){var r=e.getUTCMonth();switch(t){case"M":case"MM":return ej.M(e,t);case"Mo":return n.ordinalNumber(r+1,{unit:"month"});case"MMM":return n.month(r,{width:"abbreviated",context:"formatting"});case"MMMMM":return n.month(r,{width:"narrow",context:"formatting"});default:return n.month(r,{width:"wide",context:"formatting"})}},L:function(e,t,n){var r=e.getUTCMonth();switch(t){case"L":return String(r+1);case"LL":return eO(r+1,2);case"Lo":return n.ordinalNumber(r+1,{unit:"month"});case"LLL":return n.month(r,{width:"abbreviated",context:"standalone"});case"LLLLL":return n.month(r,{width:"narrow",context:"standalone"});default:return n.month(r,{width:"wide",context:"standalone"})}},w:function(e,t,n,r){var o=function(e,t){(0,ei.Z)(1,arguments);var n=(0,eo.Z)(e);return Math.round((eE(n,t).getTime()-(function(e,t){(0,ei.Z)(1,arguments);var n,r,o,i,a,l,c,s,u=(0,em.Z)(null!==(n=null!==(r=null!==(o=null!==(i=null==t?void 0:t.firstWeekContainsDate)&&void 0!==i?i:null==t?void 0:null===(a=t.locale)||void 0===a?void 0:null===(l=a.options)||void 0===l?void 0:l.firstWeekContainsDate)&&void 0!==o?o:ek.firstWeekContainsDate)&&void 0!==r?r:null===(c=ek.locale)||void 0===c?void 0:null===(s=c.options)||void 0===s?void 0:s.firstWeekContainsDate)&&void 0!==n?n:1),d=eC(e,t),f=new Date(0);return f.setUTCFullYear(d,0,u),f.setUTCHours(0,0,0,0),eE(f,t)})(n,t).getTime())/6048e5)+1}(e,r);return"wo"===t?n.ordinalNumber(o,{unit:"week"}):eO(o,t.length)},I:function(e,t,n){var r=function(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e);return Math.round((ew(t).getTime()-(function(e){(0,ei.Z)(1,arguments);var t=eS(e),n=new Date(0);return n.setUTCFullYear(t,0,4),n.setUTCHours(0,0,0,0),ew(n)})(t).getTime())/6048e5)+1}(e);return"Io"===t?n.ordinalNumber(r,{unit:"week"}):eO(r,t.length)},d:function(e,t,n){return"do"===t?n.ordinalNumber(e.getUTCDate(),{unit:"date"}):ej.d(e,t)},D:function(e,t,n){var r=function(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getTime();return t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0),Math.floor((n-t.getTime())/864e5)+1}(e);return"Do"===t?n.ordinalNumber(r,{unit:"dayOfYear"}):eO(r,t.length)},E:function(e,t,n){var r=e.getUTCDay();switch(t){case"E":case"EE":case"EEE":return n.day(r,{width:"abbreviated",context:"formatting"});case"EEEEE":return n.day(r,{width:"narrow",context:"formatting"});case"EEEEEE":return n.day(r,{width:"short",context:"formatting"});default:return n.day(r,{width:"wide",context:"formatting"})}},e:function(e,t,n,r){var o=e.getUTCDay(),i=(o-r.weekStartsOn+8)%7||7;switch(t){case"e":return String(i);case"ee":return eO(i,2);case"eo":return n.ordinalNumber(i,{unit:"day"});case"eee":return n.day(o,{width:"abbreviated",context:"formatting"});case"eeeee":return n.day(o,{width:"narrow",context:"formatting"});case"eeeeee":return n.day(o,{width:"short",context:"formatting"});default:return n.day(o,{width:"wide",context:"formatting"})}},c:function(e,t,n,r){var o=e.getUTCDay(),i=(o-r.weekStartsOn+8)%7||7;switch(t){case"c":return String(i);case"cc":return eO(i,t.length);case"co":return n.ordinalNumber(i,{unit:"day"});case"ccc":return n.day(o,{width:"abbreviated",context:"standalone"});case"ccccc":return n.day(o,{width:"narrow",context:"standalone"});case"cccccc":return n.day(o,{width:"short",context:"standalone"});default:return n.day(o,{width:"wide",context:"standalone"})}},i:function(e,t,n){var r=e.getUTCDay(),o=0===r?7:r;switch(t){case"i":return String(o);case"ii":return eO(o,t.length);case"io":return n.ordinalNumber(o,{unit:"day"});case"iii":return n.day(r,{width:"abbreviated",context:"formatting"});case"iiiii":return n.day(r,{width:"narrow",context:"formatting"});case"iiiiii":return n.day(r,{width:"short",context:"formatting"});default:return n.day(r,{width:"wide",context:"formatting"})}},a:function(e,t,n){var r=e.getUTCHours()/12>=1?"pm":"am";switch(t){case"a":case"aa":return n.dayPeriod(r,{width:"abbreviated",context:"formatting"});case"aaa":return n.dayPeriod(r,{width:"abbreviated",context:"formatting"}).toLowerCase();case"aaaaa":return n.dayPeriod(r,{width:"narrow",context:"formatting"});default:return n.dayPeriod(r,{width:"wide",context:"formatting"})}},b:function(e,t,n){var r,o=e.getUTCHours();switch(r=12===o?eP.noon:0===o?eP.midnight:o/12>=1?"pm":"am",t){case"b":case"bb":return n.dayPeriod(r,{width:"abbreviated",context:"formatting"});case"bbb":return n.dayPeriod(r,{width:"abbreviated",context:"formatting"}).toLowerCase();case"bbbbb":return n.dayPeriod(r,{width:"narrow",context:"formatting"});default:return n.dayPeriod(r,{width:"wide",context:"formatting"})}},B:function(e,t,n){var r,o=e.getUTCHours();switch(r=o>=17?eP.evening:o>=12?eP.afternoon:o>=4?eP.morning:eP.night,t){case"B":case"BB":case"BBB":return n.dayPeriod(r,{width:"abbreviated",context:"formatting"});case"BBBBB":return n.dayPeriod(r,{width:"narrow",context:"formatting"});default:return n.dayPeriod(r,{width:"wide",context:"formatting"})}},h:function(e,t,n){if("ho"===t){var r=e.getUTCHours()%12;return 0===r&&(r=12),n.ordinalNumber(r,{unit:"hour"})}return ej.h(e,t)},H:function(e,t,n){return"Ho"===t?n.ordinalNumber(e.getUTCHours(),{unit:"hour"}):ej.H(e,t)},K:function(e,t,n){var r=e.getUTCHours()%12;return"Ko"===t?n.ordinalNumber(r,{unit:"hour"}):eO(r,t.length)},k:function(e,t,n){var r=e.getUTCHours();return(0===r&&(r=24),"ko"===t)?n.ordinalNumber(r,{unit:"hour"}):eO(r,t.length)},m:function(e,t,n){return"mo"===t?n.ordinalNumber(e.getUTCMinutes(),{unit:"minute"}):ej.m(e,t)},s:function(e,t,n){return"so"===t?n.ordinalNumber(e.getUTCSeconds(),{unit:"second"}):ej.s(e,t)},S:function(e,t){return ej.S(e,t)},X:function(e,t,n,r){var o=(r._originalDate||e).getTimezoneOffset();if(0===o)return"Z";switch(t){case"X":return eM(o);case"XXXX":case"XX":return eI(o);default:return eI(o,":")}},x:function(e,t,n,r){var o=(r._originalDate||e).getTimezoneOffset();switch(t){case"x":return eM(o);case"xxxx":case"xx":return eI(o);default:return eI(o,":")}},O:function(e,t,n,r){var o=(r._originalDate||e).getTimezoneOffset();switch(t){case"O":case"OO":case"OOO":return"GMT"+eN(o,":");default:return"GMT"+eI(o,":")}},z:function(e,t,n,r){var o=(r._originalDate||e).getTimezoneOffset();switch(t){case"z":case"zz":case"zzz":return"GMT"+eN(o,":");default:return"GMT"+eI(o,":")}},t:function(e,t,n,r){return eO(Math.floor((r._originalDate||e).getTime()/1e3),t.length)},T:function(e,t,n,r){return eO((r._originalDate||e).getTime(),t.length)}},eT=function(e,t){switch(e){case"P":return t.date({width:"short"});case"PP":return t.date({width:"medium"});case"PPP":return t.date({width:"long"});default:return t.date({width:"full"})}},eA=function(e,t){switch(e){case"p":return t.time({width:"short"});case"pp":return t.time({width:"medium"});case"ppp":return t.time({width:"long"});default:return t.time({width:"full"})}},e_={p:eA,P:function(e,t){var n,r=e.match(/(P+)(p+)?/)||[],o=r[1],i=r[2];if(!i)return eT(e,t);switch(o){case"P":n=t.dateTime({width:"short"});break;case"PP":n=t.dateTime({width:"medium"});break;case"PPP":n=t.dateTime({width:"long"});break;default:n=t.dateTime({width:"full"})}return n.replace("{{date}}",eT(o,t)).replace("{{time}}",eA(i,t))}};function eD(e){var t=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()));return t.setUTCFullYear(e.getFullYear()),e.getTime()-t.getTime()}var eZ=["D","DD"],eL=["YY","YYYY"];function ez(e,t,n){if("YYYY"===e)throw RangeError("Use `yyyy` instead of `YYYY` (in `".concat(t,"`) for formatting years to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"));if("YY"===e)throw RangeError("Use `yy` instead of `YY` (in `".concat(t,"`) for formatting years to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"));if("D"===e)throw RangeError("Use `d` instead of `D` (in `".concat(t,"`) for formatting days of the month to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"));if("DD"===e)throw RangeError("Use `dd` instead of `DD` (in `".concat(t,"`) for formatting days of the month to the input `").concat(n,"`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md"))}var eB={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}};function eF(e){return function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.width?String(t.width):e.defaultWidth;return e.formats[n]||e.formats[e.defaultWidth]}}var eH={date:eF({formats:{full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},defaultWidth:"full"}),time:eF({formats:{full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},defaultWidth:"full"}),dateTime:eF({formats:{full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},defaultWidth:"full"})},eq={lastWeek:"'last' eeee 'at' p",yesterday:"'yesterday at' p",today:"'today at' p",tomorrow:"'tomorrow at' p",nextWeek:"eeee 'at' p",other:"P"};function eW(e){return function(t,n){var r;if("formatting"===(null!=n&&n.context?String(n.context):"standalone")&&e.formattingValues){var o=e.defaultFormattingWidth||e.defaultWidth,i=null!=n&&n.width?String(n.width):o;r=e.formattingValues[i]||e.formattingValues[o]}else{var a=e.defaultWidth,l=null!=n&&n.width?String(n.width):e.defaultWidth;r=e.values[l]||e.values[a]}return r[e.argumentCallback?e.argumentCallback(t):t]}}function eK(e){return function(t){var n,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=r.width,i=o&&e.matchPatterns[o]||e.matchPatterns[e.defaultMatchWidth],a=t.match(i);if(!a)return null;var l=a[0],c=o&&e.parsePatterns[o]||e.parsePatterns[e.defaultParseWidth],s=Array.isArray(c)?function(e,t){for(var n=0;n0?"in "+r:r+" ago":r},formatLong:eH,formatRelative:function(e,t,n,r){return eq[e]},localize:{ordinalNumber:function(e,t){var n=Number(e),r=n%100;if(r>20||r<10)switch(r%10){case 1:return n+"st";case 2:return n+"nd";case 3:return n+"rd"}return n+"th"},era:eW({values:{narrow:["B","A"],abbreviated:["BC","AD"],wide:["Before Christ","Anno Domini"]},defaultWidth:"wide"}),quarter:eW({values:{narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1st quarter","2nd quarter","3rd quarter","4th quarter"]},defaultWidth:"wide",argumentCallback:function(e){return e-1}}),month:eW({values:{narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],wide:["January","February","March","April","May","June","July","August","September","October","November","December"]},defaultWidth:"wide"}),day:eW({values:{narrow:["S","M","T","W","T","F","S"],short:["Su","Mo","Tu","We","Th","Fr","Sa"],abbreviated:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],wide:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},defaultWidth:"wide"}),dayPeriod:eW({values:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"}},defaultWidth:"wide",formattingValues:{narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"}},defaultFormattingWidth:"wide"})},match:{ordinalNumber:(a={matchPattern:/^(\d+)(th|st|nd|rd)?/i,parsePattern:/\d+/i,valueCallback:function(e){return parseInt(e,10)}},function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=e.match(a.matchPattern);if(!n)return null;var r=n[0],o=e.match(a.parsePattern);if(!o)return null;var i=a.valueCallback?a.valueCallback(o[0]):o[0];return{value:i=t.valueCallback?t.valueCallback(i):i,rest:e.slice(r.length)}}),era:eK({matchPatterns:{narrow:/^(b|a)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(before christ|before common era|anno domini|common era)/i},defaultMatchWidth:"wide",parsePatterns:{any:[/^b/i,/^(a|c)/i]},defaultParseWidth:"any"}),quarter:eK({matchPatterns:{narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](th|st|nd|rd)? quarter/i},defaultMatchWidth:"wide",parsePatterns:{any:[/1/i,/2/i,/3/i,/4/i]},defaultParseWidth:"any",valueCallback:function(e){return e+1}}),month:eK({matchPatterns:{narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,wide:/^(january|february|march|april|may|june|july|august|september|october|november|december)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},defaultParseWidth:"any"}),day:eK({matchPatterns:{narrow:/^[smtwf]/i,short:/^(su|mo|tu|we|th|fr|sa)/i,abbreviated:/^(sun|mon|tue|wed|thu|fri|sat)/i,wide:/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i},defaultMatchWidth:"wide",parsePatterns:{narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},defaultParseWidth:"any"}),dayPeriod:eK({matchPatterns:{narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},defaultMatchWidth:"any",parsePatterns:{any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},defaultParseWidth:"any"})},options:{weekStartsOn:0,firstWeekContainsDate:1}},eU=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,eG=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,eX=/^'([^]*?)'?$/,e$=/''/g,eY=/[a-zA-Z]/;function eQ(e,t,n){(0,ei.Z)(2,arguments);var r,o,i,a,l,c,s,u,d,f,p,h,m,g,v,y,b,x,w=String(t),S=null!==(r=null!==(o=null==n?void 0:n.locale)&&void 0!==o?o:ek.locale)&&void 0!==r?r:eV,k=(0,em.Z)(null!==(i=null!==(a=null!==(l=null!==(c=null==n?void 0:n.firstWeekContainsDate)&&void 0!==c?c:null==n?void 0:null===(s=n.locale)||void 0===s?void 0:null===(u=s.options)||void 0===u?void 0:u.firstWeekContainsDate)&&void 0!==l?l:ek.firstWeekContainsDate)&&void 0!==a?a:null===(d=ek.locale)||void 0===d?void 0:null===(f=d.options)||void 0===f?void 0:f.firstWeekContainsDate)&&void 0!==i?i:1);if(!(k>=1&&k<=7))throw RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var E=(0,em.Z)(null!==(p=null!==(h=null!==(m=null!==(g=null==n?void 0:n.weekStartsOn)&&void 0!==g?g:null==n?void 0:null===(v=n.locale)||void 0===v?void 0:null===(y=v.options)||void 0===y?void 0:y.weekStartsOn)&&void 0!==m?m:ek.weekStartsOn)&&void 0!==h?h:null===(b=ek.locale)||void 0===b?void 0:null===(x=b.options)||void 0===x?void 0:x.weekStartsOn)&&void 0!==p?p:0);if(!(E>=0&&E<=6))throw RangeError("weekStartsOn must be between 0 and 6 inclusively");if(!S.localize)throw RangeError("locale must contain localize property");if(!S.formatLong)throw RangeError("locale must contain formatLong property");var C=(0,eo.Z)(e);if(!function(e){return(0,ei.Z)(1,arguments),(!!ex(e)||"number"==typeof e)&&!isNaN(Number((0,eo.Z)(e)))}(C))throw RangeError("Invalid time value");var O=eD(C),j=function(e,t){return(0,ei.Z)(2,arguments),function(e,t){return(0,ei.Z)(2,arguments),new Date((0,eo.Z)(e).getTime()+(0,em.Z)(t))}(e,-(0,em.Z)(t))}(C,O),P={firstWeekContainsDate:k,weekStartsOn:E,locale:S,_originalDate:C};return w.match(eG).map(function(e){var t=e[0];return"p"===t||"P"===t?(0,e_[t])(e,S.formatLong):e}).join("").match(eU).map(function(r){if("''"===r)return"'";var o,i=r[0];if("'"===i)return(o=r.match(eX))?o[1].replace(e$,"'"):r;var a=eR[i];if(a)return null!=n&&n.useAdditionalWeekYearTokens||-1===eL.indexOf(r)||ez(r,t,String(e)),null!=n&&n.useAdditionalDayOfYearTokens||-1===eZ.indexOf(r)||ez(r,t,String(e)),a(j,r,S.localize,P);if(i.match(eY))throw RangeError("Format string contains an unescaped latin alphabet character `"+i+"`");return r}).join("")}var eJ=n(1153);let e0=(0,eJ.fn)("DateRangePicker"),e1=(e,t,n,r)=>{var o;if(n&&(e=null===(o=r.get(n))||void 0===o?void 0:o.from),e)return ea(e&&!t?e:ef([e,t]))},e2=(e,t,n,r)=>{var o,i;if(n&&(e=ea(null!==(i=null===(o=r.get(n))||void 0===o?void 0:o.to)&&void 0!==i?i:el())),e)return ea(e&&!t?e:ep([e,t]))},e6=[{value:"tdy",text:"Today",from:el()},{value:"w",text:"Last 7 days",from:ey(el(),{days:7})},{value:"t",text:"Last 30 days",from:ey(el(),{days:30})},{value:"m",text:"Month to Date",from:ec(el())},{value:"y",text:"Year to Date",from:eb(el())}],e4=(e,t,n,r)=>{let o=(null==n?void 0:n.code)||"en-US";if(!e&&!t)return"";if(e&&!t)return r?eQ(e,r):e.toLocaleDateString(o,{year:"numeric",month:"short",day:"numeric"});if(e&&t){if(function(e,t){(0,ei.Z)(2,arguments);var n=(0,eo.Z)(e),r=(0,eo.Z)(t);return n.getTime()===r.getTime()}(e,t))return r?eQ(e,r):e.toLocaleDateString(o,{year:"numeric",month:"short",day:"numeric"});if(e.getMonth()===t.getMonth()&&e.getFullYear()===t.getFullYear())return r?"".concat(eQ(e,r)," - ").concat(eQ(t,r)):"".concat(e.toLocaleDateString(o,{month:"short",day:"numeric"})," - \n ").concat(t.getDate(),", ").concat(t.getFullYear());{if(r)return"".concat(eQ(e,r)," - ").concat(eQ(t,r));let n={year:"numeric",month:"short",day:"numeric"};return"".concat(e.toLocaleDateString(o,n)," - \n ").concat(t.toLocaleDateString(o,n))}}return""};function e3(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getMonth();return t.setFullYear(t.getFullYear(),n+1,0),t.setHours(23,59,59,999),t}function e5(e,t){(0,ei.Z)(2,arguments);var n=(0,eo.Z)(e),r=(0,em.Z)(t),o=n.getFullYear(),i=n.getDate(),a=new Date(0);a.setFullYear(o,r,15),a.setHours(0,0,0,0);var l=function(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getFullYear(),r=t.getMonth(),o=new Date(0);return o.setFullYear(n,r+1,0),o.setHours(0,0,0,0),o.getDate()}(a);return n.setMonth(r,Math.min(i,l)),n}function e8(e,t){(0,ei.Z)(2,arguments);var n=(0,eo.Z)(e),r=(0,em.Z)(t);return isNaN(n.getTime())?new Date(NaN):(n.setFullYear(r),n)}function e7(e,t){(0,ei.Z)(2,arguments);var n=(0,eo.Z)(e),r=(0,eo.Z)(t);return 12*(n.getFullYear()-r.getFullYear())+(n.getMonth()-r.getMonth())}function e9(e,t){(0,ei.Z)(2,arguments);var n=(0,eo.Z)(e),r=(0,eo.Z)(t);return n.getFullYear()===r.getFullYear()&&n.getMonth()===r.getMonth()}function te(e,t){(0,ei.Z)(2,arguments);var n=(0,eo.Z)(e),r=(0,eo.Z)(t);return n.getTime()=0&&u<=6))throw RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=(0,eo.Z)(e),f=d.getDay();return d.setDate(d.getDate()-((fr.getTime()}function ti(e,t){(0,ei.Z)(2,arguments);var n=ea(e),r=ea(t);return Math.round((n.getTime()-eD(n)-(r.getTime()-eD(r)))/864e5)}function ta(e,t){(0,ei.Z)(2,arguments);var n=(0,em.Z)(t);return(0,eh.Z)(e,7*n)}function tl(e,t){(0,ei.Z)(2,arguments);var n=(0,em.Z)(t);return(0,ev.Z)(e,12*n)}function tc(e,t){(0,ei.Z)(1,arguments);var n,r,o,i,a,l,c,s,u=(0,em.Z)(null!==(n=null!==(r=null!==(o=null!==(i=null==t?void 0:t.weekStartsOn)&&void 0!==i?i:null==t?void 0:null===(a=t.locale)||void 0===a?void 0:null===(l=a.options)||void 0===l?void 0:l.weekStartsOn)&&void 0!==o?o:ek.weekStartsOn)&&void 0!==r?r:null===(c=ek.locale)||void 0===c?void 0:null===(s=c.options)||void 0===s?void 0:s.weekStartsOn)&&void 0!==n?n:0);if(!(u>=0&&u<=6))throw RangeError("weekStartsOn must be between 0 and 6 inclusively");var d=(0,eo.Z)(e),f=d.getDay();return d.setDate(d.getDate()+((fe7(l,a)&&(a=(0,ev.Z)(l,-1*((void 0===s?1:s)-1))),c&&0>e7(a,c)&&(a=c),u=ec(a),f=t.month,h=(p=(0,d.useState)(u))[0],m=[void 0===f?h:f,p[1]])[0],v=m[1],[g,function(e){if(!t.disableNavigation){var n,r=ec(e);v(r),null===(n=t.onMonthChange)||void 0===n||n.call(t,r)}}]),x=b[0],w=b[1],S=function(e,t){for(var n=t.reverseMonths,r=t.numberOfMonths,o=ec(e),i=e7(ec((0,ev.Z)(o,r)),o),a=[],l=0;l=e7(i,n)))return(0,ev.Z)(i,-(r?void 0===o?1:o:1))}}(x,y),C=function(e){return S.some(function(t){return e9(e,t)})};return th.jsx(tN.Provider,{value:{currentMonth:x,displayMonths:S,goToMonth:w,goToDate:function(e,t){C(e)||(t&&te(e,t)?w((0,ev.Z)(e,1+-1*y.numberOfMonths)):w(e))},previousMonth:E,nextMonth:k,isDateDisplayed:C},children:e.children})}function tI(){var e=(0,d.useContext)(tN);if(!e)throw Error("useNavigation must be used within a NavigationProvider");return e}function tR(e){var t,n=tk(),r=n.classNames,o=n.styles,i=n.components,a=tI().goToMonth,l=function(t){a((0,ev.Z)(t,e.displayIndex?-e.displayIndex:0))},c=null!==(t=null==i?void 0:i.CaptionLabel)&&void 0!==t?t:tE,s=th.jsx(c,{id:e.id,displayMonth:e.displayMonth});return th.jsxs("div",{className:r.caption_dropdowns,style:o.caption_dropdowns,children:[th.jsx("div",{className:r.vhidden,children:s}),th.jsx(tj,{onChange:l,displayMonth:e.displayMonth}),th.jsx(tP,{onChange:l,displayMonth:e.displayMonth})]})}function tT(e){return th.jsx("svg",tu({width:"16px",height:"16px",viewBox:"0 0 120 120"},e,{children:th.jsx("path",{d:"M69.490332,3.34314575 C72.6145263,0.218951416 77.6798462,0.218951416 80.8040405,3.34314575 C83.8617626,6.40086786 83.9268205,11.3179931 80.9992143,14.4548388 L80.8040405,14.6568542 L35.461,60 L80.8040405,105.343146 C83.8617626,108.400868 83.9268205,113.317993 80.9992143,116.454839 L80.8040405,116.656854 C77.7463184,119.714576 72.8291931,119.779634 69.6923475,116.852028 L69.490332,116.656854 L18.490332,65.6568542 C15.4326099,62.5991321 15.367552,57.6820069 18.2951583,54.5451612 L18.490332,54.3431458 L69.490332,3.34314575 Z",fill:"currentColor",fillRule:"nonzero"})}))}function tA(e){return th.jsx("svg",tu({width:"16px",height:"16px",viewBox:"0 0 120 120"},e,{children:th.jsx("path",{d:"M49.8040405,3.34314575 C46.6798462,0.218951416 41.6145263,0.218951416 38.490332,3.34314575 C35.4326099,6.40086786 35.367552,11.3179931 38.2951583,14.4548388 L38.490332,14.6568542 L83.8333725,60 L38.490332,105.343146 C35.4326099,108.400868 35.367552,113.317993 38.2951583,116.454839 L38.490332,116.656854 C41.5480541,119.714576 46.4651794,119.779634 49.602025,116.852028 L49.8040405,116.656854 L100.804041,65.6568542 C103.861763,62.5991321 103.926821,57.6820069 100.999214,54.5451612 L100.804041,54.3431458 L49.8040405,3.34314575 Z",fill:"currentColor"})}))}var t_=(0,d.forwardRef)(function(e,t){var n=tk(),r=n.classNames,o=n.styles,i=[r.button_reset,r.button];e.className&&i.push(e.className);var a=i.join(" "),l=tu(tu({},o.button_reset),o.button);return e.style&&Object.assign(l,e.style),th.jsx("button",tu({},e,{ref:t,type:"button",className:a,style:l}))});function tD(e){var t,n,r=tk(),o=r.dir,i=r.locale,a=r.classNames,l=r.styles,c=r.labels,s=c.labelPrevious,u=c.labelNext,d=r.components;if(!e.nextMonth&&!e.previousMonth)return th.jsx(th.Fragment,{});var f=s(e.previousMonth,{locale:i}),p=[a.nav_button,a.nav_button_previous].join(" "),h=u(e.nextMonth,{locale:i}),m=[a.nav_button,a.nav_button_next].join(" "),g=null!==(t=null==d?void 0:d.IconRight)&&void 0!==t?t:tA,v=null!==(n=null==d?void 0:d.IconLeft)&&void 0!==n?n:tT;return th.jsxs("div",{className:a.nav,style:l.nav,children:[!e.hidePrevious&&th.jsx(t_,{name:"previous-month","aria-label":f,className:p,style:l.nav_button_previous,disabled:!e.previousMonth,onClick:e.onPreviousClick,children:"rtl"===o?th.jsx(g,{className:a.nav_icon,style:l.nav_icon}):th.jsx(v,{className:a.nav_icon,style:l.nav_icon})}),!e.hideNext&&th.jsx(t_,{name:"next-month","aria-label":h,className:m,style:l.nav_button_next,disabled:!e.nextMonth,onClick:e.onNextClick,children:"rtl"===o?th.jsx(v,{className:a.nav_icon,style:l.nav_icon}):th.jsx(g,{className:a.nav_icon,style:l.nav_icon})})]})}function tZ(e){var t=tk().numberOfMonths,n=tI(),r=n.previousMonth,o=n.nextMonth,i=n.goToMonth,a=n.displayMonths,l=a.findIndex(function(t){return e9(e.displayMonth,t)}),c=0===l,s=l===a.length-1;return th.jsx(tD,{displayMonth:e.displayMonth,hideNext:t>1&&(c||!s),hidePrevious:t>1&&(s||!c),nextMonth:o,previousMonth:r,onPreviousClick:function(){r&&i(r)},onNextClick:function(){o&&i(o)}})}function tL(e){var t,n,r=tk(),o=r.classNames,i=r.disableNavigation,a=r.styles,l=r.captionLayout,c=r.components,s=null!==(t=null==c?void 0:c.CaptionLabel)&&void 0!==t?t:tE;return n=i?th.jsx(s,{id:e.id,displayMonth:e.displayMonth}):"dropdown"===l?th.jsx(tR,{displayMonth:e.displayMonth,id:e.id}):"dropdown-buttons"===l?th.jsxs(th.Fragment,{children:[th.jsx(tR,{displayMonth:e.displayMonth,displayIndex:e.displayIndex,id:e.id}),th.jsx(tZ,{displayMonth:e.displayMonth,displayIndex:e.displayIndex,id:e.id})]}):th.jsxs(th.Fragment,{children:[th.jsx(s,{id:e.id,displayMonth:e.displayMonth,displayIndex:e.displayIndex}),th.jsx(tZ,{displayMonth:e.displayMonth,id:e.id})]}),th.jsx("div",{className:o.caption,style:a.caption,children:n})}function tz(e){var t=tk(),n=t.footer,r=t.styles,o=t.classNames.tfoot;return n?th.jsx("tfoot",{className:o,style:r.tfoot,children:th.jsx("tr",{children:th.jsx("td",{colSpan:8,children:n})})}):th.jsx(th.Fragment,{})}function tB(){var e=tk(),t=e.classNames,n=e.styles,r=e.showWeekNumber,o=e.locale,i=e.weekStartsOn,a=e.ISOWeek,l=e.formatters.formatWeekdayName,c=e.labels.labelWeekday,s=function(e,t,n){for(var r=n?tn(new Date):tt(new Date,{locale:e,weekStartsOn:t}),o=[],i=0;i<7;i++){var a=(0,eh.Z)(r,i);o.push(a)}return o}(o,i,a);return th.jsxs("tr",{style:n.head_row,className:t.head_row,children:[r&&th.jsx("td",{style:n.head_cell,className:t.head_cell}),s.map(function(e,r){return th.jsx("th",{scope:"col",className:t.head_cell,style:n.head_cell,"aria-label":c(e,{locale:o}),children:l(e,{locale:o})},r)})]})}function tF(){var e,t=tk(),n=t.classNames,r=t.styles,o=t.components,i=null!==(e=null==o?void 0:o.HeadRow)&&void 0!==e?e:tB;return th.jsx("thead",{style:r.head,className:n.head,children:th.jsx(i,{})})}function tH(e){var t=tk(),n=t.locale,r=t.formatters.formatDay;return th.jsx(th.Fragment,{children:r(e.date,{locale:n})})}var tq=(0,d.createContext)(void 0);function tW(e){return tm(e.initialProps)?th.jsx(tK,{initialProps:e.initialProps,children:e.children}):th.jsx(tq.Provider,{value:{selected:void 0,modifiers:{disabled:[]}},children:e.children})}function tK(e){var t=e.initialProps,n=e.children,r=t.selected,o=t.min,i=t.max,a={disabled:[]};return r&&a.disabled.push(function(e){var t=i&&r.length>i-1,n=r.some(function(t){return tr(t,e)});return!!(t&&!n)}),th.jsx(tq.Provider,{value:{selected:r,onDayClick:function(e,n,a){if(null===(l=t.onDayClick)||void 0===l||l.call(t,e,n,a),(!n.selected||!o||(null==r?void 0:r.length)!==o)&&(n.selected||!i||(null==r?void 0:r.length)!==i)){var l,c,s=r?td([],r,!0):[];if(n.selected){var u=s.findIndex(function(t){return tr(e,t)});s.splice(u,1)}else s.push(e);null===(c=t.onSelect)||void 0===c||c.call(t,s,e,n,a)}},modifiers:a},children:n})}function tV(){var e=(0,d.useContext)(tq);if(!e)throw Error("useSelectMultiple must be used within a SelectMultipleProvider");return e}var tU=(0,d.createContext)(void 0);function tG(e){return tg(e.initialProps)?th.jsx(tX,{initialProps:e.initialProps,children:e.children}):th.jsx(tU.Provider,{value:{selected:void 0,modifiers:{range_start:[],range_end:[],range_middle:[],disabled:[]}},children:e.children})}function tX(e){var t=e.initialProps,n=e.children,r=t.selected,o=r||{},i=o.from,a=o.to,l=t.min,c=t.max,s={range_start:[],range_end:[],range_middle:[],disabled:[]};if(i?(s.range_start=[i],a?(s.range_end=[a],tr(i,a)||(s.range_middle=[{after:i,before:a}])):s.range_end=[i]):a&&(s.range_start=[a],s.range_end=[a]),l&&(i&&!a&&s.disabled.push({after:eg(i,l-1),before:(0,eh.Z)(i,l-1)}),i&&a&&s.disabled.push({after:i,before:(0,eh.Z)(i,l-1)}),!i&&a&&s.disabled.push({after:eg(a,l-1),before:(0,eh.Z)(a,l-1)})),c){if(i&&!a&&(s.disabled.push({before:(0,eh.Z)(i,-c+1)}),s.disabled.push({after:(0,eh.Z)(i,c-1)})),i&&a){var u=c-(ti(a,i)+1);s.disabled.push({before:eg(i,u)}),s.disabled.push({after:(0,eh.Z)(a,u)})}!i&&a&&(s.disabled.push({before:(0,eh.Z)(a,-c+1)}),s.disabled.push({after:(0,eh.Z)(a,c-1)}))}return th.jsx(tU.Provider,{value:{selected:r,onDayClick:function(e,n,o){null===(c=t.onDayClick)||void 0===c||c.call(t,e,n,o);var i,a,l,c,s,u=(a=(i=r||{}).from,l=i.to,a&&l?tr(l,e)&&tr(a,e)?void 0:tr(l,e)?{from:l,to:void 0}:tr(a,e)?void 0:to(a,e)?{from:e,to:l}:{from:a,to:e}:l?to(e,l)?{from:l,to:e}:{from:e,to:l}:a?te(e,a)?{from:e,to:a}:{from:a,to:e}:{from:e,to:void 0});null===(s=t.onSelect)||void 0===s||s.call(t,u,e,n,o)},modifiers:s},children:n})}function t$(){var e=(0,d.useContext)(tU);if(!e)throw Error("useSelectRange must be used within a SelectRangeProvider");return e}function tY(e){return Array.isArray(e)?td([],e,!0):void 0!==e?[e]:[]}(l=s||(s={})).Outside="outside",l.Disabled="disabled",l.Selected="selected",l.Hidden="hidden",l.Today="today",l.RangeStart="range_start",l.RangeEnd="range_end",l.RangeMiddle="range_middle";var tQ=s.Selected,tJ=s.Disabled,t0=s.Hidden,t1=s.Today,t2=s.RangeEnd,t6=s.RangeMiddle,t4=s.RangeStart,t3=s.Outside,t5=(0,d.createContext)(void 0);function t8(e){var t,n,r,o=tk(),i=tV(),a=t$(),l=((t={})[tQ]=tY(o.selected),t[tJ]=tY(o.disabled),t[t0]=tY(o.hidden),t[t1]=[o.today],t[t2]=[],t[t6]=[],t[t4]=[],t[t3]=[],o.fromDate&&t[tJ].push({before:o.fromDate}),o.toDate&&t[tJ].push({after:o.toDate}),tm(o)?t[tJ]=t[tJ].concat(i.modifiers[tJ]):tg(o)&&(t[tJ]=t[tJ].concat(a.modifiers[tJ]),t[t4]=a.modifiers[t4],t[t6]=a.modifiers[t6],t[t2]=a.modifiers[t2]),t),c=(n=o.modifiers,r={},Object.entries(n).forEach(function(e){var t=e[0],n=e[1];r[t]=tY(n)}),r),s=tu(tu({},l),c);return th.jsx(t5.Provider,{value:s,children:e.children})}function t7(){var e=(0,d.useContext)(t5);if(!e)throw Error("useModifiers must be used within a ModifiersProvider");return e}function t9(e,t,n){var r=Object.keys(t).reduce(function(n,r){return t[r].some(function(t){if("boolean"==typeof t)return t;if(ex(t))return tr(e,t);if(Array.isArray(t)&&t.every(ex))return t.includes(e);if(t&&"object"==typeof t&&"from"in t)return r=t.from,o=t.to,r&&o?(0>ti(o,r)&&(r=(n=[o,r])[0],o=n[1]),ti(e,r)>=0&&ti(o,e)>=0):o?tr(o,e):!!r&&tr(r,e);if(t&&"object"==typeof t&&"dayOfWeek"in t)return t.dayOfWeek.includes(e.getDay());if(t&&"object"==typeof t&&"before"in t&&"after"in t){var n,r,o,i=ti(t.before,e),a=ti(t.after,e),l=i>0,c=a<0;return to(t.before,t.after)?c&&l:l||c}return t&&"object"==typeof t&&"after"in t?ti(e,t.after)>0:t&&"object"==typeof t&&"before"in t?ti(t.before,e)>0:"function"==typeof t&&t(e)})&&n.push(r),n},[]),o={};return r.forEach(function(e){return o[e]=!0}),n&&!e9(e,n)&&(o.outside=!0),o}var ne=(0,d.createContext)(void 0);function nt(e){var t=tI(),n=t7(),r=(0,d.useState)(),o=r[0],i=r[1],a=(0,d.useState)(),l=a[0],c=a[1],s=function(e,t){for(var n,r,o=ec(e[0]),i=e3(e[e.length-1]),a=o;a<=i;){var l=t9(a,t);if(!(!l.disabled&&!l.hidden)){a=(0,eh.Z)(a,1);continue}if(l.selected)return a;l.today&&!r&&(r=a),n||(n=a),a=(0,eh.Z)(a,1)}return r||n}(t.displayMonths,n),u=(null!=o?o:l&&t.isDateDisplayed(l))?l:s,f=function(e){i(e)},p=tk(),h=function(e,r){if(o){var i=function e(t,n){var r=n.moveBy,o=n.direction,i=n.context,a=n.modifiers,l=n.retry,c=void 0===l?{count:0,lastFocused:t}:l,s=i.weekStartsOn,u=i.fromDate,d=i.toDate,f=i.locale,p=({day:eh.Z,week:ta,month:ev.Z,year:tl,startOfWeek:function(e){return i.ISOWeek?tn(e):tt(e,{locale:f,weekStartsOn:s})},endOfWeek:function(e){return i.ISOWeek?ts(e):tc(e,{locale:f,weekStartsOn:s})}})[r](t,"after"===o?1:-1);"before"===o&&u?p=ef([u,p]):"after"===o&&d&&(p=ep([d,p]));var h=!0;if(a){var m=t9(p,a);h=!m.disabled&&!m.hidden}return h?p:c.count>365?c.lastFocused:e(p,{moveBy:r,direction:o,context:i,modifiers:a,retry:tu(tu({},c),{count:c.count+1})})}(o,{moveBy:e,direction:r,context:p,modifiers:n});tr(o,i)||(t.goToDate(i,o),f(i))}};return th.jsx(ne.Provider,{value:{focusedDay:o,focusTarget:u,blur:function(){c(o),i(void 0)},focus:f,focusDayAfter:function(){return h("day","after")},focusDayBefore:function(){return h("day","before")},focusWeekAfter:function(){return h("week","after")},focusWeekBefore:function(){return h("week","before")},focusMonthBefore:function(){return h("month","before")},focusMonthAfter:function(){return h("month","after")},focusYearBefore:function(){return h("year","before")},focusYearAfter:function(){return h("year","after")},focusStartOfWeek:function(){return h("startOfWeek","before")},focusEndOfWeek:function(){return h("endOfWeek","after")}},children:e.children})}function nn(){var e=(0,d.useContext)(ne);if(!e)throw Error("useFocusContext must be used within a FocusProvider");return e}var nr=(0,d.createContext)(void 0);function no(e){return tv(e.initialProps)?th.jsx(ni,{initialProps:e.initialProps,children:e.children}):th.jsx(nr.Provider,{value:{selected:void 0},children:e.children})}function ni(e){var t=e.initialProps,n=e.children,r={selected:t.selected,onDayClick:function(e,n,r){var o,i,a;if(null===(o=t.onDayClick)||void 0===o||o.call(t,e,n,r),n.selected&&!t.required){null===(i=t.onSelect)||void 0===i||i.call(t,void 0,e,n,r);return}null===(a=t.onSelect)||void 0===a||a.call(t,e,e,n,r)}};return th.jsx(nr.Provider,{value:r,children:n})}function na(){var e=(0,d.useContext)(nr);if(!e)throw Error("useSelectSingle must be used within a SelectSingleProvider");return e}function nl(e){var t,n,r,o,i,a,l,c,u,f,p,h,m,g,v,y,b,x,w,S,k,E,C,O,j,P,N,M,I,R,T,A,_,D,Z,L,z,B,F,H,q,W,K=(0,d.useRef)(null),V=(t=e.date,n=e.displayMonth,a=tk(),l=nn(),c=t9(t,t7(),n),u=tk(),f=na(),p=tV(),h=t$(),g=(m=nn()).focusDayAfter,v=m.focusDayBefore,y=m.focusWeekAfter,b=m.focusWeekBefore,x=m.blur,w=m.focus,S=m.focusMonthBefore,k=m.focusMonthAfter,E=m.focusYearBefore,C=m.focusYearAfter,O=m.focusStartOfWeek,j=m.focusEndOfWeek,P={onClick:function(e){var n,r,o,i;tv(u)?null===(n=f.onDayClick)||void 0===n||n.call(f,t,c,e):tm(u)?null===(r=p.onDayClick)||void 0===r||r.call(p,t,c,e):tg(u)?null===(o=h.onDayClick)||void 0===o||o.call(h,t,c,e):null===(i=u.onDayClick)||void 0===i||i.call(u,t,c,e)},onFocus:function(e){var n;w(t),null===(n=u.onDayFocus)||void 0===n||n.call(u,t,c,e)},onBlur:function(e){var n;x(),null===(n=u.onDayBlur)||void 0===n||n.call(u,t,c,e)},onKeyDown:function(e){var n;switch(e.key){case"ArrowLeft":e.preventDefault(),e.stopPropagation(),"rtl"===u.dir?g():v();break;case"ArrowRight":e.preventDefault(),e.stopPropagation(),"rtl"===u.dir?v():g();break;case"ArrowDown":e.preventDefault(),e.stopPropagation(),y();break;case"ArrowUp":e.preventDefault(),e.stopPropagation(),b();break;case"PageUp":e.preventDefault(),e.stopPropagation(),e.shiftKey?E():S();break;case"PageDown":e.preventDefault(),e.stopPropagation(),e.shiftKey?C():k();break;case"Home":e.preventDefault(),e.stopPropagation(),O();break;case"End":e.preventDefault(),e.stopPropagation(),j()}null===(n=u.onDayKeyDown)||void 0===n||n.call(u,t,c,e)},onKeyUp:function(e){var n;null===(n=u.onDayKeyUp)||void 0===n||n.call(u,t,c,e)},onMouseEnter:function(e){var n;null===(n=u.onDayMouseEnter)||void 0===n||n.call(u,t,c,e)},onMouseLeave:function(e){var n;null===(n=u.onDayMouseLeave)||void 0===n||n.call(u,t,c,e)},onPointerEnter:function(e){var n;null===(n=u.onDayPointerEnter)||void 0===n||n.call(u,t,c,e)},onPointerLeave:function(e){var n;null===(n=u.onDayPointerLeave)||void 0===n||n.call(u,t,c,e)},onTouchCancel:function(e){var n;null===(n=u.onDayTouchCancel)||void 0===n||n.call(u,t,c,e)},onTouchEnd:function(e){var n;null===(n=u.onDayTouchEnd)||void 0===n||n.call(u,t,c,e)},onTouchMove:function(e){var n;null===(n=u.onDayTouchMove)||void 0===n||n.call(u,t,c,e)},onTouchStart:function(e){var n;null===(n=u.onDayTouchStart)||void 0===n||n.call(u,t,c,e)}},N=tk(),M=na(),I=tV(),R=t$(),T=tv(N)?M.selected:tm(N)?I.selected:tg(N)?R.selected:void 0,A=!!(a.onDayClick||"default"!==a.mode),(0,d.useEffect)(function(){var e;!c.outside&&l.focusedDay&&A&&tr(l.focusedDay,t)&&(null===(e=K.current)||void 0===e||e.focus())},[l.focusedDay,t,K,A,c.outside]),D=(_=[a.classNames.day],Object.keys(c).forEach(function(e){var t=a.modifiersClassNames[e];if(t)_.push(t);else if(Object.values(s).includes(e)){var n=a.classNames["day_".concat(e)];n&&_.push(n)}}),_).join(" "),Z=tu({},a.styles.day),Object.keys(c).forEach(function(e){var t;Z=tu(tu({},Z),null===(t=a.modifiersStyles)||void 0===t?void 0:t[e])}),L=Z,z=!!(c.outside&&!a.showOutsideDays||c.hidden),B=null!==(i=null===(o=a.components)||void 0===o?void 0:o.DayContent)&&void 0!==i?i:tH,F={style:L,className:D,children:th.jsx(B,{date:t,displayMonth:n,activeModifiers:c}),role:"gridcell"},H=l.focusTarget&&tr(l.focusTarget,t)&&!c.outside,q=l.focusedDay&&tr(l.focusedDay,t),W=tu(tu(tu({},F),((r={disabled:c.disabled,role:"gridcell"})["aria-selected"]=c.selected,r.tabIndex=q||H?0:-1,r)),P),{isButton:A,isHidden:z,activeModifiers:c,selectedDays:T,buttonProps:W,divProps:F});return V.isHidden?th.jsx("div",{role:"gridcell"}):V.isButton?th.jsx(t_,tu({name:"day",ref:K},V.buttonProps)):th.jsx("div",tu({},V.divProps))}function nc(e){var t=e.number,n=e.dates,r=tk(),o=r.onWeekNumberClick,i=r.styles,a=r.classNames,l=r.locale,c=r.labels.labelWeekNumber,s=(0,r.formatters.formatWeekNumber)(Number(t),{locale:l});if(!o)return th.jsx("span",{className:a.weeknumber,style:i.weeknumber,children:s});var u=c(Number(t),{locale:l});return th.jsx(t_,{name:"week-number","aria-label":u,className:a.weeknumber,style:i.weeknumber,onClick:function(e){o(t,n,e)},children:s})}function ns(e){var t,n,r,o=tk(),i=o.styles,a=o.classNames,l=o.showWeekNumber,c=o.components,s=null!==(t=null==c?void 0:c.Day)&&void 0!==t?t:nl,u=null!==(n=null==c?void 0:c.WeekNumber)&&void 0!==n?n:nc;return l&&(r=th.jsx("td",{className:a.cell,style:i.cell,children:th.jsx(u,{number:e.weekNumber,dates:e.dates})})),th.jsxs("tr",{className:a.row,style:i.row,children:[r,e.dates.map(function(t){return th.jsx("td",{className:a.cell,style:i.cell,role:"presentation",children:th.jsx(s,{displayMonth:e.displayMonth,date:t})},function(e){return(0,ei.Z)(1,arguments),Math.floor(function(e){return(0,ei.Z)(1,arguments),(0,eo.Z)(e).getTime()}(e)/1e3)}(t))})]})}function nu(e,t,n){for(var r=(null==n?void 0:n.ISOWeek)?ts(t):tc(t,n),o=(null==n?void 0:n.ISOWeek)?tn(e):tt(e,n),i=ti(r,o),a=[],l=0;l<=i;l++)a.push((0,eh.Z)(o,l));return a.reduce(function(e,t){var r=(null==n?void 0:n.ISOWeek)?function(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e);return Math.round((tn(t).getTime()-(function(e){(0,ei.Z)(1,arguments);var t=function(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getFullYear(),r=new Date(0);r.setFullYear(n+1,0,4),r.setHours(0,0,0,0);var o=tn(r),i=new Date(0);i.setFullYear(n,0,4),i.setHours(0,0,0,0);var a=tn(i);return t.getTime()>=o.getTime()?n+1:t.getTime()>=a.getTime()?n:n-1}(e),n=new Date(0);return n.setFullYear(t,0,4),n.setHours(0,0,0,0),tn(n)})(t).getTime())/6048e5)+1}(t):function(e,t){(0,ei.Z)(1,arguments);var n=(0,eo.Z)(e);return Math.round((tt(n,t).getTime()-(function(e,t){(0,ei.Z)(1,arguments);var n,r,o,i,a,l,c,s,u=(0,em.Z)(null!==(n=null!==(r=null!==(o=null!==(i=null==t?void 0:t.firstWeekContainsDate)&&void 0!==i?i:null==t?void 0:null===(a=t.locale)||void 0===a?void 0:null===(l=a.options)||void 0===l?void 0:l.firstWeekContainsDate)&&void 0!==o?o:ek.firstWeekContainsDate)&&void 0!==r?r:null===(c=ek.locale)||void 0===c?void 0:null===(s=c.options)||void 0===s?void 0:s.firstWeekContainsDate)&&void 0!==n?n:1),d=function(e,t){(0,ei.Z)(1,arguments);var n,r,o,i,a,l,c,s,u=(0,eo.Z)(e),d=u.getFullYear(),f=(0,em.Z)(null!==(n=null!==(r=null!==(o=null!==(i=null==t?void 0:t.firstWeekContainsDate)&&void 0!==i?i:null==t?void 0:null===(a=t.locale)||void 0===a?void 0:null===(l=a.options)||void 0===l?void 0:l.firstWeekContainsDate)&&void 0!==o?o:ek.firstWeekContainsDate)&&void 0!==r?r:null===(c=ek.locale)||void 0===c?void 0:null===(s=c.options)||void 0===s?void 0:s.firstWeekContainsDate)&&void 0!==n?n:1);if(!(f>=1&&f<=7))throw RangeError("firstWeekContainsDate must be between 1 and 7 inclusively");var p=new Date(0);p.setFullYear(d+1,0,f),p.setHours(0,0,0,0);var h=tt(p,t),m=new Date(0);m.setFullYear(d,0,f),m.setHours(0,0,0,0);var g=tt(m,t);return u.getTime()>=h.getTime()?d+1:u.getTime()>=g.getTime()?d:d-1}(e,t),f=new Date(0);return f.setFullYear(d,0,u),f.setHours(0,0,0,0),tt(f,t)})(n,t).getTime())/6048e5)+1}(t,n),o=e.find(function(e){return e.weekNumber===r});return o?o.dates.push(t):e.push({weekNumber:r,dates:[t]}),e},[])}function nd(e){var t,n,r,o=tk(),i=o.locale,a=o.classNames,l=o.styles,c=o.hideHead,s=o.fixedWeeks,u=o.components,d=o.weekStartsOn,f=o.firstWeekContainsDate,p=o.ISOWeek,h=function(e,t){var n=nu(ec(e),e3(e),t);if(null==t?void 0:t.useFixedWeeks){var r=function(e,t){return(0,ei.Z)(1,arguments),function(e,t,n){(0,ei.Z)(2,arguments);var r=tt(e,n),o=tt(t,n);return Math.round((r.getTime()-eD(r)-(o.getTime()-eD(o)))/6048e5)}(function(e){(0,ei.Z)(1,arguments);var t=(0,eo.Z)(e),n=t.getMonth();return t.setFullYear(t.getFullYear(),n+1,0),t.setHours(0,0,0,0),t}(e),ec(e),t)+1}(e,t);if(r<6){var o=n[n.length-1],i=o.dates[o.dates.length-1],a=ta(i,6-r),l=nu(ta(i,1),a,t);n.push.apply(n,l)}}return n}(e.displayMonth,{useFixedWeeks:!!s,ISOWeek:p,locale:i,weekStartsOn:d,firstWeekContainsDate:f}),m=null!==(t=null==u?void 0:u.Head)&&void 0!==t?t:tF,g=null!==(n=null==u?void 0:u.Row)&&void 0!==n?n:ns,v=null!==(r=null==u?void 0:u.Footer)&&void 0!==r?r:tz;return th.jsxs("table",{id:e.id,className:a.table,style:l.table,role:"grid","aria-labelledby":e["aria-labelledby"],children:[!c&&th.jsx(m,{}),th.jsx("tbody",{className:a.tbody,style:l.tbody,children:h.map(function(t){return th.jsx(g,{displayMonth:e.displayMonth,dates:t.dates,weekNumber:t.weekNumber},t.weekNumber)})}),th.jsx(v,{displayMonth:e.displayMonth})]})}var nf="undefined"!=typeof window&&window.document&&window.document.createElement?d.useLayoutEffect:d.useEffect,np=!1,nh=0;function nm(){return"react-day-picker-".concat(++nh)}function ng(e){var t,n,r,o,i,a,l,c,s=tk(),u=s.dir,f=s.classNames,p=s.styles,h=s.components,m=tI().displayMonths,g=(r=null!=(t=s.id?"".concat(s.id,"-").concat(e.displayIndex):void 0)?t:np?nm():null,i=(o=(0,d.useState)(r))[0],a=o[1],nf(function(){null===i&&a(nm())},[]),(0,d.useEffect)(function(){!1===np&&(np=!0)},[]),null!==(n=null!=t?t:i)&&void 0!==n?n:void 0),v=s.id?"".concat(s.id,"-grid-").concat(e.displayIndex):void 0,y=[f.month],b=p.month,x=0===e.displayIndex,w=e.displayIndex===m.length-1,S=!x&&!w;"rtl"===u&&(w=(l=[x,w])[0],x=l[1]),x&&(y.push(f.caption_start),b=tu(tu({},b),p.caption_start)),w&&(y.push(f.caption_end),b=tu(tu({},b),p.caption_end)),S&&(y.push(f.caption_between),b=tu(tu({},b),p.caption_between));var k=null!==(c=null==h?void 0:h.Caption)&&void 0!==c?c:tL;return th.jsxs("div",{className:y.join(" "),style:b,children:[th.jsx(k,{id:g,displayMonth:e.displayMonth,displayIndex:e.displayIndex}),th.jsx(nd,{id:v,"aria-labelledby":g,displayMonth:e.displayMonth})]},e.displayIndex)}function nv(e){var t=tk(),n=t.classNames,r=t.styles;return th.jsx("div",{className:n.months,style:r.months,children:e.children})}function ny(e){var t,n,r=e.initialProps,o=tk(),i=nn(),a=tI(),l=(0,d.useState)(!1),c=l[0],s=l[1];(0,d.useEffect)(function(){o.initialFocus&&i.focusTarget&&(c||(i.focus(i.focusTarget),s(!0)))},[o.initialFocus,c,i.focus,i.focusTarget,i]);var u=[o.classNames.root,o.className];o.numberOfMonths>1&&u.push(o.classNames.multiple_months),o.showWeekNumber&&u.push(o.classNames.with_weeknumber);var f=tu(tu({},o.styles.root),o.style),p=Object.keys(r).filter(function(e){return e.startsWith("data-")}).reduce(function(e,t){var n;return tu(tu({},e),((n={})[t]=r[t],n))},{}),h=null!==(n=null===(t=r.components)||void 0===t?void 0:t.Months)&&void 0!==n?n:nv;return th.jsx("div",tu({className:u.join(" "),style:f,dir:o.dir,id:o.id,nonce:r.nonce,title:r.title,lang:r.lang},p,{children:th.jsx(h,{children:a.displayMonths.map(function(e,t){return th.jsx(ng,{displayIndex:t,displayMonth:e},t)})})}))}function nb(e){var t=e.children,n=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&0>t.indexOf(r)&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var o=0,r=Object.getOwnPropertySymbols(e);ot.indexOf(r[o])&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(n[r[o]]=e[r[o]]);return n}(e,["children"]);return th.jsx(tS,{initialProps:n,children:th.jsx(tM,{children:th.jsx(no,{initialProps:n,children:th.jsx(tW,{initialProps:n,children:th.jsx(tG,{initialProps:n,children:th.jsx(t8,{children:th.jsx(nt,{children:t})})})})})})})}function nx(e){return th.jsx(nb,tu({},e,{children:th.jsx(ny,{initialProps:e})}))}let nw=e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z"}))},nS=e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"}))},nk=e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M4.83582 12L11.0429 18.2071L12.4571 16.7929L7.66424 12L12.4571 7.20712L11.0429 5.79291L4.83582 12ZM10.4857 12L16.6928 18.2071L18.107 16.7929L13.3141 12L18.107 7.20712L16.6928 5.79291L10.4857 12Z"}))},nE=e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M19.1642 12L12.9571 5.79291L11.5429 7.20712L16.3358 12L11.5429 16.7929L12.9571 18.2071L19.1642 12ZM13.5143 12L7.30722 5.79291L5.89301 7.20712L10.6859 12L5.89301 16.7929L7.30722 18.2071L13.5143 12Z"}))};var nC=n(84264);n(41649);var nO=n(1526),nj=n(7084),nP=n(26898);let nN={xs:{paddingX:"px-2",paddingY:"py-0.5",fontSize:"text-xs"},sm:{paddingX:"px-2.5",paddingY:"py-1",fontSize:"text-sm"},md:{paddingX:"px-3",paddingY:"py-1.5",fontSize:"text-md"},lg:{paddingX:"px-3.5",paddingY:"py-1.5",fontSize:"text-lg"},xl:{paddingX:"px-3.5",paddingY:"py-1.5",fontSize:"text-xl"}},nM={xs:{paddingX:"px-2",paddingY:"py-0.5",fontSize:"text-xs"},sm:{paddingX:"px-2.5",paddingY:"py-0.5",fontSize:"text-sm"},md:{paddingX:"px-3",paddingY:"py-0.5",fontSize:"text-md"},lg:{paddingX:"px-3.5",paddingY:"py-0.5",fontSize:"text-lg"},xl:{paddingX:"px-4",paddingY:"py-1",fontSize:"text-xl"}},nI={xs:{height:"h-4",width:"w-4"},sm:{height:"h-4",width:"w-4"},md:{height:"h-4",width:"w-4"},lg:{height:"h-5",width:"w-5"},xl:{height:"h-6",width:"w-6"}},nR={[nj.wu.Increase]:{bgColor:(0,eJ.bM)(nj.fr.Emerald,nP.K.background).bgColor,textColor:(0,eJ.bM)(nj.fr.Emerald,nP.K.text).textColor},[nj.wu.ModerateIncrease]:{bgColor:(0,eJ.bM)(nj.fr.Emerald,nP.K.background).bgColor,textColor:(0,eJ.bM)(nj.fr.Emerald,nP.K.text).textColor},[nj.wu.Decrease]:{bgColor:(0,eJ.bM)(nj.fr.Rose,nP.K.background).bgColor,textColor:(0,eJ.bM)(nj.fr.Rose,nP.K.text).textColor},[nj.wu.ModerateDecrease]:{bgColor:(0,eJ.bM)(nj.fr.Rose,nP.K.background).bgColor,textColor:(0,eJ.bM)(nj.fr.Rose,nP.K.text).textColor},[nj.wu.Unchanged]:{bgColor:(0,eJ.bM)(nj.fr.Orange,nP.K.background).bgColor,textColor:(0,eJ.bM)(nj.fr.Orange,nP.K.text).textColor}},nT={[nj.wu.Increase]:e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z"}))},[nj.wu.ModerateIncrease]:e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M16.0037 9.41421L7.39712 18.0208L5.98291 16.6066L14.5895 8H7.00373V6H18.0037V17H16.0037V9.41421Z"}))},[nj.wu.Decrease]:e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M13.0001 16.1716L18.3641 10.8076L19.7783 12.2218L12.0001 20L4.22192 12.2218L5.63614 10.8076L11.0001 16.1716V4H13.0001V16.1716Z"}))},[nj.wu.ModerateDecrease]:e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M14.5895 16.0032L5.98291 7.39664L7.39712 5.98242L16.0037 14.589V7.00324H18.0037V18.0032H7.00373V16.0032H14.5895Z"}))},[nj.wu.Unchanged]:e=>{var t=(0,u._T)(e,[]);return d.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),d.createElement("path",{d:"M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z"}))}},nA=(0,eJ.fn)("BadgeDelta");d.forwardRef((e,t)=>{let{deltaType:n=nj.wu.Increase,isIncreasePositive:r=!0,size:o=nj.u8.SM,tooltip:i,children:a,className:l}=e,c=(0,u._T)(e,["deltaType","isIncreasePositive","size","tooltip","children","className"]),s=nT[n],f=(0,eJ.Fo)(n,r),p=a?nM:nN,{tooltipProps:h,getReferenceProps:m}=(0,nO.l)();return d.createElement("span",Object.assign({ref:(0,eJ.lq)([t,h.refs.setReference]),className:(0,es.q)(nA("root"),"w-max flex-shrink-0 inline-flex justify-center items-center cursor-default rounded-tremor-full bg-opacity-20 dark:bg-opacity-25",nR[f].bgColor,nR[f].textColor,p[o].paddingX,p[o].paddingY,p[o].fontSize,l)},m,c),d.createElement(nO.Z,Object.assign({text:i},h)),d.createElement(s,{className:(0,es.q)(nA("icon"),"shrink-0",a?(0,es.q)("-ml-1 mr-1.5"):nI[o].height,nI[o].width)}),a?d.createElement("p",{className:(0,es.q)(nA("text"),"text-sm whitespace-nowrap")},a):null)}).displayName="BadgeDelta";var n_=n(47323);let nD=e=>{var{onClick:t,icon:n}=e,r=(0,u._T)(e,["onClick","icon"]);return d.createElement("button",Object.assign({type:"button",className:(0,es.q)("flex items-center justify-center p-1 h-7 w-7 outline-none focus:ring-2 transition duration-100 border border-tremor-border dark:border-dark-tremor-border hover:bg-tremor-background-muted dark:hover:bg-dark-tremor-background-muted rounded-tremor-small focus:border-tremor-brand-subtle select-none dark:focus:border-dark-tremor-brand-subtle focus:ring-tremor-brand-muted dark:focus:ring-dark-tremor-brand-muted text-tremor-content-subtle dark:text-dark-tremor-content-subtle hover:text-tremor-content dark:hover:text-dark-tremor-content")},r),d.createElement(n_.Z,{onClick:t,icon:n,variant:"simple",color:"slate",size:"sm"}))};function nZ(e){var{mode:t,defaultMonth:n,selected:r,onSelect:o,locale:i,disabled:a,enableYearNavigation:l,classNames:c,weekStartsOn:s=0}=e,f=(0,u._T)(e,["mode","defaultMonth","selected","onSelect","locale","disabled","enableYearNavigation","classNames","weekStartsOn"]);return d.createElement(nx,Object.assign({showOutsideDays:!0,mode:t,defaultMonth:n,selected:r,onSelect:o,locale:i,disabled:a,weekStartsOn:s,classNames:Object.assign({months:"flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",month:"space-y-4",caption:"flex justify-center pt-2 relative items-center",caption_label:"text-tremor-default text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis font-medium",nav:"space-x-1 flex items-center",nav_button:"flex items-center justify-center p-1 h-7 w-7 outline-none focus:ring-2 transition duration-100 border border-tremor-border dark:border-dark-tremor-border hover:bg-tremor-background-muted dark:hover:bg-dark-tremor-background-muted rounded-tremor-small focus:border-tremor-brand-subtle dark:focus:border-dark-tremor-brand-subtle focus:ring-tremor-brand-muted dark:focus:ring-dark-tremor-brand-muted text-tremor-content-subtle dark:text-dark-tremor-content-subtle hover:text-tremor-content dark:hover:text-dark-tremor-content",nav_button_previous:"absolute left-1",nav_button_next:"absolute right-1",table:"w-full border-collapse space-y-1",head_row:"flex",head_cell:"w-9 font-normal text-center text-tremor-content-subtle dark:text-dark-tremor-content-subtle",row:"flex w-full mt-0.5",cell:"text-center p-0 relative focus-within:relative text-tremor-default text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis",day:"h-9 w-9 p-0 hover:bg-tremor-background-subtle dark:hover:bg-dark-tremor-background-subtle outline-tremor-brand dark:outline-dark-tremor-brand rounded-tremor-default",day_today:"font-bold",day_selected:"aria-selected:bg-tremor-background-emphasis aria-selected:text-tremor-content-inverted dark:aria-selected:bg-dark-tremor-background-emphasis dark:aria-selected:text-dark-tremor-content-inverted ",day_disabled:"text-tremor-content-subtle dark:text-dark-tremor-content-subtle disabled:hover:bg-transparent",day_outside:"text-tremor-content-subtle dark:text-dark-tremor-content-subtle"},c),components:{IconLeft:e=>{var t=(0,u._T)(e,[]);return d.createElement(nw,Object.assign({className:"h-4 w-4"},t))},IconRight:e=>{var t=(0,u._T)(e,[]);return d.createElement(nS,Object.assign({className:"h-4 w-4"},t))},Caption:e=>{var t=(0,u._T)(e,[]);let{goToMonth:n,nextMonth:r,previousMonth:o,currentMonth:a}=tI();return d.createElement("div",{className:"flex justify-between items-center"},d.createElement("div",{className:"flex items-center space-x-1"},l&&d.createElement(nD,{onClick:()=>a&&n(tl(a,-1)),icon:nk}),d.createElement(nD,{onClick:()=>o&&n(o),icon:nw})),d.createElement(nC.Z,{className:"text-tremor-default tabular-nums capitalize text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis font-medium"},eQ(t.displayMonth,"LLLL yyy",{locale:i})),d.createElement("div",{className:"flex items-center space-x-1"},d.createElement(nD,{onClick:()=>r&&n(r),icon:nS}),l&&d.createElement(nD,{onClick:()=>a&&n(tl(a,1)),icon:nE})))}}},f))}nZ.displayName="DateRangePicker",n(27281);var nL=n(57365),nz=n(44140);let nB=el(),nF=d.forwardRef((e,t)=>{var n,r;let{value:o,defaultValue:i,onValueChange:a,enableSelect:l=!0,minDate:c,maxDate:s,placeholder:f="Select range",selectPlaceholder:p="Select range",disabled:h=!1,locale:m=eV,enableClear:g=!0,displayFormat:v,children:y,className:b,enableYearNavigation:x=!1,weekStartsOn:w=0,disabledDates:S}=e,k=(0,u._T)(e,["value","defaultValue","onValueChange","enableSelect","minDate","maxDate","placeholder","selectPlaceholder","disabled","locale","enableClear","displayFormat","children","className","enableYearNavigation","weekStartsOn","disabledDates"]),[E,C]=(0,nz.Z)(i,o),[O,j]=(0,d.useState)(!1),[P,N]=(0,d.useState)(!1),M=(0,d.useMemo)(()=>{let e=[];return c&&e.push({before:c}),s&&e.push({after:s}),[...e,...null!=S?S:[]]},[c,s,S]),I=(0,d.useMemo)(()=>{let e=new Map;return y?d.Children.forEach(y,t=>{var n;e.set(t.props.value,{text:null!==(n=(0,eu.qg)(t))&&void 0!==n?n:t.props.value,from:t.props.from,to:t.props.to})}):e6.forEach(t=>{e.set(t.value,{text:t.text,from:t.from,to:nB})}),e},[y]),R=(0,d.useMemo)(()=>{if(y)return(0,eu.sl)(y);let e=new Map;return e6.forEach(t=>e.set(t.value,t.text)),e},[y]),T=(null==E?void 0:E.selectValue)||"",A=e1(null==E?void 0:E.from,c,T,I),_=e2(null==E?void 0:E.to,s,T,I),D=A||_?e4(A,_,m,v):f,Z=ec(null!==(r=null!==(n=null!=_?_:A)&&void 0!==n?n:s)&&void 0!==r?r:nB),L=g&&!h;return d.createElement("div",Object.assign({ref:t,className:(0,es.q)("w-full min-w-[10rem] relative flex justify-between text-tremor-default max-w-sm shadow-tremor-input dark:shadow-dark-tremor-input rounded-tremor-default",b)},k),d.createElement(J,{as:"div",className:(0,es.q)("w-full",l?"rounded-l-tremor-default":"rounded-tremor-default",O&&"ring-2 ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted z-10")},d.createElement("div",{className:"relative w-full"},d.createElement(J.Button,{onFocus:()=>j(!0),onBlur:()=>j(!1),disabled:h,className:(0,es.q)("w-full outline-none text-left whitespace-nowrap truncate focus:ring-2 transition duration-100 rounded-l-tremor-default flex flex-nowrap border pl-3 py-2","rounded-l-tremor-default border-tremor-border text-tremor-content-emphasis focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:border-dark-tremor-border dark:text-dark-tremor-content-emphasis dark:focus:border-dark-tremor-brand-subtle dark:focus:ring-dark-tremor-brand-muted",l?"rounded-l-tremor-default":"rounded-tremor-default",L?"pr-8":"pr-4",(0,eu.um)((0,eu.Uh)(A||_),h))},d.createElement(en,{className:(0,es.q)(e0("calendarIcon"),"flex-none shrink-0 h-5 w-5 -ml-0.5 mr-2","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle"),"aria-hidden":"true"}),d.createElement("p",{className:"truncate"},D)),L&&A?d.createElement("button",{type:"button",className:(0,es.q)("absolute outline-none inset-y-0 right-0 flex items-center transition duration-100 mr-4"),onClick:e=>{e.preventDefault(),null==a||a({}),C({})}},d.createElement(er.Z,{className:(0,es.q)(e0("clearIcon"),"flex-none h-4 w-4","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})):null),d.createElement(ee.u,{className:"absolute z-10 min-w-min left-0",enter:"transition ease duration-100 transform",enterFrom:"opacity-0 -translate-y-4",enterTo:"opacity-100 translate-y-0",leave:"transition ease duration-100 transform",leaveFrom:"opacity-100 translate-y-0",leaveTo:"opacity-0 -translate-y-4"},d.createElement(J.Panel,{focus:!0,className:(0,es.q)("divide-y overflow-y-auto outline-none rounded-tremor-default p-3 border my-1","bg-tremor-background border-tremor-border divide-tremor-border shadow-tremor-dropdown","dark:bg-dark-tremor-background dark:border-dark-tremor-border dark:divide-dark-tremor-border dark:shadow-dark-tremor-dropdown")},d.createElement(nZ,Object.assign({mode:"range",showOutsideDays:!0,defaultMonth:Z,selected:{from:A,to:_},onSelect:e=>{null==a||a({from:null==e?void 0:e.from,to:null==e?void 0:e.to}),C({from:null==e?void 0:e.from,to:null==e?void 0:e.to})},locale:m,disabled:M,enableYearNavigation:x,classNames:{day_range_middle:(0,es.q)("!rounded-none aria-selected:!bg-tremor-background-subtle aria-selected:dark:!bg-dark-tremor-background-subtle aria-selected:!text-tremor-content aria-selected:dark:!bg-dark-tremor-background-subtle"),day_range_start:"rounded-r-none rounded-l-tremor-small aria-selected:text-tremor-brand-inverted dark:aria-selected:text-dark-tremor-brand-inverted",day_range_end:"rounded-l-none rounded-r-tremor-small aria-selected:text-tremor-brand-inverted dark:aria-selected:text-dark-tremor-brand-inverted"},weekStartsOn:w},e))))),l&&d.createElement(et.R,{as:"div",className:(0,es.q)("w-48 -ml-px rounded-r-tremor-default",P&&"ring-2 ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted z-10"),value:T,onChange:e=>{let{from:t,to:n}=I.get(e),r=null!=n?n:nB;null==a||a({from:t,to:r,selectValue:e}),C({from:t,to:r,selectValue:e})},disabled:h},e=>{var t;let{value:n}=e;return d.createElement(d.Fragment,null,d.createElement(et.R.Button,{onFocus:()=>N(!0),onBlur:()=>N(!1),className:(0,es.q)("w-full outline-none text-left whitespace-nowrap truncate rounded-r-tremor-default transition duration-100 border px-4 py-2","border-tremor-border shadow-tremor-input text-tremor-content-emphasis focus:border-tremor-brand-subtle","dark:border-dark-tremor-border dark:shadow-dark-tremor-input dark:text-dark-tremor-content-emphasis dark:focus:border-dark-tremor-brand-subtle",(0,eu.um)((0,eu.Uh)(n),h))},n&&null!==(t=R.get(n))&&void 0!==t?t:p),d.createElement(ee.u,{className:"absolute z-10 w-full inset-x-0 right-0",enter:"transition ease duration-100 transform",enterFrom:"opacity-0 -translate-y-4",enterTo:"opacity-100 translate-y-0",leave:"transition ease duration-100 transform",leaveFrom:"opacity-100 translate-y-0",leaveTo:"opacity-0 -translate-y-4"},d.createElement(et.R.Options,{className:(0,es.q)("divide-y overflow-y-auto outline-none border my-1","shadow-tremor-dropdown bg-tremor-background border-tremor-border divide-tremor-border rounded-tremor-default","dark:shadow-dark-tremor-dropdown dark:bg-dark-tremor-background dark:border-dark-tremor-border dark:divide-dark-tremor-border")},null!=y?y:e6.map(e=>d.createElement(nL.Z,{key:e.value,value:e.value},e.text)))))}))});nF.displayName="DateRangePicker"},92414:function(e,t,n){"use strict";n.d(t,{Z:function(){return v}});var r=n(5853),o=n(2265);n(42698),n(64016),n(8710);var i=n(33232),a=n(44140),l=n(58747);let c=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),o.createElement("path",{d:"M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"}))};var s=n(4537),u=n(28517),d=n(33044);let f=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",width:"100%",height:"100%",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"},t),o.createElement("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),o.createElement("line",{x1:"6",y1:"6",x2:"18",y2:"18"}))};var p=n(65954),h=n(1153),m=n(96398);let g=(0,h.fn)("MultiSelect"),v=o.forwardRef((e,t)=>{let{defaultValue:n,value:h,onValueChange:v,placeholder:y="Select...",placeholderSearch:b="Search",disabled:x=!1,icon:w,children:S,className:k}=e,E=(0,r._T)(e,["defaultValue","value","onValueChange","placeholder","placeholderSearch","disabled","icon","children","className"]),[C,O]=(0,a.Z)(n,h),{reactElementChildren:j,optionsAvailable:P}=(0,o.useMemo)(()=>{let e=o.Children.toArray(S).filter(o.isValidElement);return{reactElementChildren:e,optionsAvailable:(0,m.n0)("",e)}},[S]),[N,M]=(0,o.useState)(""),I=(null!=C?C:[]).length>0,R=(0,o.useMemo)(()=>N?(0,m.n0)(N,j):P,[N,j,P]),T=()=>{M("")};return o.createElement(u.R,Object.assign({as:"div",ref:t,defaultValue:C,value:C,onChange:e=>{null==v||v(e),O(e)},disabled:x,className:(0,p.q)("w-full min-w-[10rem] relative text-tremor-default",k)},E,{multiple:!0}),e=>{let{value:t}=e;return o.createElement(o.Fragment,null,o.createElement(u.R.Button,{className:(0,p.q)("w-full outline-none text-left whitespace-nowrap truncate rounded-tremor-default focus:ring-2 transition duration-100 border pr-8 py-1.5","border-tremor-border shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:border-dark-tremor-border dark:shadow-dark-tremor-input dark:focus:border-dark-tremor-brand-subtle dark:focus:ring-dark-tremor-brand-muted",w?"pl-11 -ml-0.5":"pl-3",(0,m.um)(t.length>0,x))},w&&o.createElement("span",{className:(0,p.q)("absolute inset-y-0 left-0 flex items-center ml-px pl-2.5")},o.createElement(w,{className:(0,p.q)(g("Icon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})),o.createElement("div",{className:"h-6 flex items-center"},t.length>0?o.createElement("div",{className:"flex flex-nowrap overflow-x-scroll [&::-webkit-scrollbar]:hidden [scrollbar-width:none] gap-x-1 mr-5 -ml-1.5 relative"},P.filter(e=>t.includes(e.props.value)).map((e,n)=>{var r;return o.createElement("div",{key:n,className:(0,p.q)("max-w-[100px] lg:max-w-[200px] flex justify-center items-center pl-2 pr-1.5 py-1 font-medium","rounded-tremor-small","bg-tremor-background-muted dark:bg-dark-tremor-background-muted","bg-tremor-background-subtle dark:bg-dark-tremor-background-subtle","text-tremor-content-default dark:text-dark-tremor-content-default","text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis")},o.createElement("div",{className:"text-xs truncate "},null!==(r=e.props.children)&&void 0!==r?r:e.props.value),o.createElement("div",{onClick:n=>{n.preventDefault();let r=t.filter(t=>t!==e.props.value);null==v||v(r),O(r)}},o.createElement(f,{className:(0,p.q)(g("clearIconItem"),"cursor-pointer rounded-tremor-full w-3.5 h-3.5 ml-2","text-tremor-content-subtle hover:text-tremor-content","dark:text-dark-tremor-content-subtle dark:hover:text-tremor-content")})))})):o.createElement("span",null,y)),o.createElement("span",{className:(0,p.q)("absolute inset-y-0 right-0 flex items-center mr-2.5")},o.createElement(l.Z,{className:(0,p.q)(g("arrowDownIcon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")}))),I&&!x?o.createElement("button",{type:"button",className:(0,p.q)("absolute inset-y-0 right-0 flex items-center mr-8"),onClick:e=>{e.preventDefault(),O([]),null==v||v([])}},o.createElement(s.Z,{className:(0,p.q)(g("clearIconAllItems"),"flex-none h-4 w-4","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})):null,o.createElement(d.u,{className:"absolute z-10 w-full",enter:"transition ease duration-100 transform",enterFrom:"opacity-0 -translate-y-4",enterTo:"opacity-100 translate-y-0",leave:"transition ease duration-100 transform",leaveFrom:"opacity-100 translate-y-0",leaveTo:"opacity-0 -translate-y-4"},o.createElement(u.R.Options,{className:(0,p.q)("divide-y overflow-y-auto outline-none rounded-tremor-default max-h-[228px] left-0 border my-1","bg-tremor-background border-tremor-border divide-tremor-border shadow-tremor-dropdown","dark:bg-dark-tremor-background dark:border-dark-tremor-border dark:divide-dark-tremor-border dark:shadow-dark-tremor-dropdown")},o.createElement("div",{className:(0,p.q)("flex items-center w-full px-2.5","bg-tremor-background-muted","dark:bg-dark-tremor-background-muted")},o.createElement("span",null,o.createElement(c,{className:(0,p.q)("flex-none w-4 h-4 mr-2","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})),o.createElement("input",{name:"search",type:"input",autoComplete:"off",placeholder:b,className:(0,p.q)("w-full focus:outline-none focus:ring-none bg-transparent text-tremor-default py-2","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis"),onKeyDown:e=>{"Space"===e.code&&""!==e.target.value&&e.stopPropagation()},onChange:e=>M(e.target.value),value:N})),o.createElement(i.Z.Provider,Object.assign({},{onBlur:{handleResetSearch:T}},{value:{selectedValue:t}}),R))))})});v.displayName="MultiSelect"},46030:function(e,t,n){"use strict";n.d(t,{Z:function(){return u}});var r=n(5853);n(42698),n(64016),n(8710);var o=n(33232),i=n(2265),a=n(65954),l=n(1153),c=n(28517);let s=(0,l.fn)("MultiSelectItem"),u=i.forwardRef((e,t)=>{let{value:n,className:u,children:d}=e,f=(0,r._T)(e,["value","className","children"]),{selectedValue:p}=(0,i.useContext)(o.Z),h=(0,l.NZ)(n,p);return i.createElement(c.R.Option,Object.assign({className:(0,a.q)(s("root"),"flex justify-start items-center cursor-default text-tremor-default p-2.5","ui-active:bg-tremor-background-muted ui-active:text-tremor-content-strong ui-selected:text-tremor-content-strong text-tremor-content-emphasis","dark:ui-active:bg-dark-tremor-background-muted dark:ui-active:text-dark-tremor-content-strong dark:ui-selected:text-dark-tremor-content-strong dark:ui-selected:bg-dark-tremor-background-muted dark:text-dark-tremor-content-emphasis",u),ref:t,key:n,value:n},f),i.createElement("input",{type:"checkbox",className:(0,a.q)(s("checkbox"),"flex-none focus:ring-none focus:outline-none cursor-pointer mr-2.5","accent-tremor-brand","dark:accent-dark-tremor-brand"),checked:h,readOnly:!0}),i.createElement("span",{className:"whitespace-nowrap truncate"},null!=d?d:n))});u.displayName="MultiSelectItem"},30150:function(e,t,n){"use strict";n.d(t,{Z:function(){return f}});var r=n(5853),o=n(2265);let i=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2.5"}),o.createElement("path",{d:"M12 4v16m8-8H4"}))},a=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({},t,{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2.5"}),o.createElement("path",{d:"M20 12H4"}))};var l=n(65954),c=n(1153),s=n(69262);let u="flex mx-auto text-tremor-content-subtle dark:text-dark-tremor-content-subtle",d="cursor-pointer hover:text-tremor-content dark:hover:text-dark-tremor-content",f=o.forwardRef((e,t)=>{let{onSubmit:n,enableStepper:f=!0,disabled:p,onValueChange:h,onChange:m}=e,g=(0,r._T)(e,["onSubmit","enableStepper","disabled","onValueChange","onChange"]),v=(0,o.useRef)(null),[y,b]=o.useState(!1),x=o.useCallback(()=>{b(!0)},[]),w=o.useCallback(()=>{b(!1)},[]),[S,k]=o.useState(!1),E=o.useCallback(()=>{k(!0)},[]),C=o.useCallback(()=>{k(!1)},[]);return o.createElement(s.Z,Object.assign({type:"number",ref:(0,c.lq)([v,t]),disabled:p,makeInputClassName:(0,c.fn)("NumberInput"),onKeyDown:e=>{var t;if("Enter"===e.key&&!e.ctrlKey&&!e.altKey&&!e.shiftKey){let e=null===(t=v.current)||void 0===t?void 0:t.value;null==n||n(parseFloat(null!=e?e:""))}"ArrowDown"===e.key&&x(),"ArrowUp"===e.key&&E()},onKeyUp:e=>{"ArrowDown"===e.key&&w(),"ArrowUp"===e.key&&C()},onChange:e=>{p||(null==h||h(parseFloat(e.target.value)),null==m||m(e))},stepper:f?o.createElement("div",{className:(0,l.q)("flex justify-center align-middle")},o.createElement("div",{tabIndex:-1,onClick:e=>e.preventDefault(),onMouseDown:e=>e.preventDefault(),onTouchStart:e=>{e.cancelable&&e.preventDefault()},onMouseUp:()=>{var e,t;p||(null===(e=v.current)||void 0===e||e.stepDown(),null===(t=v.current)||void 0===t||t.dispatchEvent(new Event("input",{bubbles:!0})))},className:(0,l.q)(!p&&d,u,"group py-[10px] px-2.5 border-l border-tremor-border dark:border-dark-tremor-border")},o.createElement(a,{"data-testid":"step-down",className:(y?"scale-95":"")+" h-4 w-4 duration-75 transition group-active:scale-95"})),o.createElement("div",{tabIndex:-1,onClick:e=>e.preventDefault(),onMouseDown:e=>e.preventDefault(),onTouchStart:e=>{e.cancelable&&e.preventDefault()},onMouseUp:()=>{var e,t;p||(null===(e=v.current)||void 0===e||e.stepUp(),null===(t=v.current)||void 0===t||t.dispatchEvent(new Event("input",{bubbles:!0})))},className:(0,l.q)(!p&&d,u,"group py-[10px] px-2.5 border-l border-tremor-border dark:border-dark-tremor-border")},o.createElement(i,{"data-testid":"step-up",className:(S?"scale-95":"")+" h-4 w-4 duration-75 transition group-active:scale-95"}))):null},g))});f.displayName="NumberInput"},27281:function(e,t,n){"use strict";n.d(t,{Z:function(){return h}});var r=n(5853),o=n(2265),i=n(58747),a=n(4537),l=n(65954),c=n(1153),s=n(96398),u=n(28517),d=n(33044),f=n(44140);let p=(0,c.fn)("Select"),h=o.forwardRef((e,t)=>{let{defaultValue:n,value:c,onValueChange:h,placeholder:m="Select...",disabled:g=!1,icon:v,enableClear:y=!0,children:b,className:x}=e,w=(0,r._T)(e,["defaultValue","value","onValueChange","placeholder","disabled","icon","enableClear","children","className"]),[S,k]=(0,f.Z)(n,c),E=(0,o.useMemo)(()=>{let e=o.Children.toArray(b).filter(o.isValidElement);return(0,s.sl)(e)},[b]);return o.createElement(u.R,Object.assign({as:"div",ref:t,defaultValue:S,value:S,onChange:e=>{null==h||h(e),k(e)},disabled:g,className:(0,l.q)("w-full min-w-[10rem] relative text-tremor-default",x)},w),e=>{var t;let{value:n}=e;return o.createElement(o.Fragment,null,o.createElement(u.R.Button,{className:(0,l.q)("w-full outline-none text-left whitespace-nowrap truncate rounded-tremor-default focus:ring-2 transition duration-100 border pr-8 py-2","border-tremor-border shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:border-dark-tremor-border dark:shadow-dark-tremor-input dark:focus:border-dark-tremor-brand-subtle dark:focus:ring-dark-tremor-brand-muted",v?"pl-10":"pl-3",(0,s.um)((0,s.Uh)(n),g))},v&&o.createElement("span",{className:(0,l.q)("absolute inset-y-0 left-0 flex items-center ml-px pl-2.5")},o.createElement(v,{className:(0,l.q)(p("Icon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})),o.createElement("span",{className:"w-[90%] block truncate"},n&&null!==(t=E.get(n))&&void 0!==t?t:m),o.createElement("span",{className:(0,l.q)("absolute inset-y-0 right-0 flex items-center mr-3")},o.createElement(i.Z,{className:(0,l.q)(p("arrowDownIcon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")}))),y&&S?o.createElement("button",{type:"button",className:(0,l.q)("absolute inset-y-0 right-0 flex items-center mr-8"),onClick:e=>{e.preventDefault(),k(""),null==h||h("")}},o.createElement(a.Z,{className:(0,l.q)(p("clearIcon"),"flex-none h-4 w-4","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})):null,o.createElement(d.u,{className:"absolute z-10 w-full",enter:"transition ease duration-100 transform",enterFrom:"opacity-0 -translate-y-4",enterTo:"opacity-100 translate-y-0",leave:"transition ease duration-100 transform",leaveFrom:"opacity-100 translate-y-0",leaveTo:"opacity-0 -translate-y-4"},o.createElement(u.R.Options,{className:(0,l.q)("divide-y overflow-y-auto outline-none rounded-tremor-default max-h-[228px] left-0 border my-1","bg-tremor-background border-tremor-border divide-tremor-border shadow-tremor-dropdown","dark:bg-dark-tremor-background dark:border-dark-tremor-border dark:divide-dark-tremor-border dark:shadow-dark-tremor-dropdown")},b)))})});h.displayName="Select"},57365:function(e,t,n){"use strict";n.d(t,{Z:function(){return c}});var r=n(5853),o=n(2265),i=n(28517),a=n(65954);let l=(0,n(1153).fn)("SelectItem"),c=o.forwardRef((e,t)=>{let{value:n,icon:c,className:s,children:u}=e,d=(0,r._T)(e,["value","icon","className","children"]);return o.createElement(i.R.Option,Object.assign({className:(0,a.q)(l("root"),"flex justify-start items-center cursor-default text-tremor-default px-2.5 py-2.5","ui-active:bg-tremor-background-muted ui-active:text-tremor-content-strong ui-selected:text-tremor-content-strong ui-selected:bg-tremor-background-muted text-tremor-content-emphasis","dark:ui-active:bg-dark-tremor-background-muted dark:ui-active:text-dark-tremor-content-strong dark:ui-selected:text-dark-tremor-content-strong dark:ui-selected:bg-dark-tremor-background-muted dark:text-dark-tremor-content-emphasis",s),ref:t,key:n,value:n},d),c&&o.createElement(c,{className:(0,a.q)(l("icon"),"flex-none w-5 h-5 mr-1.5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")}),o.createElement("span",{className:"whitespace-nowrap truncate"},null!=u?u:n))});c.displayName="SelectItem"},92858:function(e,t,n){"use strict";n.d(t,{Z:function(){return M}});var r=n(5853),o=n(2265),i=n(62963),a=n(90945),l=n(13323),c=n(17684),s=n(80004),u=n(93689),d=n(38198),f=n(47634),p=n(56314),h=n(27847),m=n(64518);let g=(0,o.createContext)(null),v=Object.assign((0,h.yV)(function(e,t){let n=(0,c.M)(),{id:r="headlessui-description-".concat(n),...i}=e,a=function e(){let t=(0,o.useContext)(g);if(null===t){let t=Error("You used a component, but it is not inside a relevant parent.");throw Error.captureStackTrace&&Error.captureStackTrace(t,e),t}return t}(),l=(0,u.T)(t);(0,m.e)(()=>a.register(r),[r,a.register]);let s={ref:l,...a.props,id:r};return(0,h.sY)({ourProps:s,theirProps:i,slot:a.slot||{},defaultTag:"p",name:a.name||"Description"})}),{});var y=n(37388);let b=(0,o.createContext)(null),x=Object.assign((0,h.yV)(function(e,t){let n=(0,c.M)(),{id:r="headlessui-label-".concat(n),passive:i=!1,...a}=e,l=function e(){let t=(0,o.useContext)(b);if(null===t){let t=Error("You used a