mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge pull request #18811 from BerriAI/litellm_stable_080126
Fix cicd 080126
This commit is contained in:
commit
57b2af416a
7 changed files with 279 additions and 180 deletions
|
|
@ -31,6 +31,7 @@ from litellm.llms.base_llm.bridges.completion_transformation import (
|
|||
CompletionTransformationBridge,
|
||||
)
|
||||
from litellm.types.llms.openai import (
|
||||
ChatCompletionAnnotation,
|
||||
ChatCompletionToolParamFunctionChunk,
|
||||
Reasoning,
|
||||
ResponsesAPIOptionalRequestParams,
|
||||
|
|
@ -778,7 +779,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
@staticmethod
|
||||
def _convert_annotations_to_chat_format(
|
||||
annotations: Optional[List[Any]],
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
) -> Optional[List["ChatCompletionAnnotation"]]:
|
||||
"""
|
||||
Convert annotations from Responses API to Chat Completions format.
|
||||
|
||||
|
|
@ -788,7 +789,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if not annotations:
|
||||
return None
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
result: List[ChatCompletionAnnotation] = []
|
||||
for annotation in annotations:
|
||||
try:
|
||||
# Convert Pydantic models to dicts (handles both v1 and v2)
|
||||
|
|
@ -803,7 +804,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
verbose_logger.debug(f"Skipping unsupported annotation type: {type(annotation)}")
|
||||
continue
|
||||
|
||||
result.append(annotation_dict)
|
||||
result.append(annotation_dict) # type: ignore
|
||||
except Exception as e:
|
||||
# Skip malformed annotations
|
||||
verbose_logger.debug(f"Skipping malformed annotation: {annotation}, error: {e}")
|
||||
|
|
|
|||
|
|
@ -4800,7 +4800,7 @@ class StandardLoggingPayloadSetup:
|
|||
"""
|
||||
Extract additional header tags for spend tracking based on config.
|
||||
"""
|
||||
extra_headers: List[str] = litellm.extra_spend_tag_headers or []
|
||||
extra_headers: List[str] = getattr(litellm, "extra_spend_tag_headers", None) or []
|
||||
if not extra_headers:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -1242,7 +1242,7 @@ class TestSpendLogsPayload:
|
|||
"model": "claude-3-7-sonnet-20250219",
|
||||
"user": "",
|
||||
"team_id": "",
|
||||
"metadata": '{"applied_guardrails": [], "batch_models": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "guardrail_information": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-3-7-sonnet-20250219", "model_map_value": {"key": "claude-3-7-sonnet-20250219", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
|
||||
"metadata": '{"applied_guardrails": [], "batch_models": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "guardrail_information": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-3-7-sonnet-20250219", "model_map_value": {"key": "claude-3-7-sonnet-20250219", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": {"accepted_prediction_tokens": null, "audio_tokens": null, "reasoning_tokens": null, "rejected_prediction_tokens": null, "text_tokens": 503, "image_tokens": null}, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
|
||||
"cache_key": "Cache OFF",
|
||||
"spend": 0.01383,
|
||||
"total_tokens": 2598,
|
||||
|
|
@ -1334,7 +1334,7 @@ class TestSpendLogsPayload:
|
|||
"model": "claude-3-7-sonnet-20250219",
|
||||
"user": "",
|
||||
"team_id": "",
|
||||
"metadata": '{"applied_guardrails": [], "batch_models": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "guardrail_information": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-3-7-sonnet-20250219", "model_map_value": {"key": "claude-3-7-sonnet-20250219", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
|
||||
"metadata": '{"applied_guardrails": [], "batch_models": null, "mcp_tool_call_metadata": null, "vector_store_request_metadata": null, "guardrail_information": null, "usage_object": {"completion_tokens": 503, "prompt_tokens": 2095, "total_tokens": 2598, "completion_tokens_details": null, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}, "model_map_information": {"model_map_key": "claude-3-7-sonnet-20250219", "model_map_value": {"key": "claude-3-7-sonnet-20250219", "max_tokens": 128000, "max_input_tokens": 200000, "max_output_tokens": 128000, "input_cost_per_token": 3e-06, "cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_character": null, "input_cost_per_token_above_128k_tokens": null, "input_cost_per_token_above_200k_tokens": null, "input_cost_per_query": null, "input_cost_per_second": null, "input_cost_per_audio_token": null, "input_cost_per_token_batches": null, "output_cost_per_token_batches": null, "output_cost_per_token": 1.5e-05, "output_cost_per_audio_token": null, "output_cost_per_character": null, "output_cost_per_token_above_128k_tokens": null, "output_cost_per_character_above_128k_tokens": null, "output_cost_per_token_above_200k_tokens": null, "output_cost_per_second": null, "output_cost_per_image": null, "output_vector_size": null, "litellm_provider": "anthropic", "mode": "chat", "supports_system_messages": null, "supports_response_schema": true, "supports_vision": true, "supports_function_calling": true, "supports_tool_choice": true, "supports_assistant_prefill": true, "supports_prompt_caching": true, "supports_audio_input": false, "supports_audio_output": false, "supports_pdf_input": true, "supports_embedding_image_input": false, "supports_native_streaming": null, "supports_web_search": false, "supports_reasoning": true, "search_context_cost_per_query": null, "tpm": null, "rpm": null, "supported_openai_params": ["stream", "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens", "tools", "tool_choice", "extra_headers", "parallel_tool_calls", "response_format", "user", "reasoning_effort", "thinking"]}}, "additional_usage_values": {"completion_tokens_details": {"accepted_prediction_tokens": null, "audio_tokens": null, "reasoning_tokens": null, "rejected_prediction_tokens": null, "text_tokens": 503, "image_tokens": null}, "prompt_tokens_details": {"audio_tokens": null, "cached_tokens": 0, "text_tokens": null, "image_tokens": null}, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}',
|
||||
"cache_key": "Cache OFF",
|
||||
"spend": 0.01383,
|
||||
"total_tokens": 2598,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class TestTextFormatConversion:
|
|||
Test that when text_format parameter is passed to litellm.aresponses,
|
||||
it gets converted to text parameter in the raw API call to OpenAI.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
class TestResponse(BaseModel):
|
||||
"""Test Pydantic model for structured output"""
|
||||
|
|
@ -42,20 +42,8 @@ class TestTextFormatConversion:
|
|||
answer: str
|
||||
confidence: float
|
||||
|
||||
class MockResponse:
|
||||
"""Mock response class for testing"""
|
||||
|
||||
def __init__(self, json_data, status_code):
|
||||
self._json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.text = json.dumps(json_data)
|
||||
self.headers = {}
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
# Mock response from OpenAI
|
||||
mock_response = {
|
||||
mock_response_data = {
|
||||
"id": "resp_123",
|
||||
"object": "response",
|
||||
"created_at": 1741476542,
|
||||
|
|
@ -101,13 +89,74 @@ class TestTextFormatConversion:
|
|||
|
||||
base_completion_call_args = self.get_base_completion_call_args()
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_post:
|
||||
# Configure the mock to return our response
|
||||
mock_post.return_value = MockResponse(mock_response, 200)
|
||||
# Mock the response_api_handler function to capture the request
|
||||
captured_request = {}
|
||||
|
||||
def mock_handler(
|
||||
model,
|
||||
input,
|
||||
responses_api_provider_config,
|
||||
response_api_optional_request_params,
|
||||
custom_llm_provider,
|
||||
litellm_params,
|
||||
logging_obj,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
timeout=None,
|
||||
client=None,
|
||||
fake_stream=False,
|
||||
litellm_metadata=None,
|
||||
shared_session=None,
|
||||
_is_async=False,
|
||||
):
|
||||
# Capture the request parameters
|
||||
captured_request["model"] = model
|
||||
captured_request["input"] = input
|
||||
captured_request["params"] = response_api_optional_request_params
|
||||
|
||||
# Return a mock ResponsesAPIResponse wrapped in a coroutine if async
|
||||
async def async_response():
|
||||
return ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
object="response",
|
||||
created_at=1741476542,
|
||||
status="completed",
|
||||
model="gpt-4o",
|
||||
output=mock_response_data["output"],
|
||||
usage=ResponseAPIUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=20,
|
||||
total_tokens=30,
|
||||
),
|
||||
text=mock_response_data.get("text"),
|
||||
error=None,
|
||||
incomplete_details=None,
|
||||
)
|
||||
|
||||
if _is_async:
|
||||
return async_response()
|
||||
else:
|
||||
return ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
object="response",
|
||||
created_at=1741476542,
|
||||
status="completed",
|
||||
model="gpt-4o",
|
||||
output=mock_response_data["output"],
|
||||
usage=ResponseAPIUsage(
|
||||
input_tokens=10,
|
||||
output_tokens=20,
|
||||
total_tokens=30,
|
||||
),
|
||||
text=mock_response_data.get("text"),
|
||||
error=None,
|
||||
incomplete_details=None,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.base_llm_http_handler.response_api_handler",
|
||||
new=mock_handler,
|
||||
):
|
||||
litellm._turn_on_debug()
|
||||
litellm.set_verbose = True
|
||||
|
||||
|
|
@ -118,21 +167,19 @@ class TestTextFormatConversion:
|
|||
**base_completion_call_args,
|
||||
)
|
||||
|
||||
# Verify the request was made correctly
|
||||
mock_post.assert_called_once()
|
||||
request_body = mock_post.call_args.kwargs["json"]
|
||||
print("Request body:", json.dumps(request_body, indent=4))
|
||||
# Verify the captured request
|
||||
print("Captured request:", json.dumps(captured_request, indent=4, default=str))
|
||||
|
||||
# Validate that text_format was converted to text parameter
|
||||
assert (
|
||||
"text" in request_body
|
||||
), "text parameter should be present in request body"
|
||||
"text" in captured_request["params"]
|
||||
), "text parameter should be present in request params"
|
||||
assert (
|
||||
"text_format" not in request_body
|
||||
), "text_format should not be in request body"
|
||||
"text_format" not in captured_request["params"]
|
||||
), "text_format should not be in request params"
|
||||
|
||||
# Validate the text parameter structure
|
||||
text_param = request_body["text"]
|
||||
text_param = captured_request["params"]["text"]
|
||||
assert "format" in text_param, "text parameter should have format field"
|
||||
assert (
|
||||
text_param["format"]["type"] == "json_schema"
|
||||
|
|
@ -156,7 +203,7 @@ class TestTextFormatConversion:
|
|||
), "schema should have confidence property"
|
||||
|
||||
# Validate other request parameters
|
||||
assert request_body["input"] == "What is the capital of France?"
|
||||
assert captured_request["input"] == "What is the capital of France?"
|
||||
|
||||
# Validate the response
|
||||
print("Response:", json.dumps(response, indent=4, default=str))
|
||||
|
|
|
|||
|
|
@ -42,34 +42,45 @@ from litellm._lazy_imports import (
|
|||
|
||||
def _clear_names_from_globals(names: tuple):
|
||||
"""Clear all names from litellm globals."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
for name in names:
|
||||
if name in litellm.__dict__:
|
||||
del litellm.__dict__[name]
|
||||
if name in litellm_globals:
|
||||
del litellm_globals[name]
|
||||
|
||||
|
||||
def _clear_names_from_utils_globals(names: tuple):
|
||||
"""Clear all names from litellm.utils globals."""
|
||||
# Get the actual globals dict, not a copy
|
||||
utils_globals = sys.modules["litellm.utils"].__dict__
|
||||
for name in names:
|
||||
if name in litellm.utils.__dict__:
|
||||
del litellm.utils.__dict__[name]
|
||||
if name in utils_globals:
|
||||
del utils_globals[name]
|
||||
|
||||
|
||||
def _verify_only_requested_name_imported(name: str, all_names: tuple):
|
||||
"""Verify that only the requested name is in globals, not the others."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
for other_name in all_names:
|
||||
if other_name != name:
|
||||
assert other_name not in litellm.__dict__, f"{other_name} should not be imported when importing {name}"
|
||||
assert other_name not in litellm_globals, f"{other_name} should not be imported when importing {name}"
|
||||
|
||||
|
||||
def _verify_only_requested_name_imported_in_utils(name: str, all_names: tuple):
|
||||
"""Verify that only the requested name is in utils globals, not the others."""
|
||||
# Get the actual globals dict, not a copy
|
||||
utils_globals = sys.modules["litellm.utils"].__dict__
|
||||
for other_name in all_names:
|
||||
if other_name != name:
|
||||
assert other_name not in litellm.utils.__dict__, f"{other_name} should not be imported when importing {name}"
|
||||
assert other_name not in utils_globals, f"{other_name} should not be imported when importing {name}"
|
||||
|
||||
|
||||
def test_cost_calculator_lazy_imports():
|
||||
"""Test that all cost calculator functions can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
# Test each name individually - only that name should be imported
|
||||
for name in COST_CALCULATOR_NAMES:
|
||||
# Clear all names before importing just one
|
||||
|
|
@ -78,7 +89,7 @@ def test_cost_calculator_lazy_imports():
|
|||
func = _lazy_import_cost_calculator(name)
|
||||
assert func is not None
|
||||
assert callable(func)
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
# Verify only the requested name is in globals, not the others
|
||||
_verify_only_requested_name_imported(name, COST_CALCULATOR_NAMES)
|
||||
|
|
@ -86,6 +97,9 @@ def test_cost_calculator_lazy_imports():
|
|||
|
||||
def test_litellm_logging_lazy_imports():
|
||||
"""Test that all litellm_logging items can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
# Test each name individually - only that name should be imported
|
||||
for name in LITELLM_LOGGING_NAMES:
|
||||
# Clear all names before importing just one
|
||||
|
|
@ -93,7 +107,7 @@ def test_litellm_logging_lazy_imports():
|
|||
|
||||
item = _lazy_import_litellm_logging(name)
|
||||
assert item is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
# Verify only the requested name is in globals, not the others
|
||||
_verify_only_requested_name_imported(name, LITELLM_LOGGING_NAMES)
|
||||
|
|
@ -101,6 +115,9 @@ def test_litellm_logging_lazy_imports():
|
|||
|
||||
def test_utils_lazy_imports():
|
||||
"""Test that all utils functions can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
# Test each name individually - only that name should be imported
|
||||
for name in UTILS_NAMES:
|
||||
# Clear all names before importing just one
|
||||
|
|
@ -108,7 +125,7 @@ def test_utils_lazy_imports():
|
|||
|
||||
attr = _lazy_import_utils(name)
|
||||
assert attr is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
# Verify only the requested name is in globals, not the others
|
||||
_verify_only_requested_name_imported(name, UTILS_NAMES)
|
||||
|
|
@ -116,6 +133,9 @@ def test_utils_lazy_imports():
|
|||
|
||||
def test_caching_lazy_imports():
|
||||
"""Test that all caching classes can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
# Test each name individually - only that name should be imported
|
||||
for name in CACHING_NAMES:
|
||||
# Clear all names before importing just one
|
||||
|
|
@ -123,7 +143,7 @@ def test_caching_lazy_imports():
|
|||
|
||||
cls = _lazy_import_caching(name)
|
||||
assert cls is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
# Verify only the requested name is in globals, not the others
|
||||
_verify_only_requested_name_imported(name, CACHING_NAMES)
|
||||
|
|
@ -131,71 +151,89 @@ def test_caching_lazy_imports():
|
|||
|
||||
def test_token_counter_lazy_imports():
|
||||
"""Test that token counter utilities can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in TOKEN_COUNTER_NAMES:
|
||||
_clear_names_from_globals(TOKEN_COUNTER_NAMES)
|
||||
|
||||
func = _lazy_import_token_counter(name)
|
||||
assert func is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
_verify_only_requested_name_imported(name, TOKEN_COUNTER_NAMES)
|
||||
|
||||
|
||||
def test_bedrock_types_lazy_imports():
|
||||
"""Test that Bedrock type aliases can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in BEDROCK_TYPES_NAMES:
|
||||
_clear_names_from_globals(BEDROCK_TYPES_NAMES)
|
||||
|
||||
alias = _lazy_import_bedrock_types(name)
|
||||
assert alias is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
_verify_only_requested_name_imported(name, BEDROCK_TYPES_NAMES)
|
||||
|
||||
|
||||
def test_types_utils_lazy_imports():
|
||||
"""Test that common types.utils symbols can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in TYPES_UTILS_NAMES:
|
||||
_clear_names_from_globals(TYPES_UTILS_NAMES)
|
||||
|
||||
obj = _lazy_import_types_utils(name)
|
||||
assert obj is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
_verify_only_requested_name_imported(name, TYPES_UTILS_NAMES)
|
||||
|
||||
|
||||
def test_llm_client_cache_lazy_imports():
|
||||
"""Test that LLM client cache class and singleton can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in LLM_CLIENT_CACHE_NAMES:
|
||||
_clear_names_from_globals(LLM_CLIENT_CACHE_NAMES)
|
||||
|
||||
obj = _lazy_import_llm_client_cache(name)
|
||||
assert obj is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
_verify_only_requested_name_imported(name, LLM_CLIENT_CACHE_NAMES)
|
||||
|
||||
|
||||
def test_http_handler_lazy_imports():
|
||||
"""Test that HTTP handler singletons can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in HTTP_HANDLER_NAMES:
|
||||
_clear_names_from_globals(HTTP_HANDLER_NAMES)
|
||||
|
||||
handler = _lazy_import_http_handlers(name)
|
||||
assert handler is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
_verify_only_requested_name_imported(name, HTTP_HANDLER_NAMES)
|
||||
|
||||
|
||||
def test_dotprompt_lazy_imports():
|
||||
"""Test that dotprompt globals can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in DOTPROMPT_NAMES:
|
||||
_clear_names_from_globals(DOTPROMPT_NAMES)
|
||||
|
||||
obj = _lazy_import_dotprompt(name)
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
# Only the setter must be callable; others may be None by default
|
||||
if name == "set_global_prompt_directory":
|
||||
|
|
@ -245,12 +283,15 @@ def test_unknown_attribute_raises_error():
|
|||
|
||||
def test_llm_config_lazy_imports():
|
||||
"""Test that LLM config classes can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in LLM_CONFIG_NAMES:
|
||||
_clear_names_from_globals(LLM_CONFIG_NAMES)
|
||||
|
||||
obj = _lazy_import_llm_configs(name)
|
||||
assert obj is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
# Config classes should be classes/types
|
||||
assert isinstance(obj, type), f"{name} should be a class"
|
||||
|
||||
|
|
@ -259,12 +300,15 @@ def test_llm_config_lazy_imports():
|
|||
|
||||
def test_types_lazy_imports():
|
||||
"""Test that type classes can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in TYPES_NAMES:
|
||||
_clear_names_from_globals(TYPES_NAMES)
|
||||
|
||||
obj = _lazy_import_types(name)
|
||||
assert obj is not None
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
# Type classes should be classes/types
|
||||
assert isinstance(obj, type), f"{name} should be a class"
|
||||
|
||||
|
|
@ -273,25 +317,31 @@ def test_types_lazy_imports():
|
|||
|
||||
def test_llm_provider_logic_lazy_imports():
|
||||
"""Test that LLM provider logic functions can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
litellm_globals = sys.modules["litellm"].__dict__
|
||||
|
||||
for name in LLM_PROVIDER_LOGIC_NAMES:
|
||||
_clear_names_from_globals(LLM_PROVIDER_LOGIC_NAMES)
|
||||
|
||||
func = _lazy_import_llm_provider_logic(name)
|
||||
assert func is not None
|
||||
assert callable(func)
|
||||
assert name in litellm.__dict__
|
||||
assert name in litellm_globals
|
||||
|
||||
_verify_only_requested_name_imported(name, LLM_PROVIDER_LOGIC_NAMES)
|
||||
|
||||
|
||||
def test_utils_module_lazy_imports():
|
||||
"""Test that utils module attributes can be lazy imported."""
|
||||
# Get the actual globals dict, not a copy
|
||||
utils_globals = sys.modules["litellm.utils"].__dict__
|
||||
|
||||
for name in UTILS_MODULE_NAMES:
|
||||
_clear_names_from_utils_globals(UTILS_MODULE_NAMES)
|
||||
|
||||
obj = _lazy_import_utils_module(name)
|
||||
assert obj is not None
|
||||
assert name in litellm.utils.__dict__
|
||||
assert name in utils_globals
|
||||
|
||||
_verify_only_requested_name_imported_in_utils(name, UTILS_MODULE_NAMES)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ class TestIsEncryptedResponseId:
|
|||
|
||||
def test_is_encrypted_response_id_valid(self, responses_id_security):
|
||||
"""Test that a properly encrypted response ID is identified correctly"""
|
||||
with patch(
|
||||
"litellm.proxy.hooks.responses_id_security.decrypt_value_helper"
|
||||
# Patch at the module level where it's imported
|
||||
import litellm.proxy.hooks.responses_id_security as responses_module
|
||||
|
||||
with patch.object(
|
||||
responses_module, "decrypt_value_helper"
|
||||
) as mock_decrypt:
|
||||
mock_decrypt.return_value = f"{SpecialEnums.LITELM_MANAGED_FILE_ID_PREFIX.value}response_id:resp_123;user_id:user-456"
|
||||
|
||||
|
|
@ -56,8 +59,11 @@ class TestIsEncryptedResponseId:
|
|||
|
||||
def test_is_encrypted_response_id_invalid(self, responses_id_security):
|
||||
"""Test that an unencrypted response ID returns False"""
|
||||
with patch(
|
||||
"litellm.proxy.hooks.responses_id_security.decrypt_value_helper"
|
||||
# Patch at the module level where it's imported
|
||||
import litellm.proxy.hooks.responses_id_security as responses_module
|
||||
|
||||
with patch.object(
|
||||
responses_module, "decrypt_value_helper"
|
||||
) as mock_decrypt:
|
||||
mock_decrypt.return_value = None
|
||||
|
||||
|
|
@ -71,8 +77,11 @@ class TestDecryptResponseId:
|
|||
|
||||
def test_decrypt_response_id_valid(self, responses_id_security):
|
||||
"""Test decrypting a valid encrypted response ID"""
|
||||
with patch(
|
||||
"litellm.proxy.hooks.responses_id_security.decrypt_value_helper"
|
||||
# Patch at the module level where it's imported
|
||||
import litellm.proxy.hooks.responses_id_security as responses_module
|
||||
|
||||
with patch.object(
|
||||
responses_module, "decrypt_value_helper"
|
||||
) as mock_decrypt:
|
||||
mock_decrypt.return_value = f"{SpecialEnums.LITELM_MANAGED_FILE_ID_PREFIX.value}response_id:resp_original_123;user_id:user-456;team_id:team-789"
|
||||
|
||||
|
|
@ -86,8 +95,11 @@ class TestDecryptResponseId:
|
|||
|
||||
def test_decrypt_response_id_no_encryption(self, responses_id_security):
|
||||
"""Test decrypting a non-encrypted response ID"""
|
||||
with patch(
|
||||
"litellm.proxy.hooks.responses_id_security.decrypt_value_helper"
|
||||
# Patch at the module level where it's imported
|
||||
import litellm.proxy.hooks.responses_id_security as responses_module
|
||||
|
||||
with patch.object(
|
||||
responses_module, "decrypt_value_helper"
|
||||
) as mock_decrypt:
|
||||
mock_decrypt.return_value = None
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
|||
from litellm.llms.gemini.videos.transformation import GeminiVideoConfig
|
||||
from litellm.llms.openai.videos.transformation import OpenAIVideoConfig
|
||||
from litellm.types.videos.main import VideoObject, VideoResponse
|
||||
from litellm.videos import main as videos_main
|
||||
from litellm.videos.main import (
|
||||
avideo_generation,
|
||||
avideo_status,
|
||||
|
|
@ -31,32 +32,29 @@ class TestVideoGeneration:
|
|||
|
||||
def test_video_generation_basic(self):
|
||||
"""Test basic video generation functionality."""
|
||||
# Mock the video generation response
|
||||
mock_response = VideoObject(
|
||||
id="video_123",
|
||||
object="video",
|
||||
status="queued",
|
||||
created_at=1712697600,
|
||||
# Use mock_response parameter for reliable testing
|
||||
response = video_generation(
|
||||
prompt="Show them running around the room",
|
||||
model="sora-2",
|
||||
seconds="8",
|
||||
size="720x1280",
|
||||
seconds="8"
|
||||
mock_response={
|
||||
"id": "video_123",
|
||||
"object": "video",
|
||||
"status": "queued",
|
||||
"created_at": 1712697600,
|
||||
"model": "sora-2",
|
||||
"size": "720x1280",
|
||||
"seconds": "8"
|
||||
}
|
||||
)
|
||||
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_generation_handler.return_value = mock_response
|
||||
|
||||
response = video_generation(
|
||||
prompt="Show them running around the room",
|
||||
model="sora-2",
|
||||
seconds="8",
|
||||
size="720x1280"
|
||||
)
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_123"
|
||||
assert response.model == "sora-2"
|
||||
assert response.size == "720x1280"
|
||||
assert response.seconds == "8"
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_123"
|
||||
assert response.status == "queued"
|
||||
assert response.model == "sora-2"
|
||||
assert response.size == "720x1280"
|
||||
assert response.seconds == "8"
|
||||
|
||||
def test_video_generation_with_mock_response(self):
|
||||
"""Test video generation with mock response."""
|
||||
|
|
@ -97,26 +95,27 @@ class TestVideoGeneration:
|
|||
progress=50
|
||||
)
|
||||
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_generation_handler.return_value = mock_response
|
||||
|
||||
import asyncio
|
||||
|
||||
async def test_async():
|
||||
response = await avideo_generation(
|
||||
prompt="A cat playing with a ball",
|
||||
model="sora-2",
|
||||
seconds="5",
|
||||
size="720x1280"
|
||||
)
|
||||
return response
|
||||
|
||||
response = asyncio.run(test_async())
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_async_123"
|
||||
assert response.status == "processing"
|
||||
assert response.progress == 50
|
||||
# Mock the async_video_generation_handler to return the mock_response
|
||||
async_mock = AsyncMock(return_value=mock_response)
|
||||
with patch.object(videos_main.base_llm_http_handler, 'async_video_generation_handler', async_mock):
|
||||
with patch.object(videos_main.base_llm_http_handler, 'video_generation_handler', side_effect=lambda **kwargs: async_mock(**kwargs)):
|
||||
import asyncio
|
||||
|
||||
async def test_async():
|
||||
response = await avideo_generation(
|
||||
prompt="A cat playing with a ball",
|
||||
model="sora-2",
|
||||
seconds="5",
|
||||
size="720x1280"
|
||||
)
|
||||
return response
|
||||
|
||||
response = asyncio.run(test_async())
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_async_123"
|
||||
assert response.status == "processing"
|
||||
assert response.progress == 50
|
||||
|
||||
def test_video_generation_parameter_validation(self):
|
||||
"""Test video generation parameter validation."""
|
||||
|
|
@ -132,9 +131,7 @@ class TestVideoGeneration:
|
|||
|
||||
def test_video_generation_error_handling(self):
|
||||
"""Test video generation error handling."""
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_generation_handler.side_effect = Exception("API Error")
|
||||
|
||||
with patch.object(videos_main.base_llm_http_handler, 'video_generation_handler', side_effect=Exception("API Error")):
|
||||
with pytest.raises(Exception):
|
||||
video_generation(
|
||||
prompt="Test video",
|
||||
|
|
@ -443,32 +440,28 @@ class TestVideoGeneration:
|
|||
|
||||
def test_video_status_basic(self):
|
||||
"""Test basic video status functionality."""
|
||||
# Mock the video status response
|
||||
mock_response = VideoObject(
|
||||
id="video_123",
|
||||
object="video",
|
||||
status="completed",
|
||||
created_at=1712697600,
|
||||
completed_at=1712697660,
|
||||
# Use mock_response parameter for reliable testing
|
||||
response = video_status(
|
||||
video_id="video_123",
|
||||
model="sora-2",
|
||||
progress=100,
|
||||
size="720x1280",
|
||||
seconds="8"
|
||||
mock_response={
|
||||
"id": "video_123",
|
||||
"object": "video",
|
||||
"status": "completed",
|
||||
"created_at": 1712697600,
|
||||
"completed_at": 1712697660,
|
||||
"model": "sora-2",
|
||||
"progress": 100,
|
||||
"size": "720x1280",
|
||||
"seconds": "8"
|
||||
}
|
||||
)
|
||||
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_status_handler.return_value = mock_response
|
||||
|
||||
response = video_status(
|
||||
video_id="video_123",
|
||||
model="sora-2"
|
||||
)
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_123"
|
||||
assert response.status == "completed"
|
||||
assert response.progress == 100
|
||||
assert response.model == "sora-2"
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_123"
|
||||
assert response.status == "completed"
|
||||
assert response.progress == 100
|
||||
assert response.model == "sora-2"
|
||||
|
||||
def test_video_status_with_mock_response(self):
|
||||
"""Test video status with mock response."""
|
||||
|
|
@ -506,24 +499,25 @@ class TestVideoGeneration:
|
|||
progress=0
|
||||
)
|
||||
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_status_handler.return_value = mock_response
|
||||
|
||||
import asyncio
|
||||
|
||||
async def test_async():
|
||||
response = await avideo_status(
|
||||
video_id="video_async_123",
|
||||
model="sora-2"
|
||||
)
|
||||
return response
|
||||
|
||||
response = asyncio.run(test_async())
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_async_123"
|
||||
assert response.status == "queued"
|
||||
assert response.progress == 0
|
||||
# Mock the async_video_status_handler to return the mock_response
|
||||
async_mock = AsyncMock(return_value=mock_response)
|
||||
with patch.object(videos_main.base_llm_http_handler, 'async_video_status_handler', async_mock):
|
||||
with patch.object(videos_main.base_llm_http_handler, 'video_status_handler', side_effect=lambda **kwargs: async_mock(**kwargs)):
|
||||
import asyncio
|
||||
|
||||
async def test_async():
|
||||
response = await avideo_status(
|
||||
video_id="video_async_123",
|
||||
model="sora-2"
|
||||
)
|
||||
return response
|
||||
|
||||
response = asyncio.run(test_async())
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_async_123"
|
||||
assert response.status == "queued"
|
||||
assert response.progress == 0
|
||||
|
||||
def test_video_status_parameter_validation(self):
|
||||
"""Test video status parameter validation."""
|
||||
|
|
@ -539,9 +533,7 @@ class TestVideoGeneration:
|
|||
|
||||
def test_video_status_error_handling(self):
|
||||
"""Test video status error handling."""
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_status_handler.side_effect = Exception("API Error")
|
||||
|
||||
with patch.object(videos_main.base_llm_http_handler, 'video_status_handler', side_effect=Exception("API Error")):
|
||||
with pytest.raises(Exception):
|
||||
video_status(
|
||||
video_id="test_video_id",
|
||||
|
|
@ -672,33 +664,30 @@ class TestVideoGeneration:
|
|||
|
||||
def test_video_status_async_inside_async_function(self):
|
||||
"""Test that sync video_status works inside async functions (no asyncio.run issues)."""
|
||||
mock_response = VideoObject(
|
||||
id="video_sync_in_async",
|
||||
object="video",
|
||||
status="completed",
|
||||
created_at=1712697600,
|
||||
model="sora-2",
|
||||
progress=100
|
||||
)
|
||||
import asyncio
|
||||
|
||||
with patch('litellm.videos.main.base_llm_http_handler') as mock_handler:
|
||||
mock_handler.video_status_handler.return_value = mock_response
|
||||
|
||||
import asyncio
|
||||
|
||||
async def test_sync_in_async():
|
||||
# This should work without asyncio.run() issues
|
||||
response = video_status(
|
||||
video_id="video_sync_in_async",
|
||||
model="sora-2"
|
||||
)
|
||||
return response
|
||||
|
||||
response = asyncio.run(test_sync_in_async())
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_sync_in_async"
|
||||
assert response.status == "completed"
|
||||
async def test_sync_in_async():
|
||||
# This should work without asyncio.run() issues
|
||||
# Use mock_response parameter for reliable testing
|
||||
response = video_status(
|
||||
video_id="video_sync_in_async",
|
||||
model="sora-2",
|
||||
mock_response={
|
||||
"id": "video_sync_in_async",
|
||||
"object": "video",
|
||||
"status": "completed",
|
||||
"created_at": 1712697600,
|
||||
"model": "sora-2",
|
||||
"progress": 100
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
response = asyncio.run(test_sync_in_async())
|
||||
|
||||
assert isinstance(response, VideoObject)
|
||||
assert response.id == "video_sync_in_async"
|
||||
assert response.status == "completed"
|
||||
|
||||
def test_video_status_url_construction(self):
|
||||
"""Test video status URL construction."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue