diff --git a/aider/exceptions.py b/aider/exceptions.py index 5cbd023cc4b..05df4e45d75 100644 --- a/aider/exceptions.py +++ b/aider/exceptions.py @@ -68,10 +68,17 @@ def _load(self, strict=False): # with `Error`. if var.endswith("Error") and issubclass(getattr(litellm, var), BaseException): if var not in self.exception_info: - raise ValueError(f"{var} is in litellm but not in aider's exceptions list") + if strict: + raise ValueError(f"{var} is in litellm but not in aider's exceptions list") for var in self.exception_info: - ex = getattr(litellm, var) + ex = getattr(litellm, var, None) + if ex is None: + if strict: + raise ValueError( + f"{var} is in aider's exceptions list but not found in litellm" + ) + continue self.exceptions[ex] = self.exception_info[var] def exceptions_tuple(self): diff --git a/tests/basic/test_exceptions.py b/tests/basic/test_exceptions.py index 5f9c095f8b6..213d1d5f8df 100644 --- a/tests/basic/test_exceptions.py +++ b/tests/basic/test_exceptions.py @@ -1,3 +1,7 @@ +from unittest.mock import patch + +import pytest + from aider.exceptions import ExInfo, LiteLLMExceptions @@ -82,3 +86,76 @@ def test_openrouter_error(): assert "OpenRouter" in ex_info.description assert "overloaded" in ex_info.description assert "rate" in ex_info.description + + +def test_missing_litellm_exception_skipped(): + """Test that _load() skips exceptions not present in litellm (non-strict mode)""" + import litellm + + original = getattr(litellm, "BadGatewayError", None) + with patch.object(litellm, "BadGatewayError", create=False): + delattr(litellm, "BadGatewayError") + try: + # Use a fresh instance with its own exceptions dict + lex = LiteLLMExceptions.__new__(LiteLLMExceptions) + lex.exceptions = dict() + lex._load() + + # Should initialize without error + assert len(lex.exceptions) > 0 + + # exceptions_tuple should return a valid tuple without BadGatewayError + tup = lex.exceptions_tuple() + assert isinstance(tup, tuple) + assert len(tup) > 0 + + # BadGatewayError's class should not be in the exceptions dict + for cls in tup: + assert cls.__name__ != "BadGatewayError" + + # get_ex_info should return default ExInfo for unknown exception + class FakeError(Exception): + pass + + info = lex.get_ex_info(FakeError()) + assert info.name is None + assert info.retry is None + assert info.description is None + finally: + if original is not None: + litellm.BadGatewayError = original + + +def test_strict_mode_raises_for_missing_exception(): + """Test that _load(strict=True) raises ValueError for missing litellm exceptions""" + import litellm + + original = getattr(litellm, "BadGatewayError", None) + with patch.object(litellm, "BadGatewayError", create=False): + delattr(litellm, "BadGatewayError") + try: + lex = LiteLLMExceptions.__new__(LiteLLMExceptions) + lex.exceptions = dict() + with pytest.raises(ValueError, match="not found in litellm"): + lex._load(strict=True) + finally: + if original is not None: + litellm.BadGatewayError = original + + +def test_strict_mode_raises_for_unknown_litellm_exception(): + """Test that _load(strict=True) raises when litellm has an Error not in EXCEPTIONS""" + import litellm + + # Add a fake exception class to litellm + class FakeNewError(BaseException): + pass + + litellm.FakeNewError = FakeNewError + try: + lex = LiteLLMExceptions.__new__(LiteLLMExceptions) + lex.exceptions = dict() + with pytest.raises(ValueError, match="FakeNewError is in litellm"): + lex._load(strict=True) + finally: + delattr(litellm, "FakeNewError")