diff --git a/nanobot/providers/base.py b/nanobot/providers/base.py index d95180c42..062dbbd93 100644 --- a/nanobot/providers/base.py +++ b/nanobot/providers/base.py @@ -987,14 +987,16 @@ class LLMProvider(ABC): except (TypeError, ValueError): error_status_code = None + raw_error_type = getattr(exc, "error_type", None) + raw_error_code = getattr(exc, "error_code", None) detail = str(exc).strip() or type(exc).__name__ return LLMResponse( content=f"Error calling LLM: {detail}", finish_reason="error", error_status_code=error_status_code, error_kind=error_kind, - error_type=getattr(exc, "error_type", None), - error_code=getattr(exc, "error_code", None), + error_type=str(raw_error_type) if raw_error_type is not None else None, + error_code=str(raw_error_code) if raw_error_code is not None else None, error_should_retry=error_should_retry, ) diff --git a/tests/agent/test_runner_fallback.py b/tests/agent/test_runner_fallback.py index 15231a244..5cff85679 100644 --- a/tests/agent/test_runner_fallback.py +++ b/tests/agent/test_runner_fallback.py @@ -460,6 +460,28 @@ class TestFallbackWhenPrimaryRaises: assert exception is primary._exc assert response.error_kind == "authentication" + @pytest.mark.asyncio + async def test_non_string_exception_metadata_is_normalized_before_fallback(self) -> None: + """Provider exception metadata must be string-like before fallback consumes it.""" + + class NumericMetadataError(Exception): + error_type = 429 + error_code = 429 + status_code = 429 + + primary = _RaisingProvider("primary", NumericMetadataError("rate limited")) + + response, exception = await FallbackProvider._call_provider( + lambda provider, kwargs: provider.chat(**kwargs), + primary, + {}, + ) + + assert exception is primary._exc + assert response.error_type == "429" + assert response.error_code == "429" + assert FallbackProvider._should_fallback(response) is True + @pytest.mark.parametrize( "exc", [