diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index 351c4f6bc4..09b5265191 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -12,7 +12,10 @@ WORKDIR /app USER root # Install build dependencies -RUN apk add --no-cache gcc python3-dev openssl openssl-dev +RUN apk add --no-cache \ + build-base \ + python3-dev \ + openssl-dev RUN pip install --upgrade pip && \ diff --git a/litellm/batches/main.py b/litellm/batches/main.py index 5279dd70bc..838ee95b2b 100644 --- a/litellm/batches/main.py +++ b/litellm/batches/main.py @@ -18,7 +18,6 @@ from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast import httpx from openai.types.batch import BatchRequestCounts -from openai.types.batch import Metadata as BatchMetadata import litellm from litellm._logging import verbose_logger diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index 71429e4191..4152257314 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -1246,18 +1246,168 @@ class AWSEventStreamDecoder: thinking_blocks_list.append(_thinking_block) return thinking_blocks_list + def _initialize_converse_response_id(self, chunk_data: dict): + """Initialize response_id from chunk data if not already set.""" + if self.response_id is None: + if "messageStart" in chunk_data: + conversation_id = chunk_data["messageStart"].get("conversationId") + if conversation_id: + self.response_id = f"chatcmpl-{conversation_id}" + else: + # Fallback to generating a UUID if the first chunk is not messageStart + self.response_id = f"chatcmpl-{uuid.uuid4()}" + + def _handle_converse_start_event( + self, + start_obj: ContentBlockStartEvent, + ) -> tuple[ + Optional[ChatCompletionToolCallChunk], + dict, + Optional[ + List[ + Union[ + ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock + ] + ] + ], + ]: + """Handle 'start' event in converse chunk parsing.""" + tool_use: Optional[ChatCompletionToolCallChunk] = None + provider_specific_fields: dict = {} + thinking_blocks: Optional[ + List[ + Union[ + ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock + ] + ] + ] = None + + self.content_blocks = [] # reset + if start_obj is not None: + if "toolUse" in start_obj and start_obj["toolUse"] is not None: + ## check tool name was formatted by litellm + _response_tool_name = start_obj["toolUse"]["name"] + response_tool_name = get_bedrock_tool_name( + response_tool_name=_response_tool_name + ) + self.tool_calls_index = ( + 0 + if self.tool_calls_index is None + else self.tool_calls_index + 1 + ) + tool_use = { + "id": start_obj["toolUse"]["toolUseId"], + "type": "function", + "function": { + "name": response_tool_name, + "arguments": "", + }, + "index": self.tool_calls_index, + } + elif ( + "reasoningContent" in start_obj + and start_obj["reasoningContent"] is not None + ): # redacted thinking can be in start object + thinking_blocks = self.translate_thinking_blocks( + start_obj["reasoningContent"] + ) + provider_specific_fields = { + "reasoningContent": start_obj["reasoningContent"], + } + return tool_use, provider_specific_fields, thinking_blocks + + def _handle_converse_delta_event( + self, + delta_obj: ContentBlockDeltaEvent, + index: int, + ) -> tuple[ + str, + Optional[ChatCompletionToolCallChunk], + dict, + Optional[str], + Optional[ + List[ + Union[ + ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock + ] + ] + ], + ]: + """Handle 'delta' event in converse chunk parsing.""" + text = "" + tool_use: Optional[ChatCompletionToolCallChunk] = None + provider_specific_fields: dict = {} + reasoning_content: Optional[str] = None + thinking_blocks: Optional[ + List[ + Union[ + ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock + ] + ] + ] = None + + self.content_blocks.append(delta_obj) + if "text" in delta_obj: + text = delta_obj["text"] + elif "toolUse" in delta_obj: + tool_use = { + "id": None, + "type": "function", + "function": { + "name": None, + "arguments": delta_obj["toolUse"]["input"], + }, + "index": ( + self.tool_calls_index + if self.tool_calls_index is not None + else index + ), + } + elif "reasoningContent" in delta_obj: + provider_specific_fields = { + "reasoningContent": delta_obj["reasoningContent"], + } + reasoning_content = self.extract_reasoning_content_str( + delta_obj["reasoningContent"] + ) + thinking_blocks = self.translate_thinking_blocks( + delta_obj["reasoningContent"] + ) + if ( + thinking_blocks + and len(thinking_blocks) > 0 + and reasoning_content is None + ): + reasoning_content = "" # set to non-empty string to ensure consistency with Anthropic + return text, tool_use, provider_specific_fields, reasoning_content, thinking_blocks + + def _handle_converse_stop_event( + self, index: int + ) -> Optional[ChatCompletionToolCallChunk]: + """Handle stop/contentBlockIndex event in converse chunk parsing.""" + tool_use: Optional[ChatCompletionToolCallChunk] = None + is_empty = self.check_empty_tool_call_args() + if is_empty: + tool_use = { + "id": None, + "type": "function", + "function": { + "name": None, + "arguments": "{}", + }, + "index": ( + self.tool_calls_index + if self.tool_calls_index is not None + else index + ), + } + return tool_use + def converse_chunk_parser(self, chunk_data: dict) -> ModelResponseStream: try: # Capture the conversationId from the first messageStart event # and use it as the consistent ID for all subsequent chunks. - if self.response_id is None: - if "messageStart" in chunk_data: - conversation_id = chunk_data["messageStart"].get("conversationId") - if conversation_id: - self.response_id = f"chatcmpl-{conversation_id}" - else: - # Fallback to generating a UUID if the first chunk is not messageStart - self.response_id = f"chatcmpl-{uuid.uuid4()}" + self._initialize_converse_response_id(chunk_data) verbose_logger.debug("\n\nRaw Chunk: {}\n\n".format(chunk_data)) text = "" @@ -1277,91 +1427,22 @@ class AWSEventStreamDecoder: index = int(chunk_data.get("contentBlockIndex", 0)) if "start" in chunk_data: start_obj = ContentBlockStartEvent(**chunk_data["start"]) - self.content_blocks = [] # reset - if start_obj is not None: - if "toolUse" in start_obj and start_obj["toolUse"] is not None: - ## check tool name was formatted by litellm - _response_tool_name = start_obj["toolUse"]["name"] - response_tool_name = get_bedrock_tool_name( - response_tool_name=_response_tool_name - ) - self.tool_calls_index = ( - 0 - if self.tool_calls_index is None - else self.tool_calls_index + 1 - ) - tool_use = { - "id": start_obj["toolUse"]["toolUseId"], - "type": "function", - "function": { - "name": response_tool_name, - "arguments": "", - }, - "index": self.tool_calls_index, - } - elif ( - "reasoningContent" in start_obj - and start_obj["reasoningContent"] is not None - ): # redacted thinking can be in start object - thinking_blocks = self.translate_thinking_blocks( - start_obj["reasoningContent"] - ) - provider_specific_fields = { - "reasoningContent": start_obj["reasoningContent"], - } + tool_use, provider_specific_fields, thinking_blocks = ( + self._handle_converse_start_event(start_obj) + ) elif "delta" in chunk_data: delta_obj = ContentBlockDeltaEvent(**chunk_data["delta"]) - self.content_blocks.append(delta_obj) - if "text" in delta_obj: - text = delta_obj["text"] - elif "toolUse" in delta_obj: - tool_use = { - "id": None, - "type": "function", - "function": { - "name": None, - "arguments": delta_obj["toolUse"]["input"], - }, - "index": ( - self.tool_calls_index - if self.tool_calls_index is not None - else index - ), - } - elif "reasoningContent" in delta_obj: - provider_specific_fields = { - "reasoningContent": delta_obj["reasoningContent"], - } - reasoning_content = self.extract_reasoning_content_str( - delta_obj["reasoningContent"] - ) - thinking_blocks = self.translate_thinking_blocks( - delta_obj["reasoningContent"] - ) - if ( - thinking_blocks - and len(thinking_blocks) > 0 - and reasoning_content is None - ): - reasoning_content = "" # set to non-empty string to ensure consistency with Anthropic + ( + text, + tool_use, + provider_specific_fields, + reasoning_content, + thinking_blocks, + ) = self._handle_converse_delta_event(delta_obj, index) elif ( "contentBlockIndex" in chunk_data ): # stop block, no 'start' or 'delta' object - is_empty = self.check_empty_tool_call_args() - if is_empty: - tool_use = { - "id": None, - "type": "function", - "function": { - "name": None, - "arguments": "{}", - }, - "index": ( - self.tool_calls_index - if self.tool_calls_index is not None - else index - ), - } + tool_use = self._handle_converse_stop_event(index) elif "stopReason" in chunk_data: finish_reason = map_finish_reason(chunk_data.get("stopReason", "stop")) elif "usage" in chunk_data: diff --git a/litellm/llms/github_copilot/responses/transformation.py b/litellm/llms/github_copilot/responses/transformation.py index a85c37fd9b..cc96e3415f 100644 --- a/litellm/llms/github_copilot/responses/transformation.py +++ b/litellm/llms/github_copilot/responses/transformation.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Union from uuid import uuid4 from litellm._logging import verbose_logger +from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.exceptions import AuthenticationError from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.types.llms.openai import ( @@ -273,18 +274,29 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig): """ return self._contains_vision_content(input_param) - def _contains_vision_content(self, value: Any) -> bool: + def _contains_vision_content( + self, value: Any, depth: int = 0, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH + ) -> bool: """ Recursively check if a value contains vision content. Looks for items with type="input_image" in the structure. """ + if depth > max_depth: + verbose_logger.warning( + f"[GitHub Copilot] Max recursion depth {max_depth} reached while checking for vision content" + ) + return False + if value is None: return False # Check arrays if isinstance(value, list): - return any(self._contains_vision_content(item) for item in value) + return any( + self._contains_vision_content(item, depth=depth + 1, max_depth=max_depth) + for item in value + ) # Only check dict/object types if not isinstance(value, dict): @@ -298,7 +310,8 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig): # Check content field recursively if "content" in value and isinstance(value["content"], list): return any( - self._contains_vision_content(item) for item in value["content"] + self._contains_vision_content(item, depth=depth + 1, max_depth=max_depth) + for item in value["content"] ) return False diff --git a/litellm/llms/vertex_ai/image_edit/vertex_imagen_transformation.py b/litellm/llms/vertex_ai/image_edit/vertex_imagen_transformation.py index caf347a1fb..ad650e3849 100644 --- a/litellm/llms/vertex_ai/image_edit/vertex_imagen_transformation.py +++ b/litellm/llms/vertex_ai/image_edit/vertex_imagen_transformation.py @@ -10,6 +10,7 @@ from httpx._types import RequestFiles import litellm +from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexLLM from litellm.secret_managers.main import get_secret_str @@ -286,11 +287,18 @@ class VertexAIImagenImageEditConfig(BaseImageEditConfig, VertexLLM): return reference_images - def _read_all_bytes(self, image: Any) -> bytes: + def _read_all_bytes( + self, image: Any, depth: int = 0, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH + ) -> bytes: + if depth > max_depth: + raise ValueError( + f"Max recursion depth {max_depth} reached while reading image bytes for Vertex AI Imagen image edit." + ) + if isinstance(image, (list, tuple)): for item in image: if item is not None: - return self._read_all_bytes(item) + return self._read_all_bytes(item, depth=depth + 1, max_depth=max_depth) raise ValueError("Unsupported image type for Vertex AI Imagen image edit.") if isinstance(image, dict): @@ -302,9 +310,9 @@ class VertexAIImagenImageEditConfig(BaseImageEditConfig, VertexLLM): return base64.b64decode(value) except Exception: continue - return self._read_all_bytes(value) + return self._read_all_bytes(value, depth=depth + 1, max_depth=max_depth) if "path" in image: - return self._read_all_bytes(image["path"]) + return self._read_all_bytes(image["path"], depth=depth + 1, max_depth=max_depth) if isinstance(image, bytes): return image diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 61123743c6..4e0a3e258c 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -647,7 +647,7 @@ if MCP_AVAILABLE: allowed_mcp_server_ids = ( await global_mcp_server_manager.get_allowed_mcp_servers(user_api_key_auth) ) - allowed_mcp_servers = global_mcp_server_manager.get_mcp_servers_from_ids( + allowed_mcp_servers = global_mcp_server_manager.get_mcp_servers_from_ids( # type: ignore[attr-defined] allowed_mcp_server_ids ) @@ -1173,7 +1173,7 @@ if MCP_AVAILABLE: ) ) - allowed_mcp_servers = global_mcp_server_manager.get_mcp_servers_from_ids( + allowed_mcp_servers = global_mcp_server_manager.get_mcp_servers_from_ids( # type: ignore[attr-defined] allowed_mcp_server_ids ) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index ac7082edb6..b0f13f6afb 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -22,7 +22,6 @@ from litellm.proxy.common_utils.openai_endpoint_utils import ( ) from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, - convert_b64_uid_to_unified_uid, get_batch_id_from_unified_batch_id, get_model_id_from_unified_batch_id, get_models_from_unified_file_id, diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index ddd838a3de..f2b6abfc26 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -58,7 +58,6 @@ if MCP_AVAILABLE: from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view from litellm.proxy.management_helpers.utils import management_endpoint_wrapper - from litellm.types.mcp_server.mcp_server_manager import MCPInfo def _redact_mcp_credentials( mcp_server: LiteLLM_MCPServerTable, diff --git a/litellm/proxy/prompts/prompt_endpoints.py b/litellm/proxy/prompts/prompt_endpoints.py index 8fe5adf1e2..2861e8594a 100644 --- a/litellm/proxy/prompts/prompt_endpoints.py +++ b/litellm/proxy/prompts/prompt_endpoints.py @@ -554,8 +554,6 @@ async def update_prompt( }' ``` """ - from datetime import datetime - from litellm.proxy.prompts.prompt_registry import IN_MEMORY_PROMPT_REGISTRY from litellm.proxy.proxy_server import prisma_client @@ -925,7 +923,7 @@ async def test_prompt( # Use conversation history for user/assistant messages messages = system_messages + request.conversation_history else: - messages = rendered_messages + messages = rendered_messages # type: ignore[assignment] # Use PromptTemplate's optional_params which already extracts all parameters optional_params = template.optional_params.copy() diff --git a/litellm/proxy/public_endpoints/public_endpoints.py b/litellm/proxy/public_endpoints/public_endpoints.py index 3b7abbf4f0..71c91fac6f 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -9,7 +9,6 @@ from litellm.proxy.public_endpoints.provider_create_metadata import ( ) from litellm.types.agents import AgentCard from litellm.types.mcp import MCPPublicServer -from litellm.types.mcp_server.mcp_server_manager import MCPServer from litellm.types.proxy.management_endpoints.model_management_endpoints import ( ModelGroupInfoProxy, ) diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index e0890aaef5..f8f5154e2d 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -110,7 +110,7 @@ class LiteLLM_Proxy_MCP_Handler: allowed_mcp_server_ids = ( await global_mcp_server_manager.get_allowed_mcp_servers(user_api_key_auth) ) - allowed_mcp_servers = global_mcp_server_manager.get_mcp_servers_from_ids( + allowed_mcp_servers = global_mcp_server_manager.get_mcp_servers_from_ids( # type: ignore[attr-defined] allowed_mcp_server_ids ) diff --git a/litellm/router.py b/litellm/router.py index 998e19739b..841391653d 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -835,8 +835,8 @@ class Router: litellm.acancel_batch, call_type="acancel_batch" ) - def _initialize_specialized_endpoints(self): - """Helper to initialize specialized router endpoints (vector store, OCR, search, video, container).""" + def _initialize_vector_store_endpoints(self): + """Initialize vector store endpoints.""" from litellm.vector_stores.main import acreate, asearch, create, search self.avector_store_search = self.factory_function( @@ -852,6 +852,8 @@ class Router: create, call_type="vector_store_create" ) + def _initialize_vector_store_file_endpoints(self): + """Initialize vector store file endpoints.""" from litellm.vector_store_files.main import ( acreate as avector_store_file_create_fn, ) @@ -921,6 +923,8 @@ class Router: vector_store_file_delete_fn, call_type="vector_store_file_delete" ) + def _initialize_google_genai_endpoints(self): + """Initialize Google GenAI endpoints.""" from litellm.google_genai import ( agenerate_content, agenerate_content_stream, @@ -941,6 +945,8 @@ class Router: generate_content_stream, call_type="generate_content_stream" ) + def _initialize_ocr_search_endpoints(self): + """Initialize OCR and search endpoints.""" from litellm.ocr import aocr, ocr self.aocr = self.factory_function(aocr, call_type="aocr") @@ -951,6 +957,8 @@ class Router: self.asearch = self.factory_function(asearch, call_type="asearch") self.search = self.factory_function(search, call_type="search") + def _initialize_video_endpoints(self): + """Initialize video endpoints.""" from litellm.videos import ( avideo_content, avideo_generation, @@ -989,6 +997,8 @@ class Router: ) self.video_remix = self.factory_function(video_remix, call_type="video_remix") + def _initialize_container_endpoints(self): + """Initialize container endpoints.""" from litellm.containers import ( acreate_container, adelete_container, @@ -1025,6 +1035,15 @@ class Router: delete_container, call_type="delete_container" ) + def _initialize_specialized_endpoints(self): + """Helper to initialize specialized router endpoints (vector store, OCR, search, video, container).""" + self._initialize_vector_store_endpoints() + self._initialize_vector_store_file_endpoints() + self._initialize_google_genai_endpoints() + self._initialize_ocr_search_endpoints() + self._initialize_video_endpoints() + self._initialize_container_endpoints() + def initialize_router_endpoints(self): self._initialize_core_endpoints() self._initialize_specialized_endpoints() diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 5c89413bfe..6e34bbca3a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2728,7 +2728,7 @@ class LiteLLMFineTuningJob(FineTuningJob): class LiteLLMBatch(Batch): _hidden_params: dict = {} - usage: Optional[Usage] = None + usage: Optional[Usage] = None # type: ignore[assignment] def __contains__(self, key): # Define custom behavior for the 'in' operator diff --git a/requirements.txt b/requirements.txt index 3133370a71..3a426d83e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -55,7 +55,7 @@ jinja2==3.1.6 # for prompt templates aiohttp==3.12.14 # for network calls aioboto3==13.4.0 # for async sagemaker calls tenacity==8.5.0 # for retrying requests, when litellm.num_retries set -pydantic==2.11.0 # proxy + openai req. + mcp +pydantic>=2.11,<3 # proxy + openai req. + mcp jsonschema==4.22.0 # validating json schema websockets==13.1.0 # for realtime API soundfile==0.12.1 # for audio file processing diff --git a/tests/code_coverage_tests/recursive_detector.py b/tests/code_coverage_tests/recursive_detector.py index fd98fa4fce..cac245f8d5 100644 --- a/tests/code_coverage_tests/recursive_detector.py +++ b/tests/code_coverage_tests/recursive_detector.py @@ -30,6 +30,8 @@ IGNORE_FUNCTIONS = [ "_fix_enum_empty_strings", # max depth set., "get_access_token", # max depth set., "_redact_base64", # max depth set. + "_contains_vision_content", # max depth set. + "_read_all_bytes", # max depth set. ] diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index 8971a84649..f691389de5 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -26,7 +26,7 @@ test("admin login test", async ({ page }) => { await loginButton.click(); const tabs = [ "Virtual Keys", - "Test Key", + "Playground", "Models", "Usage", "Teams", diff --git a/tests/router_unit_tests/test_router_endpoints.py b/tests/router_unit_tests/test_router_endpoints.py index 04c582f601..68931cebc9 100644 --- a/tests/router_unit_tests/test_router_endpoints.py +++ b/tests/router_unit_tests/test_router_endpoints.py @@ -872,3 +872,201 @@ def test_initialize_specialized_endpoints(): for endpoint in specialized_endpoints: assert hasattr(router, endpoint) assert callable(getattr(router, endpoint)) + + +def test_initialize_vector_store_endpoints(): + """ + Test that _initialize_vector_store_endpoints correctly sets up vector store endpoints. + """ + router = Router( + model_list=[ + { + "model_name": "test-model", + "litellm_params": { + "model": "openai/test-model", + "api_key": "fake-api-key", + }, + } + ] + ) + + router._initialize_vector_store_endpoints() + + vector_store_endpoints = [ + "avector_store_search", + "avector_store_create", + "vector_store_search", + "vector_store_create", + ] + + for endpoint in vector_store_endpoints: + assert hasattr(router, endpoint) + assert callable(getattr(router, endpoint)) + + +def test_initialize_vector_store_file_endpoints(): + """ + Test that _initialize_vector_store_file_endpoints correctly sets up vector store file endpoints. + """ + router = Router( + model_list=[ + { + "model_name": "test-model", + "litellm_params": { + "model": "openai/test-model", + "api_key": "fake-api-key", + }, + } + ] + ) + + router._initialize_vector_store_file_endpoints() + + vector_store_file_endpoints = [ + "avector_store_file_create", + "vector_store_file_create", + "avector_store_file_list", + "vector_store_file_list", + "avector_store_file_retrieve", + "vector_store_file_retrieve", + "avector_store_file_content", + "vector_store_file_content", + "avector_store_file_update", + "vector_store_file_update", + "avector_store_file_delete", + "vector_store_file_delete", + ] + + for endpoint in vector_store_file_endpoints: + assert hasattr(router, endpoint) + assert callable(getattr(router, endpoint)) + + +def test_initialize_google_genai_endpoints(): + """ + Test that _initialize_google_genai_endpoints correctly sets up Google GenAI endpoints. + """ + router = Router( + model_list=[ + { + "model_name": "test-model", + "litellm_params": { + "model": "openai/test-model", + "api_key": "fake-api-key", + }, + } + ] + ) + + router._initialize_google_genai_endpoints() + + google_genai_endpoints = [ + "agenerate_content", + "generate_content", + "agenerate_content_stream", + "generate_content_stream", + ] + + for endpoint in google_genai_endpoints: + assert hasattr(router, endpoint) + assert callable(getattr(router, endpoint)) + + +def test_initialize_ocr_search_endpoints(): + """ + Test that _initialize_ocr_search_endpoints correctly sets up OCR and search endpoints. + """ + router = Router( + model_list=[ + { + "model_name": "test-model", + "litellm_params": { + "model": "openai/test-model", + "api_key": "fake-api-key", + }, + } + ] + ) + + router._initialize_ocr_search_endpoints() + + ocr_search_endpoints = [ + "aocr", + "ocr", + "asearch", + "search", + ] + + for endpoint in ocr_search_endpoints: + assert hasattr(router, endpoint) + assert callable(getattr(router, endpoint)) + + +def test_initialize_video_endpoints(): + """ + Test that _initialize_video_endpoints correctly sets up video endpoints. + """ + router = Router( + model_list=[ + { + "model_name": "test-model", + "litellm_params": { + "model": "openai/test-model", + "api_key": "fake-api-key", + }, + } + ] + ) + + router._initialize_video_endpoints() + + video_endpoints = [ + "avideo_generation", + "video_generation", + "avideo_list", + "video_list", + "avideo_status", + "video_status", + "avideo_content", + "video_content", + "avideo_remix", + "video_remix", + ] + + for endpoint in video_endpoints: + assert hasattr(router, endpoint) + assert callable(getattr(router, endpoint)) + + +def test_initialize_container_endpoints(): + """ + Test that _initialize_container_endpoints correctly sets up container endpoints. + """ + router = Router( + model_list=[ + { + "model_name": "test-model", + "litellm_params": { + "model": "openai/test-model", + "api_key": "fake-api-key", + }, + } + ] + ) + + router._initialize_container_endpoints() + + container_endpoints = [ + "acreate_container", + "create_container", + "alist_containers", + "list_containers", + "aretrieve_container", + "retrieve_container", + "adelete_container", + "delete_container", + ] + + for endpoint in container_endpoints: + assert hasattr(router, endpoint) + assert callable(getattr(router, endpoint)) diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx index 9e0fe07c64..2c6e2848ac 100644 --- a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx @@ -1,7 +1,6 @@ +import { Drawer, List, Skeleton, Tag, Typography } from "antd"; import React, { useEffect, useState } from "react"; -import { Drawer, List, Tag, Typography, Skeleton, Button } from "antd"; import { getPromptVersions, PromptSpec } from "../../networking"; -import NotificationsManager from "../../molecules/notifications_manager"; const { Text } = Typography; @@ -70,9 +69,7 @@ const VersionHistorySidePanel: React.FC = ({ {loading ? ( ) : versions.length === 0 ? ( -
- No version history available. -
+
No version history available.
) : ( = ({
onSelectVersion?.(item)} >
- - {getVersionNumber(item.prompt_id)} - - {index === 0 && Latest} + {getVersionNumber(item.prompt_id)} + {index === 0 && ( + + Latest + + )}
{isSelected && ( @@ -101,11 +98,9 @@ const VersionHistorySidePanel: React.FC = ({ )}
- +
- - {formatDate(item.created_at)} - + {formatDate(item.created_at)} {item.prompt_info?.prompt_type === "db" ? "Saved to Database" : "Config Prompt"} @@ -120,4 +115,3 @@ const VersionHistorySidePanel: React.FC = ({ }; export default VersionHistorySidePanel; - diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts index a711a76852..4372622232 100644 --- a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts @@ -17,7 +17,7 @@ export const useConversation = (prompt: any, accessToken: string | null) => { const extractedVariables = extractVariables(prompt); const allVariablesFilled = extractedVariables.every( - (varName) => variables[varName] && variables[varName].trim() !== "" + (varName) => variables[varName] && variables[varName].trim() !== "", ); const scrollToBottom = () => { @@ -115,6 +115,7 @@ export const useConversation = (prompt: any, accessToken: string | null) => { let usage: TokenUsage | undefined; setMessages((prev) => [...prev, { role: "assistant", content: "" }]); + // eslint-disable-next-line no-constant-condition while (true) { const { done, value } = await reader.read(); if (done) break; @@ -182,10 +183,7 @@ export const useConversation = (prompt: any, accessToken: string | null) => { setMessages((prev) => { const lastMsg = prev[prev.length - 1]; if (lastMsg && lastMsg.role === "assistant" && lastMsg.content === "") { - return [ - ...prev.slice(0, -1), - { role: "assistant", content: `Error: ${error.message}` }, - ]; + return [...prev.slice(0, -1), { role: "assistant", content: `Error: ${error.message}` }]; } return [...prev, { role: "assistant", content: `Error: ${error.message}` }]; }); @@ -242,4 +240,3 @@ export const useConversation = (prompt: any, accessToken: string | null) => { handleVariableChange, }; }; - diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx index f9e76da6ec..751370cc60 100644 --- a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import ToolModal from "../tool_modal"; import NotificationsManager from "../../molecules/notifications_manager"; import { createPromptCall, updatePromptCall } from "../../networking"; @@ -25,29 +25,27 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess, } } return { - name: "New prompt", - model: "gpt-4o", - config: { - temperature: 1, - max_tokens: 1000, - }, - tools: [], - developerMessage: "", - messages: [ - { - role: "user", - content: "Enter task specifics. Use {{template_variables}} for dynamic inputs", + name: "New prompt", + model: "gpt-4o", + config: { + temperature: 1, + max_tokens: 1000, }, - ], + tools: [], + developerMessage: "", + messages: [ + { + role: "user", + content: "Enter task specifics. Use {{template_variables}} for dynamic inputs", + }, + ], }; }; const [prompt, setPrompt] = useState(getInitialPrompt()); const [editMode, setEditMode] = useState(!!initialPromptData); const [showHistoryModal, setShowHistoryModal] = useState(false); - const [activeVersionId, setActiveVersionId] = useState( - initialPromptData?.prompt_spec?.prompt_id - ); + const [activeVersionId, setActiveVersionId] = useState(initialPromptData?.prompt_spec?.prompt_id); const [showToolModal, setShowToolModal] = useState(false); const [showNameModal, setShowNameModal] = useState(false); @@ -194,8 +192,8 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess, await updatePromptCall(accessToken, initialPromptData.prompt_spec.prompt_id, promptData); NotificationsManager.success("Prompt updated successfully!"); } else { - await createPromptCall(accessToken, promptData); - NotificationsManager.success("Prompt created successfully!"); + await createPromptCall(accessToken, promptData); + NotificationsManager.success("Prompt created successfully!"); } onSuccess(); onClose(); @@ -258,9 +256,7 @@ const PromptEditorView: React.FC = ({ onClose, onSuccess,