From 81ee1653afc72c9665bb43b4dcf636c5af208e90 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 09:12:27 -0700 Subject: [PATCH 01/50] use correct type hints for audio transcriptions --- litellm/batches/main.py | 4 +- litellm/caching.py | 2 +- litellm/litellm_core_utils/core_helpers.py | 30 +-- .../llms/AzureOpenAI/audio_transcriptions.py | 192 ++++++++++++++++++ litellm/llms/{ => AzureOpenAI}/azure.py | 189 +---------------- litellm/llms/OpenAI/audio_transcriptions.py | 177 ++++++++++++++++ litellm/llms/{ => OpenAI}/openai.py | 180 +--------------- litellm/llms/azure_text.py | 4 +- litellm/main.py | 28 +-- litellm/router.py | 19 +- litellm/types/llms/openai.py | 13 +- litellm/types/utils.py | 1 + litellm/utils.py | 11 +- 13 files changed, 420 insertions(+), 430 deletions(-) create mode 100644 litellm/llms/AzureOpenAI/audio_transcriptions.py rename litellm/llms/{ => AzureOpenAI}/azure.py (94%) create mode 100644 litellm/llms/OpenAI/audio_transcriptions.py rename litellm/llms/{ => OpenAI}/openai.py (95%) diff --git a/litellm/batches/main.py b/litellm/batches/main.py index e927a18b666..cd81cc44fb9 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -20,8 +20,8 @@ import httpx import litellm from litellm import client -from litellm.llms.azure import AzureBatchesAPI -from litellm.llms.openai import OpenAIBatchesAPI +from litellm.llms.AzureOpenAI.azure import AzureBatchesAPI +from litellm.llms.OpenAI.openai import OpenAIBatchesAPI from litellm.secret_managers.main import get_secret from litellm.types.llms.openai import ( Batch, diff --git a/litellm/caching.py b/litellm/caching.py index 13da3cb1eaf..7f67ee4553f 100644 --- a/litellm/caching.py +++ b/litellm/caching.py @@ -17,7 +17,7 @@ import time import traceback from datetime import timedelta from enum import Enum -from typing import Any, BinaryIO, List, Literal, Optional, Union +from typing import Any, List, Literal, Optional, Union from openai._models import BaseModel as OpenAIObject diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index a9e53531670..9f5075c2286 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -1,9 +1,10 @@ # What is this? ## Helper utilities import os -from typing import BinaryIO, List, Literal, Optional, Tuple +from typing import List, Literal, Optional, Tuple from litellm._logging import verbose_logger +from litellm.types.utils import FileTypes def map_finish_reason( @@ -88,18 +89,19 @@ def _get_parent_otel_span_from_kwargs(kwargs: Optional[dict] = None): return None -def get_file_check_sum(_file: BinaryIO): +def get_audio_file_name(file_obj: FileTypes) -> str: """ - Helper to safely get file checksum - used as a cache key + Safely get the name of a file-like object or return its string representation. + + Args: + file_obj (Any): A file-like object or any other object. + + Returns: + str: The name of the file if available, otherwise a string representation of the object. """ - try: - file_descriptor = _file.fileno() - file_stat = os.fstat(file_descriptor) - file_size = str(file_stat.st_size) - file_checksum = _file.name + file_size - return file_checksum - except Exception as e: - verbose_logger.error(f"Error getting file_checksum: {(str(e))}") - file_checksum = _file.name - return file_checksum - return file_checksum + if hasattr(file_obj, "name"): + return getattr(file_obj, "name") + elif hasattr(file_obj, "__str__"): + return str(file_obj) + else: + return repr(file_obj) diff --git a/litellm/llms/AzureOpenAI/audio_transcriptions.py b/litellm/llms/AzureOpenAI/audio_transcriptions.py new file mode 100644 index 00000000000..db373797abc --- /dev/null +++ b/litellm/llms/AzureOpenAI/audio_transcriptions.py @@ -0,0 +1,192 @@ +import uuid +from typing import Optional, Union + +import httpx +from openai import AsyncAzureOpenAI, AzureOpenAI +from pydantic import BaseModel + +import litellm +from litellm.litellm_core_utils.core_helpers import get_audio_file_name +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.types.utils import FileTypes +from litellm.utils import TranscriptionResponse, convert_to_model_response_object + +from .azure import ( + AzureChatCompletion, + get_azure_ad_token_from_oidc, + select_azure_base_url_or_endpoint, +) + + +class AzureAudioTranscription(AzureChatCompletion): + def audio_transcriptions( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + model_response: TranscriptionResponse, + timeout: float, + max_retries: int, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + api_version: Optional[str] = None, + client=None, + azure_ad_token: Optional[str] = None, + logging_obj=None, + atranscription: bool = False, + ): + data = {"model": model, "file": audio_file, **optional_params} + + # init AzureOpenAI Client + azure_client_params = { + "api_version": api_version, + "azure_endpoint": api_base, + "azure_deployment": model, + "timeout": timeout, + } + + azure_client_params = select_azure_base_url_or_endpoint( + azure_client_params=azure_client_params + ) + if api_key is not None: + azure_client_params["api_key"] = api_key + elif azure_ad_token is not None: + if azure_ad_token.startswith("oidc/"): + azure_ad_token = get_azure_ad_token_from_oidc(azure_ad_token) + azure_client_params["azure_ad_token"] = azure_ad_token + + if max_retries is not None: + azure_client_params["max_retries"] = max_retries + + if atranscription is True: + return self.async_audio_transcriptions( + audio_file=audio_file, + data=data, + model_response=model_response, + timeout=timeout, + api_key=api_key, + api_base=api_base, + client=client, + azure_client_params=azure_client_params, + max_retries=max_retries, + logging_obj=logging_obj, + ) + if client is None: + azure_client = AzureOpenAI(http_client=litellm.client_session, **azure_client_params) # type: ignore + else: + azure_client = client + + ## LOGGING + logging_obj.pre_call( + input=f"audio_file_{uuid.uuid4()}", + api_key=azure_client.api_key, + additional_args={ + "headers": {"Authorization": f"Bearer {azure_client.api_key}"}, + "api_base": azure_client._base_url._uri_reference, + "atranscription": True, + "complete_input_dict": data, + }, + ) + + response = azure_client.audio.transcriptions.create( + **data, timeout=timeout # type: ignore + ) + + if isinstance(response, BaseModel): + stringified_response = response.model_dump() + else: + stringified_response = TranscriptionResponse(text=response).model_dump() + + ## LOGGING + logging_obj.post_call( + input=get_audio_file_name(audio_file), + api_key=api_key, + additional_args={"complete_input_dict": data}, + original_response=stringified_response, + ) + hidden_params = {"model": "whisper-1", "custom_llm_provider": "azure"} + final_response = convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore + return final_response + + async def async_audio_transcriptions( + self, + audio_file: FileTypes, + data: dict, + model_response: TranscriptionResponse, + timeout: float, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + client=None, + azure_client_params=None, + max_retries=None, + logging_obj=None, + ): + response = None + try: + if client is None: + async_azure_client = AsyncAzureOpenAI( + **azure_client_params, + http_client=litellm.aclient_session, + ) + else: + async_azure_client = client + + ## LOGGING + logging_obj.pre_call( + input=f"audio_file_{uuid.uuid4()}", + api_key=async_azure_client.api_key, + additional_args={ + "headers": { + "Authorization": f"Bearer {async_azure_client.api_key}" + }, + "api_base": async_azure_client._base_url._uri_reference, + "atranscription": True, + "complete_input_dict": data, + }, + ) + + raw_response = ( + await async_azure_client.audio.transcriptions.with_raw_response.create( + **data, timeout=timeout + ) + ) # type: ignore + + headers = dict(raw_response.headers) + response = raw_response.parse() + + if isinstance(response, BaseModel): + stringified_response = response.model_dump() + else: + stringified_response = TranscriptionResponse(text=response).model_dump() + + ## LOGGING + logging_obj.post_call( + input=get_audio_file_name(audio_file), + api_key=api_key, + additional_args={ + "headers": { + "Authorization": f"Bearer {async_azure_client.api_key}" + }, + "api_base": async_azure_client._base_url._uri_reference, + "atranscription": True, + "complete_input_dict": data, + }, + original_response=stringified_response, + ) + hidden_params = {"model": "whisper-1", "custom_llm_provider": "azure"} + response = convert_to_model_response_object( + _response_headers=headers, + response_object=stringified_response, + model_response_object=model_response, + hidden_params=hidden_params, + response_type="audio_transcription", + ) # type: ignore + return response + except Exception as e: + ## LOGGING + logging_obj.post_call( + input=input, + api_key=api_key, + original_response=str(e), + ) + raise e diff --git a/litellm/llms/azure.py b/litellm/llms/AzureOpenAI/azure.py similarity index 94% rename from litellm/llms/azure.py rename to litellm/llms/AzureOpenAI/azure.py index 222961e10d0..a14644e1875 100644 --- a/litellm/llms/azure.py +++ b/litellm/llms/AzureOpenAI/azure.py @@ -4,17 +4,7 @@ import os import time import types import uuid -from typing import ( - Any, - BinaryIO, - Callable, - Coroutine, - Iterable, - List, - Literal, - Optional, - Union, -) +from typing import Any, Callable, Coroutine, Iterable, List, Literal, Optional, Union import httpx # type: ignore import requests @@ -27,6 +17,7 @@ from litellm import ImageResponse, OpenAIConfig from litellm.caching import DualCache from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler +from litellm.types.utils import FileTypes from litellm.utils import ( Choices, CustomStreamWrapper, @@ -39,7 +30,7 @@ from litellm.utils import ( modify_url, ) -from ..types.llms.openai import ( +from ...types.llms.openai import ( Assistant, AssistantEventHandler, AssistantStreamManager, @@ -63,7 +54,7 @@ from ..types.llms.openai import ( SyncCursorPage, Thread, ) -from .base import BaseLLM +from ..base import BaseLLM azure_ad_cache = DualCache() @@ -1570,178 +1561,6 @@ class AzureChatCompletion(BaseLLM): else: raise AzureOpenAIError(status_code=500, message=str(e)) - def audio_transcriptions( - self, - model: str, - audio_file: BinaryIO, - optional_params: dict, - model_response: TranscriptionResponse, - timeout: float, - max_retries: int, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - api_version: Optional[str] = None, - client=None, - azure_ad_token: Optional[str] = None, - logging_obj=None, - atranscription: bool = False, - ): - data = {"model": model, "file": audio_file, **optional_params} - - # init AzureOpenAI Client - azure_client_params = { - "api_version": api_version, - "azure_endpoint": api_base, - "azure_deployment": model, - "timeout": timeout, - } - - azure_client_params = select_azure_base_url_or_endpoint( - azure_client_params=azure_client_params - ) - if api_key is not None: - azure_client_params["api_key"] = api_key - elif azure_ad_token is not None: - if azure_ad_token.startswith("oidc/"): - azure_ad_token = get_azure_ad_token_from_oidc(azure_ad_token) - azure_client_params["azure_ad_token"] = azure_ad_token - - if max_retries is not None: - azure_client_params["max_retries"] = max_retries - - if atranscription is True: - return self.async_audio_transcriptions( - audio_file=audio_file, - data=data, - model_response=model_response, - timeout=timeout, - api_key=api_key, - api_base=api_base, - client=client, - azure_client_params=azure_client_params, - max_retries=max_retries, - logging_obj=logging_obj, - ) - if client is None: - azure_client = AzureOpenAI(http_client=litellm.client_session, **azure_client_params) # type: ignore - else: - azure_client = client - - ## LOGGING - logging_obj.pre_call( - input=f"audio_file_{uuid.uuid4()}", - api_key=azure_client.api_key, - additional_args={ - "headers": {"Authorization": f"Bearer {azure_client.api_key}"}, - "api_base": azure_client._base_url._uri_reference, - "atranscription": True, - "complete_input_dict": data, - }, - ) - - response = azure_client.audio.transcriptions.create( - **data, timeout=timeout # type: ignore - ) - - if isinstance(response, BaseModel): - stringified_response = response.model_dump() - else: - stringified_response = TranscriptionResponse(text=response).model_dump() - - ## LOGGING - logging_obj.post_call( - input=audio_file.name, - api_key=api_key, - additional_args={"complete_input_dict": data}, - original_response=stringified_response, - ) - hidden_params = {"model": "whisper-1", "custom_llm_provider": "azure"} - final_response = convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore - return final_response - - async def async_audio_transcriptions( - self, - audio_file: BinaryIO, - data: dict, - model_response: TranscriptionResponse, - timeout: float, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - client=None, - azure_client_params=None, - max_retries=None, - logging_obj=None, - ): - response = None - try: - if client is None: - async_azure_client = AsyncAzureOpenAI( - **azure_client_params, - http_client=litellm.aclient_session, - ) - else: - async_azure_client = client - - ## LOGGING - logging_obj.pre_call( - input=f"audio_file_{uuid.uuid4()}", - api_key=async_azure_client.api_key, - additional_args={ - "headers": { - "Authorization": f"Bearer {async_azure_client.api_key}" - }, - "api_base": async_azure_client._base_url._uri_reference, - "atranscription": True, - "complete_input_dict": data, - }, - ) - - raw_response = ( - await async_azure_client.audio.transcriptions.with_raw_response.create( - **data, timeout=timeout - ) - ) # type: ignore - - headers = dict(raw_response.headers) - response = raw_response.parse() - - if isinstance(response, BaseModel): - stringified_response = response.model_dump() - else: - stringified_response = TranscriptionResponse(text=response).model_dump() - - ## LOGGING - logging_obj.post_call( - input=audio_file.name, - api_key=api_key, - additional_args={ - "headers": { - "Authorization": f"Bearer {async_azure_client.api_key}" - }, - "api_base": async_azure_client._base_url._uri_reference, - "atranscription": True, - "complete_input_dict": data, - }, - original_response=stringified_response, - ) - hidden_params = {"model": "whisper-1", "custom_llm_provider": "azure"} - response = convert_to_model_response_object( - _response_headers=headers, - response_object=stringified_response, - model_response_object=model_response, - hidden_params=hidden_params, - response_type="audio_transcription", - ) # type: ignore - return response - except Exception as e: - ## LOGGING - logging_obj.post_call( - input=input, - api_key=api_key, - original_response=str(e), - ) - raise e - def audio_speech( self, model: str, diff --git a/litellm/llms/OpenAI/audio_transcriptions.py b/litellm/llms/OpenAI/audio_transcriptions.py new file mode 100644 index 00000000000..587ee471eb9 --- /dev/null +++ b/litellm/llms/OpenAI/audio_transcriptions.py @@ -0,0 +1,177 @@ +from typing import Optional, Union + +import httpx +from openai import AsyncOpenAI, OpenAI +from pydantic import BaseModel + +import litellm +from litellm.litellm_core_utils.core_helpers import get_audio_file_name +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.types.utils import FileTypes +from litellm.utils import TranscriptionResponse, convert_to_model_response_object + +from .openai import OpenAIChatCompletion + + +class OpenAIAudioTranscription(OpenAIChatCompletion): + # Audio Transcriptions + async def make_openai_audio_transcriptions_request( + self, + openai_aclient: AsyncOpenAI, + data: dict, + timeout: Union[float, httpx.Timeout], + ): + """ + Helper to: + - call openai_aclient.audio.transcriptions.with_raw_response when litellm.return_response_headers is True + - call openai_aclient.audio.transcriptions.create by default + """ + try: + if litellm.return_response_headers is True: + raw_response = ( + await openai_aclient.audio.transcriptions.with_raw_response.create( + **data, timeout=timeout + ) + ) # type: ignore + headers = dict(raw_response.headers) + response = raw_response.parse() + return headers, response + else: + response = await openai_aclient.audio.transcriptions.create(**data, timeout=timeout) # type: ignore + return None, response + except Exception as e: + raise e + + def make_sync_openai_audio_transcriptions_request( + self, + openai_client: OpenAI, + data: dict, + timeout: Union[float, httpx.Timeout], + ): + """ + Helper to: + - call openai_aclient.audio.transcriptions.with_raw_response when litellm.return_response_headers is True + - call openai_aclient.audio.transcriptions.create by default + """ + try: + if litellm.return_response_headers is True: + raw_response = ( + openai_client.audio.transcriptions.with_raw_response.create( + **data, timeout=timeout + ) + ) # type: ignore + headers = dict(raw_response.headers) + response = raw_response.parse() + return headers, response + else: + response = openai_client.audio.transcriptions.create(**data, timeout=timeout) # type: ignore + return None, response + except Exception as e: + raise e + + def audio_transcriptions( + self, + model: str, + audio_file: FileTypes, + optional_params: dict, + model_response: TranscriptionResponse, + timeout: float, + max_retries: int, + api_key: Optional[str], + api_base: Optional[str], + client=None, + logging_obj=None, + atranscription: bool = False, + ): + data = {"model": model, "file": audio_file, **optional_params} + if atranscription is True: + return self.async_audio_transcriptions( + audio_file=audio_file, + data=data, + model_response=model_response, + timeout=timeout, + api_key=api_key, + api_base=api_base, + client=client, + max_retries=max_retries, + logging_obj=logging_obj, + ) + + openai_client = self._get_openai_client( + is_async=False, + api_key=api_key, + api_base=api_base, + timeout=timeout, + max_retries=max_retries, + ) + _, response = self.make_sync_openai_audio_transcriptions_request( + openai_client=openai_client, + data=data, + timeout=timeout, + ) + + if isinstance(response, BaseModel): + stringified_response = response.model_dump() + else: + stringified_response = TranscriptionResponse(text=response).model_dump() + + ## LOGGING + logging_obj.post_call( + input=get_audio_file_name(audio_file), + api_key=api_key, + additional_args={"complete_input_dict": data}, + original_response=stringified_response, + ) + hidden_params = {"model": "whisper-1", "custom_llm_provider": "openai"} + final_response = convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore + return final_response + + async def async_audio_transcriptions( + self, + audio_file: FileTypes, + data: dict, + model_response: TranscriptionResponse, + timeout: float, + logging_obj: LiteLLMLoggingObj, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + client=None, + max_retries=None, + ): + try: + openai_aclient = self._get_openai_client( + is_async=True, + api_key=api_key, + api_base=api_base, + timeout=timeout, + max_retries=max_retries, + client=client, + ) + + headers, response = await self.make_openai_audio_transcriptions_request( + openai_aclient=openai_aclient, + data=data, + timeout=timeout, + ) + logging_obj.model_call_details["response_headers"] = headers + if isinstance(response, BaseModel): + stringified_response = response.model_dump() + else: + stringified_response = TranscriptionResponse(text=response).model_dump() + ## LOGGING + logging_obj.post_call( + input=get_audio_file_name(audio_file), + api_key=api_key, + additional_args={"complete_input_dict": data}, + original_response=stringified_response, + ) + hidden_params = {"model": "whisper-1", "custom_llm_provider": "openai"} + return convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore + except Exception as e: + ## LOGGING + logging_obj.post_call( + input=input, + api_key=api_key, + original_response=str(e), + ) + raise e diff --git a/litellm/llms/openai.py b/litellm/llms/OpenAI/openai.py similarity index 95% rename from litellm/llms/openai.py rename to litellm/llms/OpenAI/openai.py index e7a10c5cd8a..8d112f52ae9 100644 --- a/litellm/llms/openai.py +++ b/litellm/llms/OpenAI/openai.py @@ -4,16 +4,7 @@ import os import time import traceback import types -from typing import ( - Any, - BinaryIO, - Callable, - Coroutine, - Iterable, - Literal, - Optional, - Union, -) +from typing import Any, Callable, Coroutine, Iterable, Literal, Optional, Union import httpx import openai @@ -33,14 +24,13 @@ from litellm.utils import ( Message, ModelResponse, TextCompletionResponse, - TranscriptionResponse, Usage, convert_to_model_response_object, ) -from ..types.llms.openai import * -from .base import BaseLLM -from .prompt_templates.factory import custom_prompt, prompt_factory +from ...types.llms.openai import * +from ..base import BaseLLM +from ..prompt_templates.factory import custom_prompt, prompt_factory class OpenAIError(Exception): @@ -1608,168 +1598,6 @@ class OpenAIChatCompletion(BaseLLM): else: raise OpenAIError(status_code=500, message=str(e)) - # Audio Transcriptions - async def make_openai_audio_transcriptions_request( - self, - openai_aclient: AsyncOpenAI, - data: dict, - timeout: Union[float, httpx.Timeout], - ): - """ - Helper to: - - call openai_aclient.audio.transcriptions.with_raw_response when litellm.return_response_headers is True - - call openai_aclient.audio.transcriptions.create by default - """ - try: - if litellm.return_response_headers is True: - raw_response = ( - await openai_aclient.audio.transcriptions.with_raw_response.create( - **data, timeout=timeout - ) - ) # type: ignore - headers = dict(raw_response.headers) - response = raw_response.parse() - return headers, response - else: - response = await openai_aclient.audio.transcriptions.create(**data, timeout=timeout) # type: ignore - return None, response - except Exception as e: - raise e - - def make_sync_openai_audio_transcriptions_request( - self, - openai_client: OpenAI, - data: dict, - timeout: Union[float, httpx.Timeout], - ): - """ - Helper to: - - call openai_aclient.audio.transcriptions.with_raw_response when litellm.return_response_headers is True - - call openai_aclient.audio.transcriptions.create by default - """ - try: - if litellm.return_response_headers is True: - raw_response = ( - openai_client.audio.transcriptions.with_raw_response.create( - **data, timeout=timeout - ) - ) # type: ignore - headers = dict(raw_response.headers) - response = raw_response.parse() - return headers, response - else: - response = openai_client.audio.transcriptions.create(**data, timeout=timeout) # type: ignore - return None, response - except Exception as e: - raise e - - def audio_transcriptions( - self, - model: str, - audio_file: BinaryIO, - optional_params: dict, - model_response: TranscriptionResponse, - timeout: float, - max_retries: int, - api_key: Optional[str], - api_base: Optional[str], - client=None, - logging_obj=None, - atranscription: bool = False, - ): - data = {"model": model, "file": audio_file, **optional_params} - if atranscription is True: - return self.async_audio_transcriptions( - audio_file=audio_file, - data=data, - model_response=model_response, - timeout=timeout, - api_key=api_key, - api_base=api_base, - client=client, - max_retries=max_retries, - logging_obj=logging_obj, - ) - - openai_client = self._get_openai_client( - is_async=False, - api_key=api_key, - api_base=api_base, - timeout=timeout, - max_retries=max_retries, - ) - _, response = self.make_sync_openai_audio_transcriptions_request( - openai_client=openai_client, - data=data, - timeout=timeout, - ) - - if isinstance(response, BaseModel): - stringified_response = response.model_dump() - else: - stringified_response = TranscriptionResponse(text=response).model_dump() - - ## LOGGING - logging_obj.post_call( - input=audio_file.name, - api_key=api_key, - additional_args={"complete_input_dict": data}, - original_response=stringified_response, - ) - hidden_params = {"model": "whisper-1", "custom_llm_provider": "openai"} - final_response = convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore - return final_response - - async def async_audio_transcriptions( - self, - audio_file: BinaryIO, - data: dict, - model_response: TranscriptionResponse, - timeout: float, - logging_obj: LiteLLMLoggingObj, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - client=None, - max_retries=None, - ): - try: - openai_aclient = self._get_openai_client( - is_async=True, - api_key=api_key, - api_base=api_base, - timeout=timeout, - max_retries=max_retries, - client=client, - ) - - headers, response = await self.make_openai_audio_transcriptions_request( - openai_aclient=openai_aclient, - data=data, - timeout=timeout, - ) - logging_obj.model_call_details["response_headers"] = headers - if isinstance(response, BaseModel): - stringified_response = response.model_dump() - else: - stringified_response = TranscriptionResponse(text=response).model_dump() - ## LOGGING - logging_obj.post_call( - input=audio_file.name, - api_key=api_key, - additional_args={"complete_input_dict": data}, - original_response=stringified_response, - ) - hidden_params = {"model": "whisper-1", "custom_llm_provider": "openai"} - return convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore - except Exception as e: - ## LOGGING - logging_obj.post_call( - input=input, - api_key=api_key, - original_response=str(e), - ) - raise e - def audio_speech( self, model: str, diff --git a/litellm/llms/azure_text.py b/litellm/llms/azure_text.py index fb6e4875e27..9a8d462e564 100644 --- a/litellm/llms/azure_text.py +++ b/litellm/llms/azure_text.py @@ -1,7 +1,7 @@ import json import types # type: ignore import uuid -from typing import Any, BinaryIO, Callable, Optional, Union +from typing import Any, Callable, Optional, Union import httpx import requests @@ -19,8 +19,8 @@ from litellm.utils import ( convert_to_model_response_object, ) -from ..llms.openai import OpenAITextCompletion, OpenAITextCompletionConfig from .base import BaseLLM +from .OpenAI.openai import OpenAITextCompletion, OpenAITextCompletionConfig from .prompt_templates.factory import custom_prompt, prompt_factory openai_text_completion_config = OpenAITextCompletionConfig() diff --git a/litellm/main.py b/litellm/main.py index 9e7297e11f5..bb2c1c47f4d 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -22,18 +22,7 @@ import uuid from concurrent.futures import ThreadPoolExecutor from copy import deepcopy from functools import partial -from typing import ( - Any, - BinaryIO, - Callable, - Dict, - List, - Literal, - Mapping, - Optional, - Type, - Union, -) +from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Type, Union import dotenv import httpx @@ -93,8 +82,9 @@ from .llms import ( from .llms.AI21 import completion as ai21 from .llms.anthropic.chat import AnthropicChatCompletion from .llms.anthropic.completion import AnthropicTextCompletion -from .llms.azure import AzureChatCompletion, _check_dynamic_azure_params from .llms.azure_text import AzureTextCompletion +from .llms.AzureOpenAI.audio_transcriptions import AzureAudioTranscription +from .llms.AzureOpenAI.azure import AzureChatCompletion, _check_dynamic_azure_params from .llms.bedrock import image_generation as bedrock_image_generation # type: ignore from .llms.bedrock.chat import BedrockConverseLLM, BedrockLLM from .llms.bedrock.embed.embedding import BedrockEmbedding @@ -104,7 +94,8 @@ from .llms.cohere import embed as cohere_embed from .llms.custom_llm import CustomLLM, custom_chat_llm_router from .llms.databricks import DatabricksChatCompletion from .llms.huggingface_restapi import Huggingface -from .llms.openai import OpenAIChatCompletion, OpenAITextCompletion +from .llms.OpenAI.audio_transcriptions import OpenAIAudioTranscription +from .llms.OpenAI.openai import OpenAIChatCompletion, OpenAITextCompletion from .llms.predibase import PredibaseChatCompletion from .llms.prompt_templates.factory import ( custom_prompt, @@ -146,6 +137,7 @@ from .types.llms.openai import HttpxBinaryResponseContent from .types.utils import ( AdapterCompletionStreamWrapper, ChatCompletionMessageToolCall, + FileTypes, HiddenParams, all_litellm_params, ) @@ -169,11 +161,13 @@ from litellm.utils import ( ####### ENVIRONMENT VARIABLES ################### openai_chat_completions = OpenAIChatCompletion() openai_text_completions = OpenAITextCompletion() +openai_audio_transcriptions = OpenAIAudioTranscription() databricks_chat_completions = DatabricksChatCompletion() anthropic_chat_completions = AnthropicChatCompletion() anthropic_text_completions = AnthropicTextCompletion() azure_chat_completions = AzureChatCompletion() azure_text_completions = AzureTextCompletion() +azure_audio_transcriptions = AzureAudioTranscription() huggingface = Huggingface() predibase_chat_completions = PredibaseChatCompletion() codestral_text_completions = CodestralTextCompletion() @@ -4614,7 +4608,7 @@ async def atranscription(*args, **kwargs) -> TranscriptionResponse: @client def transcription( model: str, - file: BinaryIO, + file: FileTypes, ## OPTIONAL OPENAI PARAMS ## language: Optional[str] = None, prompt: Optional[str] = None, @@ -4704,7 +4698,7 @@ def transcription( or get_secret("AZURE_API_KEY") ) # type: ignore - response = azure_chat_completions.audio_transcriptions( + response = azure_audio_transcriptions.audio_transcriptions( model=model, audio_file=file, optional_params=optional_params, @@ -4738,7 +4732,7 @@ def transcription( or litellm.openai_key or get_secret("OPENAI_API_KEY") ) # type: ignore - response = openai_chat_completions.audio_transcriptions( + response = openai_audio_transcriptions.audio_transcriptions( model=model, audio_file=file, optional_params=optional_params, diff --git a/litellm/router.py b/litellm/router.py index 2743a36b9ef..233331e8004 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -24,18 +24,7 @@ import traceback import uuid from collections import defaultdict from datetime import datetime -from typing import ( - Any, - BinaryIO, - Dict, - Iterable, - List, - Literal, - Optional, - Tuple, - TypedDict, - Union, -) +from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, TypedDict, Union import httpx import openai @@ -48,7 +37,7 @@ from litellm.assistants.main import AssistantDeleted from litellm.caching import DualCache, InMemoryCache, RedisCache from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging -from litellm.llms.azure import get_azure_ad_token_from_oidc +from litellm.llms.AzureOpenAI.azure import get_azure_ad_token_from_oidc from litellm.router_strategy.least_busy import LeastBusyLoggingHandler from litellm.router_strategy.lowest_cost import LowestCostLoggingHandler from litellm.router_strategy.lowest_latency import LowestLatencyLoggingHandler @@ -1342,7 +1331,7 @@ class Router: self.fail_calls[model_name] += 1 raise e - async def atranscription(self, file: BinaryIO, model: str, **kwargs): + async def atranscription(self, file: FileTypes, model: str, **kwargs): """ Example Usage: @@ -1386,7 +1375,7 @@ class Router: ) raise e - async def _atranscription(self, file: BinaryIO, model: str, **kwargs): + async def _atranscription(self, file: FileTypes, model: str, **kwargs): try: verbose_router_logger.debug( f"Inside _atranscription()- model: {model}; kwargs: {kwargs}" diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 788199c00d5..9d65fe87ed9 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1,16 +1,5 @@ from os import PathLike -from typing import ( - IO, - Any, - BinaryIO, - Iterable, - List, - Literal, - Mapping, - Optional, - Tuple, - Union, -) +from typing import IO, Any, Iterable, List, Literal, Mapping, Optional, Tuple, Union from openai._legacy_response import HttpxBinaryResponseContent from openai.lib.streaming._assistants import ( diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 72cc98b3e27..696fb5b837b 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -5,6 +5,7 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Tuple, Union from openai._models import BaseModel as OpenAIObject +from openai.types.audio.transcription_create_params import FileTypes from openai.types.completion_usage import CompletionUsage from pydantic import ConfigDict, Field, PrivateAttr from typing_extensions import Callable, Dict, Required, TypedDict, override diff --git a/litellm/utils.py b/litellm/utils.py index 33d3a59a338..7587563d5a4 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -58,7 +58,7 @@ import litellm.litellm_core_utils import litellm.litellm_core_utils.json_validation_rule from litellm.caching import DualCache from litellm.litellm_core_utils.core_helpers import ( - get_file_check_sum, + get_audio_file_name, map_finish_reason, ) from litellm.litellm_core_utils.exception_mapping_utils import get_error_message @@ -86,6 +86,7 @@ from litellm.types.utils import ( Delta, Embedding, EmbeddingResponse, + FileTypes, ImageResponse, Message, ModelInfo, @@ -161,7 +162,6 @@ except Exception as e: from concurrent.futures import ThreadPoolExecutor from typing import ( Any, - BinaryIO, Callable, Dict, Iterable, @@ -566,14 +566,13 @@ def function_setup( call_type == CallTypes.atranscription.value or call_type == CallTypes.transcription.value ): - _file_name: BinaryIO = args[1] if len(args) > 1 else kwargs["file"] - file_checksum = get_file_check_sum(_file=_file_name) - file_name = _file_name.name + _file_obj: FileTypes = args[1] if len(args) > 1 else kwargs["file"] + file_checksum = get_audio_file_name(file_obj=_file_obj) if "metadata" in kwargs: kwargs["metadata"]["file_checksum"] = file_checksum else: kwargs["metadata"] = {"file_checksum": file_checksum} - messages = file_name + messages = _file_obj elif ( call_type == CallTypes.aspeech.value or call_type == CallTypes.speech.value ): From ed627bc5d278dc6f8d90820e7e35cf2a5e63ecd9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 09:56:33 -0700 Subject: [PATCH 02/50] fix linting error --- litellm/assistants/main.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/litellm/assistants/main.py b/litellm/assistants/main.py index ba169f5e203..0ea5860ae7c 100644 --- a/litellm/assistants/main.py +++ b/litellm/assistants/main.py @@ -21,8 +21,8 @@ from litellm.utils import ( supports_httpx_timeout, ) -from ..llms.azure import AzureAssistantsAPI -from ..llms.openai import OpenAIAssistantsAPI +from ..llms.AzureOpenAI.azure import AzureAssistantsAPI +from ..llms.OpenAI.openai import OpenAIAssistantsAPI from ..types.llms.openai import * from ..types.router import * from .utils import get_optional_params_add_message @@ -184,6 +184,21 @@ def get_assistants( request=httpx.Request(method="create_thread", url="https://github.com/BerriAI/litellm"), # type: ignore ), ) + + if response is None: + raise litellm.exceptions.BadRequestError( + message="LiteLLM doesn't support {} for 'get_assistants'. Only 'openai' is supported.".format( + custom_llm_provider + ), + model="n/a", + llm_provider=custom_llm_provider, + response=httpx.Response( + status_code=400, + content="Unsupported provider", + request=httpx.Request(method="create_thread", url="https://github.com/BerriAI/litellm"), # type: ignore + ), + ) + return response From 25887c1846de9dc91d711ab7c6e5cede6cba70ee Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:09:44 -0700 Subject: [PATCH 03/50] fix import error --- litellm/__init__.py | 4 ++-- litellm/files/main.py | 2 +- .../litellm_core_utils/audio_utils/utils.py | 23 +++++++++++++++++++ litellm/litellm_core_utils/core_helpers.py | 19 --------------- .../llms/AzureOpenAI/audio_transcriptions.py | 2 +- litellm/llms/OpenAI/audio_transcriptions.py | 2 +- .../text_to_speech/text_to_speech_handler.py | 2 +- .../client_initalization_utils.py | 6 +++-- litellm/utils.py | 12 ++++++---- 9 files changed, 40 insertions(+), 32 deletions(-) create mode 100644 litellm/litellm_core_utils/audio_utils/utils.py diff --git a/litellm/__init__.py b/litellm/__init__.py index 8b28ab80c2b..ce753b11099 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -923,7 +923,7 @@ from .llms.bedrock.embed.amazon_titan_v2_transformation import ( AmazonTitanV2Config, ) from .llms.bedrock.embed.cohere_transformation import BedrockCohereEmbeddingConfig -from .llms.openai import ( +from .llms.OpenAI.openai import ( OpenAIConfig, OpenAITextCompletionConfig, MistralConfig, @@ -938,7 +938,7 @@ from .llms.AI21.chat import AI21ChatConfig from .llms.fireworks_ai import FireworksAIConfig from .llms.volcengine import VolcEngineConfig from .llms.text_completion_codestral import MistralTextCompletionConfig -from .llms.azure import ( +from .llms.AzureOpenAI.azure import ( AzureOpenAIConfig, AzureOpenAIError, AzureOpenAIAssistantsAPIConfig, diff --git a/litellm/files/main.py b/litellm/files/main.py index 1ed1c1e611f..84fb506522a 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -16,7 +16,7 @@ import httpx import litellm from litellm import client, get_secret from litellm.llms.files_apis.azure import AzureOpenAIFilesAPI -from litellm.llms.openai import FileDeleted, FileObject, OpenAIFilesAPI +from litellm.llms.OpenAI.openai import FileDeleted, FileObject, OpenAIFilesAPI from litellm.types.llms.openai import ( Batch, CreateFileRequest, diff --git a/litellm/litellm_core_utils/audio_utils/utils.py b/litellm/litellm_core_utils/audio_utils/utils.py new file mode 100644 index 00000000000..ab19dac9cc4 --- /dev/null +++ b/litellm/litellm_core_utils/audio_utils/utils.py @@ -0,0 +1,23 @@ +""" +Utils used for litellm.transcription() and litellm.atranscription() +""" + +from litellm.types.utils import FileTypes + + +def get_audio_file_name(file_obj: FileTypes) -> str: + """ + Safely get the name of a file-like object or return its string representation. + + Args: + file_obj (Any): A file-like object or any other object. + + Returns: + str: The name of the file if available, otherwise a string representation of the object. + """ + if hasattr(file_obj, "name"): + return getattr(file_obj, "name") + elif hasattr(file_obj, "__str__"): + return str(file_obj) + else: + return repr(file_obj) diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 9f5075c2286..269844ce8e5 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -4,7 +4,6 @@ import os from typing import List, Literal, Optional, Tuple from litellm._logging import verbose_logger -from litellm.types.utils import FileTypes def map_finish_reason( @@ -87,21 +86,3 @@ def _get_parent_otel_span_from_kwargs(kwargs: Optional[dict] = None): return kwargs["litellm_parent_otel_span"] except: return None - - -def get_audio_file_name(file_obj: FileTypes) -> str: - """ - Safely get the name of a file-like object or return its string representation. - - Args: - file_obj (Any): A file-like object or any other object. - - Returns: - str: The name of the file if available, otherwise a string representation of the object. - """ - if hasattr(file_obj, "name"): - return getattr(file_obj, "name") - elif hasattr(file_obj, "__str__"): - return str(file_obj) - else: - return repr(file_obj) diff --git a/litellm/llms/AzureOpenAI/audio_transcriptions.py b/litellm/llms/AzureOpenAI/audio_transcriptions.py index db373797abc..cecdfdc2110 100644 --- a/litellm/llms/AzureOpenAI/audio_transcriptions.py +++ b/litellm/llms/AzureOpenAI/audio_transcriptions.py @@ -6,7 +6,7 @@ from openai import AsyncAzureOpenAI, AzureOpenAI from pydantic import BaseModel import litellm -from litellm.litellm_core_utils.core_helpers import get_audio_file_name +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.types.utils import FileTypes from litellm.utils import TranscriptionResponse, convert_to_model_response_object diff --git a/litellm/llms/OpenAI/audio_transcriptions.py b/litellm/llms/OpenAI/audio_transcriptions.py index 587ee471eb9..cfa0b0b1a1d 100644 --- a/litellm/llms/OpenAI/audio_transcriptions.py +++ b/litellm/llms/OpenAI/audio_transcriptions.py @@ -5,7 +5,7 @@ from openai import AsyncOpenAI, OpenAI from pydantic import BaseModel import litellm -from litellm.litellm_core_utils.core_helpers import get_audio_file_name +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.types.utils import FileTypes from litellm.utils import TranscriptionResponse, convert_to_model_response_object diff --git a/litellm/llms/vertex_ai_and_google_ai_studio/text_to_speech/text_to_speech_handler.py b/litellm/llms/vertex_ai_and_google_ai_studio/text_to_speech/text_to_speech_handler.py index 99ebfae1ed9..bc2424ecc71 100644 --- a/litellm/llms/vertex_ai_and_google_ai_studio/text_to_speech/text_to_speech_handler.py +++ b/litellm/llms/vertex_ai_and_google_ai_studio/text_to_speech/text_to_speech_handler.py @@ -12,7 +12,7 @@ from litellm.llms.custom_httpx.http_handler import ( _get_async_httpx_client, _get_httpx_client, ) -from litellm.llms.openai import HttpxBinaryResponseContent +from litellm.llms.OpenAI.openai import HttpxBinaryResponseContent from litellm.llms.vertex_ai_and_google_ai_studio.gemini.vertex_and_google_ai_studio_gemini import ( VertexLLM, ) diff --git a/litellm/router_utils/client_initalization_utils.py b/litellm/router_utils/client_initalization_utils.py index 9d68891c4b9..4f750336e0d 100644 --- a/litellm/router_utils/client_initalization_utils.py +++ b/litellm/router_utils/client_initalization_utils.py @@ -8,7 +8,7 @@ import openai import litellm from litellm._logging import verbose_router_logger -from litellm.llms.azure import get_azure_ad_token_from_oidc +from litellm.llms.AzureOpenAI.azure import get_azure_ad_token_from_oidc from litellm.secret_managers.get_azure_ad_token_provider import ( get_azure_ad_token_provider, ) @@ -337,7 +337,9 @@ def set_client(litellm_router_instance: LitellmRouter, model: dict): azure_client_params["azure_ad_token_provider"] = ( azure_ad_token_provider ) - from litellm.llms.azure import select_azure_base_url_or_endpoint + from litellm.llms.AzureOpenAI.azure import ( + select_azure_base_url_or_endpoint, + ) # this decides if we should set azure_endpoint or base_url on Azure OpenAI Client # required to support GPT-4 vision enhancements, since base_url needs to be set on Azure OpenAI Client diff --git a/litellm/utils.py b/litellm/utils.py index 7587563d5a4..48bf7b2370b 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -55,12 +55,10 @@ from tokenizers import Tokenizer import litellm import litellm._service_logger # for storing API inputs, outputs, and metadata import litellm.litellm_core_utils +import litellm.litellm_core_utils.audio_utils.utils import litellm.litellm_core_utils.json_validation_rule from litellm.caching import DualCache -from litellm.litellm_core_utils.core_helpers import ( - get_audio_file_name, - map_finish_reason, -) +from litellm.litellm_core_utils.core_helpers import map_finish_reason from litellm.litellm_core_utils.exception_mapping_utils import get_error_message from litellm.litellm_core_utils.get_llm_provider_logic import ( _is_non_openai_azure_model, @@ -567,7 +565,11 @@ def function_setup( or call_type == CallTypes.transcription.value ): _file_obj: FileTypes = args[1] if len(args) > 1 else kwargs["file"] - file_checksum = get_audio_file_name(file_obj=_file_obj) + file_checksum = ( + litellm.litellm_core_utils.audio_utils.utils.get_audio_file_name( + file_obj=_file_obj + ) + ) if "metadata" in kwargs: kwargs["metadata"]["file_checksum"] = file_checksum else: From e1c989ad3d79a2022a07df02c72f130f61ea7d04 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:19:10 -0700 Subject: [PATCH 04/50] fix import error --- litellm/tests/test_assistants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/tests/test_assistants.py b/litellm/tests/test_assistants.py index c376eecc224..0806697d79f 100644 --- a/litellm/tests/test_assistants.py +++ b/litellm/tests/test_assistants.py @@ -20,15 +20,15 @@ from typing_extensions import override import litellm from litellm import create_thread, get_thread -from litellm.llms.openai import ( +from litellm.llms.OpenAI.openai import ( AssistantEventHandler, AsyncAssistantEventHandler, AsyncCursorPage, MessageData, OpenAIAssistantsAPI, ) -from litellm.llms.openai import OpenAIMessage as Message -from litellm.llms.openai import SyncCursorPage, Thread +from litellm.llms.OpenAI.openai import OpenAIMessage as Message +from litellm.llms.OpenAI.openai import SyncCursorPage, Thread """ V0 Scope: From e9cb1e085303d148079aafc7d7979052508eb150 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:27:35 -0700 Subject: [PATCH 05/50] fix import error --- litellm/tests/test_secret_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_secret_manager.py b/litellm/tests/test_secret_manager.py index a380e6287a4..397128ecb06 100644 --- a/litellm/tests/test_secret_manager.py +++ b/litellm/tests/test_secret_manager.py @@ -16,7 +16,7 @@ sys.path.insert( ) # Adds the parent directory to the system path import pytest -from litellm.llms.azure import get_azure_ad_token_from_oidc +from litellm.llms.AzureOpenAI.azure import get_azure_ad_token_from_oidc from litellm.llms.bedrock.chat import BedrockConverseLLM, BedrockLLM from litellm.secret_managers.aws_secret_manager import load_aws_secret_manager from litellm.secret_managers.main import get_secret From 219fa492dcb1c6ecb165fd15bfe03a26401aeda5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:41:13 -0700 Subject: [PATCH 06/50] fix import --- .../context_caching/vertex_ai_context_caching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai_and_google_ai_studio/context_caching/vertex_ai_context_caching.py b/litellm/llms/vertex_ai_and_google_ai_studio/context_caching/vertex_ai_context_caching.py index d087d721295..a82da7ad8e8 100644 --- a/litellm/llms/vertex_ai_and_google_ai_studio/context_caching/vertex_ai_context_caching.py +++ b/litellm/llms/vertex_ai_and_google_ai_studio/context_caching/vertex_ai_context_caching.py @@ -7,7 +7,7 @@ import litellm from litellm.caching import Cache from litellm.litellm_core_utils.litellm_logging import Logging from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler -from litellm.llms.openai import AllMessageValues +from litellm.llms.OpenAI.openai import AllMessageValues from litellm.types.llms.vertex_ai import ( CachedContentListAllResponseBody, RequestBody, From 936e486c9597a250ba7b34012e6364e3422c5b21 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:40:45 -0700 Subject: [PATCH 07/50] fix import --- litellm/tests/test_audio_speech.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm/tests/test_audio_speech.py b/litellm/tests/test_audio_speech.py index 2c710d37e7e..4e45b995383 100644 --- a/litellm/tests/test_audio_speech.py +++ b/litellm/tests/test_audio_speech.py @@ -60,7 +60,7 @@ async def test_audio_speech_litellm(sync_mode, model, api_base, api_key): optional_params={}, ) - from litellm.llms.openai import HttpxBinaryResponseContent + from litellm.llms.OpenAI.openai import HttpxBinaryResponseContent assert isinstance(response, HttpxBinaryResponseContent) else: @@ -78,7 +78,7 @@ async def test_audio_speech_litellm(sync_mode, model, api_base, api_key): optional_params={}, ) - from litellm.llms.openai import HttpxBinaryResponseContent + from litellm.llms.OpenAI.openai import HttpxBinaryResponseContent assert isinstance(response, HttpxBinaryResponseContent) @@ -115,7 +115,7 @@ async def test_audio_speech_router(mode): optional_params={}, ) - from litellm.llms.openai import HttpxBinaryResponseContent + from litellm.llms.OpenAI.openai import HttpxBinaryResponseContent assert isinstance(response, HttpxBinaryResponseContent) @@ -146,7 +146,7 @@ async def test_audio_speech_litellm_vertex(sync_mode): from types import SimpleNamespace - from litellm.llms.openai import HttpxBinaryResponseContent + from litellm.llms.OpenAI.openai import HttpxBinaryResponseContent response.stream_to_file(speech_file_path) From 542b333c95e92055fa90389b9cea62cefaa5ed6c Mon Sep 17 00:00:00 2001 From: OrangeWolf Date: Thu, 5 Sep 2024 23:21:41 +0800 Subject: [PATCH 08/50] Update utils.py (#5530) fix KeyError (cause by typo?) --- litellm/types/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 696fb5b837b..e9fe7d963b7 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -504,7 +504,7 @@ class Usage(CompletionUsage): if "prompt_cache_hit_tokens" in params and isinstance( params["prompt_cache_hit_tokens"], int ): - self._cache_read_input_tokens = params["prompt_cache_hit_tokens=0"] + self._cache_read_input_tokens = params["prompt_cache_hit_tokens"] for k, v in params.items(): setattr(self, k, v) From 8ca7c2c9afb86c79e67b148de012238067a891ff Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 5 Sep 2024 09:27:42 -0700 Subject: [PATCH 09/50] test(test_function_call_parsing.py): handle anthropic internal server error --- litellm/tests/test_function_call_parsing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/tests/test_function_call_parsing.py b/litellm/tests/test_function_call_parsing.py index fab9cf110c9..98c3af4abe6 100644 --- a/litellm/tests/test_function_call_parsing.py +++ b/litellm/tests/test_function_call_parsing.py @@ -134,11 +134,11 @@ def trade(model_name: str) -> List[Trade]: "function": {"name": tool_spec["function"]["name"]}, # type: ignore }, ) + calls = response.choices[0].message.tool_calls + trades = [trade for call in calls for trade in parse_call(call)] + return trades except litellm.InternalServerError: pass - calls = response.choices[0].message.tool_calls - trades = [trade for call in calls for trade in parse_call(call)] - return trades @pytest.mark.parametrize( From a30917311e77279a3522ba39d6c804d097ef51c6 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 14:42:56 -0700 Subject: [PATCH 10/50] fix import --- litellm/llms/AzureOpenAI/azure.py | 2 +- litellm/llms/OpenAI/openai.py | 2 +- .../vertex_ai_partner_models/main.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/AzureOpenAI/azure.py b/litellm/llms/AzureOpenAI/azure.py index a14644e1875..098086c8b98 100644 --- a/litellm/llms/AzureOpenAI/azure.py +++ b/litellm/llms/AzureOpenAI/azure.py @@ -2291,7 +2291,7 @@ class AzureAssistantsAPI(BaseLLM): """ Here's an example: ``` - from litellm.llms.openai import OpenAIAssistantsAPI, MessageData + from litellm.llms.OpenAI.openai import OpenAIAssistantsAPI, MessageData # create thread message: MessageData = {"role": "user", "content": "Hey, how's it going?"} diff --git a/litellm/llms/OpenAI/openai.py b/litellm/llms/OpenAI/openai.py index 8d112f52ae9..8021ccd59e4 100644 --- a/litellm/llms/OpenAI/openai.py +++ b/litellm/llms/OpenAI/openai.py @@ -3056,7 +3056,7 @@ class OpenAIAssistantsAPI(BaseLLM): """ Here's an example: ``` - from litellm.llms.openai import OpenAIAssistantsAPI, MessageData + from litellm.llms.OpenAI.openai import OpenAIAssistantsAPI, MessageData # create thread message: MessageData = {"role": "user", "content": "Hey, how's it going?"} diff --git a/litellm/llms/vertex_ai_and_google_ai_studio/vertex_ai_partner_models/main.py b/litellm/llms/vertex_ai_and_google_ai_studio/vertex_ai_partner_models/main.py index 60c1fa607d8..69909765e82 100644 --- a/litellm/llms/vertex_ai_and_google_ai_studio/vertex_ai_partner_models/main.py +++ b/litellm/llms/vertex_ai_and_google_ai_studio/vertex_ai_partner_models/main.py @@ -81,7 +81,7 @@ class VertexAIPartnerModels(BaseLLM): from google.cloud import aiplatform from litellm.llms.databricks import DatabricksChatCompletion - from litellm.llms.openai import OpenAIChatCompletion + from litellm.llms.OpenAI.openai import OpenAIChatCompletion from litellm.llms.text_completion_codestral import CodestralTextCompletion from litellm.llms.vertex_ai_and_google_ai_studio.gemini.vertex_and_google_ai_studio_gemini import ( VertexLLM, From c154c5e230c0a73b1cd96db596519ee5bda714bf Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 5 Sep 2024 12:40:31 -0700 Subject: [PATCH 11/50] docs(configs.md): update to clarify you can use os.environ/ for any config value --- docs/my-website/docs/proxy/configs.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/my-website/docs/proxy/configs.md b/docs/my-website/docs/proxy/configs.md index a50b3f6460d..f117bf49f78 100644 --- a/docs/my-website/docs/proxy/configs.md +++ b/docs/my-website/docs/proxy/configs.md @@ -409,12 +409,12 @@ You can view your cost once you set up [Virtual keys](https://docs.litellm.ai/do ## Load API Keys -### Load API Keys from Environment +### Load API Keys / config values from Environment -If you have secrets saved in your environment, and don't want to expose them in the config.yaml, here's how to load model-specific keys from the environment. +If you have secrets saved in your environment, and don't want to expose them in the config.yaml, here's how to load model-specific keys from the environment. **This works for ANY value on the config.yaml** -```python -os.environ["AZURE_NORTH_AMERICA_API_KEY"] = "your-azure-api-key" +```yaml +os.environ/ # runs os.getenv("YOUR-ENV-VAR") ``` ```yaml @@ -424,7 +424,7 @@ model_list: 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_NORTH_AMERICA_API_KEY + api_key: os.environ/AZURE_NORTH_AMERICA_API_KEY # 👈 KEY CHANGE ``` [**See Code**](https://github.com/BerriAI/litellm/blob/c12d6c3fe80e1b5e704d9846b246c059defadce7/litellm/utils.py#L2366) From 0426aa5642d031afb71d1e10e34f9a3fed88c88c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:26:48 -0700 Subject: [PATCH 12/50] run test again --- litellm/tests/test_streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_streaming.py b/litellm/tests/test_streaming.py index 43313b7f7a8..772ed8a645c 100644 --- a/litellm/tests/test_streaming.py +++ b/litellm/tests/test_streaming.py @@ -20,7 +20,7 @@ from litellm.utils import ModelResponseListIterator sys.path.insert( 0, os.path.abspath("../..") -) # Adds the parent directory to the system path +) # Adds the parent directory to the system-path from dotenv import load_dotenv load_dotenv() From b27ef1ca5b19066dab19071697c1579a90115010 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:39:30 -0700 Subject: [PATCH 13/50] run ci/cd on main --- litellm/tests/test_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index 538212dc352..a4a6606c48e 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -11,7 +11,7 @@ import os sys.path.insert( 0, os.path.abspath("../..") -) # Adds the parent directory to the system path +) # Adds the parent directory to the system-path import os from unittest.mock import AsyncMock, MagicMock, patch From 53794f773e2609b4a4fb6f7a86b60c5a073b7a25 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:47:18 -0700 Subject: [PATCH 14/50] fix typing error on test --- litellm/tests/test_function_call_parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_function_call_parsing.py b/litellm/tests/test_function_call_parsing.py index 98c3af4abe6..966a29c0a4f 100644 --- a/litellm/tests/test_function_call_parsing.py +++ b/litellm/tests/test_function_call_parsing.py @@ -37,7 +37,7 @@ class Trade: return Trade(order) -def trade(model_name: str) -> List[Trade]: +def trade(model_name: str) -> List[Trade]: # type: ignore def parse_order(order: dict) -> Trade: action = order["action"] From 795db8672a0b08827e5f7c49c441a9d1453d32f2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 16:26:37 -0700 Subject: [PATCH 15/50] fix log /audio to langfuse --- litellm/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/utils.py b/litellm/utils.py index 48bf7b2370b..c362a7b5a06 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -574,7 +574,7 @@ def function_setup( kwargs["metadata"]["file_checksum"] = file_checksum else: kwargs["metadata"] = {"file_checksum": file_checksum} - messages = _file_obj + messages = file_checksum elif ( call_type == CallTypes.aspeech.value or call_type == CallTypes.speech.value ): From 16a9f41682fcb362f66ff86884bec19c3f7eb287 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:16:25 -0700 Subject: [PATCH 16/50] fix allow internal user and internal viewer to view usage --- ui/litellm-dashboard/src/components/leftnav.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index c8f5745ed49..728a35076dd 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -12,6 +12,8 @@ interface SidebarProps { defaultSelectedKey: string[] | null; } +const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"]; + const Sidebar: React.FC = ({ setPage, userRole, @@ -62,7 +64,7 @@ const Sidebar: React.FC = ({ Models ) : null} - {userRole == "Admin" ? ( + {rolesAllowedToSeeUsage.includes(userRole) ? ( setPage("usage")}> Usage From 9afa39630ca14eee881f7db2b617fc14dc7dc351 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:41:43 -0700 Subject: [PATCH 17/50] add /spend/tags as allowed route for internal user --- litellm/proxy/_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 00f0cb7e300..c507df3b647 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -344,6 +344,7 @@ class LiteLLMRoutes(enum.Enum): "/key/update", "/key/delete", "/key/info", + "/global/spend/tags", ] + spend_tracking_routes + sso_only_routes From 29c86ebf02250a6bcba7f0c5fe346ff4865b87a7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 10:57:12 -0700 Subject: [PATCH 18/50] use helper functions per endpoint --- ui/litellm-dashboard/src/components/usage.tsx | 248 +++++++++--------- 1 file changed, 131 insertions(+), 117 deletions(-) diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index 23c64d4373b..89be0315a2f 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -256,125 +256,139 @@ const UsagePage: React.FC = ({ const valueFormatter = (number: number) => `$ ${new Intl.NumberFormat("us").format(number).toString()}`; + const fetchAndSetData = async ( + fetchFunction: () => Promise, + setStateFunction: React.Dispatch>, + errorMessage: string + ) => { + try { + const data = await fetchFunction(); + setStateFunction(data); + } catch (error) { + console.error(errorMessage, error); + // Optionally, update UI to reflect error state for this specific data + } + }; + + const fetchOverallSpend = () => fetchAndSetData( + () => accessToken ? adminSpendLogsCall(accessToken) : Promise.reject("No access token"), + setKeySpendData, + "Error fetching overall spend" + ); + + const fetchProviderSpend = () => fetchAndSetData( + () => accessToken && token ? adminspendByProvider(accessToken, token, startTime, endTime) : Promise.reject("No access token or token"), + setSpendByProvider, + "Error fetching provider spend" + ); + + const fetchTopKeys = async () => { + if (!accessToken) return; + await fetchAndSetData( + async () => { + const top_keys = await adminTopKeysCall(accessToken); + return top_keys.map((k: any) => ({ + key: (k["key_alias"] || k["key_name"] || k["api_key"]).substring(0, 10), + spend: k["total_spend"], + })); + }, + setTopKeys, + "Error fetching top keys" + ); + }; + + const fetchTopModels = async () => { + if (!accessToken) return; + await fetchAndSetData( + async () => { + const top_models = await adminTopModelsCall(accessToken); + return top_models.map((k: any) => ({ + key: k["model"], + spend: k["total_spend"], + })); + }, + setTopModels, + "Error fetching top models" + ); + }; + + const fetchTeamSpend = async () => { + if (!accessToken) return; + await fetchAndSetData( + async () => { + const teamSpend = await teamSpendLogsCall(accessToken); + setTeamSpendData(teamSpend.daily_spend); + setUniqueTeamIds(teamSpend.teams); + return teamSpend.total_spend_per_team.map((tspt: any) => ({ + name: tspt["team_id"] || "", + value: (tspt["total_spend"] || 0).toFixed(2), + })); + }, + setTotalSpendPerTeam, + "Error fetching team spend" + ); + }; + + const fetchTagNames = () => { + if (!accessToken) return; + fetchAndSetData( + async () => { + const all_tag_names = await allTagNamesCall(accessToken); + return all_tag_names.tag_names; + }, + setAllTagNames, + "Error fetching tag names" + ); + }; + + const fetchTopTags = () => { + if (!accessToken) return; + fetchAndSetData( + () => tagsSpendLogsCall(accessToken, dateValue.from?.toISOString(), dateValue.to?.toISOString(), undefined), + (data) => setTopTagsData(data.spend_per_tag), + "Error fetching top tags" + ); + }; + + const fetchTopEndUsers = () => { + if (!accessToken) return; + fetchAndSetData( + () => adminTopEndUsersCall(accessToken, null, undefined, undefined), + setTopUsers, + "Error fetching top end users" + ); + }; + + const fetchGlobalActivity = () => { + if (!accessToken) return; + fetchAndSetData( + () => adminGlobalActivity(accessToken, startTime, endTime), + setGlobalActivity, + "Error fetching global activity" + ); + }; + + const fetchGlobalActivityPerModel = () => { + if (!accessToken) return; + fetchAndSetData( + () => adminGlobalActivityPerModel(accessToken, startTime, endTime), + setGlobalActivityPerModel, + "Error fetching global activity per model" + ); + }; + useEffect(() => { if (accessToken && token && userRole && userID) { - const fetchData = async () => { - try { - /** - * If user is Admin - query the global views endpoints - * If user is App Owner - use the normal spend logs call - */ - console.log(`user role: ${userRole}`); - if (userRole == "Admin" || userRole == "Admin Viewer") { - const overall_spend = await adminSpendLogsCall(accessToken); - setKeySpendData(overall_spend); - - const provider_spend = await adminspendByProvider(accessToken, token, startTime, endTime); - console.log("provider_spend", provider_spend); - setSpendByProvider(provider_spend); - - - const top_keys = await adminTopKeysCall(accessToken); - const filtered_keys = top_keys.map((k: any) => ({ - key: (k["key_alias"] || k["key_name"] || k["api_key"]).substring( - 0, - 10 - ), - spend: k["total_spend"], - })); - setTopKeys(filtered_keys); - const top_models = await adminTopModelsCall(accessToken); - const filtered_models = top_models.map((k: any) => ({ - key: k["model"], - spend: k["total_spend"], - })); - setTopModels(filtered_models); - - const teamSpend = await teamSpendLogsCall(accessToken); - console.log("teamSpend", teamSpend); - setTeamSpendData(teamSpend.daily_spend); - setUniqueTeamIds(teamSpend.teams) - - let total_spend_per_team = teamSpend.total_spend_per_team; - // in total_spend_per_team, replace null team_id with "" and replace null total_spend with 0 - - total_spend_per_team = total_spend_per_team.map((tspt: any) => { - tspt["name"] = tspt["team_id"] || ""; - tspt["value"] = tspt["total_spend"] || 0; - // round the value to 2 decimal places - - tspt["value"] = tspt["value"].toFixed(2); - - - return tspt; - }) - - setTotalSpendPerTeam(total_spend_per_team); - - // all_tag_names -> used for dropdown - const all_tag_names = await allTagNamesCall(accessToken); - setAllTagNames(all_tag_names.tag_names); - - //get top tags - const top_tags = await tagsSpendLogsCall(accessToken, dateValue.from?.toISOString(), dateValue.to?.toISOString(), undefined); - setTopTagsData(top_tags.spend_per_tag); - - - // get spend per end-user - let spend_user_call = await adminTopEndUsersCall(accessToken, null, undefined, undefined); - setTopUsers(spend_user_call); - - console.log("spend/user result", spend_user_call); - - let global_activity_response = await adminGlobalActivity(accessToken, startTime, endTime); - setGlobalActivity(global_activity_response) - - let global_activity_per_model = await adminGlobalActivityPerModel(accessToken, startTime, endTime); - console.log("global activity per model", global_activity_per_model); - setGlobalActivityPerModel(global_activity_per_model) - - - } else if (userRole == "App Owner") { - await userSpendLogsCall( - accessToken, - token, - userRole, - userID, - startTime, - endTime - ).then(async (response) => { - console.log("result from spend logs call", response); - if ("daily_spend" in response) { - // this is from clickhouse analytics - // - let daily_spend = response["daily_spend"]; - console.log("daily spend", daily_spend); - setKeySpendData(daily_spend); - let topApiKeys = response.top_api_keys; - setTopKeys(topApiKeys); - } else { - const topKeysResponse = await keyInfoCall( - accessToken, - getTopKeys(response) - ); - const filtered_keys = topKeysResponse["info"].map((k: any) => ({ - key: ( - k["key_name"] || - k["key_alias"] - ).substring(0, 10), - spend: k["spend"], - })); - setTopKeys(filtered_keys); - setKeySpendData(response); - } - }); - } - } catch (error) { - console.error("There was an error fetching the data", error); - // Optionally, update your UI to reflect the error state here as well - } - }; - fetchData(); + fetchOverallSpend(); + fetchProviderSpend(); + fetchTopKeys(); + fetchTopModels(); + fetchTeamSpend(); + fetchTagNames(); + fetchTopTags(); + fetchTopEndUsers(); + fetchGlobalActivity(); + fetchGlobalActivityPerModel(); } }, [accessToken, token, userRole, userID, startTime, endTime]); From 191dfc7e1105210871999f5d4dfa775d353e5389 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:11:59 -0700 Subject: [PATCH 19/50] fix create view - MonthlyGlobalSpendPerUserPerKey --- litellm/proxy/utils.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 2423fb105a9..0d6edd7702e 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -969,11 +969,12 @@ class PrismaClient: 'Last30dKeysBySpend', 'Last30dModelsBySpend', 'MonthlyGlobalSpendPerKey', + 'MonthlyGlobalSpendPerUserPerKey', 'Last30dTopEndUsersSpend' ) """ ) - if ret[0]["sum"] == 6: + if ret[0]["sum"] == 7: print("All necessary views exist!") # noqa return except Exception: @@ -1097,6 +1098,31 @@ class PrismaClient: await self.db.execute_raw(query=sql_query) print("MonthlyGlobalSpendPerKey Created!") # noqa + try: + await self.db.query_raw( + """SELECT 1 FROM "MonthlyGlobalSpendPerUserPerKey" LIMIT 1""" + ) + print("MonthlyGlobalSpendPerUserPerKey Exists!") # noqa + except Exception as e: + sql_query = """ + CREATE OR REPLACE VIEW "MonthlyGlobalSpendPerUserPerKey" AS + SELECT + DATE("startTime") AS date, + SUM("spend") AS spend, + api_key as api_key, + "user" as "user" + FROM + "LiteLLM_SpendLogs" + WHERE + "startTime" >= (CURRENT_DATE - INTERVAL '20 days') + GROUP BY + DATE("startTime"), + "user", + api_key; + """ + await self.db.execute_raw(query=sql_query) + + print("MonthlyGlobalSpendPerUserPerKey Created!") # noqa try: await self.db.query_raw( From 0fc7d81bf464e19e8009c8ef65eee30dbac9542c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:14:03 -0700 Subject: [PATCH 20/50] show /spend/logs for internal users --- .../spend_management_endpoints.py | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 28db48d7ff8..5f9d99c1bd9 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1515,6 +1515,7 @@ async def view_spend_logs( default=None, description="Time till which to view key spend", ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ View all spend logs, if request_id is provided, only logs for that request_id will be returned @@ -1545,6 +1546,12 @@ async def view_spend_logs( """ from litellm.proxy.proxy_server import prisma_client + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + user_id = user_api_key_dict.user_id + try: verbose_proxy_logger.debug("inside view_spend_logs") if prisma_client is None: @@ -1733,6 +1740,45 @@ async def global_spend_reset(): } +async def global_spend_for_internal_user( + api_key: Optional[str] = None, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise ProxyException( + message="Prisma Client is not initialized", + type="internal_error", + param="None", + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + try: + + user_id = user_api_key_dict.user_id + if user_id is None: + raise ValueError(f"/global/spend/logs Error: User ID is None") + if api_key is not None: + sql_query = """ + SELECT * FROM "MonthlyGlobalSpendPerUserPerKey" + WHERE "api_key" = $1 AND "user" = $2 + ORDER BY "date"; + """ + + response = await prisma_client.db.query_raw(sql_query, api_key, user_id) + + return response + + sql_query = """SELECT * FROM "MonthlyGlobalSpendPerUserPerKey" WHERE "user" = $1 ORDER BY "date";""" + + response = await prisma_client.db.query_raw(sql_query, user_id) + + return response + except Exception as e: + verbose_proxy_logger.error(f"/global/spend/logs Error: {str(e)}") + raise e + + @router.get( "/global/spend/logs", tags=["Budget & Spend Tracking"], @@ -1743,7 +1789,8 @@ async def global_spend_logs( api_key: str = fastapi.Query( default=None, description="API Key to get global spend (spend per day for last 30d). Admin-only endpoint", - ) + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ [BETA] This is a beta endpoint. It will change. @@ -1764,6 +1811,17 @@ async def global_spend_logs( param="None", code=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + response = await global_spend_for_internal_user( + api_key=api_key, user_api_key_dict=user_api_key_dict + ) + + return response + if api_key is None: sql_query = """SELECT * FROM "MonthlyGlobalSpend" ORDER BY "date";""" @@ -1784,6 +1842,7 @@ async def global_spend_logs( except Exception as e: error_trace = traceback.format_exc() error_str = str(e) + "\n" + error_trace + verbose_proxy_logger.error(f"/global/spend/logs Error: {error_str}") if isinstance(e, HTTPException): raise ProxyException( message=getattr(e, "detail", f"/global/spend/logs Error({error_str})"), From b3ca358c39ce12c9f450a8f8c484a002bbe1c51e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:34:41 -0700 Subject: [PATCH 21/50] add usage endpoints for internal user --- litellm/proxy/_types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c507df3b647..082493be1f7 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -345,6 +345,12 @@ class LiteLLMRoutes(enum.Enum): "/key/delete", "/key/info", "/global/spend/tags", + "/global/spend/keys", + "/global/spend/models", + "global/spend/provider", + "/global/spend/end_users", + "/global/activity", + "/global/activity/model", ] + spend_tracking_routes + sso_only_routes From 372fcb6e4cbfc53dffd483b5c63cacab3195942f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:35:04 -0700 Subject: [PATCH 22/50] allow internal user to view their own spend --- .../spend_management_endpoints.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 5f9d99c1bd9..75dd9e280c0 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1910,6 +1910,52 @@ async def global_spend(): ) +async def global_spend_key_internal_user( + user_api_key_dict: UserAPIKeyAuth, limit: int = 10 +): + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + + user_id = user_api_key_dict.user_id + if user_id is None: + raise HTTPException(status_code=500, detail={"error": "No user_id found"}) + + sql_query = """ + WITH top_api_keys AS ( + SELECT + api_key, + SUM(spend) as total_spend + FROM + "LiteLLM_SpendLogs" + WHERE + "user" = $1 + GROUP BY + api_key + ORDER BY + total_spend DESC + LIMIT $2 -- Adjust this number to get more or fewer top keys + ) + SELECT + t.api_key, + t.total_spend, + v.key_alias, + v.key_name + FROM + top_api_keys t + LEFT JOIN + "LiteLLM_VerificationToken" v ON t.api_key = v.token + ORDER BY + t.total_spend DESC; + + """ + + response = await prisma_client.db.query_raw(sql_query, user_id, limit) + + return response + + @router.get( "/global/spend/keys", tags=["Budget & Spend Tracking"], @@ -1920,7 +1966,8 @@ async def global_spend_keys( limit: int = fastapi.Query( default=None, description="Number of keys to get. Will return Top 'n' keys.", - ) + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ [BETA] This is a beta endpoint. It will change. @@ -1929,6 +1976,15 @@ async def global_spend_keys( """ from litellm.proxy.proxy_server import prisma_client + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + response = await global_spend_key_internal_user( + user_api_key_dict=user_api_key_dict + ) + + return response if prisma_client is None: raise HTTPException(status_code=500, detail={"error": "No db connected"}) sql_query = f"""SELECT * FROM "Last30dKeysBySpend" LIMIT {limit};""" From 4e5d4c15837456de99595c4ee4d93fdeb645c1f8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:38:48 -0700 Subject: [PATCH 23/50] allow internal user to view global/spend/models --- .../spend_management_endpoints.py | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 75dd9e280c0..6abded79a3c 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -2172,6 +2172,39 @@ LIMIT 100 return response +async def global_spend_models_internal_user( + user_api_key_dict: UserAPIKeyAuth, limit: int = 10 +): + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + + user_id = user_api_key_dict.user_id + if user_id is None: + raise HTTPException(status_code=500, detail={"error": "No user_id found"}) + + sql_query = """ + SELECT + model, + SUM(spend) as total_spend, + SUM(total_tokens) as total_tokens + FROM + "LiteLLM_SpendLogs" + WHERE + "user" = $1 + GROUP BY + model + ORDER BY + total_spend DESC + LIMIT $2; + """ + + response = await prisma_client.db.query_raw(sql_query, user_id, limit) + + return response + + @router.get( "/global/spend/models", tags=["Budget & Spend Tracking"], @@ -2180,17 +2213,27 @@ LIMIT 100 ) async def global_spend_models( limit: int = fastapi.Query( - default=None, + default=10, description="Number of models to get. Will return Top 'n' models.", - ) + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ [BETA] This is a beta endpoint. It will change. - Use this to get the top 'n' keys with the highest spend, ordered by spend. + Use this to get the top 'n' models with the highest spend, ordered by spend. """ from litellm.proxy.proxy_server import prisma_client + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + response = await global_spend_models_internal_user( + user_api_key_dict=user_api_key_dict, limit=limit + ) + return response + if prisma_client is None: raise HTTPException(status_code=500, detail={"error": "No db connected"}) From 72f9c92a13f8d596d7d58d4ef97ffc34cf2eae79 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:44:44 -0700 Subject: [PATCH 24/50] add global/spend/provider --- .../spend_management_endpoints.py | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 6abded79a3c..b87e1d78fef 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -711,6 +711,7 @@ async def get_global_spend_provider( default=None, description="Time till which to view spend", ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Get breakdown of spend per provider @@ -748,19 +749,42 @@ async def get_global_spend_provider( f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" ) - sql_query = """ + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + user_id = user_api_key_dict.user_id + if user_id is None: + raise HTTPException( + status_code=400, detail={"error": "No user_id found"} + ) - SELECT - model_id, - SUM(spend) AS spend - FROM "LiteLLM_SpendLogs" - WHERE "startTime" BETWEEN $1::date AND $2::date AND length(model_id) > 0 - GROUP BY model_id - """ + sql_query = """ + SELECT + model_id, + SUM(spend) AS spend + FROM "LiteLLM_SpendLogs" + WHERE "startTime" BETWEEN $1::date AND $2::date + AND length(model_id) > 0 + AND "user" = $3 + GROUP BY model_id + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date_obj, end_date_obj, user_id + ) + else: + sql_query = """ + SELECT + model_id, + SUM(spend) AS spend + FROM "LiteLLM_SpendLogs" + WHERE "startTime" BETWEEN $1::date AND $2::date AND length(model_id) > 0 + GROUP BY model_id + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date_obj, end_date_obj + ) - db_response = await prisma_client.db.query_raw( - sql_query, start_date_obj, end_date_obj - ) if db_response is None: return [] From bbcc26a91e53bcb2460f96bfad064db7baad9b7a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:48:58 -0700 Subject: [PATCH 25/50] fix /global/spend/provider --- litellm/proxy/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 082493be1f7..67acf71e52d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -347,7 +347,7 @@ class LiteLLMRoutes(enum.Enum): "/global/spend/tags", "/global/spend/keys", "/global/spend/models", - "global/spend/provider", + "/global/spend/provider", "/global/spend/end_users", "/global/activity", "/global/activity/model", From a1ca3329d227e1ca296251a3f7d043f154305d73 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 12:53:44 -0700 Subject: [PATCH 26/50] fix allow internal user to view their own usage --- .../spend_management_endpoints.py | 129 ++++++++++++++---- 1 file changed, 104 insertions(+), 25 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index b87e1d78fef..86f95936279 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -177,6 +177,35 @@ async def view_spend_tags( ) +async def get_global_activity_internal_user( + user_api_key_dict: UserAPIKeyAuth, start_date: datetime, end_date: datetime +): + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + + user_id = user_api_key_dict.user_id + if user_id is None: + raise HTTPException(status_code=500, detail={"error": "No user_id found"}) + + sql_query = """ + SELECT + date_trunc('day', "startTime") AS date, + COUNT(*) AS api_requests, + SUM(total_tokens) AS total_tokens + FROM "LiteLLM_SpendLogs" + WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' + AND "user" = $3 + GROUP BY date_trunc('day', "startTime") + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date, end_date, user_id + ) + + return db_response + + @router.get( "/global/activity", tags=["Budget & Spend Tracking"], @@ -195,6 +224,7 @@ async def get_global_activity( default=None, description="Time till which to view spend", ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Get number of API Requests, total tokens through proxy @@ -236,18 +266,27 @@ async def get_global_activity( f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" ) - sql_query = """ - SELECT - date_trunc('day', "startTime") AS date, - COUNT(*) AS api_requests, - SUM(total_tokens) AS total_tokens - FROM "LiteLLM_SpendLogs" - WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' - GROUP BY date_trunc('day', "startTime") - """ - db_response = await prisma_client.db.query_raw( - sql_query, start_date_obj, end_date_obj - ) + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + db_response = await get_global_activity_internal_user( + user_api_key_dict, start_date_obj, end_date_obj + ) + else: + + sql_query = """ + SELECT + date_trunc('day', "startTime") AS date, + COUNT(*) AS api_requests, + SUM(total_tokens) AS total_tokens + FROM "LiteLLM_SpendLogs" + WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' + GROUP BY date_trunc('day', "startTime") + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date_obj, end_date_obj + ) if db_response is None: return [] @@ -282,6 +321,36 @@ async def get_global_activity( ) +async def get_global_activity_model_internal_user( + user_api_key_dict: UserAPIKeyAuth, start_date: datetime, end_date: datetime +): + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + + user_id = user_api_key_dict.user_id + if user_id is None: + raise HTTPException(status_code=500, detail={"error": "No user_id found"}) + + sql_query = """ + SELECT + model_group, + date_trunc('day', "startTime") AS date, + COUNT(*) AS api_requests, + SUM(total_tokens) AS total_tokens + FROM "LiteLLM_SpendLogs" + WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' + AND "user" = $3 + GROUP BY model_group, date_trunc('day', "startTime") + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date, end_date, user_id + ) + + return db_response + + @router.get( "/global/activity/model", tags=["Budget & Spend Tracking"], @@ -300,6 +369,7 @@ async def get_global_activity_model( default=None, description="Time till which to view spend", ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Get number of API Requests, total tokens through proxy - Grouped by MODEL @@ -364,19 +434,28 @@ async def get_global_activity_model( f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" ) - sql_query = """ - SELECT - model_group, - date_trunc('day', "startTime") AS date, - COUNT(*) AS api_requests, - SUM(total_tokens) AS total_tokens - FROM "LiteLLM_SpendLogs" - WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' - GROUP BY model_group, date_trunc('day', "startTime") - """ - db_response = await prisma_client.db.query_raw( - sql_query, start_date_obj, end_date_obj - ) + if ( + user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER + or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY + ): + db_response = await get_global_activity_model_internal_user( + user_api_key_dict, start_date_obj, end_date_obj + ) + else: + + sql_query = """ + SELECT + model_group, + date_trunc('day', "startTime") AS date, + COUNT(*) AS api_requests, + SUM(total_tokens) AS total_tokens + FROM "LiteLLM_SpendLogs" + WHERE "startTime" BETWEEN $1::date AND $2::date + interval '1 day' + GROUP BY model_group, date_trunc('day', "startTime") + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date_obj, end_date_obj + ) if db_response is None: return [] From 17252609389a235e50fc3ef7da0dbec7fd491c58 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 13:04:20 -0700 Subject: [PATCH 27/50] ui add a check for isAdminOrAdminViewer --- ui/litellm-dashboard/src/components/usage.tsx | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index 89be0315a2f..fbe43aa12f2 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -132,6 +132,13 @@ type DataDict = { [key: string]: unknown }; type UserData = { user_id: string; spend: number }; +const isAdminOrAdminViewer = (role: string | null): boolean => { + if (role === null) return false; + return role === 'Admin' || role === 'Admin Viewer'; +}; + + + const UsagePage: React.FC = ({ accessToken, token, @@ -379,16 +386,21 @@ const UsagePage: React.FC = ({ useEffect(() => { if (accessToken && token && userRole && userID) { + + fetchOverallSpend(); fetchProviderSpend(); fetchTopKeys(); fetchTopModels(); - fetchTeamSpend(); - fetchTagNames(); - fetchTopTags(); - fetchTopEndUsers(); fetchGlobalActivity(); fetchGlobalActivityPerModel(); + + if (isAdminOrAdminViewer(userRole)) { + fetchTeamSpend(); + fetchTagNames(); + fetchTopTags(); + fetchTopEndUsers(); + } } }, [accessToken, token, userRole, userID, startTime, endTime]); @@ -399,9 +411,17 @@ const UsagePage: React.FC = ({ All Up - Team Based Usage - Customer Usage - Tag Based Usage + + {isAdminOrAdminViewer(userRole) ? ( + <> + Team Based Usage + Customer Usage + Tag Based Usage + + ) : ( + <>
+ + )}
From b590d807affe6fd760579314a70a2ae7f56dd45e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 13:08:13 -0700 Subject: [PATCH 28/50] fix test_call_with_key_over_budget --- litellm/tests/test_key_generate_prisma.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/litellm/tests/test_key_generate_prisma.py b/litellm/tests/test_key_generate_prisma.py index 708025d1d6d..962d61afba5 100644 --- a/litellm/tests/test_key_generate_prisma.py +++ b/litellm/tests/test_key_generate_prisma.py @@ -1492,7 +1492,10 @@ def test_call_with_key_over_budget(prisma_client): proxy_logging_obj=proxy_logging_obj, ) # test spend_log was written and we can read it - spend_logs = await view_spend_logs(request_id=request_id) + spend_logs = await view_spend_logs( + request_id=request_id, + user_api_key_dict=UserAPIKeyAuth(api_key=generated_key), + ) print("read spend logs", spend_logs) assert len(spend_logs) == 1 From 3e5485ca776441b2933c7bc646436ffe9262e6a1 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 13:13:58 -0700 Subject: [PATCH 29/50] add ui testing folder --- tests/proxy_admin_ui_tests/test_usage_endpoints.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/proxy_admin_ui_tests/test_usage_endpoints.py diff --git a/tests/proxy_admin_ui_tests/test_usage_endpoints.py b/tests/proxy_admin_ui_tests/test_usage_endpoints.py new file mode 100644 index 00000000000..cb5a9f7ad7f --- /dev/null +++ b/tests/proxy_admin_ui_tests/test_usage_endpoints.py @@ -0,0 +1,14 @@ +""" +Tests the following endpoints used by the UI + +/global/spend/logs +/global/spend/keys +/global/spend/models +/global/activity +/global/activity/model + + +For all tests - test the following: +- Response is valid +- Response for Admin User is different from response from Internal User +""" From b75581452a44ed0317a3147c26df31b580b8daad Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 13:17:03 -0700 Subject: [PATCH 30/50] fix tests on viewing spend logs --- litellm/tests/test_key_generate_prisma.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/litellm/tests/test_key_generate_prisma.py b/litellm/tests/test_key_generate_prisma.py index 962d61afba5..a8d48e5e510 100644 --- a/litellm/tests/test_key_generate_prisma.py +++ b/litellm/tests/test_key_generate_prisma.py @@ -1610,7 +1610,10 @@ def test_call_with_key_over_budget_no_cache(prisma_client): proxy_logging_obj=proxy_logging_obj, ) # test spend_log was written and we can read it - spend_logs = await view_spend_logs(request_id=request_id) + spend_logs = await view_spend_logs( + request_id=request_id, + user_api_key_dict=UserAPIKeyAuth(api_key=generated_key), + ) print("read spend logs", spend_logs) assert len(spend_logs) == 1 @@ -1730,7 +1733,10 @@ def test_call_with_key_over_model_budget(prisma_client): proxy_logging_obj=proxy_logging_obj, ) # test spend_log was written and we can read it - spend_logs = await view_spend_logs(request_id=request_id) + spend_logs = await view_spend_logs( + request_id=request_id, + user_api_key_dict=UserAPIKeyAuth(api_key=generated_key), + ) print("read spend logs", spend_logs) assert len(spend_logs) == 1 @@ -2299,7 +2305,10 @@ async def test_proxy_load_test_db(prisma_client): await asyncio.sleep(120) try: # call spend logs - spend_logs = await view_spend_logs(api_key=generated_key) + spend_logs = await view_spend_logs( + api_key=generated_key, + user_api_key_dict=UserAPIKeyAuth(api_key=generated_key), + ) print(f"len responses: {len(spend_logs)}") assert len(spend_logs) == n From 1a74f50e2cce70ae58ce681e99d4e8ed79bd7579 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 13:30:51 -0700 Subject: [PATCH 31/50] add test for internal vs admin user --- .../test_usage_endpoints.py | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/tests/proxy_admin_ui_tests/test_usage_endpoints.py b/tests/proxy_admin_ui_tests/test_usage_endpoints.py index cb5a9f7ad7f..b77d74fbf61 100644 --- a/tests/proxy_admin_ui_tests/test_usage_endpoints.py +++ b/tests/proxy_admin_ui_tests/test_usage_endpoints.py @@ -12,3 +12,162 @@ For all tests - test the following: - Response is valid - Response for Admin User is different from response from Internal User """ + +import os +import sys +import traceback +import uuid +from datetime import datetime + +from dotenv import load_dotenv +from fastapi import Request +from fastapi.routing import APIRoute + +load_dotenv() +import io +import os +import time + +# this file is to test litellm/proxy + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path +import asyncio +import logging + +import pytest + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.proxy.management_endpoints.internal_user_endpoints import ( + new_user, + user_info, + user_update, +) +from litellm.proxy.management_endpoints.key_management_endpoints import ( + delete_key_fn, + generate_key_fn, + generate_key_helper_fn, + info_key_fn, + regenerate_key_fn, + update_key_fn, +) +from litellm.proxy.management_endpoints.team_endpoints import ( + new_team, + team_info, + update_team, +) +from litellm.proxy.proxy_server import ( + LitellmUserRoles, + audio_transcriptions, + chat_completion, + completion, + embeddings, + image_generation, + model_list, + moderations, + new_end_user, + user_api_key_auth, +) +from litellm.proxy.spend_tracking.spend_management_endpoints import ( + global_spend, + global_spend_logs, + spend_key_fn, + spend_user_fn, + view_spend_logs, +) +from litellm.proxy.utils import PrismaClient, ProxyLogging, hash_token, update_spend + +verbose_proxy_logger.setLevel(level=logging.DEBUG) + +from starlette.datastructures import URL + +from litellm.caching import DualCache +from litellm.proxy._types import ( + DynamoDBArgs, + GenerateKeyRequest, + KeyRequest, + LiteLLM_UpperboundKeyGenerateParams, + NewCustomerRequest, + NewTeamRequest, + NewUserRequest, + ProxyErrorTypes, + ProxyException, + UpdateKeyRequest, + UpdateTeamRequest, + UpdateUserRequest, + UserAPIKeyAuth, +) +from litellm.proxy.utils import DBClient + +proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache()) + + +@pytest.fixture +def prisma_client(): + from litellm.proxy.proxy_cli import append_query_params + + ### add connection pool + pool timeout args + params = {"connection_limit": 100, "pool_timeout": 60} + database_url = os.getenv("DATABASE_URL") + modified_url = append_query_params(database_url, params) + os.environ["DATABASE_URL"] = modified_url + + # Assuming DBClient is a class that needs to be instantiated + prisma_client = PrismaClient( + database_url=os.environ["DATABASE_URL"], proxy_logging_obj=proxy_logging_obj + ) + + # Reset litellm.proxy.proxy_server.prisma_client to None + litellm.proxy.proxy_server.custom_db_client = None + litellm.proxy.proxy_server.litellm_proxy_budget_name = ( + f"litellm-proxy-budget-{time.time()}" + ) + litellm.proxy.proxy_server.user_custom_key_generate = None + + return prisma_client + + +@pytest.mark.asyncio() +async def test_view_daily_spend_ui(prisma_client): + print("prisma client=", prisma_client) + 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() + from litellm.proxy.proxy_server import user_api_key_cache + + spend_logs_for_admin = await global_spend_logs( + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", + user_role=LitellmUserRoles.PROXY_ADMIN, + ), + api_key=None, + ) + + print("spend_logs_for_admin=", spend_logs_for_admin) + + spend_logs_for_internal_user = await global_spend_logs( + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", user_role=LitellmUserRoles.INTERNAL_USER, user_id="1234" + ), + api_key=None, + ) + + print("spend_logs_for_internal_user=", spend_logs_for_internal_user) + + # Calculate total spend for admin + admin_total_spend = sum(log.get("spend", 0) for log in spend_logs_for_admin) + + # Calculate total spend for internal user (0 in this case, but we'll keep it generic) + internal_user_total_spend = sum( + log.get("spend", 0) for log in spend_logs_for_internal_user + ) + + print("total_spend_for_admin=", admin_total_spend) + print("total_spend_for_internal_user=", internal_user_total_spend) + + assert ( + admin_total_spend > internal_user_total_spend + ), "Admin should have more spend than internal user" From 96573d93a3c34425cc10526e639ea6b0e8c7b17e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:06:53 -0700 Subject: [PATCH 32/50] add test for ui usage endpoints --- .../test_usage_endpoints.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/tests/proxy_admin_ui_tests/test_usage_endpoints.py b/tests/proxy_admin_ui_tests/test_usage_endpoints.py index b77d74fbf61..9918015a1c6 100644 --- a/tests/proxy_admin_ui_tests/test_usage_endpoints.py +++ b/tests/proxy_admin_ui_tests/test_usage_endpoints.py @@ -73,6 +73,8 @@ from litellm.proxy.proxy_server import ( from litellm.proxy.spend_tracking.spend_management_endpoints import ( global_spend, global_spend_logs, + global_spend_models, + global_spend_keys, spend_key_fn, spend_user_fn, view_spend_logs, @@ -171,3 +173,149 @@ async def test_view_daily_spend_ui(prisma_client): assert ( admin_total_spend > internal_user_total_spend ), "Admin should have more spend than internal user" + + +@pytest.mark.asyncio +async def test_global_spend_models(prisma_client): + print("prisma client=", prisma_client) + 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() + + # Test for admin user + models_spend_for_admin = await global_spend_models( + limit=10, + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", + user_role=LitellmUserRoles.PROXY_ADMIN, + ), + ) + + print("models_spend_for_admin=", models_spend_for_admin) + + # Test for internal user + models_spend_for_internal_user = await global_spend_models( + limit=10, + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", user_role=LitellmUserRoles.INTERNAL_USER, user_id="1234" + ), + ) + + print("models_spend_for_internal_user=", models_spend_for_internal_user) + + # Assertions + assert isinstance(models_spend_for_admin, list), "Admin response should be a list" + assert isinstance( + models_spend_for_internal_user, list + ), "Internal user response should be a list" + + # Check if the response has the expected shape for both admin and internal user + expected_keys = ["model", "total_spend"] + + if len(models_spend_for_admin) > 0: + assert all( + key in models_spend_for_admin[0] for key in expected_keys + ), f"Admin response should contain keys: {expected_keys}" + assert isinstance( + models_spend_for_admin[0]["model"], str + ), "Model should be a string" + assert isinstance( + models_spend_for_admin[0]["total_spend"], (int, float) + ), "Total spend should be a number" + + if len(models_spend_for_internal_user) > 0: + assert all( + key in models_spend_for_internal_user[0] for key in expected_keys + ), f"Internal user response should contain keys: {expected_keys}" + assert isinstance( + models_spend_for_internal_user[0]["model"], str + ), "Model should be a string" + assert isinstance( + models_spend_for_internal_user[0]["total_spend"], (int, float) + ), "Total spend should be a number" + + # Check if the lists are sorted by total_spend in descending order + if len(models_spend_for_admin) > 1: + assert all( + models_spend_for_admin[i]["total_spend"] + >= models_spend_for_admin[i + 1]["total_spend"] + for i in range(len(models_spend_for_admin) - 1) + ), "Admin response should be sorted by total_spend in descending order" + + if len(models_spend_for_internal_user) > 1: + assert all( + models_spend_for_internal_user[i]["total_spend"] + >= models_spend_for_internal_user[i + 1]["total_spend"] + for i in range(len(models_spend_for_internal_user) - 1) + ), "Internal user response should be sorted by total_spend in descending order" + + # Check if admin has access to more or equal models compared to internal user + assert len(models_spend_for_admin) >= len( + models_spend_for_internal_user + ), "Admin should have access to at least as many models as internal user" + + # Check if the response contains expected fields + if len(models_spend_for_admin) > 0: + assert all( + key in models_spend_for_admin[0] for key in ["model", "total_spend"] + ), "Admin response should contain model, total_spend, and total_tokens" + + if len(models_spend_for_internal_user) > 0: + assert all( + key in models_spend_for_internal_user[0] for key in ["model", "total_spend"] + ), "Internal user response should contain model, total_spend, and total_tokens" + + +@pytest.mark.asyncio +async def test_global_spend_keys(prisma_client): + print("prisma client=", prisma_client) + 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() + + # Test for admin user + keys_spend_for_admin = await global_spend_keys( + limit=10, + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", + user_role=LitellmUserRoles.PROXY_ADMIN, + ), + ) + + print("keys_spend_for_admin=", keys_spend_for_admin) + + # Test for internal user + keys_spend_for_internal_user = await global_spend_keys( + limit=10, + user_api_key_dict=UserAPIKeyAuth( + api_key="sk-1234", user_role=LitellmUserRoles.INTERNAL_USER, user_id="1234" + ), + ) + + print("keys_spend_for_internal_user=", keys_spend_for_internal_user) + + # Assertions + assert isinstance(keys_spend_for_admin, list), "Admin response should be a list" + assert isinstance( + keys_spend_for_internal_user, list + ), "Internal user response should be a list" + + # Check if admin has access to more or equal keys compared to internal user + assert len(keys_spend_for_admin) >= len( + keys_spend_for_internal_user + ), "Admin should have access to at least as many keys as internal user" + + # Check if the response contains expected fields + if len(keys_spend_for_admin) > 0: + assert all( + key in keys_spend_for_admin[0] + for key in ["api_key", "total_spend", "key_alias", "key_name"] + ), "Admin response should contain api_key, total_spend, key_alias, and key_name" + + if len(keys_spend_for_internal_user) > 0: + assert all( + key in keys_spend_for_internal_user[0] + for key in ["api_key", "total_spend", "key_alias", "key_name"] + ), "Internal user response should contain api_key, total_spend, key_alias, and key_name" From dac4908c1f939965c4ba83f9aff9dacd3f029d03 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:15:41 -0700 Subject: [PATCH 33/50] add step for ui testing --- .circleci/config.yml | 73 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5cd3c50d504..4a594bf12e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,6 +111,73 @@ jobs: # Store test results - store_test_results: path: test-results + ui_endpoint_testing: + docker: + - image: cimg/python:3.11 + working_directory: ~/project + + steps: + - checkout + - run: + name: Check if litellm dir was updated or if pyproject.toml was modified + command: | + if [ -n "$(git diff --name-only $CIRCLE_SHA1^..$CIRCLE_SHA1 | grep -E 'pyproject\.toml|litellm/')" ]; then + echo "litellm updated" + else + echo "No changes to litellm or pyproject.toml. Skipping tests." + circleci step halt + fi + - restore_cache: + keys: + - v1-dependencies-{{ checksum ".circleci/requirements.txt" }} + - run: + name: Install Dependencies + command: | + python -m pip install --upgrade pip + python -m pip install -r .circleci/requirements.txt + pip install "pytest==7.3.1" + pip install "pytest-retry==1.6.3" + pip install "pytest-asyncio==0.21.1" + pip install mypy + pip install pyarrow + pip install numpydoc + pip install openai==1.40.0 + pip install prisma==0.11.0 + pip install "httpx==0.24.1" + pip install "respx==0.21.1" + pip install fastapi + pip install "gunicorn==21.2.0" + pip install "anyio==4.2.0" + pip install "aiodynamo==23.10.1" + pip install "asyncio==3.4.3" + pip install "apscheduler==3.10.4" + pip install "pytest-mock==3.12.0" + pip install python-multipart + pip install "pydantic==2.7.1" + pip install "jsonschema==4.22.0" + - save_cache: + paths: + - ./venv + key: v1-dependencies-{{ checksum ".circleci/requirements.txt" }} + - run: + name: Run prisma ./entrypoint.sh + command: | + set +e + chmod +x entrypoint.sh + ./entrypoint.sh + set -e + # Run pytest and generate JUnit XML report + - run: + name: Run tests + command: | + pwd + ls + python -m pytest -vv tests/proxy_admin_ui_tests -x --junitxml=test-results/junit.xml --durations=5 + no_output_timeout: 120m + + # Store test results + - store_test_results: + path: test-results installing_litellm_on_python: docker: @@ -539,6 +606,12 @@ workflows: only: - main - /litellm_.*/ + - ui_endpoint_testing: + filters: + branches: + only: + - main + - /litellm_.*/ - build_and_test: filters: branches: From 34839ae7cc8130ba46ecc032744db72d82b4b955 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:17:17 -0700 Subject: [PATCH 34/50] run again --- litellm/tests/test_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index a4a6606c48e..35f9cd85b4c 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -23,7 +23,7 @@ from litellm import RateLimitError, Timeout, completion, completion_cost, embedd from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.prompt_templates.factory import anthropic_messages_pt -# litellm.num_retries=3 +# litellm.num_retries = 3 litellm.cache = None litellm.success_callback = [] user_message = "Write a short poem about the sky" From c535099fe04fde6ae2af92684553e806d77613e2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:20:21 -0700 Subject: [PATCH 35/50] run test again --- .circleci/config.yml | 3 +++ litellm/tests/test_completion.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4a594bf12e6..e88c6ad9d9e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -138,6 +138,9 @@ jobs: pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" + pip install tiktoken + pip install aiohttp + pip install click pip install mypy pip install pyarrow pip install numpydoc diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index 35f9cd85b4c..a4a6606c48e 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -23,7 +23,7 @@ from litellm import RateLimitError, Timeout, completion, completion_cost, embedd from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.prompt_templates.factory import anthropic_messages_pt -# litellm.num_retries = 3 +# litellm.num_retries=3 litellm.cache = None litellm.success_callback = [] user_message = "Write a short poem about the sky" From 14d737de14abae5d119e06902bbe6b0a54976de3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:24:00 -0700 Subject: [PATCH 36/50] move folder key gen prisma is in --- .circleci/config.yml | 2 +- .../proxy_admin_ui_tests}/test_key_generate_prisma.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {litellm/tests => tests/proxy_admin_ui_tests}/test_key_generate_prisma.py (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index e88c6ad9d9e..87000a35510 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -321,7 +321,7 @@ jobs: command: | pwd ls - python -m pytest -s -vv tests/ -x --junitxml=test-results/junit.xml --durations=5 --ignore=tests/otel_tests --ignore=tests/pass_through_tests + python -m pytest -s -vv tests/ -x --junitxml=test-results/junit.xml --durations=5 --ignore=tests/otel_tests --ignore=tests/pass_through_tests --ignore=tests/proxy_admin_ui_tests no_output_timeout: 120m # Store test results diff --git a/litellm/tests/test_key_generate_prisma.py b/tests/proxy_admin_ui_tests/test_key_generate_prisma.py similarity index 100% rename from litellm/tests/test_key_generate_prisma.py rename to tests/proxy_admin_ui_tests/test_key_generate_prisma.py From 2ca91c21563cafc088d2abb95ed8251e2d363d1f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:26:12 -0700 Subject: [PATCH 37/50] run ci/cd agaiin --- .circleci/config.yml | 1 + litellm/tests/test_completion.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87000a35510..830ad9c4377 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -158,6 +158,7 @@ jobs: pip install python-multipart pip install "pydantic==2.7.1" pip install "jsonschema==4.22.0" + pip install "backoff==2.2.1" - save_cache: paths: - ./venv diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index a4a6606c48e..35f9cd85b4c 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -23,7 +23,7 @@ from litellm import RateLimitError, Timeout, completion, completion_cost, embedd from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.prompt_templates.factory import anthropic_messages_pt -# litellm.num_retries=3 +# litellm.num_retries = 3 litellm.cache = None litellm.success_callback = [] user_message = "Write a short poem about the sky" From 30eb74013740ecc987a6e7f588cb29508fd71fa6 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:34:17 -0700 Subject: [PATCH 38/50] use requirements txt --- .circleci/config.yml | 1 + litellm/tests/test_completion.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 830ad9c4377..e47e5f3c80b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -135,6 +135,7 @@ jobs: command: | python -m pip install --upgrade pip python -m pip install -r .circleci/requirements.txt + python -m pip install -r ../requirements.txt pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index 35f9cd85b4c..d6b4c9b37de 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -23,7 +23,7 @@ from litellm import RateLimitError, Timeout, completion, completion_cost, embedd from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.prompt_templates.factory import anthropic_messages_pt -# litellm.num_retries = 3 +# litellm.num_retries= 3 litellm.cache = None litellm.success_callback = [] user_message = "Write a short poem about the sky" From 6c0d578a20834f1840eef91a7c354246863c141f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:38:49 -0700 Subject: [PATCH 39/50] run ci/cd again --- .circleci/config.yml | 2 +- litellm/tests/test_completion.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e47e5f3c80b..785a45b883e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -135,7 +135,7 @@ jobs: command: | python -m pip install --upgrade pip python -m pip install -r .circleci/requirements.txt - python -m pip install -r ../requirements.txt + python -m pip install -r requirements.txt pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index d6b4c9b37de..35f9cd85b4c 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -23,7 +23,7 @@ from litellm import RateLimitError, Timeout, completion, completion_cost, embedd from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.prompt_templates.factory import anthropic_messages_pt -# litellm.num_retries= 3 +# litellm.num_retries = 3 litellm.cache = None litellm.success_callback = [] user_message = "Write a short poem about the sky" From 49ebf4d0dee054898df27d0c0985f20a26498007 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:42:40 -0700 Subject: [PATCH 40/50] run ci - cd again --- .circleci/config.yml | 22 ---------------------- litellm/tests/test_completion.py | 3 ++- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 785a45b883e..5df62535b71 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -134,32 +134,10 @@ jobs: name: Install Dependencies command: | python -m pip install --upgrade pip - python -m pip install -r .circleci/requirements.txt python -m pip install -r requirements.txt pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" - pip install tiktoken - pip install aiohttp - pip install click - pip install mypy - pip install pyarrow - pip install numpydoc - pip install openai==1.40.0 - pip install prisma==0.11.0 - pip install "httpx==0.24.1" - pip install "respx==0.21.1" - pip install fastapi - pip install "gunicorn==21.2.0" - pip install "anyio==4.2.0" - pip install "aiodynamo==23.10.1" - pip install "asyncio==3.4.3" - pip install "apscheduler==3.10.4" - pip install "pytest-mock==3.12.0" - pip install python-multipart - pip install "pydantic==2.7.1" - pip install "jsonschema==4.22.0" - pip install "backoff==2.2.1" - save_cache: paths: - ./venv diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index 35f9cd85b4c..fedb69f4e2a 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -11,7 +11,8 @@ import os sys.path.insert( 0, os.path.abspath("../..") -) # Adds the parent directory to the system-path +) +# Adds the parent directory to the system-path import os from unittest.mock import AsyncMock, MagicMock, patch From 45953b9924ecdc69585e4a23a3d9a32442d70630 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:46:13 -0700 Subject: [PATCH 41/50] add error message on test --- .circleci/config.yml | 12 ------------ .../proxy_admin_ui_tests/test_key_generate_prisma.py | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5df62535b71..dccaa2b1122 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -118,18 +118,6 @@ jobs: steps: - checkout - - run: - name: Check if litellm dir was updated or if pyproject.toml was modified - command: | - if [ -n "$(git diff --name-only $CIRCLE_SHA1^..$CIRCLE_SHA1 | grep -E 'pyproject\.toml|litellm/')" ]; then - echo "litellm updated" - else - echo "No changes to litellm or pyproject.toml. Skipping tests." - circleci step halt - fi - - restore_cache: - keys: - - v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - run: name: Install Dependencies command: | diff --git a/tests/proxy_admin_ui_tests/test_key_generate_prisma.py b/tests/proxy_admin_ui_tests/test_key_generate_prisma.py index a8d48e5e510..adf0e8aea96 100644 --- a/tests/proxy_admin_ui_tests/test_key_generate_prisma.py +++ b/tests/proxy_admin_ui_tests/test_key_generate_prisma.py @@ -537,6 +537,7 @@ def test_call_with_user_over_budget(prisma_client): asyncio.run(test()) except Exception as e: + print("got an errror=", e) error_detail = e.message assert "ExceededBudget:" in error_detail assert isinstance(e, ProxyException) From 96cc51e7130e196a9d0c516667e3e710a755843a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:50:39 -0700 Subject: [PATCH 42/50] move prisma test to correct location --- .../tests}/test_key_generate_prisma.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests/proxy_admin_ui_tests => litellm/tests}/test_key_generate_prisma.py (100%) diff --git a/tests/proxy_admin_ui_tests/test_key_generate_prisma.py b/litellm/tests/test_key_generate_prisma.py similarity index 100% rename from tests/proxy_admin_ui_tests/test_key_generate_prisma.py rename to litellm/tests/test_key_generate_prisma.py From eca20e32933587f870d5403c504713bae86cc1f7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:39:30 -0700 Subject: [PATCH 43/50] run ci/cd on main --- litellm/tests/test_completion.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/litellm/tests/test_completion.py b/litellm/tests/test_completion.py index fedb69f4e2a..35f9cd85b4c 100644 --- a/litellm/tests/test_completion.py +++ b/litellm/tests/test_completion.py @@ -11,8 +11,7 @@ import os sys.path.insert( 0, os.path.abspath("../..") -) -# Adds the parent directory to the system-path +) # Adds the parent directory to the system-path import os from unittest.mock import AsyncMock, MagicMock, patch From 2c1f89eb30c975a59c6e45fe070d14ed2a213291 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:31:41 -0700 Subject: [PATCH 44/50] fix on /user/info show all keys - even expired ones --- litellm/proxy/management_endpoints/internal_user_endpoints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 2a359f92e01..87741362728 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -392,7 +392,6 @@ async def user_info( user_id=user_id, table_name="key", query_type="find_all", - expires=datetime.now(), ) if user_info is None: From 538cc6279c590e08515250ada7f6d88050943e1f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 15:57:52 -0700 Subject: [PATCH 45/50] ui show when key expires --- ui/litellm-dashboard/src/components/view_key_table.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx index d54fb1386ad..70e8c520459 100644 --- a/ui/litellm-dashboard/src/components/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/view_key_table.tsx @@ -722,6 +722,7 @@ const ViewKeyTable: React.FC = ({ Key Alias Secret Key + Expires Spend (USD) Budget (USD) Budget Reset @@ -762,6 +763,15 @@ const ViewKeyTable: React.FC = ({ {item.key_name} + + {item.expires != null ? ( +
+

{new Date(item.expires).toLocaleString()}

+
+ ) : ( +

Never

+ )} +
{(() => { From d401ce3488f7c8a7325146758174bf841c8921be Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 5 Sep 2024 17:05:39 -0700 Subject: [PATCH 46/50] ui new build --- litellm/proxy/_experimental/out/404.html | 1 + .../_buildManifest.js | 0 .../_ssgManifest.js | 0 .../out/_next/static/chunks/131-73d0a4f8e09896fe.js | 8 -------- .../out/_next/static/chunks/131-cb6bfe24e23e121b.js | 8 ++++++++ .../{505-5a85dd1c70cda98a.js => 505-5ff3c318fddfa35c.js} | 0 .../{605-35a95945041f7699.js => 605-8e4b96f972af8eaf.js} | 4 ++-- .../out/_next/static/chunks/777-5360b5460eba0779.js | 1 - .../out/_next/static/chunks/777-9a618afdad1cc2b1.js | 1 + ...{page-baad96761e038837.js => page-2772a2e192058ffc.js} | 0 ...{page-0034957a9fa387e0.js => page-83315f8855cb18f2.js} | 0 .../out/_next/static/chunks/app/page-6529f5693b4e825e.js | 1 - .../out/_next/static/chunks/app/page-721b5a38003185e2.js | 1 + litellm/proxy/_experimental/out/index.html | 2 +- litellm/proxy/_experimental/out/index.txt | 4 ++-- litellm/proxy/_experimental/out/model_hub.html | 1 + litellm/proxy/_experimental/out/model_hub.txt | 4 ++-- litellm/proxy/_experimental/out/onboarding.html | 1 + litellm/proxy/_experimental/out/onboarding.txt | 4 ++-- ui/litellm-dashboard/out/404.html | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 .../out/_next/static/chunks/131-73d0a4f8e09896fe.js | 8 -------- .../out/_next/static/chunks/131-cb6bfe24e23e121b.js | 8 ++++++++ .../{505-5a85dd1c70cda98a.js => 505-5ff3c318fddfa35c.js} | 0 .../{605-35a95945041f7699.js => 605-8e4b96f972af8eaf.js} | 4 ++-- .../out/_next/static/chunks/777-5360b5460eba0779.js | 1 - .../out/_next/static/chunks/777-9a618afdad1cc2b1.js | 1 + ...{page-baad96761e038837.js => page-2772a2e192058ffc.js} | 0 ...{page-0034957a9fa387e0.js => page-83315f8855cb18f2.js} | 0 .../out/_next/static/chunks/app/page-6529f5693b4e825e.js | 1 - .../out/_next/static/chunks/app/page-721b5a38003185e2.js | 1 + 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 ++-- 38 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 litellm/proxy/_experimental/out/404.html rename litellm/proxy/_experimental/out/_next/static/{auooEytIka5iEx4r3srzM => EuyyVHyF3EBRFOgf1sJe_}/_buildManifest.js (100%) rename litellm/proxy/_experimental/out/_next/static/{auooEytIka5iEx4r3srzM => EuyyVHyF3EBRFOgf1sJe_}/_ssgManifest.js (100%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/131-73d0a4f8e09896fe.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/131-cb6bfe24e23e121b.js rename litellm/proxy/_experimental/out/_next/static/chunks/{505-5a85dd1c70cda98a.js => 505-5ff3c318fddfa35c.js} (100%) rename litellm/proxy/_experimental/out/_next/static/chunks/{605-35a95945041f7699.js => 605-8e4b96f972af8eaf.js} (53%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/777-5360b5460eba0779.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/777-9a618afdad1cc2b1.js rename litellm/proxy/_experimental/out/_next/static/chunks/app/model_hub/{page-baad96761e038837.js => page-2772a2e192058ffc.js} (100%) rename litellm/proxy/_experimental/out/_next/static/chunks/app/onboarding/{page-0034957a9fa387e0.js => page-83315f8855cb18f2.js} (100%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/page-6529f5693b4e825e.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/app/page-721b5a38003185e2.js create mode 100644 litellm/proxy/_experimental/out/model_hub.html create mode 100644 litellm/proxy/_experimental/out/onboarding.html rename ui/litellm-dashboard/out/_next/static/{auooEytIka5iEx4r3srzM => EuyyVHyF3EBRFOgf1sJe_}/_buildManifest.js (100%) rename ui/litellm-dashboard/out/_next/static/{auooEytIka5iEx4r3srzM => EuyyVHyF3EBRFOgf1sJe_}/_ssgManifest.js (100%) delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/131-73d0a4f8e09896fe.js create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/131-cb6bfe24e23e121b.js rename ui/litellm-dashboard/out/_next/static/chunks/{505-5a85dd1c70cda98a.js => 505-5ff3c318fddfa35c.js} (100%) rename ui/litellm-dashboard/out/_next/static/chunks/{605-35a95945041f7699.js => 605-8e4b96f972af8eaf.js} (53%) delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/777-5360b5460eba0779.js create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/777-9a618afdad1cc2b1.js rename ui/litellm-dashboard/out/_next/static/chunks/app/model_hub/{page-baad96761e038837.js => page-2772a2e192058ffc.js} (100%) rename ui/litellm-dashboard/out/_next/static/chunks/app/onboarding/{page-0034957a9fa387e0.js => page-83315f8855cb18f2.js} (100%) delete mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/page-6529f5693b4e825e.js create mode 100644 ui/litellm-dashboard/out/_next/static/chunks/app/page-721b5a38003185e2.js diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404.html new file mode 100644 index 00000000000..5a2b8c2cd78 --- /dev/null +++ b/litellm/proxy/_experimental/out/404.html @@ -0,0 +1 @@ +404: This page could not be found.LiteLLM Dashboard

404

This page could not be found.

\ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/auooEytIka5iEx4r3srzM/_buildManifest.js b/litellm/proxy/_experimental/out/_next/static/EuyyVHyF3EBRFOgf1sJe_/_buildManifest.js similarity index 100% rename from litellm/proxy/_experimental/out/_next/static/auooEytIka5iEx4r3srzM/_buildManifest.js rename to litellm/proxy/_experimental/out/_next/static/EuyyVHyF3EBRFOgf1sJe_/_buildManifest.js diff --git a/litellm/proxy/_experimental/out/_next/static/auooEytIka5iEx4r3srzM/_ssgManifest.js b/litellm/proxy/_experimental/out/_next/static/EuyyVHyF3EBRFOgf1sJe_/_ssgManifest.js similarity index 100% rename from litellm/proxy/_experimental/out/_next/static/auooEytIka5iEx4r3srzM/_ssgManifest.js rename to litellm/proxy/_experimental/out/_next/static/EuyyVHyF3EBRFOgf1sJe_/_ssgManifest.js diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/131-73d0a4f8e09896fe.js b/litellm/proxy/_experimental/out/_next/static/chunks/131-73d0a4f8e09896fe.js deleted file mode 100644 index f012416c927..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/131-73d0a4f8e09896fe.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[131],{84174:function(e,t,n){n.d(t,{Z:function(){return s}});var a=n(14749),r=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"}}]},name:"copy",theme:"outlined"},o=n(60688),s=r.forwardRef(function(e,t){return r.createElement(o.Z,(0,a.Z)({},e,{ref:t,icon:i}))})},50459:function(e,t,n){n.d(t,{Z:function(){return s}});var a=n(14749),r=n(2265),i={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"}}]},name:"right",theme:"outlined"},o=n(60688),s=r.forwardRef(function(e,t){return r.createElement(o.Z,(0,a.Z)({},e,{ref:t,icon:i}))})},92836:function(e,t,n){n.d(t,{Z:function(){return p}});var a=n(69703),r=n(80991),i=n(2898),o=n(99250),s=n(65492),l=n(2265),c=n(41608),d=n(50027);n(18174),n(21871),n(41213);let u=(0,s.fn)("Tab"),p=l.forwardRef((e,t)=>{let{icon:n,className:p,children:g}=e,m=(0,a._T)(e,["icon","className","children"]),b=(0,l.useContext)(c.O),f=(0,l.useContext)(d.Z);return l.createElement(r.O,Object.assign({ref:t,className:(0,o.q)(u("root"),"flex whitespace-nowrap truncate max-w-xs outline-none focus:ring-0 text-tremor-default transition duration-100",f?(0,s.bM)(f,i.K.text).selectTextColor:"solid"===b?"ui-selected:text-tremor-content-emphasis dark:ui-selected:text-dark-tremor-content-emphasis":"ui-selected:text-tremor-brand dark:ui-selected:text-dark-tremor-brand",function(e,t){switch(e){case"line":return(0,o.q)("ui-selected:border-b-2 hover:border-b-2 border-transparent transition duration-100 -mb-px px-2 py-2","hover:border-tremor-content hover:text-tremor-content-emphasis text-tremor-content","dark:hover:border-dark-tremor-content-emphasis dark:hover:text-dark-tremor-content-emphasis dark:text-dark-tremor-content",t?(0,s.bM)(t,i.K.border).selectBorderColor:"ui-selected:border-tremor-brand dark:ui-selected:border-dark-tremor-brand");case"solid":return(0,o.q)("border-transparent border rounded-tremor-small px-2.5 py-1","ui-selected:border-tremor-border ui-selected:bg-tremor-background ui-selected:shadow-tremor-input hover:text-tremor-content-emphasis ui-selected:text-tremor-brand","dark:ui-selected:border-dark-tremor-border dark:ui-selected:bg-dark-tremor-background dark:ui-selected:shadow-dark-tremor-input dark:hover:text-dark-tremor-content-emphasis dark:ui-selected:text-dark-tremor-brand",t?(0,s.bM)(t,i.K.text).selectTextColor:"text-tremor-content dark:text-dark-tremor-content")}}(b,f),p)},m),n?l.createElement(n,{className:(0,o.q)(u("icon"),"flex-none h-5 w-5",g?"mr-2":"")}):null,g?l.createElement("span",null,g):null)});p.displayName="Tab"},26734:function(e,t,n){n.d(t,{Z:function(){return c}});var a=n(69703),r=n(80991),i=n(99250),o=n(65492),s=n(2265);let l=(0,o.fn)("TabGroup"),c=s.forwardRef((e,t)=>{let{defaultIndex:n,index:o,onIndexChange:c,children:d,className:u}=e,p=(0,a._T)(e,["defaultIndex","index","onIndexChange","children","className"]);return s.createElement(r.O.Group,Object.assign({as:"div",ref:t,defaultIndex:n,selectedIndex:o,onChange:c,className:(0,i.q)(l("root"),"w-full",u)},p),d)});c.displayName="TabGroup"},41608:function(e,t,n){n.d(t,{O:function(){return c},Z:function(){return u}});var a=n(69703),r=n(2265),i=n(50027);n(18174),n(21871),n(41213);var o=n(80991),s=n(99250);let l=(0,n(65492).fn)("TabList"),c=(0,r.createContext)("line"),d={line:(0,s.q)("flex border-b space-x-4","border-tremor-border","dark:border-dark-tremor-border"),solid:(0,s.q)("inline-flex p-0.5 rounded-tremor-default space-x-1.5","bg-tremor-background-subtle","dark:bg-dark-tremor-background-subtle")},u=r.forwardRef((e,t)=>{let{color:n,variant:u="line",children:p,className:g}=e,m=(0,a._T)(e,["color","variant","children","className"]);return r.createElement(o.O.List,Object.assign({ref:t,className:(0,s.q)(l("root"),"justify-start overflow-x-clip",d[u],g)},m),r.createElement(c.Provider,{value:u},r.createElement(i.Z.Provider,{value:n},p)))});u.displayName="TabList"},32126:function(e,t,n){n.d(t,{Z:function(){return d}});var a=n(69703);n(50027);var r=n(18174);n(21871);var i=n(41213),o=n(99250),s=n(65492),l=n(2265);let c=(0,s.fn)("TabPanel"),d=l.forwardRef((e,t)=>{let{children:n,className:s}=e,d=(0,a._T)(e,["children","className"]),{selectedValue:u}=(0,l.useContext)(i.Z),p=u===(0,l.useContext)(r.Z);return l.createElement("div",Object.assign({ref:t,className:(0,o.q)(c("root"),"w-full mt-2",p?"":"hidden",s),"aria-selected":p?"true":"false"},d),n)});d.displayName="TabPanel"},23682:function(e,t,n){n.d(t,{Z:function(){return u}});var a=n(69703),r=n(80991);n(50027);var i=n(18174);n(21871);var o=n(41213),s=n(99250),l=n(65492),c=n(2265);let d=(0,l.fn)("TabPanels"),u=c.forwardRef((e,t)=>{let{children:n,className:l}=e,u=(0,a._T)(e,["children","className"]);return c.createElement(r.O.Panels,Object.assign({as:"div",ref:t,className:(0,s.q)(d("root"),"w-full",l)},u),e=>{let{selectedIndex:t}=e;return c.createElement(o.Z.Provider,{value:{selectedValue:t}},c.Children.map(n,(e,t)=>c.createElement(i.Z.Provider,{value:t},e)))})});u.displayName="TabPanels"},50027:function(e,t,n){n.d(t,{Z:function(){return i}});var a=n(2265),r=n(54942);n(99250);let i=(0,a.createContext)(r.fr.Blue)},18174:function(e,t,n){n.d(t,{Z:function(){return a}});let a=(0,n(2265).createContext)(0)},21871:function(e,t,n){n.d(t,{Z:function(){return a}});let a=(0,n(2265).createContext)(void 0)},41213:function(e,t,n){n.d(t,{Z:function(){return a}});let a=(0,n(2265).createContext)({selectedValue:void 0,handleValueChange:void 0})},21467:function(e,t,n){n.d(t,{i:function(){return s}});var a=n(2265),r=n(44329),i=n(54165),o=n(57499);function s(e){return t=>a.createElement(i.ZP,{theme:{token:{motion:!1,zIndexPopupBase:0}}},a.createElement(e,Object.assign({},t)))}t.Z=(e,t,n,i)=>s(s=>{let{prefixCls:l,style:c}=s,d=a.useRef(null),[u,p]=a.useState(0),[g,m]=a.useState(0),[b,f]=(0,r.Z)(!1,{value:s.open}),{getPrefixCls:E}=a.useContext(o.E_),h=E(t||"select",l);a.useEffect(()=>{if(f(!0),"undefined"!=typeof ResizeObserver){let e=new ResizeObserver(e=>{let t=e[0].target;p(t.offsetHeight+8),m(t.offsetWidth)}),t=setInterval(()=>{var a;let r=n?".".concat(n(h)):".".concat(h,"-dropdown"),i=null===(a=d.current)||void 0===a?void 0:a.querySelector(r);i&&(clearInterval(t),e.observe(i))},10);return()=>{clearInterval(t),e.disconnect()}}},[]);let S=Object.assign(Object.assign({},s),{style:Object.assign(Object.assign({},c),{margin:0}),open:b,visible:b,getPopupContainer:()=>d.current});return i&&(S=i(S)),a.createElement("div",{ref:d,style:{paddingBottom:u,position:"relative",minWidth:g}},a.createElement(e,Object.assign({},S)))})},99129:function(e,t,n){let a;n.d(t,{Z:function(){return eY}});var r=n(63787),i=n(2265),o=n(37274),s=n(57499),l=n(54165),c=n(99537),d=n(77136),u=n(20653),p=n(40388),g=n(16480),m=n.n(g),b=n(51761),f=n(47387),E=n(70595),h=n(24750),S=n(89211),y=n(1861),T=n(51350),A=e=>{let{type:t,children:n,prefixCls:a,buttonProps:r,close:o,autoFocus:s,emitEvent:l,isSilent:c,quitOnNullishReturnValue:d,actionFn:u}=e,p=i.useRef(!1),g=i.useRef(null),[m,b]=(0,S.Z)(!1),f=function(){null==o||o.apply(void 0,arguments)};i.useEffect(()=>{let e=null;return s&&(e=setTimeout(()=>{var e;null===(e=g.current)||void 0===e||e.focus()})),()=>{e&&clearTimeout(e)}},[]);let E=e=>{e&&e.then&&(b(!0),e.then(function(){b(!1,!0),f.apply(void 0,arguments),p.current=!1},e=>{if(b(!1,!0),p.current=!1,null==c||!c())return Promise.reject(e)}))};return i.createElement(y.ZP,Object.assign({},(0,T.nx)(t),{onClick:e=>{let t;if(!p.current){if(p.current=!0,!u){f();return}if(l){var n;if(t=u(e),d&&!((n=t)&&n.then)){p.current=!1,f(e);return}}else if(u.length)t=u(o),p.current=!1;else if(!(t=u())){f();return}E(t)}},loading:m,prefixCls:a},r,{ref:g}),n)};let R=i.createContext({}),{Provider:I}=R;var N=()=>{let{autoFocusButton:e,cancelButtonProps:t,cancelTextLocale:n,isSilent:a,mergedOkCancel:r,rootPrefixCls:o,close:s,onCancel:l,onConfirm:c}=(0,i.useContext)(R);return r?i.createElement(A,{isSilent:a,actionFn:l,close:function(){null==s||s.apply(void 0,arguments),null==c||c(!1)},autoFocus:"cancel"===e,buttonProps:t,prefixCls:"".concat(o,"-btn")},n):null},_=()=>{let{autoFocusButton:e,close:t,isSilent:n,okButtonProps:a,rootPrefixCls:r,okTextLocale:o,okType:s,onConfirm:l,onOk:c}=(0,i.useContext)(R);return i.createElement(A,{isSilent:n,type:s||"primary",actionFn:c,close:function(){null==t||t.apply(void 0,arguments),null==l||l(!0)},autoFocus:"ok"===e,buttonProps:a,prefixCls:"".concat(r,"-btn")},o)},v=n(81303),w=n(14749),k=n(80406),C=n(88804),O=i.createContext({}),x=n(5239),L=n(31506),D=n(91010),P=n(4295),M=n(72480);function F(e,t,n){var a=t;return!a&&n&&(a="".concat(e,"-").concat(n)),a}function U(e,t){var n=e["page".concat(t?"Y":"X","Offset")],a="scroll".concat(t?"Top":"Left");if("number"!=typeof n){var r=e.document;"number"!=typeof(n=r.documentElement[a])&&(n=r.body[a])}return n}var B=n(49367),G=n(74084),$=i.memo(function(e){return e.children},function(e,t){return!t.shouldUpdate}),H={width:0,height:0,overflow:"hidden",outline:"none"},z=i.forwardRef(function(e,t){var n,a,r,o=e.prefixCls,s=e.className,l=e.style,c=e.title,d=e.ariaId,u=e.footer,p=e.closable,g=e.closeIcon,b=e.onClose,f=e.children,E=e.bodyStyle,h=e.bodyProps,S=e.modalRender,y=e.onMouseDown,T=e.onMouseUp,A=e.holderRef,R=e.visible,I=e.forceRender,N=e.width,_=e.height,v=e.classNames,k=e.styles,C=i.useContext(O).panel,L=(0,G.x1)(A,C),D=(0,i.useRef)(),P=(0,i.useRef)();i.useImperativeHandle(t,function(){return{focus:function(){var e;null===(e=D.current)||void 0===e||e.focus()},changeActive:function(e){var t=document.activeElement;e&&t===P.current?D.current.focus():e||t!==D.current||P.current.focus()}}});var M={};void 0!==N&&(M.width=N),void 0!==_&&(M.height=_),u&&(n=i.createElement("div",{className:m()("".concat(o,"-footer"),null==v?void 0:v.footer),style:(0,x.Z)({},null==k?void 0:k.footer)},u)),c&&(a=i.createElement("div",{className:m()("".concat(o,"-header"),null==v?void 0:v.header),style:(0,x.Z)({},null==k?void 0:k.header)},i.createElement("div",{className:"".concat(o,"-title"),id:d},c))),p&&(r=i.createElement("button",{type:"button",onClick:b,"aria-label":"Close",className:"".concat(o,"-close")},g||i.createElement("span",{className:"".concat(o,"-close-x")})));var F=i.createElement("div",{className:m()("".concat(o,"-content"),null==v?void 0:v.content),style:null==k?void 0:k.content},r,a,i.createElement("div",(0,w.Z)({className:m()("".concat(o,"-body"),null==v?void 0:v.body),style:(0,x.Z)((0,x.Z)({},E),null==k?void 0:k.body)},h),f),n);return i.createElement("div",{key:"dialog-element",role:"dialog","aria-labelledby":c?d:null,"aria-modal":"true",ref:L,style:(0,x.Z)((0,x.Z)({},l),M),className:m()(o,s),onMouseDown:y,onMouseUp:T},i.createElement("div",{tabIndex:0,ref:D,style:H,"aria-hidden":"true"}),i.createElement($,{shouldUpdate:R||I},S?S(F):F),i.createElement("div",{tabIndex:0,ref:P,style:H,"aria-hidden":"true"}))}),j=i.forwardRef(function(e,t){var n=e.prefixCls,a=e.title,r=e.style,o=e.className,s=e.visible,l=e.forceRender,c=e.destroyOnClose,d=e.motionName,u=e.ariaId,p=e.onVisibleChanged,g=e.mousePosition,b=(0,i.useRef)(),f=i.useState(),E=(0,k.Z)(f,2),h=E[0],S=E[1],y={};function T(){var e,t,n,a,r,i=(n={left:(t=(e=b.current).getBoundingClientRect()).left,top:t.top},r=(a=e.ownerDocument).defaultView||a.parentWindow,n.left+=U(r),n.top+=U(r,!0),n);S(g?"".concat(g.x-i.left,"px ").concat(g.y-i.top,"px"):"")}return h&&(y.transformOrigin=h),i.createElement(B.ZP,{visible:s,onVisibleChanged:p,onAppearPrepare:T,onEnterPrepare:T,forceRender:l,motionName:d,removeOnLeave:c,ref:b},function(s,l){var c=s.className,d=s.style;return i.createElement(z,(0,w.Z)({},e,{ref:t,title:a,ariaId:u,prefixCls:n,holderRef:l,style:(0,x.Z)((0,x.Z)((0,x.Z)({},d),r),y),className:m()(o,c)}))})});function V(e){var t=e.prefixCls,n=e.style,a=e.visible,r=e.maskProps,o=e.motionName,s=e.className;return i.createElement(B.ZP,{key:"mask",visible:a,motionName:o,leavedClassName:"".concat(t,"-mask-hidden")},function(e,a){var o=e.className,l=e.style;return i.createElement("div",(0,w.Z)({ref:a,style:(0,x.Z)((0,x.Z)({},l),n),className:m()("".concat(t,"-mask"),o,s)},r))})}function W(e){var t=e.prefixCls,n=void 0===t?"rc-dialog":t,a=e.zIndex,r=e.visible,o=void 0!==r&&r,s=e.keyboard,l=void 0===s||s,c=e.focusTriggerAfterClose,d=void 0===c||c,u=e.wrapStyle,p=e.wrapClassName,g=e.wrapProps,b=e.onClose,f=e.afterOpenChange,E=e.afterClose,h=e.transitionName,S=e.animation,y=e.closable,T=e.mask,A=void 0===T||T,R=e.maskTransitionName,I=e.maskAnimation,N=e.maskClosable,_=e.maskStyle,v=e.maskProps,C=e.rootClassName,O=e.classNames,U=e.styles,B=(0,i.useRef)(),G=(0,i.useRef)(),$=(0,i.useRef)(),H=i.useState(o),z=(0,k.Z)(H,2),W=z[0],q=z[1],Y=(0,D.Z)();function K(e){null==b||b(e)}var Z=(0,i.useRef)(!1),X=(0,i.useRef)(),Q=null;return(void 0===N||N)&&(Q=function(e){Z.current?Z.current=!1:G.current===e.target&&K(e)}),(0,i.useEffect)(function(){o&&(q(!0),(0,L.Z)(G.current,document.activeElement)||(B.current=document.activeElement))},[o]),(0,i.useEffect)(function(){return function(){clearTimeout(X.current)}},[]),i.createElement("div",(0,w.Z)({className:m()("".concat(n,"-root"),C)},(0,M.Z)(e,{data:!0})),i.createElement(V,{prefixCls:n,visible:A&&o,motionName:F(n,R,I),style:(0,x.Z)((0,x.Z)({zIndex:a},_),null==U?void 0:U.mask),maskProps:v,className:null==O?void 0:O.mask}),i.createElement("div",(0,w.Z)({tabIndex:-1,onKeyDown:function(e){if(l&&e.keyCode===P.Z.ESC){e.stopPropagation(),K(e);return}o&&e.keyCode===P.Z.TAB&&$.current.changeActive(!e.shiftKey)},className:m()("".concat(n,"-wrap"),p,null==O?void 0:O.wrapper),ref:G,onClick:Q,style:(0,x.Z)((0,x.Z)((0,x.Z)({zIndex:a},u),null==U?void 0:U.wrapper),{},{display:W?null:"none"})},g),i.createElement(j,(0,w.Z)({},e,{onMouseDown:function(){clearTimeout(X.current),Z.current=!0},onMouseUp:function(){X.current=setTimeout(function(){Z.current=!1})},ref:$,closable:void 0===y||y,ariaId:Y,prefixCls:n,visible:o&&W,onClose:K,onVisibleChanged:function(e){if(e)!function(){if(!(0,L.Z)(G.current,document.activeElement)){var e;null===(e=$.current)||void 0===e||e.focus()}}();else{if(q(!1),A&&B.current&&d){try{B.current.focus({preventScroll:!0})}catch(e){}B.current=null}W&&(null==E||E())}null==f||f(e)},motionName:F(n,h,S)}))))}j.displayName="Content",n(53850);var q=function(e){var t=e.visible,n=e.getContainer,a=e.forceRender,r=e.destroyOnClose,o=void 0!==r&&r,s=e.afterClose,l=e.panelRef,c=i.useState(t),d=(0,k.Z)(c,2),u=d[0],p=d[1],g=i.useMemo(function(){return{panel:l}},[l]);return(i.useEffect(function(){t&&p(!0)},[t]),a||!o||u)?i.createElement(O.Provider,{value:g},i.createElement(C.Z,{open:t||a||u,autoDestroy:!1,getContainer:n,autoLock:t||u},i.createElement(W,(0,w.Z)({},e,{destroyOnClose:o,afterClose:function(){null==s||s(),p(!1)}})))):null};q.displayName="Dialog";var Y=function(e,t,n){let a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:i.createElement(v.Z,null),r=arguments.length>4&&void 0!==arguments[4]&&arguments[4];if("boolean"==typeof e?!e:void 0===t?!r:!1===t||null===t)return[!1,null];let o="boolean"==typeof t||null==t?a:t;return[!0,n?n(o):o]},K=n(22127),Z=n(86718),X=n(47137),Q=n(92801),J=n(48563);function ee(){}let et=i.createContext({add:ee,remove:ee});var en=n(17094),ea=()=>{let{cancelButtonProps:e,cancelTextLocale:t,onCancel:n}=(0,i.useContext)(R);return i.createElement(y.ZP,Object.assign({onClick:n},e),t)},er=()=>{let{confirmLoading:e,okButtonProps:t,okType:n,okTextLocale:a,onOk:r}=(0,i.useContext)(R);return i.createElement(y.ZP,Object.assign({},(0,T.nx)(n),{loading:e,onClick:r},t),a)},ei=n(4678);function eo(e,t){return i.createElement("span",{className:"".concat(e,"-close-x")},t||i.createElement(v.Z,{className:"".concat(e,"-close-icon")}))}let es=e=>{let t;let{okText:n,okType:a="primary",cancelText:o,confirmLoading:s,onOk:l,onCancel:c,okButtonProps:d,cancelButtonProps:u,footer:p}=e,[g]=(0,E.Z)("Modal",(0,ei.A)()),m={confirmLoading:s,okButtonProps:d,cancelButtonProps:u,okTextLocale:n||(null==g?void 0:g.okText),cancelTextLocale:o||(null==g?void 0:g.cancelText),okType:a,onOk:l,onCancel:c},b=i.useMemo(()=>m,(0,r.Z)(Object.values(m)));return"function"==typeof p||void 0===p?(t=i.createElement(i.Fragment,null,i.createElement(ea,null),i.createElement(er,null)),"function"==typeof p&&(t=p(t,{OkBtn:er,CancelBtn:ea})),t=i.createElement(I,{value:b},t)):t=p,i.createElement(en.n,{disabled:!1},t)};var el=n(11303),ec=n(13703),ed=n(58854),eu=n(80316),ep=n(76585),eg=n(8985);function em(e){return{position:e,inset:0}}let eb=e=>{let{componentCls:t,antCls:n}=e;return[{["".concat(t,"-root")]:{["".concat(t).concat(n,"-zoom-enter, ").concat(t).concat(n,"-zoom-appear")]:{transform:"none",opacity:0,animationDuration:e.motionDurationSlow,userSelect:"none"},["".concat(t).concat(n,"-zoom-leave ").concat(t,"-content")]:{pointerEvents:"none"},["".concat(t,"-mask")]:Object.assign(Object.assign({},em("fixed")),{zIndex:e.zIndexPopupBase,height:"100%",backgroundColor:e.colorBgMask,pointerEvents:"none",["".concat(t,"-hidden")]:{display:"none"}}),["".concat(t,"-wrap")]:Object.assign(Object.assign({},em("fixed")),{zIndex:e.zIndexPopupBase,overflow:"auto",outline:0,WebkitOverflowScrolling:"touch",["&:has(".concat(t).concat(n,"-zoom-enter), &:has(").concat(t).concat(n,"-zoom-appear)")]:{pointerEvents:"none"}})}},{["".concat(t,"-root")]:(0,ec.J$)(e)}]},ef=e=>{let{componentCls:t}=e;return[{["".concat(t,"-root")]:{["".concat(t,"-wrap-rtl")]:{direction:"rtl"},["".concat(t,"-centered")]:{textAlign:"center","&::before":{display:"inline-block",width:0,height:"100%",verticalAlign:"middle",content:'""'},[t]:{top:0,display:"inline-block",paddingBottom:0,textAlign:"start",verticalAlign:"middle"}},["@media (max-width: ".concat(e.screenSMMax,"px)")]:{[t]:{maxWidth:"calc(100vw - 16px)",margin:"".concat((0,eg.bf)(e.marginXS)," auto")},["".concat(t,"-centered")]:{[t]:{flex:1}}}}},{[t]:Object.assign(Object.assign({},(0,el.Wf)(e)),{pointerEvents:"none",position:"relative",top:100,width:"auto",maxWidth:"calc(100vw - ".concat((0,eg.bf)(e.calc(e.margin).mul(2).equal()),")"),margin:"0 auto",paddingBottom:e.paddingLG,["".concat(t,"-title")]:{margin:0,color:e.titleColor,fontWeight:e.fontWeightStrong,fontSize:e.titleFontSize,lineHeight:e.titleLineHeight,wordWrap:"break-word"},["".concat(t,"-content")]:{position:"relative",backgroundColor:e.contentBg,backgroundClip:"padding-box",border:0,borderRadius:e.borderRadiusLG,boxShadow:e.boxShadow,pointerEvents:"auto",padding:e.contentPadding},["".concat(t,"-close")]:Object.assign({position:"absolute",top:e.calc(e.modalHeaderHeight).sub(e.modalCloseBtnSize).div(2).equal(),insetInlineEnd:e.calc(e.modalHeaderHeight).sub(e.modalCloseBtnSize).div(2).equal(),zIndex:e.calc(e.zIndexPopupBase).add(10).equal(),padding:0,color:e.modalCloseIconColor,fontWeight:e.fontWeightStrong,lineHeight:1,textDecoration:"none",background:"transparent",borderRadius:e.borderRadiusSM,width:e.modalCloseBtnSize,height:e.modalCloseBtnSize,border:0,outline:0,cursor:"pointer",transition:"color ".concat(e.motionDurationMid,", background-color ").concat(e.motionDurationMid),"&-x":{display:"flex",fontSize:e.fontSizeLG,fontStyle:"normal",lineHeight:"".concat((0,eg.bf)(e.modalCloseBtnSize)),justifyContent:"center",textTransform:"none",textRendering:"auto"},"&:hover":{color:e.modalIconHoverColor,backgroundColor:e.closeBtnHoverBg,textDecoration:"none"},"&:active":{backgroundColor:e.closeBtnActiveBg}},(0,el.Qy)(e)),["".concat(t,"-header")]:{color:e.colorText,background:e.headerBg,borderRadius:"".concat((0,eg.bf)(e.borderRadiusLG)," ").concat((0,eg.bf)(e.borderRadiusLG)," 0 0"),marginBottom:e.headerMarginBottom,padding:e.headerPadding,borderBottom:e.headerBorderBottom},["".concat(t,"-body")]:{fontSize:e.fontSize,lineHeight:e.lineHeight,wordWrap:"break-word",padding:e.bodyPadding},["".concat(t,"-footer")]:{textAlign:"end",background:e.footerBg,marginTop:e.footerMarginTop,padding:e.footerPadding,borderTop:e.footerBorderTop,borderRadius:e.footerBorderRadius,["> ".concat(e.antCls,"-btn + ").concat(e.antCls,"-btn")]:{marginInlineStart:e.marginXS}},["".concat(t,"-open")]:{overflow:"hidden"}})},{["".concat(t,"-pure-panel")]:{top:"auto",padding:0,display:"flex",flexDirection:"column",["".concat(t,"-content,\n ").concat(t,"-body,\n ").concat(t,"-confirm-body-wrapper")]:{display:"flex",flexDirection:"column",flex:"auto"},["".concat(t,"-confirm-body")]:{marginBottom:"auto"}}}]},eE=e=>{let{componentCls:t}=e;return{["".concat(t,"-root")]:{["".concat(t,"-wrap-rtl")]:{direction:"rtl",["".concat(t,"-confirm-body")]:{direction:"rtl"}}}}},eh=e=>{let t=e.padding,n=e.fontSizeHeading5,a=e.lineHeightHeading5;return(0,eu.TS)(e,{modalHeaderHeight:e.calc(e.calc(a).mul(n).equal()).add(e.calc(t).mul(2).equal()).equal(),modalFooterBorderColorSplit:e.colorSplit,modalFooterBorderStyle:e.lineType,modalFooterBorderWidth:e.lineWidth,modalIconHoverColor:e.colorIconHover,modalCloseIconColor:e.colorIcon,modalCloseBtnSize:e.fontHeight,modalConfirmIconSize:e.fontHeight,modalTitleHeight:e.calc(e.titleFontSize).mul(e.titleLineHeight).equal()})},eS=e=>({footerBg:"transparent",headerBg:e.colorBgElevated,titleLineHeight:e.lineHeightHeading5,titleFontSize:e.fontSizeHeading5,contentBg:e.colorBgElevated,titleColor:e.colorTextHeading,closeBtnHoverBg:e.wireframe?"transparent":e.colorFillContent,closeBtnActiveBg:e.wireframe?"transparent":e.colorFillContentHover,contentPadding:e.wireframe?0:"".concat((0,eg.bf)(e.paddingMD)," ").concat((0,eg.bf)(e.paddingContentHorizontalLG)),headerPadding:e.wireframe?"".concat((0,eg.bf)(e.padding)," ").concat((0,eg.bf)(e.paddingLG)):0,headerBorderBottom:e.wireframe?"".concat((0,eg.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit):"none",headerMarginBottom:e.wireframe?0:e.marginXS,bodyPadding:e.wireframe?e.paddingLG:0,footerPadding:e.wireframe?"".concat((0,eg.bf)(e.paddingXS)," ").concat((0,eg.bf)(e.padding)):0,footerBorderTop:e.wireframe?"".concat((0,eg.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit):"none",footerBorderRadius:e.wireframe?"0 0 ".concat((0,eg.bf)(e.borderRadiusLG)," ").concat((0,eg.bf)(e.borderRadiusLG)):0,footerMarginTop:e.wireframe?0:e.marginSM,confirmBodyPadding:e.wireframe?"".concat((0,eg.bf)(2*e.padding)," ").concat((0,eg.bf)(2*e.padding)," ").concat((0,eg.bf)(e.paddingLG)):0,confirmIconMarginInlineEnd:e.wireframe?e.margin:e.marginSM,confirmBtnsMarginTop:e.wireframe?e.marginLG:e.marginSM});var ey=(0,ep.I$)("Modal",e=>{let t=eh(e);return[ef(t),eE(t),eb(t),(0,ed._y)(t,"zoom")]},eS,{unitless:{titleLineHeight:!0}}),eT=n(92935),eA=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&0>t.indexOf(a)&&(n[a]=e[a]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var r=0,a=Object.getOwnPropertySymbols(e);rt.indexOf(a[r])&&Object.prototype.propertyIsEnumerable.call(e,a[r])&&(n[a[r]]=e[a[r]]);return n};(0,K.Z)()&&window.document.documentElement&&document.documentElement.addEventListener("click",e=>{a={x:e.pageX,y:e.pageY},setTimeout(()=>{a=null},100)},!0);var eR=e=>{var t;let{getPopupContainer:n,getPrefixCls:r,direction:o,modal:l}=i.useContext(s.E_),c=t=>{let{onCancel:n}=e;null==n||n(t)},{prefixCls:d,className:u,rootClassName:p,open:g,wrapClassName:E,centered:h,getContainer:S,closeIcon:y,closable:T,focusTriggerAfterClose:A=!0,style:R,visible:I,width:N=520,footer:_,classNames:w,styles:k}=e,C=eA(e,["prefixCls","className","rootClassName","open","wrapClassName","centered","getContainer","closeIcon","closable","focusTriggerAfterClose","style","visible","width","footer","classNames","styles"]),O=r("modal",d),x=r(),L=(0,eT.Z)(O),[D,P,M]=ey(O,L),F=m()(E,{["".concat(O,"-centered")]:!!h,["".concat(O,"-wrap-rtl")]:"rtl"===o}),U=null!==_&&i.createElement(es,Object.assign({},e,{onOk:t=>{let{onOk:n}=e;null==n||n(t)},onCancel:c})),[B,G]=Y(T,y,e=>eo(O,e),i.createElement(v.Z,{className:"".concat(O,"-close-icon")}),!0),$=function(e){let t=i.useContext(et),n=i.useRef();return(0,J.zX)(a=>{if(a){let r=e?a.querySelector(e):a;t.add(r),n.current=r}else t.remove(n.current)})}(".".concat(O,"-content")),[H,z]=(0,b.Cn)("Modal",C.zIndex);return D(i.createElement(Q.BR,null,i.createElement(X.Ux,{status:!0,override:!0},i.createElement(Z.Z.Provider,{value:z},i.createElement(q,Object.assign({width:N},C,{zIndex:H,getContainer:void 0===S?n:S,prefixCls:O,rootClassName:m()(P,p,M,L),footer:U,visible:null!=g?g:I,mousePosition:null!==(t=C.mousePosition)&&void 0!==t?t:a,onClose:c,closable:B,closeIcon:G,focusTriggerAfterClose:A,transitionName:(0,f.m)(x,"zoom",e.transitionName),maskTransitionName:(0,f.m)(x,"fade",e.maskTransitionName),className:m()(P,u,null==l?void 0:l.className),style:Object.assign(Object.assign({},null==l?void 0:l.style),R),classNames:Object.assign(Object.assign({wrapper:F},null==l?void 0:l.classNames),w),styles:Object.assign(Object.assign({},null==l?void 0:l.styles),k),panelRef:$}))))))};let eI=e=>{let{componentCls:t,titleFontSize:n,titleLineHeight:a,modalConfirmIconSize:r,fontSize:i,lineHeight:o,modalTitleHeight:s,fontHeight:l,confirmBodyPadding:c}=e,d="".concat(t,"-confirm");return{[d]:{"&-rtl":{direction:"rtl"},["".concat(e.antCls,"-modal-header")]:{display:"none"},["".concat(d,"-body-wrapper")]:Object.assign({},(0,el.dF)()),["&".concat(t," ").concat(t,"-body")]:{padding:c},["".concat(d,"-body")]:{display:"flex",flexWrap:"nowrap",alignItems:"start",["> ".concat(e.iconCls)]:{flex:"none",fontSize:r,marginInlineEnd:e.confirmIconMarginInlineEnd,marginTop:e.calc(e.calc(l).sub(r).equal()).div(2).equal()},["&-has-title > ".concat(e.iconCls)]:{marginTop:e.calc(e.calc(s).sub(r).equal()).div(2).equal()}},["".concat(d,"-paragraph")]:{display:"flex",flexDirection:"column",flex:"auto",rowGap:e.marginXS,maxWidth:"calc(100% - ".concat((0,eg.bf)(e.calc(e.modalConfirmIconSize).add(e.marginSM).equal()),")")},["".concat(d,"-title")]:{color:e.colorTextHeading,fontWeight:e.fontWeightStrong,fontSize:n,lineHeight:a},["".concat(d,"-content")]:{color:e.colorText,fontSize:i,lineHeight:o},["".concat(d,"-btns")]:{textAlign:"end",marginTop:e.confirmBtnsMarginTop,["".concat(e.antCls,"-btn + ").concat(e.antCls,"-btn")]:{marginBottom:0,marginInlineStart:e.marginXS}}},["".concat(d,"-error ").concat(d,"-body > ").concat(e.iconCls)]:{color:e.colorError},["".concat(d,"-warning ").concat(d,"-body > ").concat(e.iconCls,",\n ").concat(d,"-confirm ").concat(d,"-body > ").concat(e.iconCls)]:{color:e.colorWarning},["".concat(d,"-info ").concat(d,"-body > ").concat(e.iconCls)]:{color:e.colorInfo},["".concat(d,"-success ").concat(d,"-body > ").concat(e.iconCls)]:{color:e.colorSuccess}}};var eN=(0,ep.bk)(["Modal","confirm"],e=>[eI(eh(e))],eS,{order:-1e3}),e_=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&0>t.indexOf(a)&&(n[a]=e[a]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var r=0,a=Object.getOwnPropertySymbols(e);rt.indexOf(a[r])&&Object.prototype.propertyIsEnumerable.call(e,a[r])&&(n[a[r]]=e[a[r]]);return n};function ev(e){let{prefixCls:t,icon:n,okText:a,cancelText:o,confirmPrefixCls:s,type:l,okCancel:g,footer:b,locale:f}=e,h=e_(e,["prefixCls","icon","okText","cancelText","confirmPrefixCls","type","okCancel","footer","locale"]),S=n;if(!n&&null!==n)switch(l){case"info":S=i.createElement(p.Z,null);break;case"success":S=i.createElement(c.Z,null);break;case"error":S=i.createElement(d.Z,null);break;default:S=i.createElement(u.Z,null)}let y=null!=g?g:"confirm"===l,T=null!==e.autoFocusButton&&(e.autoFocusButton||"ok"),[A]=(0,E.Z)("Modal"),R=f||A,v=a||(y?null==R?void 0:R.okText:null==R?void 0:R.justOkText),w=Object.assign({autoFocusButton:T,cancelTextLocale:o||(null==R?void 0:R.cancelText),okTextLocale:v,mergedOkCancel:y},h),k=i.useMemo(()=>w,(0,r.Z)(Object.values(w))),C=i.createElement(i.Fragment,null,i.createElement(N,null),i.createElement(_,null)),O=void 0!==e.title&&null!==e.title,x="".concat(s,"-body");return i.createElement("div",{className:"".concat(s,"-body-wrapper")},i.createElement("div",{className:m()(x,{["".concat(x,"-has-title")]:O})},S,i.createElement("div",{className:"".concat(s,"-paragraph")},O&&i.createElement("span",{className:"".concat(s,"-title")},e.title),i.createElement("div",{className:"".concat(s,"-content")},e.content))),void 0===b||"function"==typeof b?i.createElement(I,{value:k},i.createElement("div",{className:"".concat(s,"-btns")},"function"==typeof b?b(C,{OkBtn:_,CancelBtn:N}):C)):b,i.createElement(eN,{prefixCls:t}))}let ew=e=>{let{close:t,zIndex:n,afterClose:a,open:r,keyboard:o,centered:s,getContainer:l,maskStyle:c,direction:d,prefixCls:u,wrapClassName:p,rootPrefixCls:g,bodyStyle:E,closable:S=!1,closeIcon:y,modalRender:T,focusTriggerAfterClose:A,onConfirm:R,styles:I}=e,N="".concat(u,"-confirm"),_=e.width||416,v=e.style||{},w=void 0===e.mask||e.mask,k=void 0!==e.maskClosable&&e.maskClosable,C=m()(N,"".concat(N,"-").concat(e.type),{["".concat(N,"-rtl")]:"rtl"===d},e.className),[,O]=(0,h.ZP)(),x=i.useMemo(()=>void 0!==n?n:O.zIndexPopupBase+b.u6,[n,O]);return i.createElement(eR,{prefixCls:u,className:C,wrapClassName:m()({["".concat(N,"-centered")]:!!e.centered},p),onCancel:()=>{null==t||t({triggerCancel:!0}),null==R||R(!1)},open:r,title:"",footer:null,transitionName:(0,f.m)(g||"","zoom",e.transitionName),maskTransitionName:(0,f.m)(g||"","fade",e.maskTransitionName),mask:w,maskClosable:k,style:v,styles:Object.assign({body:E,mask:c},I),width:_,zIndex:x,afterClose:a,keyboard:o,centered:s,getContainer:l,closable:S,closeIcon:y,modalRender:T,focusTriggerAfterClose:A},i.createElement(ev,Object.assign({},e,{confirmPrefixCls:N})))};var ek=e=>{let{rootPrefixCls:t,iconPrefixCls:n,direction:a,theme:r}=e;return i.createElement(l.ZP,{prefixCls:t,iconPrefixCls:n,direction:a,theme:r},i.createElement(ew,Object.assign({},e)))},eC=[];let eO="",ex=e=>{var t,n;let{prefixCls:a,getContainer:r,direction:o}=e,l=(0,ei.A)(),c=(0,i.useContext)(s.E_),d=eO||c.getPrefixCls(),u=a||"".concat(d,"-modal"),p=r;return!1===p&&(p=void 0),i.createElement(ek,Object.assign({},e,{rootPrefixCls:d,prefixCls:u,iconPrefixCls:c.iconPrefixCls,theme:c.theme,direction:null!=o?o:c.direction,locale:null!==(n=null===(t=c.locale)||void 0===t?void 0:t.Modal)&&void 0!==n?n:l,getContainer:p}))};function eL(e){let t;let n=(0,l.w6)(),a=document.createDocumentFragment(),s=Object.assign(Object.assign({},e),{close:u,open:!0});function c(){for(var t=arguments.length,n=Array(t),i=0;ie&&e.triggerCancel);e.onCancel&&s&&e.onCancel.apply(e,[()=>{}].concat((0,r.Z)(n.slice(1))));for(let e=0;e{let t=n.getPrefixCls(void 0,eO),r=n.getIconPrefixCls(),s=n.getTheme(),c=i.createElement(ex,Object.assign({},e));(0,o.s)(i.createElement(l.ZP,{prefixCls:t,iconPrefixCls:r,theme:s},n.holderRender?n.holderRender(c):c),a)})}function u(){for(var t=arguments.length,n=Array(t),a=0;a{"function"==typeof e.afterClose&&e.afterClose(),c.apply(this,n)}})).visible&&delete s.visible,d(s)}return d(s),eC.push(u),{destroy:u,update:function(e){d(s="function"==typeof e?e(s):Object.assign(Object.assign({},s),e))}}}function eD(e){return Object.assign(Object.assign({},e),{type:"warning"})}function eP(e){return Object.assign(Object.assign({},e),{type:"info"})}function eM(e){return Object.assign(Object.assign({},e),{type:"success"})}function eF(e){return Object.assign(Object.assign({},e),{type:"error"})}function eU(e){return Object.assign(Object.assign({},e),{type:"confirm"})}var eB=n(21467),eG=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&0>t.indexOf(a)&&(n[a]=e[a]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var r=0,a=Object.getOwnPropertySymbols(e);rt.indexOf(a[r])&&Object.prototype.propertyIsEnumerable.call(e,a[r])&&(n[a[r]]=e[a[r]]);return n},e$=(0,eB.i)(e=>{let{prefixCls:t,className:n,closeIcon:a,closable:r,type:o,title:l,children:c,footer:d}=e,u=eG(e,["prefixCls","className","closeIcon","closable","type","title","children","footer"]),{getPrefixCls:p}=i.useContext(s.E_),g=p(),b=t||p("modal"),f=(0,eT.Z)(g),[E,h,S]=ey(b,f),y="".concat(b,"-confirm"),T={};return T=o?{closable:null!=r&&r,title:"",footer:"",children:i.createElement(ev,Object.assign({},e,{prefixCls:b,confirmPrefixCls:y,rootPrefixCls:g,content:c}))}:{closable:null==r||r,title:l,footer:null!==d&&i.createElement(es,Object.assign({},e)),children:c},E(i.createElement(z,Object.assign({prefixCls:b,className:m()(h,"".concat(b,"-pure-panel"),o&&y,o&&"".concat(y,"-").concat(o),n,S,f)},u,{closeIcon:eo(b,a),closable:r},T)))}),eH=n(79474),ez=function(e,t){var n={};for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&0>t.indexOf(a)&&(n[a]=e[a]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var r=0,a=Object.getOwnPropertySymbols(e);rt.indexOf(a[r])&&Object.prototype.propertyIsEnumerable.call(e,a[r])&&(n[a[r]]=e[a[r]]);return n},ej=i.forwardRef((e,t)=>{var n,{afterClose:a,config:o}=e,l=ez(e,["afterClose","config"]);let[c,d]=i.useState(!0),[u,p]=i.useState(o),{direction:g,getPrefixCls:m}=i.useContext(s.E_),b=m("modal"),f=m(),h=function(){d(!1);for(var e=arguments.length,t=Array(e),n=0;ne&&e.triggerCancel);u.onCancel&&a&&u.onCancel.apply(u,[()=>{}].concat((0,r.Z)(t.slice(1))))};i.useImperativeHandle(t,()=>({destroy:h,update:e=>{p(t=>Object.assign(Object.assign({},t),e))}}));let S=null!==(n=u.okCancel)&&void 0!==n?n:"confirm"===u.type,[y]=(0,E.Z)("Modal",eH.Z.Modal);return i.createElement(ek,Object.assign({prefixCls:b,rootPrefixCls:f},u,{close:h,open:c,afterClose:()=>{var e;a(),null===(e=u.afterClose)||void 0===e||e.call(u)},okText:u.okText||(S?null==y?void 0:y.okText:null==y?void 0:y.justOkText),direction:u.direction||g,cancelText:u.cancelText||(null==y?void 0:y.cancelText)},l))});let eV=0,eW=i.memo(i.forwardRef((e,t)=>{let[n,a]=function(){let[e,t]=i.useState([]);return[e,i.useCallback(e=>(t(t=>[].concat((0,r.Z)(t),[e])),()=>{t(t=>t.filter(t=>t!==e))}),[])]}();return i.useImperativeHandle(t,()=>({patchElement:a}),[]),i.createElement(i.Fragment,null,n)}));function eq(e){return eL(eD(e))}eR.useModal=function(){let e=i.useRef(null),[t,n]=i.useState([]);i.useEffect(()=>{t.length&&((0,r.Z)(t).forEach(e=>{e()}),n([]))},[t]);let a=i.useCallback(t=>function(a){var o;let s,l;eV+=1;let c=i.createRef(),d=new Promise(e=>{s=e}),u=!1,p=i.createElement(ej,{key:"modal-".concat(eV),config:t(a),ref:c,afterClose:()=>{null==l||l()},isSilent:()=>u,onConfirm:e=>{s(e)}});return(l=null===(o=e.current)||void 0===o?void 0:o.patchElement(p))&&eC.push(l),{destroy:()=>{function e(){var e;null===(e=c.current)||void 0===e||e.destroy()}c.current?e():n(t=>[].concat((0,r.Z)(t),[e]))},update:e=>{function t(){var t;null===(t=c.current)||void 0===t||t.update(e)}c.current?t():n(e=>[].concat((0,r.Z)(e),[t]))},then:e=>(u=!0,d.then(e))}},[]);return[i.useMemo(()=>({info:a(eP),success:a(eM),error:a(eF),warning:a(eD),confirm:a(eU)}),[]),i.createElement(eW,{key:"modal-holder",ref:e})]},eR.info=function(e){return eL(eP(e))},eR.success=function(e){return eL(eM(e))},eR.error=function(e){return eL(eF(e))},eR.warning=eq,eR.warn=eq,eR.confirm=function(e){return eL(eU(e))},eR.destroyAll=function(){for(;eC.length;){let e=eC.pop();e&&e()}},eR.config=function(e){let{rootPrefixCls:t}=e;eO=t},eR._InternalPanelDoNotUseOrYouWillBeFired=e$;var eY=eR},13703:function(e,t,n){n.d(t,{J$:function(){return s}});var a=n(8985),r=n(59353);let i=new a.E4("antFadeIn",{"0%":{opacity:0},"100%":{opacity:1}}),o=new a.E4("antFadeOut",{"0%":{opacity:1},"100%":{opacity:0}}),s=function(e){let t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],{antCls:n}=e,a="".concat(n,"-fade"),s=t?"&":"";return[(0,r.R)(a,i,o,e.motionDurationMid,t),{["\n ".concat(s).concat(a,"-enter,\n ").concat(s).concat(a,"-appear\n ")]:{opacity:0,animationTimingFunction:"linear"},["".concat(s).concat(a,"-leave")]:{animationTimingFunction:"linear"}}]}},44056:function(e){e.exports=function(e,n){for(var a,r,i,o=e||"",s=n||"div",l={},c=0;c4&&m.slice(0,4)===o&&s.test(t)&&("-"===t.charAt(4)?b=o+(n=t.slice(5).replace(l,u)).charAt(0).toUpperCase()+n.slice(1):(g=(p=t).slice(4),t=l.test(g)?p:("-"!==(g=g.replace(c,d)).charAt(0)&&(g="-"+g),o+g)),f=r),new f(b,t))};var s=/^data[-\w.:]+$/i,l=/-[a-z]/g,c=/[A-Z]/g;function d(e){return"-"+e.toLowerCase()}function u(e){return e.charAt(1).toUpperCase()}},31872:function(e,t,n){var a=n(96130),r=n(64730),i=n(61861),o=n(46982),s=n(83671),l=n(53618);e.exports=a([i,r,o,s,l])},83671:function(e,t,n){var a=n(7667),r=n(13585),i=a.booleanish,o=a.number,s=a.spaceSeparated;e.exports=r({transform:function(e,t){return"role"===t?t:"aria-"+t.slice(4).toLowerCase()},properties:{ariaActiveDescendant:null,ariaAtomic:i,ariaAutoComplete:null,ariaBusy:i,ariaChecked:i,ariaColCount:o,ariaColIndex:o,ariaColSpan:o,ariaControls:s,ariaCurrent:null,ariaDescribedBy:s,ariaDetails:null,ariaDisabled:i,ariaDropEffect:s,ariaErrorMessage:null,ariaExpanded:i,ariaFlowTo:s,ariaGrabbed:i,ariaHasPopup:null,ariaHidden:i,ariaInvalid:null,ariaKeyShortcuts:null,ariaLabel:null,ariaLabelledBy:s,ariaLevel:o,ariaLive:null,ariaModal:i,ariaMultiLine:i,ariaMultiSelectable:i,ariaOrientation:null,ariaOwns:s,ariaPlaceholder:null,ariaPosInSet:o,ariaPressed:i,ariaReadOnly:i,ariaRelevant:null,ariaRequired:i,ariaRoleDescription:s,ariaRowCount:o,ariaRowIndex:o,ariaRowSpan:o,ariaSelected:i,ariaSetSize:o,ariaSort:null,ariaValueMax:o,ariaValueMin:o,ariaValueNow:o,ariaValueText:null,role:null}})},53618:function(e,t,n){var a=n(7667),r=n(13585),i=n(46640),o=a.boolean,s=a.overloadedBoolean,l=a.booleanish,c=a.number,d=a.spaceSeparated,u=a.commaSeparated;e.exports=r({space:"html",attributes:{acceptcharset:"accept-charset",classname:"class",htmlfor:"for",httpequiv:"http-equiv"},transform:i,mustUseProperty:["checked","multiple","muted","selected"],properties:{abbr:null,accept:u,acceptCharset:d,accessKey:d,action:null,allow:null,allowFullScreen:o,allowPaymentRequest:o,allowUserMedia:o,alt:null,as:null,async:o,autoCapitalize:null,autoComplete:d,autoFocus:o,autoPlay:o,capture:o,charSet:null,checked:o,cite:null,className:d,cols:c,colSpan:null,content:null,contentEditable:l,controls:o,controlsList:d,coords:c|u,crossOrigin:null,data:null,dateTime:null,decoding:null,default:o,defer:o,dir:null,dirName:null,disabled:o,download:s,draggable:l,encType:null,enterKeyHint:null,form:null,formAction:null,formEncType:null,formMethod:null,formNoValidate:o,formTarget:null,headers:d,height:c,hidden:o,high:c,href:null,hrefLang:null,htmlFor:d,httpEquiv:d,id:null,imageSizes:null,imageSrcSet:u,inputMode:null,integrity:null,is:null,isMap:o,itemId:null,itemProp:d,itemRef:d,itemScope:o,itemType:d,kind:null,label:null,lang:null,language:null,list:null,loading:null,loop:o,low:c,manifest:null,max:null,maxLength:c,media:null,method:null,min:null,minLength:c,multiple:o,muted:o,name:null,nonce:null,noModule:o,noValidate:o,onAbort:null,onAfterPrint:null,onAuxClick:null,onBeforePrint:null,onBeforeUnload:null,onBlur:null,onCancel:null,onCanPlay:null,onCanPlayThrough:null,onChange:null,onClick:null,onClose:null,onContextMenu:null,onCopy:null,onCueChange:null,onCut:null,onDblClick:null,onDrag:null,onDragEnd:null,onDragEnter:null,onDragExit:null,onDragLeave:null,onDragOver:null,onDragStart:null,onDrop:null,onDurationChange:null,onEmptied:null,onEnded:null,onError:null,onFocus:null,onFormData:null,onHashChange:null,onInput:null,onInvalid:null,onKeyDown:null,onKeyPress:null,onKeyUp:null,onLanguageChange:null,onLoad:null,onLoadedData:null,onLoadedMetadata:null,onLoadEnd:null,onLoadStart:null,onMessage:null,onMessageError:null,onMouseDown:null,onMouseEnter:null,onMouseLeave:null,onMouseMove:null,onMouseOut:null,onMouseOver:null,onMouseUp:null,onOffline:null,onOnline:null,onPageHide:null,onPageShow:null,onPaste:null,onPause:null,onPlay:null,onPlaying:null,onPopState:null,onProgress:null,onRateChange:null,onRejectionHandled:null,onReset:null,onResize:null,onScroll:null,onSecurityPolicyViolation:null,onSeeked:null,onSeeking:null,onSelect:null,onSlotChange:null,onStalled:null,onStorage:null,onSubmit:null,onSuspend:null,onTimeUpdate:null,onToggle:null,onUnhandledRejection:null,onUnload:null,onVolumeChange:null,onWaiting:null,onWheel:null,open:o,optimum:c,pattern:null,ping:d,placeholder:null,playsInline:o,poster:null,preload:null,readOnly:o,referrerPolicy:null,rel:d,required:o,reversed:o,rows:c,rowSpan:c,sandbox:d,scope:null,scoped:o,seamless:o,selected:o,shape:null,size:c,sizes:null,slot:null,span:c,spellCheck:l,src:null,srcDoc:null,srcLang:null,srcSet:u,start:c,step:null,style:null,tabIndex:c,target:null,title:null,translate:null,type:null,typeMustMatch:o,useMap:null,value:l,width:c,wrap:null,align:null,aLink:null,archive:d,axis:null,background:null,bgColor:null,border:c,borderColor:null,bottomMargin:c,cellPadding:null,cellSpacing:null,char:null,charOff:null,classId:null,clear:null,code:null,codeBase:null,codeType:null,color:null,compact:o,declare:o,event:null,face:null,frame:null,frameBorder:null,hSpace:c,leftMargin:c,link:null,longDesc:null,lowSrc:null,marginHeight:c,marginWidth:c,noResize:o,noHref:o,noShade:o,noWrap:o,object:null,profile:null,prompt:null,rev:null,rightMargin:c,rules:null,scheme:null,scrolling:l,standby:null,summary:null,text:null,topMargin:c,valueType:null,version:null,vAlign:null,vLink:null,vSpace:c,allowTransparency:null,autoCorrect:null,autoSave:null,disablePictureInPicture:o,disableRemotePlayback:o,prefix:null,property:null,results:c,security:null,unselectable:null}})},46640:function(e,t,n){var a=n(25852);e.exports=function(e,t){return a(e,t.toLowerCase())}},25852:function(e){e.exports=function(e,t){return t in e?e[t]:t}},13585:function(e,t,n){var a=n(39900),r=n(94949),i=n(7478);e.exports=function(e){var t,n,o=e.space,s=e.mustUseProperty||[],l=e.attributes||{},c=e.properties,d=e.transform,u={},p={};for(t in c)n=new i(t,d(l,t),c[t],o),-1!==s.indexOf(t)&&(n.mustUseProperty=!0),u[t]=n,p[a(t)]=t,p[a(n.attribute)]=t;return new r(u,p,o)}},7478:function(e,t,n){var a=n(74108),r=n(7667);e.exports=s,s.prototype=new a,s.prototype.defined=!0;var i=["boolean","booleanish","overloadedBoolean","number","commaSeparated","spaceSeparated","commaOrSpaceSeparated"],o=i.length;function s(e,t,n,s){var l,c,d,u=-1;for(s&&(this.space=s),a.call(this,e,t);++u