diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 585454f944b..af8185aa215 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -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}") diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 5448fe7c771..ab55022f8ce 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -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 diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index 56bba39e6c3..57ec019e420 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -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, diff --git a/tests/test_litellm/responses/test_text_format_conversion.py b/tests/test_litellm/responses/test_text_format_conversion.py index 645f0f2e148..c7a79d9c461 100644 --- a/tests/test_litellm/responses/test_text_format_conversion.py +++ b/tests/test_litellm/responses/test_text_format_conversion.py @@ -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)) diff --git a/tests/test_litellm/test_lazy_imports.py b/tests/test_litellm/test_lazy_imports.py index 660933efac5..48d78c0b01b 100644 --- a/tests/test_litellm/test_lazy_imports.py +++ b/tests/test_litellm/test_lazy_imports.py @@ -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) diff --git a/tests/test_litellm/test_responses_id_security.py b/tests/test_litellm/test_responses_id_security.py index 6b04479326e..2addf504f7c 100644 --- a/tests/test_litellm/test_responses_id_security.py +++ b/tests/test_litellm/test_responses_id_security.py @@ -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 diff --git a/tests/test_litellm/test_video_generation.py b/tests/test_litellm/test_video_generation.py index 87012f05155..73bfa71d20b 100644 --- a/tests/test_litellm/test_video_generation.py +++ b/tests/test_litellm/test_video_generation.py @@ -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."""