mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-18 04:28:19 +00:00
[Infra] CI/CD Fixes (#16937)
* Attempt CI/CD Fix * Adding test for coverage * Adding max depth to copilot and vertex * Fixing mypy lint and docker database * Fixing UI build issues * Update playwright test
This commit is contained in:
@@ -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 && \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+21
-2
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ test("admin login test", async ({ page }) => {
|
||||
await loginButton.click();
|
||||
const tabs = [
|
||||
"Virtual Keys",
|
||||
"Test Key",
|
||||
"Playground",
|
||||
"Models",
|
||||
"Usage",
|
||||
"Teams",
|
||||
|
||||
@@ -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))
|
||||
|
||||
+11
-17
@@ -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<VersionHistorySidePanelProps> = ({
|
||||
{loading ? (
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
) : versions.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No version history available.
|
||||
</div>
|
||||
<div className="text-center py-8 text-gray-500">No version history available.</div>
|
||||
) : (
|
||||
<List
|
||||
dataSource={versions}
|
||||
@@ -82,18 +79,18 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
|
||||
<div
|
||||
key={item.prompt_id}
|
||||
className={`mb-4 p-4 rounded-lg border cursor-pointer transition-all hover:shadow-md ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200 bg-white hover:border-blue-300"
|
||||
isSelected ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white hover:border-blue-300"
|
||||
}`}
|
||||
onClick={() => onSelectVersion?.(item)}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="m-0">
|
||||
{getVersionNumber(item.prompt_id)}
|
||||
</Tag>
|
||||
{index === 0 && <Tag color="blue" className="m-0">Latest</Tag>}
|
||||
<Tag className="m-0">{getVersionNumber(item.prompt_id)}</Tag>
|
||||
{index === 0 && (
|
||||
<Tag color="blue" className="m-0">
|
||||
Latest
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Tag color="green" className="m-0">
|
||||
@@ -101,11 +98,9 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text className="text-sm text-gray-600 font-medium">
|
||||
{formatDate(item.created_at)}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600 font-medium">{formatDate(item.created_at)}</Text>
|
||||
<Text type="secondary" className="text-xs">
|
||||
{item.prompt_info?.prompt_type === "db" ? "Saved to Database" : "Config Prompt"}
|
||||
</Text>
|
||||
@@ -120,4 +115,3 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
|
||||
};
|
||||
|
||||
export default VersionHistorySidePanel;
|
||||
|
||||
|
||||
+3
-6
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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<PromptEditorViewProps> = ({ 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<PromptType>(getInitialPrompt());
|
||||
const [editMode, setEditMode] = useState<boolean>(!!initialPromptData);
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||
const [activeVersionId, setActiveVersionId] = useState<string | undefined>(
|
||||
initialPromptData?.prompt_spec?.prompt_id
|
||||
);
|
||||
const [activeVersionId, setActiveVersionId] = useState<string | undefined>(initialPromptData?.prompt_spec?.prompt_id);
|
||||
|
||||
const [showToolModal, setShowToolModal] = useState(false);
|
||||
const [showNameModal, setShowNameModal] = useState(false);
|
||||
@@ -194,8 +192,8 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ 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<PromptEditorViewProps> = ({ onClose, onSuccess,
|
||||
<div className="ml-auto inline-flex items-center bg-gray-200 rounded-full p-0.5">
|
||||
<button
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full transition-colors ${
|
||||
viewMode === "pretty"
|
||||
? "bg-white text-gray-900 shadow-sm"
|
||||
: "text-gray-600"
|
||||
viewMode === "pretty" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600"
|
||||
}`}
|
||||
onClick={() => setViewMode("pretty")}
|
||||
>
|
||||
@@ -268,9 +264,7 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-1 text-xs font-medium rounded-full transition-colors ${
|
||||
viewMode === "dotprompt"
|
||||
? "bg-white text-gray-900 shadow-sm"
|
||||
: "text-gray-600"
|
||||
viewMode === "dotprompt" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600"
|
||||
}`}
|
||||
onClick={() => setViewMode("dotprompt")}
|
||||
>
|
||||
@@ -346,4 +340,3 @@ const PromptEditorView: React.FC<PromptEditorViewProps> = ({ onClose, onSuccess,
|
||||
};
|
||||
|
||||
export default PromptEditorView;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user