diff --git a/ruff-tests.toml b/ruff-tests.toml index c760a53ed91..8e90d6432df 100644 --- a/ruff-tests.toml +++ b/ruff-tests.toml @@ -14,6 +14,9 @@ # B018 a bare attribute access or literal, usually a call missing its parens # PLW0127 `x = x` self-assignment, dead code that reads like a narrowing or a fixup # PLR0133 comparison of two constants, e.g. `assert True == True` +# B017 `pytest.raises(Exception)` accepts the TypeError a refactor introduced just as +# readily as the rejection under test, so a crash reads as a pass. Narrow to the +# real type, or add `match=` where the code genuinely raises a bare Exception # # No target-version here on purpose: it resolves from requires-python (>=3.10), so # 3.11-only builtins like BaseExceptionGroup are correctly flagged in a tree that @@ -21,4 +24,4 @@ line-length = 120 -lint.select = ["F821", "B011", "B015", "B018", "PT015", "PLR0133", "PLW0127"] +lint.select = ["F821", "B011", "B015", "B017", "B018", "PT015", "PLR0133", "PLW0127"] diff --git a/tests/guardrails_tests/test_bedrock_guardrails.py b/tests/guardrails_tests/test_bedrock_guardrails.py index 823ee05839f..c7a5b79bbce 100644 --- a/tests/guardrails_tests/test_bedrock_guardrails.py +++ b/tests/guardrails_tests/test_bedrock_guardrails.py @@ -197,6 +197,7 @@ async def test_bedrock_guardrails_block_responses_api(): @pytest.mark.asyncio async def test_bedrock_guardrails_with_streaming(): + from fastapi import HTTPException from litellm.proxy.utils import ProxyLogging from litellm.types.guardrails import GuardrailEventHooks @@ -204,7 +205,7 @@ async def test_bedrock_guardrails_with_streaming(): mock_user_api_key_cache = MagicMock(spec=DualCache) mock_user_api_key_dict = UserAPIKeyAuth() - with pytest.raises(Exception): # Assert that this raises an exception + with pytest.raises(HTTPException): proxy_logging_obj = ProxyLogging( user_api_key_cache=mock_user_api_key_cache, premium_user=True, diff --git a/tests/guardrails_tests/test_eu_ai_act_article5.py b/tests/guardrails_tests/test_eu_ai_act_article5.py index bda7bf6f517..0903e6c5416 100644 --- a/tests/guardrails_tests/test_eu_ai_act_article5.py +++ b/tests/guardrails_tests/test_eu_ai_act_article5.py @@ -20,6 +20,7 @@ from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_fil from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import ( ContentFilterCategoryConfig, ) +from fastapi import HTTPException # Test cases: (sentence, expected_result, reason) @@ -275,7 +276,7 @@ class TestEUAIActEdgeCases: for sentence in sentences: request_data = {"messages": [{"role": "user", "content": sentence}]} - with pytest.raises(Exception): + with pytest.raises(HTTPException): await content_filter_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, @@ -289,7 +290,7 @@ class TestEUAIActEdgeCases: request_data = {"messages": [{"role": "user", "content": sentence}]} # Should block (contains multiple violations) - with pytest.raises(Exception): + with pytest.raises(HTTPException): await content_filter_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, diff --git a/tests/guardrails_tests/test_eu_ai_act_french_3_scenarios.py b/tests/guardrails_tests/test_eu_ai_act_french_3_scenarios.py index bc121330a45..6b9774d9cde 100644 --- a/tests/guardrails_tests/test_eu_ai_act_french_3_scenarios.py +++ b/tests/guardrails_tests/test_eu_ai_act_french_3_scenarios.py @@ -19,6 +19,7 @@ from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_fil from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import ( ContentFilterCategoryConfig, ) +from fastapi import HTTPException @pytest.fixture @@ -228,7 +229,7 @@ class TestFrenchEdgeCases: request_data = {"messages": [{"role": "user", "content": sentence}]} # Should block (contains "build" and "système de crédit social") - with pytest.raises(Exception): + with pytest.raises(HTTPException): await content_filter_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, @@ -257,7 +258,7 @@ class TestFrenchEdgeCases: request_data = {"messages": [{"role": "user", "content": sentence}]} # Should block (case-insensitive) - with pytest.raises(Exception): + with pytest.raises(HTTPException): await content_filter_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, diff --git a/tests/guardrails_tests/test_semantic_guard.py b/tests/guardrails_tests/test_semantic_guard.py index a7e6230d029..c9f4a902895 100644 --- a/tests/guardrails_tests/test_semantic_guard.py +++ b/tests/guardrails_tests/test_semantic_guard.py @@ -10,6 +10,7 @@ sys.path.insert(0, os.path.abspath("../..")) from unittest.mock import MagicMock import pytest +from fastapi import HTTPException class TestRouteLoader: @@ -307,7 +308,7 @@ class TestContentFilterSqlInjectionTemplate: @pytest.mark.asyncio async def test_sql_always_block(self, sql_injection_guardrail, sentence, reason): request_data = {"messages": [{"role": "user", "content": sentence}]} - with pytest.raises(Exception): + with pytest.raises(HTTPException): await sql_injection_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, @@ -343,7 +344,7 @@ class TestContentFilterSqlInjectionTemplate: self, sql_injection_guardrail, sentence, reason ): request_data = {"messages": [{"role": "user", "content": sentence}]} - with pytest.raises(Exception): + with pytest.raises(HTTPException): await sql_injection_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, @@ -552,7 +553,7 @@ class TestContentFilterPromptInjectionTemplate: @pytest.mark.asyncio async def test_always_block(self, content_filter_guardrail, sentence, reason): request_data = {"messages": [{"role": "user", "content": sentence}]} - with pytest.raises(Exception): + with pytest.raises(HTTPException): await content_filter_guardrail.apply_guardrail( inputs={"texts": [sentence]}, request_data=request_data, diff --git a/tests/image_gen_tests/test_image_generation.py b/tests/image_gen_tests/test_image_generation.py index ad141a651e8..9047557c493 100644 --- a/tests/image_gen_tests/test_image_generation.py +++ b/tests/image_gen_tests/test_image_generation.py @@ -444,7 +444,7 @@ async def test_azure_image_generation_request_body(): ) as mock_post: mock_post.side_effect = Exception("test") - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await aimage_generation( model="azure/gpt-image-1", prompt="test prompt", diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index 697c3837602..2b539b97c9b 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -1334,7 +1334,7 @@ def test_validate_chat_completion_user_messages(messages, expected_bool): validate_chat_completion_user_messages(messages=messages) else: ## Invalid message - with pytest.raises(Exception): + with pytest.raises(Exception, match="Invalid user message at index 0"): validate_chat_completion_user_messages(messages=messages) @@ -1354,7 +1354,7 @@ def test_validate_chat_completion_tool_choice(tool_choice, expected_bool): if expected_bool: validate_chat_completion_tool_choice(tool_choice=tool_choice) else: - with pytest.raises(Exception): + with pytest.raises(Exception, match="Invalid tool choice"): validate_chat_completion_tool_choice(tool_choice=tool_choice) diff --git a/tests/llm_responses_api_testing/base_responses_api.py b/tests/llm_responses_api_testing/base_responses_api.py index f5751aa79e8..d5057944ba7 100644 --- a/tests/llm_responses_api_testing/base_responses_api.py +++ b/tests/llm_responses_api_testing/base_responses_api.py @@ -28,6 +28,7 @@ from openai.types.responses.response_create_params import ( ResponseInputParam, ) from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler +import openai def validate_responses_api_response(response, final_chunk: bool = False): @@ -700,12 +701,12 @@ class BaseResponsesAPITest(ABC): base_completion_call_args = self.get_base_completion_call_args() if sync_mode: - with pytest.raises(Exception): + with pytest.raises(openai.APIError): litellm.cancel_responses( response_id="invalid_response_id_12345", **base_completion_call_args ) else: - with pytest.raises(Exception): + with pytest.raises(openai.APIError): await litellm.acancel_responses( response_id="invalid_response_id_12345", **base_completion_call_args ) diff --git a/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py b/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py index 5683d973ac9..cc493cc5a28 100644 --- a/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py +++ b/tests/llm_translation/test_llm_response_utils/test_convert_dict_to_chat_completion.py @@ -1243,7 +1243,7 @@ def test_convert_to_model_response_object_with_error_code_only(): }, } - with pytest.raises(Exception): + with pytest.raises(Exception) as exc_info: # noqa: B017 # bare Exception raised, so status_code is the assertion convert_to_model_response_object( model_response_object=ModelResponse(), response_object=response_object, @@ -1255,6 +1255,8 @@ def test_convert_to_model_response_object_with_error_code_only(): convert_tool_call_to_json_mode=False, ) + assert exc_info.value.status_code == 500 + def test_model_prefix_preservation(): """ @@ -2473,14 +2475,14 @@ class TestConvertToModelResponseObjectCompletion: assert "reasoning_content" not in (message.provider_specific_fields or {}) def test_response_none_raises(self): - with pytest.raises(Exception): + with pytest.raises(Exception, match="Invalid response object"): convert_to_model_response_object( response_object=None, model_response_object=ModelResponse(), ) def test_model_response_none_raises(self): - with pytest.raises(Exception): + with pytest.raises(Exception, match="Invalid response object"): convert_to_model_response_object( response_object={ "choices": [ diff --git a/tests/llm_translation/test_openai.py b/tests/llm_translation/test_openai.py index 10ed5f1ef68..405dbb0e6ec 100644 --- a/tests/llm_translation/test_openai.py +++ b/tests/llm_translation/test_openai.py @@ -1458,7 +1458,7 @@ def test_responses_gpt54_with_xhigh_reasoning(): # Stop execution right after request generation to avoid external API calls. mock_responses.side_effect = RuntimeError("stop_after_request_build") - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): litellm.completion( model="openai/responses/gpt-5.4", messages=[{"role": "user", "content": "What is 2+2?"}], diff --git a/tests/local_testing/test_get_llm_provider.py b/tests/local_testing/test_get_llm_provider.py index 4c3e13da17a..0e667b82a66 100644 --- a/tests/local_testing/test_get_llm_provider.py +++ b/tests/local_testing/test_get_llm_provider.py @@ -569,5 +569,5 @@ class TestClaudeModelPatternMatching: ) set_fallback_generalizations([]) - with pytest.raises(Exception): + with pytest.raises(litellm.BadRequestError): litellm.get_llm_provider(model="claude-opus-4-9") diff --git a/tests/openai_endpoints_tests/test_e2e_openai_responses_api.py b/tests/openai_endpoints_tests/test_e2e_openai_responses_api.py index 220a44f0792..be565972b94 100644 --- a/tests/openai_endpoints_tests/test_e2e_openai_responses_api.py +++ b/tests/openai_endpoints_tests/test_e2e_openai_responses_api.py @@ -1,5 +1,5 @@ import httpx -from openai import OpenAI, BadRequestError +from openai import OpenAI, BadRequestError, APIStatusError import pytest @@ -87,7 +87,7 @@ def test_basic_response(): print("DELETE response=", delete_response) # expect an error when getting the response again since it was deleted - with pytest.raises(Exception): + with pytest.raises(APIStatusError): get_response = client.responses.retrieve(response.id) @@ -195,6 +195,6 @@ def test_cancel_streaming_response(): def test_cancel_invalid_response_id(): client = get_test_client() - with pytest.raises(Exception): + with pytest.raises(APIStatusError): # Try to cancel a non-existent response ID client.responses.cancel("invalid_response_id_12345") diff --git a/tests/proxy_unit_tests/test_auth_checks.py b/tests/proxy_unit_tests/test_auth_checks.py index e58e6c9694b..a7fd68def2a 100644 --- a/tests/proxy_unit_tests/test_auth_checks.py +++ b/tests/proxy_unit_tests/test_auth_checks.py @@ -330,6 +330,7 @@ async def test_wildcard_access_after_cost_map_reload(key_models, model, expect_t Fix: each reload now calls litellm.add_known_models(model_cost_map=new_map) with the fetched map passed explicitly to avoid any reference ambiguity. """ + from litellm.proxy._types import ProxyException from litellm.proxy.auth.auth_checks import can_key_call_model # Build a new cost map that includes the brand-new model — exactly what @@ -378,7 +379,7 @@ async def test_wildcard_access_after_cost_map_reload(key_models, model, expect_t llm_router=router, ) else: - with pytest.raises(Exception): + with pytest.raises(ProxyException): await can_key_call_model( model=model, llm_model_list=llm_model_list, diff --git a/tests/proxy_unit_tests/test_jwt.py b/tests/proxy_unit_tests/test_jwt.py index beaa120dcb9..a5836c59694 100644 --- a/tests/proxy_unit_tests/test_jwt.py +++ b/tests/proxy_unit_tests/test_jwt.py @@ -41,6 +41,7 @@ from litellm.proxy.auth.handle_jwt import JWTHandler, JWTAuthManager from litellm.proxy.management_endpoints.team_endpoints import new_team from litellm.proxy.proxy_server import chat_completion from typing import Literal, Optional +from litellm.proxy._types import ProxyException public_key = { "kty": "RSA", @@ -1045,9 +1046,7 @@ async def test_allow_access_by_email( assert result is not None # Adjust this based on your actual response check else: # Expect the call to fail - with pytest.raises( - Exception - ): # Replace with the actual exception raised on failure + with pytest.raises(ProxyException): resp = await user_api_key_auth(request=request, api_key=bearer_token) print(resp) diff --git a/tests/proxy_unit_tests/test_proxy_config_unit_test.py b/tests/proxy_unit_tests/test_proxy_config_unit_test.py index e6b38f31b48..99b0dc4fd13 100644 --- a/tests/proxy_unit_tests/test_proxy_config_unit_test.py +++ b/tests/proxy_unit_tests/test_proxy_config_unit_test.py @@ -53,7 +53,7 @@ async def test_read_config_from_bad_file_path(): """ proxy_config_instance = ProxyConfig() config_path = "non-existent-file.yaml" - with pytest.raises(Exception): + with pytest.raises(Exception, match="Config file not found"): config = await proxy_config_instance.get_config(config_file_path=config_path) diff --git a/tests/proxy_unit_tests/test_proxy_utils.py b/tests/proxy_unit_tests/test_proxy_utils.py index ad852c16905..00ed6d13f63 100644 --- a/tests/proxy_unit_tests/test_proxy_utils.py +++ b/tests/proxy_unit_tests/test_proxy_utils.py @@ -29,6 +29,7 @@ from litellm.proxy.litellm_pre_call_utils import ( _get_dynamic_logging_metadata, add_litellm_data_to_request, ) +from pydantic import ValidationError pytestmark = pytest.mark.xdist_group("proxy_heavy") @@ -1695,13 +1696,13 @@ def test_update_key_request_validation(): """ from litellm.proxy._types import UpdateKeyRequest - with pytest.raises(Exception): + with pytest.raises(ValidationError): UpdateKeyRequest( key="test_key", temp_budget_increase=100, ) - with pytest.raises(Exception): + with pytest.raises(ValidationError): UpdateKeyRequest( key="test_key", temp_budget_expiry="2024-01-20T00:00:00Z", @@ -1848,7 +1849,7 @@ async def test_end_user_transactions_reset(): mock_client.db.tx = AsyncMock(side_effect=Exception("DB Error")) # Call function - should raise error - with pytest.raises(Exception): + with pytest.raises(TypeError): await ProxyUpdateSpend.update_end_user_spend( n_retry_times=0, prisma_client=mock_client, @@ -1878,7 +1879,7 @@ async def test_spend_logs_cleanup_after_error(): original_logs = mock_client.spend_log_transactions.copy() # Call function - should raise error - with pytest.raises(Exception): + with pytest.raises(TypeError): await ProxyUpdateSpend.update_spend_logs( n_retry_times=0, prisma_client=mock_client, diff --git a/tests/proxy_unit_tests/test_skills_db.py b/tests/proxy_unit_tests/test_skills_db.py index 5f420bc314a..9548e78d6ed 100644 --- a/tests/proxy_unit_tests/test_skills_db.py +++ b/tests/proxy_unit_tests/test_skills_db.py @@ -26,6 +26,7 @@ from litellm.proxy import proxy_server from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.utils import PrismaClient, ProxyLogging from litellm.types.utils import LlmProviders +import openai proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache()) @@ -254,7 +255,7 @@ async def test_delete_skill_sdk(prisma_client): assert result.type == "skill_deleted" # Verify skill no longer exists - with pytest.raises(Exception): + with pytest.raises(openai.APIError): await aget_skill( skill_id=created_skill.id, custom_llm_provider=LlmProviders.LITELLM_PROXY.value, diff --git a/tests/proxy_unit_tests/test_user_api_key_auth.py b/tests/proxy_unit_tests/test_user_api_key_auth.py index ccf710c5708..58dbe3ad370 100644 --- a/tests/proxy_unit_tests/test_user_api_key_auth.py +++ b/tests/proxy_unit_tests/test_user_api_key_auth.py @@ -436,7 +436,7 @@ def test_ui_token_route_access(route, user_role, should_be_allowed): ) assert result is True else: - with pytest.raises(Exception): + with pytest.raises(Exception, match="Only proxy admin can be used to generate"): _is_api_route_allowed( route=route, request=request, diff --git a/tests/test_litellm/caching/test_redis_cache.py b/tests/test_litellm/caching/test_redis_cache.py index 59200719197..6a76decd5b1 100644 --- a/tests/test_litellm/caching/test_redis_cache.py +++ b/tests/test_litellm/caching/test_redis_cache.py @@ -520,13 +520,15 @@ async def test_circuit_breaker_covers_lua_script_execution(redis_no_ping): counted toward taking Redis out of the pool and kept paying a full socket timeout each, which is the traffic the outage hurts most. """ + from redis.exceptions import ConnectionError as RedisConnectionError + from litellm.constants import REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD cache = RedisCache(host="127.0.0.1", port=_closed_port(), socket_timeout=0.5) run_script = cache.async_register_script("return 1") for _ in range(REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD): - with pytest.raises(Exception): + with pytest.raises(RedisConnectionError): await run_script(keys=["lit4930"], args=[1]) with pytest.raises(Exception, match="circuit breaker is open"): @@ -603,8 +605,11 @@ async def test_only_connectivity_failures_open_the_breaker(error, opens_breaker) async def failing_call(): raise raised - for _ in range(breaker.failure_threshold + 1): - with pytest.raises(Exception): + for _ in range(breaker.failure_threshold): + with pytest.raises(type(raised)): await _run_under_circuit_breaker(breaker, "op", failing_call) + with pytest.raises(Exception, match="circuit breaker is open" if opens_breaker else "boom"): + await _run_under_circuit_breaker(breaker, "op", failing_call) + assert breaker.is_open() is opens_breaker diff --git a/tests/test_litellm/containers/test_container_api.py b/tests/test_litellm/containers/test_container_api.py index 4032c072594..de6fd1bc8ce 100644 --- a/tests/test_litellm/containers/test_container_api.py +++ b/tests/test_litellm/containers/test_container_api.py @@ -384,7 +384,7 @@ class TestContainerAPI: "container_create_handler", side_effect=Exception("API Error"), ): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): create_container( name="Error Test Container", custom_llm_provider="openai" ) diff --git a/tests/test_litellm/interactions/test_agents_main_and_utils.py b/tests/test_litellm/interactions/test_agents_main_and_utils.py index 7c0183d20c6..f5523cf1cf8 100644 --- a/tests/test_litellm/interactions/test_agents_main_and_utils.py +++ b/tests/test_litellm/interactions/test_agents_main_and_utils.py @@ -314,7 +314,7 @@ class TestAsyncErrorWrapping: handler.create_agent.side_effect = RuntimeError("kaboom") with patch(_HANDLER_PATH, handler): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await acreate(name="waverunner", api_key="AIza") @pytest.mark.asyncio @@ -323,7 +323,7 @@ class TestAsyncErrorWrapping: handler.get_agent.side_effect = RuntimeError("kaboom") with patch(_HANDLER_PATH, handler): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await aget(name="waverunner", api_key="AIza") @pytest.mark.asyncio @@ -332,7 +332,7 @@ class TestAsyncErrorWrapping: handler.list_agents.side_effect = RuntimeError("kaboom") with patch(_HANDLER_PATH, handler): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await alist(api_key="AIza") @pytest.mark.asyncio @@ -341,7 +341,7 @@ class TestAsyncErrorWrapping: handler.delete_agent.side_effect = RuntimeError("kaboom") with patch(_HANDLER_PATH, handler): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await adelete(name="waverunner", api_key="AIza") @pytest.mark.asyncio @@ -350,5 +350,5 @@ class TestAsyncErrorWrapping: handler.list_agent_versions.side_effect = RuntimeError("kaboom") with patch(_HANDLER_PATH, handler): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await alist_versions(name="waverunner", api_key="AIza") diff --git a/tests/test_litellm/interactions/test_google_interactions_integration.py b/tests/test_litellm/interactions/test_google_interactions_integration.py index 41f0fa0d7fb..49cd978c683 100644 --- a/tests/test_litellm/interactions/test_google_interactions_integration.py +++ b/tests/test_litellm/interactions/test_google_interactions_integration.py @@ -18,6 +18,7 @@ sys.path.insert(0, os.path.abspath("../../..")) import litellm import litellm.interactions as interactions +import openai # Test API key - should be set in environment GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") @@ -258,7 +259,7 @@ class TestGoogleInteractionsErrorHandling: def test_invalid_model(self, api_key): """Test error handling for invalid model.""" - with pytest.raises(Exception): + with pytest.raises(openai.APIError): interactions.create( model="gemini/invalid-model-name-xyz", input="Hello", @@ -267,7 +268,7 @@ class TestGoogleInteractionsErrorHandling: def test_missing_model_and_agent(self, api_key): """Test error when neither model nor agent is provided.""" - with pytest.raises(Exception): # Can be ValueError or APIConnectionError + with pytest.raises((ValueError, litellm.APIConnectionError)): interactions.create( input="Hello", api_key=api_key, diff --git a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py index 0414836fa79..c6aac4d3991 100644 --- a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py +++ b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py @@ -288,7 +288,7 @@ def test_capability_info_backfills_requested_provider(restore_generalizations): def test_routing_only_match_does_not_resolve_model_info(restore_generalizations): restore_generalizations([{"name": "route", "pattern": r"^ceeco-", "model_info": {"litellm_provider": "openai"}}]) litellm.get_model_info.cache_clear() - with pytest.raises(Exception): + with pytest.raises(Exception, match="This model isn't mapped yet"): litellm.get_model_info("ceeco-fast-1", custom_llm_provider="openai") @@ -470,7 +470,7 @@ def test_shipped_adaptive_rule_requires_claude_prefix(shipped_cost_map): model = "openai/team-sonnet-5-1-alias" assert model not in litellm.model_cost assert match_capability_generalizations("team-sonnet-5-1-alias") is None - with pytest.raises(Exception): + with pytest.raises(Exception, match="This model isn't mapped yet"): litellm.get_model_info(model) @@ -496,7 +496,7 @@ def test_shipped_rules_lose_to_exact_entries_across_cost_ladder_variants(shipped from litellm.types.utils import ModelResponse, Usage assert "claude-haiku-4-5-20251001" in litellm.model_cost - with pytest.raises(Exception): + with pytest.raises(Exception, match="This model isn't mapped yet"): litellm.get_model_info("claude-haiku-4-5-20251001", custom_llm_provider="bedrock") entry = litellm.model_cost["us.anthropic.claude-haiku-4-5-20251001-v1:0"] diff --git a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py index dff54515098..ccf353b1b6c 100644 --- a/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py +++ b/tests/test_litellm/litellm_core_utils/test_realtime_streaming.py @@ -62,7 +62,7 @@ def test_realtime_streaming_store_message(): # Test 3: Invalid message format invalid_msg = "invalid json" - with pytest.raises(Exception): + with pytest.raises(json.JSONDecodeError): streaming.store_message(invalid_msg) # Test 4: Message type not in logged events diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py index 05b44fffbc5..fc7c81a9bab 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py @@ -4176,7 +4176,7 @@ async def test_stream_wrapper_anext_max_duration_timeout_restores_consumer_corre wrapper._stream_created_time = time.time() - 10 - with pytest.raises(Exception): + with pytest.raises(litellm.Timeout): await wrapper.__anext__() assert trace_id_var.get() == "outer-trace-max-duration" @@ -4323,7 +4323,9 @@ def test_handle_stream_fallback_error_restores_context_only_after_exception_mapp monkeypatch.setattr("litellm.litellm_core_utils.streaming_handler.exception_type", fake_exception_type) - with pytest.raises(Exception): + from litellm.exceptions import MidStreamFallbackError + + with pytest.raises(MidStreamFallbackError): wrapper._handle_stream_fallback_error(RuntimeError("boom")) # The mapper ran while the stream's own ids were still active. diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py index 060c3e459d0..654b0097546 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py @@ -61,7 +61,7 @@ def test_anthropic_messages_handler_skips_the_gateway_on_recursion(): "litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp", new=AsyncMock(return_value={"routed": True}), ) as routed: - with pytest.raises(Exception): + with pytest.raises(ValueError): anthropic_messages_handler( max_tokens=100, messages=[{"role": "user", "content": "hi"}], @@ -80,7 +80,7 @@ def test_anthropic_messages_handler_leaves_native_tools_alone(): "litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp", new=AsyncMock(return_value={"routed": True}), ) as routed: - with pytest.raises(Exception): + with pytest.raises(ValueError): anthropic_messages_handler( max_tokens=100, messages=[{"role": "user", "content": "hi"}], diff --git a/tests/test_litellm/llms/azure/videos/test_azure_video_transformation.py b/tests/test_litellm/llms/azure/videos/test_azure_video_transformation.py index d4f7a75895e..97c9e590d08 100644 --- a/tests/test_litellm/llms/azure/videos/test_azure_video_transformation.py +++ b/tests/test_litellm/llms/azure/videos/test_azure_video_transformation.py @@ -19,6 +19,7 @@ from litellm.types.videos.main import ( VideoCreateOptionalRequestParams, ) from litellm.types.router import GenericLiteLLMParams +from pydantic import ValidationError class TestAzureVideoConfig: @@ -299,7 +300,7 @@ class TestAzureVideoConfig: logging_obj = MagicMock() # Test that error responses raise exceptions - with pytest.raises(Exception): + with pytest.raises(ValidationError): self.config.transform_video_create_response( model=self.model, raw_response=mock_response, logging_obj=logging_obj ) diff --git a/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py b/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py index b93ffdb0b44..e4402bbec49 100644 --- a/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py +++ b/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py @@ -270,7 +270,7 @@ async def test_asearch_does_not_leak_server_key_to_caller_api_base( new_callable=AsyncMock, ) as mock_get, ): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await litellm.asearch( query="secrets", search_provider="serper", @@ -319,7 +319,7 @@ async def test_query_param_key_not_leaked_with_dummy_caller_key( "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.get", fake_get, ): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): await litellm.asearch( query="secrets", search_provider=provider, diff --git a/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py b/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py index b0b092a541f..6a8cd29692f 100644 --- a/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py +++ b/tests/test_litellm/llms/custom_httpx/test_aiohttp_transport.py @@ -1077,7 +1077,7 @@ async def test_session_closed_retry_does_not_close_concurrent_replacement(): raise StopAsyncIteration("stop after retry dispatch") with patch.object(transport, "_make_aiohttp_request", side_effect=fake_make_request): - with pytest.raises(Exception): + with pytest.raises(StopAsyncIteration): await transport.handle_async_request(httpx.Request("GET", "http://example.com")) try: diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py index f69ba7df938..52f1a6a99b8 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py @@ -866,7 +866,7 @@ class TestGithubCopilotTransformResponse: ) model_response = ModelResponse() - with pytest.raises(Exception): + with pytest.raises(json.JSONDecodeError): config.transform_response( model="github_copilot/claude-opus-4.7", raw_response=raw_response, diff --git a/tests/test_litellm/llms/huggingface/rerank/test_huggingface_rerank_transformation.py b/tests/test_litellm/llms/huggingface/rerank/test_huggingface_rerank_transformation.py index b7ae8aa5fb1..9d6b7290eb6 100644 --- a/tests/test_litellm/llms/huggingface/rerank/test_huggingface_rerank_transformation.py +++ b/tests/test_litellm/llms/huggingface/rerank/test_huggingface_rerank_transformation.py @@ -232,7 +232,7 @@ def test_huggingface_rerank_error_handling(mock_post): mock_response.text = "Unauthorized" mock_post.return_value = mock_response - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): litellm.rerank( model="huggingface/BAAI/bge-reranker-base", query="hello", diff --git a/tests/test_litellm/llms/langflow/chat/test_langflow_chat_transformation.py b/tests/test_litellm/llms/langflow/chat/test_langflow_chat_transformation.py index c03919a0659..b3c4e0f1858 100644 --- a/tests/test_litellm/llms/langflow/chat/test_langflow_chat_transformation.py +++ b/tests/test_litellm/llms/langflow/chat/test_langflow_chat_transformation.py @@ -233,7 +233,7 @@ def test_langflow_extra_body_cannot_inject_tweaks_into_run_payload(): return resp with patch.object(HTTPHandler, "post", side_effect=fake_post): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): litellm.completion( model="langflow/my-flow", messages=[{"role": "user", "content": "hello"}], diff --git a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py index 957fc7dbcf4..7383513fb96 100644 --- a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py +++ b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py @@ -40,6 +40,7 @@ from litellm.llms.vertex_ai.files.transformation import ( _openai_batch_jsonl_entry_to_vertex_rows, ) from litellm.types.llms.openai import CreateFileRequest +from litellm.llms.vertex_ai.common_utils import VertexAIError def _upload_stream(transformed) -> BaseFileUploadStream: @@ -561,7 +562,7 @@ class TestStreamingMediaUpload: async def test_failed_upload_raises(self): raw = _make_openai_jsonl_bytes(80) - with pytest.raises(Exception): + with pytest.raises(VertexAIError): await self._run(raw, status=403) async def test_request_timeout_is_forwarded(self): diff --git a/tests/test_litellm/llms/xai/test_xai_oauth.py b/tests/test_litellm/llms/xai/test_xai_oauth.py index 45fa6a405f2..3fc4c35052e 100644 --- a/tests/test_litellm/llms/xai/test_xai_oauth.py +++ b/tests/test_litellm/llms/xai/test_xai_oauth.py @@ -556,7 +556,7 @@ def test_get_llm_provider_uses_single_xai_provider(monkeypatch): def test_xai_oauth_alias_is_not_a_provider(): - with pytest.raises(Exception): + with pytest.raises(litellm.BadRequestError): get_llm_provider("xai_oauth/grok-4") diff --git a/tests/test_litellm/models/test_models.py b/tests/test_litellm/models/test_models.py index 187c7aa7f5e..669dba8e466 100644 --- a/tests/test_litellm/models/test_models.py +++ b/tests/test_litellm/models/test_models.py @@ -39,6 +39,7 @@ from litellm.models.verification_token import ( LiteLLM_DeletedVerificationToken, LiteLLM_VerificationToken, ) +from pydantic import ValidationError class TestBudget: @@ -421,7 +422,7 @@ class TestBudgetTableFull: assert budget.max_budget == 10.0 def test_full_requires_created_at(self): - with pytest.raises(Exception): + with pytest.raises(ValidationError): LiteLLM_BudgetTableFull(budget_id="b1") @@ -480,7 +481,7 @@ class TestMCPServerTable: assert server.env == {} def test_mcp_server_requires_transport(self): - with pytest.raises(Exception): + with pytest.raises(ValidationError): LiteLLM_MCPServerTable(server_id="s1") @@ -538,7 +539,7 @@ class TestManagedTables: assert table.flat_model_file_ids == ["file-abc"] def test_managed_object_table_requires_purpose(self): - with pytest.raises(Exception): + with pytest.raises(ValidationError): LiteLLM_ManagedObjectTable( unified_object_id="o1", model_object_id="m1", file_object={} ) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index 051df30dfcb..aa6e63b7ca0 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -4440,7 +4440,7 @@ async def test_call_mcp_tool_logs_failure_via_post_call_failure_hook(): proxy_logging_mock, ), ): - with pytest.raises(Exception): + with pytest.raises(Exception, match="boom"): await call_mcp_tool( name="test_server-any_tool", arguments={"x": 1}, diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index 2f4c2d3870f..6d6e20e9c36 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -2809,7 +2809,7 @@ def test_team_update_gate_rejects_without_org_context(): request.method = "POST" request.query_params = {} - with pytest.raises(Exception): + with pytest.raises(Exception, match="Only proxy admin can be used to generate"): RouteChecks.non_proxy_admin_allowed_routes_check( user_obj=user_obj, _user_role=LitellmUserRoles.INTERNAL_USER.value, @@ -2829,7 +2829,7 @@ def test_team_update_gate_rejects_cross_org_admin_with_resolved_org(): request.method = "POST" request.query_params = {} - with pytest.raises(Exception): + with pytest.raises(Exception, match="Only proxy admin can be used to generate"): RouteChecks.non_proxy_admin_allowed_routes_check( user_obj=user_obj, _user_role=LitellmUserRoles.INTERNAL_USER.value, @@ -2951,7 +2951,7 @@ def test_patch_team_gate_rejects_regular_internal_user(): ) valid_token = UserAPIKeyAuth(user_id="regular-user", user_role=LitellmUserRoles.INTERNAL_USER.value) - with pytest.raises(Exception): + with pytest.raises(Exception, match="Only proxy admin can be used to generate"): RouteChecks.non_proxy_admin_allowed_routes_check( user_obj=user_obj, _user_role=LitellmUserRoles.INTERNAL_USER.value, @@ -2967,7 +2967,7 @@ def test_patch_team_gate_rejects_cross_org_admin(): user_obj = _make_org_admin_user("org-1") valid_token = UserAPIKeyAuth(user_id="org-admin-user", user_role=LitellmUserRoles.INTERNAL_USER.value) - with pytest.raises(Exception): + with pytest.raises(Exception, match="Only proxy admin can be used to generate"): RouteChecks.non_proxy_admin_allowed_routes_check( user_obj=user_obj, _user_role=LitellmUserRoles.INTERNAL_USER.value, @@ -2987,7 +2987,7 @@ def test_patch_team_gate_rejects_view_only_admin(): ) valid_token = UserAPIKeyAuth(user_id="viewer", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value) - with pytest.raises(Exception): + with pytest.raises(HTTPException): RouteChecks.non_proxy_admin_allowed_routes_check( user_obj=user_obj, _user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value, diff --git a/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py b/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py index b216071828e..863d9204cff 100644 --- a/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py @@ -979,7 +979,7 @@ async def test_create__exception_calls_failure_hook(harness, openai_env_creds): ) harness.litellm_acreate.side_effect = ValueError("provider boom") - with pytest.raises(Exception): + with pytest.raises(ProxyException): await call_create(harness) harness.logging.post_call_failure_hook.assert_called_once() @@ -1437,7 +1437,7 @@ async def test_retrieve__uses_aretrieve_batch_route_type(retrieve_harness, opena async def test_retrieve__exception_calls_failure_hook(retrieve_harness, openai_env_creds): retrieve_harness.litellm_aretrieve.side_effect = ValueError("provider boom") - with pytest.raises(Exception): + with pytest.raises(ProxyException): await call_retrieve(retrieve_harness, "batch-raw-xyz") retrieve_harness.logging.post_call_failure_hook.assert_called_once() @@ -1844,7 +1844,7 @@ async def test_list__uses_alist_batches_route_type(list_harness): async def test_list__exception_calls_failure_hook(list_harness): list_harness.litellm_alist.side_effect = ValueError("provider boom") - with pytest.raises(Exception): + with pytest.raises(ProxyException): await call_list(list_harness) list_harness.logging.post_call_failure_hook.assert_called_once() @@ -2233,7 +2233,7 @@ async def test_cancel__uses_acancel_batch_route_type(cancel_harness, openai_env_ async def test_cancel__exception_calls_failure_hook(cancel_harness, openai_env_creds): cancel_harness.litellm_acancel.side_effect = ValueError("provider boom") - with pytest.raises(Exception): + with pytest.raises(ProxyException): await call_cancel(cancel_harness, "batch-raw-xyz") cancel_harness.logging.post_call_failure_hook.assert_called_once() diff --git a/tests/test_litellm/proxy/db/test_prisma_self_heal.py b/tests/test_litellm/proxy/db/test_prisma_self_heal.py index e8e7568d6d8..cc47cf4a7e4 100644 --- a/tests/test_litellm/proxy/db/test_prisma_self_heal.py +++ b/tests/test_litellm/proxy/db/test_prisma_self_heal.py @@ -507,7 +507,7 @@ async def test_engine_confirmed_dead_persists_across_failed_heavy_reconnect( client._reap_all_zombies = MagicMock() with patch.dict(os.environ, {"DATABASE_URL": "postgresql://test"}): - with pytest.raises(Exception): + with pytest.raises(RuntimeError): await client._run_reconnect_cycle(timeout_seconds=5.0) # The flag must STILL be True so the next attempt re-enters the heavy diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py index e3516b6eda7..5d971bf1212 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py @@ -2113,7 +2113,7 @@ async def test_make_bedrock_api_request_forwards_guardrail_action(): ): mock_post.return_value = mock_bedrock_response - with pytest.raises(Exception): + with pytest.raises(Exception, match="blocked"): await guardrail.make_bedrock_api_request( source="INPUT", messages=request_data["messages"], diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py index 253d989f203..c779150ad3e 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py @@ -22,6 +22,7 @@ from litellm.proxy.guardrails.guardrail_hooks.presidio import ( from litellm.exceptions import GuardrailRaisedException from litellm.types.guardrails import LitellmParams, PiiAction, PiiEntityType from litellm.types.utils import Choices, Message, ModelResponse +from litellm.exceptions import BlockedPiiEntityError def _make_mock_session_iterator( @@ -1345,7 +1346,7 @@ def test_blocking_respects_threshold_filter(): {"entity_type": PiiEntityType.CREDIT_CARD, "score": 0.95, "start": 0, "end": 4} ] filtered_high = guardrail.filter_analyze_results_by_score(high_score_results) - with pytest.raises(Exception): + with pytest.raises(BlockedPiiEntityError): guardrail.raise_exception_if_blocked_entities_detected(filtered_high) diff --git a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py index 5caca95095f..f933cf6655e 100644 --- a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py @@ -53,6 +53,7 @@ from litellm.types.proxy.management_endpoints.scim_v2 import ( SCIMUserGroup, SCIMUserName, ) +from litellm.proxy._types import ProxyException @pytest.mark.asyncio @@ -3216,7 +3217,7 @@ async def test_delete_user_surfaces_prune_failure_and_keeps_user(mocker): AsyncMock(side_effect=Exception("database connection lost")), ) - with pytest.raises(Exception): + with pytest.raises(ProxyException): await delete_user(user_id=user_id) mock_prisma_client.db.litellm_usertable.delete.assert_not_awaited() diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index 11b7f4553ac..8a036d7e62f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -668,6 +668,8 @@ def test_validate_sort_params(): """ Test that validate_sort_params returns None if sort_by is None """ + from fastapi import HTTPException + from litellm.proxy.management_endpoints.internal_user_endpoints import ( _validate_sort_params, ) @@ -676,7 +678,7 @@ def test_validate_sort_params(): assert _validate_sort_params(None, "desc") is None assert _validate_sort_params("user_id", "asc") == {"user_id": "asc"} assert _validate_sort_params("user_id", "desc") == {"user_id": "desc"} - with pytest.raises(Exception): + with pytest.raises(HTTPException): _validate_sort_params("user_id", "invalid") diff --git a/tests/test_litellm/proxy/management_helpers/test_team_metadata_validation.py b/tests/test_litellm/proxy/management_helpers/test_team_metadata_validation.py index d22c3db0f7e..4c66f4aadf1 100644 --- a/tests/test_litellm/proxy/management_helpers/test_team_metadata_validation.py +++ b/tests/test_litellm/proxy/management_helpers/test_team_metadata_validation.py @@ -22,6 +22,7 @@ from litellm.proxy.management_helpers.team_metadata_validation import ( run_team_metadata_validation, validate_team_metadata_if_configured, ) +from pydantic import ValidationError def _registry_with(validator): @@ -634,7 +635,7 @@ def test_parse_schema_round_trips_fields_in_order(): ], ) def test_parse_schema_malformed_raises(raw): - with pytest.raises(Exception): + with pytest.raises(ValidationError): parse_team_metadata_schema(raw) diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index 00097166c13..090acf2dbb0 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -350,7 +350,7 @@ async def test_pass_through_request_failure_handler(): mock_user_api_key_dict = MagicMock() # Call the function with a target that will trigger an HTTPError - with pytest.raises(Exception): + with pytest.raises(ProxyException): await pass_through_request( request=mock_request, target="http://test.com", @@ -1154,7 +1154,7 @@ async def test_pass_through_request_uses_resolved_timeout(): mock_user_api_key_dict = MagicMock() - with pytest.raises(Exception): + with pytest.raises(TypeError): await pass_through_request( request=mock_request, target="http://test.com", diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_passthrough_post_call_guardrails.py b/tests/test_litellm/proxy/pass_through_endpoints/test_passthrough_post_call_guardrails.py index 470179a0429..a48e9e9e17f 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_passthrough_post_call_guardrails.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_passthrough_post_call_guardrails.py @@ -18,6 +18,7 @@ from litellm.integrations.custom_guardrail import ( CustomGuardrail, ModifyResponseException, ) +from litellm.proxy._types import ProxyException _PT_MOD = "litellm.proxy.pass_through_endpoints.pass_through_endpoints" _COLLECT = "litellm.proxy.pass_through_endpoints.passthrough_guardrails.PassthroughGuardrailHandler.collect_guardrails" @@ -251,7 +252,7 @@ class TestPassthroughPostCallGuardrails: ) with _common_patches(mock_proxy_logging, mock_response): - with pytest.raises(Exception): + with pytest.raises(ProxyException): await pass_through_request( request=_make_mock_request(), target="https://example.com/v1/generateContent", diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index d8047cca728..54ae279d005 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -28,6 +28,7 @@ from litellm.proxy.proxy_server import ( ) from .conftest import normalize +from pydantic import ValidationError # --------------------------------------------------------------------------- # _is_remote_module_url @@ -393,7 +394,7 @@ def test_ProxyConfig__load_yaml_file_returns_parsed_dict(tmp_path): def test_ProxyConfig__load_yaml_file_raises_on_missing_file(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(Exception, match="Error loading yaml file"): pc._load_yaml_file("/no/such/file.yaml") @@ -418,7 +419,7 @@ async def test_ProxyConfig__get_config_from_file_loads_yaml(tmp_path): @pytest.mark.asyncio async def test_ProxyConfig__get_config_from_file_missing_path_raises(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(Exception, match="Config file not found"): await pc._get_config_from_file(config_file_path="/no/such/file.yaml") @@ -476,7 +477,7 @@ async def test_ProxyConfig_save_config_invalid_path_raises(monkeypatch): monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False) monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): await pc.save_config({"x": 1}) @@ -641,7 +642,7 @@ def test_ProxyConfig__get_team_config_returns_match(): def test_ProxyConfig__get_team_config_missing_team_id_raises(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(Exception, match="team_id missing from team"): pc._get_team_config(team_id="t1", all_teams_config=[{"no_id_field": True}]) @@ -671,7 +672,7 @@ def test_ProxyConfig_load_team_config_no_settings_returns_empty(): assert out == {} # Error-style: a misconfigured team list without team_id raises. pc.config = {"litellm_settings": {"default_team_settings": [{"no_id": True}]}} - with pytest.raises(Exception): + with pytest.raises(Exception, match="team_id missing from team"): pc.load_team_config(team_id="anything") @@ -698,7 +699,7 @@ def test_ProxyConfig__init_cache_sets_litellm_cache(monkeypatch): def test_ProxyConfig__init_cache_invalid_params_raises(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(AttributeError): pc._init_cache(cache_params={"type": "this-cache-type-does-not-exist"}) @@ -765,7 +766,7 @@ async def test_ProxyConfig_get_config_missing_file_raises(monkeypatch): monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False) monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False) pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(Exception, match="Config file not found"): await pc.get_config(config_file_path="/no/such/path.yaml") @@ -1041,7 +1042,7 @@ def test_ProxyConfig_load_credential_list_returns_items(): def test_ProxyConfig_load_credential_list_invalid_entry_raises(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(ValidationError): pc.load_credential_list({"credential_list": [{"missing_required": True}]}) @@ -1381,7 +1382,7 @@ async def test_ProxyConfig_load_config_missing_file_raises(monkeypatch): monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False) monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False) pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(Exception, match="Config file not found"): await pc.load_config(router=None, config_file_path="/no/file.yaml") @@ -1492,7 +1493,7 @@ async def test_ProxyConfig__init_non_llm_configs_empty_config(): async def test_ProxyConfig__init_non_llm_configs_premium_invalid_worker_registry_raises(monkeypatch): monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True) pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(ValidationError): await pc._init_non_llm_configs( config={"worker_registry": [{"totally": "invalid"}]}, config_file_path=None, @@ -1572,7 +1573,7 @@ async def test_ProxyConfig__init_policy_engine_none_config_noop(): # None config returns early without raising. await pc._init_policy_engine(config=None, prisma_client=None, llm_router=None) # Error-style: invalid policies value should raise. - with pytest.raises(Exception): + with pytest.raises(AttributeError): await pc._init_policy_engine( config={"policies": "not-a-list"}, prisma_client=None, @@ -1601,7 +1602,7 @@ def test_ProxyConfig__load_alerting_settings_noop_when_no_alerting(): def test_ProxyConfig__load_alerting_settings_invalid_alerting_raises(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(RuntimeError): # alerting must be iterable — int triggers an error. pc._load_alerting_settings({"alerting": 12345}) @@ -1823,7 +1824,7 @@ async def test_ProxyConfig__delete_deployment_invalid_models_raises(monkeypatch) fake_router.get_model_ids = MagicMock(return_value=[]) monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", fake_router) pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(AttributeError): # Non-model objects without expected attrs trigger an error. await pc._delete_deployment(db_models=[{"not_a_model": True}]) @@ -2721,7 +2722,7 @@ async def test_ProxyConfig__update_general_settings_none_input_noop(): result = await pc._update_general_settings(db_general_settings=None) assert result is None # Error-style: dict() will fail on non-mapping non-None input. - with pytest.raises(Exception): + with pytest.raises(TypeError): await pc._update_general_settings(db_general_settings=12345) # type: ignore[arg-type] @@ -2743,7 +2744,7 @@ def test_ProxyConfig__update_config_fields_merges_dict(): def test_ProxyConfig__update_config_fields_invalid_param_raises(): pc = ProxyConfig() - with pytest.raises(Exception): + with pytest.raises(TypeError): # Missing required arg. pc._update_config_fields(current_config={}, param_name="general_settings") # type: ignore[call-arg] diff --git a/tests/test_litellm/proxy/test_proxy_utils.py b/tests/test_litellm/proxy/test_proxy_utils.py index 07877514b69..fe79ef25da6 100644 --- a/tests/test_litellm/proxy/test_proxy_utils.py +++ b/tests/test_litellm/proxy/test_proxy_utils.py @@ -1584,7 +1584,7 @@ async def test_prisma_health_check_failure_redacts_database_credentials(caplog): client._report_health_check_failure = AsyncMock() with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"): - with pytest.raises(Exception): + with pytest.raises(Exception, match="could not connect to"): await PrismaClient.health_check(client) emitted = [record.getMessage() for record in caplog.records if record.name == "LiteLLM Proxy"] diff --git a/tests/test_litellm/test_dashscope_image_generation.py b/tests/test_litellm/test_dashscope_image_generation.py index af95e2ca6b4..c9f0df4febb 100644 --- a/tests/test_litellm/test_dashscope_image_generation.py +++ b/tests/test_litellm/test_dashscope_image_generation.py @@ -17,6 +17,7 @@ from litellm.llms.dashscope.image_generation.transformation import ( ) from litellm.types.utils import ImageObject, ImageResponse from litellm.utils import get_llm_provider +from litellm.llms.base_llm.chat.transformation import BaseLLMException # --------------------------------------------------------------------------- @@ -247,7 +248,7 @@ class TestDashScopeImageGenerationConfig: "message": "Size not supported", } - with pytest.raises(Exception): + with pytest.raises(BaseLLMException): self.cfg.transform_image_generation_response( model="qwen-image-2.0", raw_response=mock_resp, @@ -268,7 +269,7 @@ class TestDashScopeImageGenerationConfig: "message": "Size not supported", } - with pytest.raises(Exception): + with pytest.raises(BaseLLMException): self.cfg.transform_image_generation_response( model="qwen-image-2.0", raw_response=mock_resp, diff --git a/tests/test_litellm/test_get_blog_posts.py b/tests/test_litellm/test_get_blog_posts.py index 32edc5423d0..241dce23633 100644 --- a/tests/test_litellm/test_get_blog_posts.py +++ b/tests/test_litellm/test_get_blog_posts.py @@ -12,6 +12,7 @@ from litellm.litellm_core_utils.get_blog_posts import ( GetBlogPosts, get_blog_posts, ) +from xml.etree import ElementTree SAMPLE_RSS = """\ @@ -71,7 +72,7 @@ def test_parse_rss_to_posts_multiple(): def test_parse_rss_to_posts_invalid_xml(): - with pytest.raises(Exception): + with pytest.raises(ElementTree.ParseError): GetBlogPosts.parse_rss_to_posts("not xml") diff --git a/tests/test_litellm/test_project_tags_pydantic.py b/tests/test_litellm/test_project_tags_pydantic.py index b3f58df2325..c04cf2c686b 100644 --- a/tests/test_litellm/test_project_tags_pydantic.py +++ b/tests/test_litellm/test_project_tags_pydantic.py @@ -1,5 +1,6 @@ import pytest from litellm.proxy._types import NewProjectRequest, UpdateProjectRequest +from pydantic import ValidationError def test_new_project_request_tags(): @@ -21,11 +22,11 @@ def test_update_project_request_tags(): def test_new_project_request_invalid_tags_type(): # tags must be a list — a string should raise a ValidationError - with pytest.raises(Exception): + with pytest.raises(ValidationError): NewProjectRequest(project_id="test_proj", team_id="team_1", tags="not-a-list") def test_update_project_request_invalid_tags_type(): # tags must be a list — a string should raise a ValidationError - with pytest.raises(Exception): + with pytest.raises(ValidationError): UpdateProjectRequest(project_id="test_proj", tags="not-a-list") diff --git a/tests/test_litellm/test_redact_string_in_error_paths.py b/tests/test_litellm/test_redact_string_in_error_paths.py index d01a9da6617..8eddc2b1a5a 100644 --- a/tests/test_litellm/test_redact_string_in_error_paths.py +++ b/tests/test_litellm/test_redact_string_in_error_paths.py @@ -234,7 +234,7 @@ class TestRouterFallbackFailureTracebackRedaction: raise ValueError(f"primary deployment failed api_key={secret}") except ValueError as original_exception: with caplog.at_level(logging.DEBUG, logger="LiteLLM Router"): - with pytest.raises(Exception): + with pytest.raises(ValueError): await router.async_function_with_fallbacks_common_utils( e=original_exception, disable_fallbacks=False, diff --git a/tests/test_litellm/test_retrieve_batch_bedrock_dispatch.py b/tests/test_litellm/test_retrieve_batch_bedrock_dispatch.py index 9df18a9f0f0..aa057b7bc73 100644 --- a/tests/test_litellm/test_retrieve_batch_bedrock_dispatch.py +++ b/tests/test_litellm/test_retrieve_batch_bedrock_dispatch.py @@ -23,6 +23,7 @@ import pytest sys.path.insert(0, os.path.abspath("../..")) import litellm # noqa: E402 +import openai ASYNC_INVOKE_ARN = "arn:aws:bedrock:us-west-2:123456789012:async-invoke/abc123def456" MIJ_ARN = "arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/abc1234567" @@ -134,7 +135,7 @@ def test_unrelated_bedrock_arn_falls_through_to_provider_config(mock_handlers): # Use a plausible-but-unsupported Bedrock ARN family. unrelated_arn = "arn:aws:bedrock:us-west-2:123456789012:provisioned-model/xyz" - with pytest.raises(Exception): + with pytest.raises(litellm.BadRequestError): # Will raise because no provider_config exists for this path — # that's fine, we just need to assert neither bedrock handler ran # before the failure. @@ -152,7 +153,7 @@ def test_non_bedrock_id_skips_bedrock_dispatch_entirely(mock_handlers): block — they belong to other providers' retrieve flows.""" async_invoke, mij, _ = mock_handlers - with pytest.raises(Exception): + with pytest.raises(openai.OpenAIError): litellm.retrieve_batch( batch_id="batch_abc123", custom_llm_provider="openai", diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index 4a06a5dfb2e..9894fcef163 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -367,7 +367,7 @@ async def test_arouter_with_tags_and_fallbacks(): enable_tag_filtering=True, ) - with pytest.raises(Exception): + with pytest.raises(litellm.InternalServerError): response = await router.acompletion( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Hello, world!"}], @@ -5707,7 +5707,7 @@ async def test_team_scoped_model_fallback_cross_team_blocked(): fallbacks=[{"primary-model": ["fallback-model"]}], ) - with pytest.raises(Exception): + with pytest.raises(litellm.InternalServerError): await router.acompletion( model="primary-model", messages=[{"role": "user", "content": "Hello"}], diff --git a/tests/test_litellm/test_router_weighted_failover.py b/tests/test_litellm/test_router_weighted_failover.py index 0115638e1fe..162312a8c67 100644 --- a/tests/test_litellm/test_router_weighted_failover.py +++ b/tests/test_litellm/test_router_weighted_failover.py @@ -391,7 +391,7 @@ async def test_no_failover_when_flag_off(): # enable_weighted_failover defaults to False ) - with pytest.raises(Exception): + with pytest.raises(litellm.InternalServerError): await router.acompletion( model="test-model", messages=[{"role": "user", "content": "hi"}], @@ -515,7 +515,7 @@ async def test_failover_exhausted_raises_original_error_class(): enable_weighted_failover=True, ) - with pytest.raises(Exception): + with pytest.raises(litellm.InternalServerError): await router.acompletion( model="test-model", messages=[{"role": "user", "content": "hi"}], @@ -648,7 +648,7 @@ async def test_failover_skipped_for_non_simple_shuffle(): enable_weighted_failover=True, ) - with pytest.raises(Exception): + with pytest.raises(litellm.InternalServerError): await router.acompletion( model="test-model", messages=[{"role": "user", "content": "hi"}], diff --git a/tests/test_litellm/test_video_generation.py b/tests/test_litellm/test_video_generation.py index 3d0472ef96e..117ca72c34f 100644 --- a/tests/test_litellm/test_video_generation.py +++ b/tests/test_litellm/test_video_generation.py @@ -151,7 +151,7 @@ class TestVideoGeneration: "video_generation_handler", side_effect=Exception("API Error"), ): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): video_generation(prompt="Test video", model="sora-2") def test_video_generation_provider_config(self): @@ -739,7 +739,7 @@ class TestVideoGeneration: "video_status_handler", side_effect=Exception("API Error"), ): - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): video_status(video_id="test_video_id", model="sora-2") def test_video_status_request_transformation(self): diff --git a/tests/test_litellm/videos/test_main.py b/tests/test_litellm/videos/test_main.py index a04a89ded99..38667e93eee 100644 --- a/tests/test_litellm/videos/test_main.py +++ b/tests/test_litellm/videos/test_main.py @@ -321,7 +321,7 @@ def test_get_character__mock_response_short_circuits(seams): def test_unsupported_provider_raises_without_dispatch(seams): seams.get_config.return_value = None - with pytest.raises(Exception): + with pytest.raises(litellm.APIConnectionError): videos_main.video_status(video_id=AZURE_VIDEO_ID) seams.handler.video_status_handler.assert_not_called() diff --git a/tests/vector_store_tests/test_ragflow_vector_store.py b/tests/vector_store_tests/test_ragflow_vector_store.py index cb4cfd75c1f..46751b64cce 100644 --- a/tests/vector_store_tests/test_ragflow_vector_store.py +++ b/tests/vector_store_tests/test_ragflow_vector_store.py @@ -16,6 +16,7 @@ from tests.vector_store_tests.base_vector_store_test import BaseVectorStoreTest from litellm.llms.ragflow.vector_stores.transformation import RAGFlowVectorStoreConfig from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.types.vector_stores import VectorStoreCreateOptionalRequestParams +from litellm.llms.base_llm.chat.transformation import BaseLLMException class TestRAGFlowVectorStore(BaseVectorStoreTest): @@ -233,7 +234,7 @@ class TestRAGFlowVectorStore(BaseVectorStoreTest): "message": "Dataset name 'test-dataset' already exists", } - with pytest.raises(Exception): # Should raise BaseLLMException + with pytest.raises(BaseLLMException): config.transform_create_vector_store_response(mock_response) def test_transform_create_vector_store_response_missing_id(self):