From 6806a6e9211824dc180912ed24533e348bc1e0ae Mon Sep 17 00:00:00 2001 From: timothy-jeong Date: Sat, 11 Oct 2025 17:58:50 +0900 Subject: [PATCH 1/4] Add genai.errors handling in GoogleModel - Add error handling helper method `_handle_google_error` - Convert Google API errors to ModelHTTPError with proper status codes - Map specific function-related errors (400-level) appropriately - Keep original error details in response body - Add test cases for API error handling Resolves: #3088 --- pydantic_ai_slim/pydantic_ai/models/google.py | 11 +++-- tests/models/test_google.py | 46 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 3a5cfe9258..99ed738b0c 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -14,7 +14,7 @@ from .._output import OutputObjectDefinition from .._run_context import RunContext from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool -from ..exceptions import UserError +from ..exceptions import ModelHTTPError, UserError from ..messages import ( BinaryContent, BuiltinToolCallPart, @@ -51,7 +51,7 @@ ) try: - from google.genai import Client + from google.genai import Client, errors from google.genai.types import ( BlobDict, CodeExecutionResult, @@ -394,7 +394,12 @@ async def _generate_content( ) -> GenerateContentResponse | Awaitable[AsyncIterator[GenerateContentResponse]]: contents, config = await self._build_content_and_config(messages, model_settings, model_request_parameters) func = self.client.aio.models.generate_content_stream if stream else self.client.aio.models.generate_content - return await func(model=self._model_name, contents=contents, config=config) # type: ignore + try: + return await func(model=self._model_name, contents=contents, config=config) # type: ignore + except errors.APIError as e: # pragma: no cover + if (status_code := e.code) >= 400: + raise ModelHTTPError(status_code=status_code, model_name=self._model_name, body=e.details) from e + raise # pragma: lax no cover async def _build_content_and_config( self, diff --git a/tests/models/test_google.py b/tests/models/test_google.py index 5bda43c7b3..363fa6d5a3 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -10,6 +10,7 @@ from httpx import Timeout from inline_snapshot import Is, snapshot from pydantic import BaseModel +from pytest_mock import MockerFixture from typing_extensions import TypedDict from pydantic_ai import ( @@ -43,7 +44,7 @@ ) from pydantic_ai.agent import Agent from pydantic_ai.builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool -from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior, UserError +from pydantic_ai.exceptions import ModelHTTPError, ModelRetry, UnexpectedModelBehavior, UserError from pydantic_ai.messages import ( BuiltinToolCallEvent, # pyright: ignore[reportDeprecated] BuiltinToolResultEvent, # pyright: ignore[reportDeprecated] @@ -57,6 +58,7 @@ from ..parts_from_messages import part_types_from_messages with try_import() as imports_successful: + from google.genai import errors from google.genai.types import ( FinishReason as GoogleFinishReason, GenerateContentResponse, @@ -3217,3 +3219,45 @@ async def test_cache_point_filtering(): assert len(content) == 2 assert content[0] == {'text': 'text before'} assert content[1] == {'text': 'text after'} + + +# API 에러 테스트 데이터 +@pytest.mark.parametrize( + 'error_class,error_response,expected_status', + [ + ( + errors.ServerError, + {'error': {'code': 503, 'message': 'The service is currently unavailable.', 'status': 'UNAVAILABLE'}}, + 503, + ), + ( + errors.ClientError, + {'error': {'code': 400, 'message': 'Invalid request parameters', 'status': 'INVALID_ARGUMENT'}}, + 400, + ), + ( + errors.ClientError, + {'error': {'code': 429, 'message': 'Rate limit exceeded', 'status': 'RESOURCE_EXHAUSTED'}}, + 429, + ), + ], +) +async def test_google_api_errors_are_handled( + allow_model_requests: None, + google_provider: GoogleProvider, + mocker: MockerFixture, + error_class: type[errors.APIError], + error_response: dict[str, Any], + expected_status: int, +): + model = GoogleModel('gemini-1.5-flash', provider=google_provider) + mocked_error = error_class(expected_status, error_response) + mocker.patch.object(model.client.aio.models, 'generate_content', side_effect=mocked_error) + + agent = Agent(model=model) + + with pytest.raises(ModelHTTPError) as exc_info: + await agent.run('This prompt will trigger the mocked error.') + + assert exc_info.value.status_code == expected_status + assert error_response['error']['message'] in str(exc_info.value.body) \ No newline at end of file From 7362aea9ba1429e371e48464c8861f9da8256d80 Mon Sep 17 00:00:00 2001 From: timothy-jeong Date: Fri, 17 Oct 2025 15:51:08 +0900 Subject: [PATCH 2/4] Refactor: simplify `ModelHTTPError` handling - Align with other model implementations (openai, groq) by removing unnecessary error transformation logic Resolves: #3088 --- pydantic_ai_slim/pydantic_ai/models/google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 99ed738b0c..6820479687 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -396,7 +396,7 @@ async def _generate_content( func = self.client.aio.models.generate_content_stream if stream else self.client.aio.models.generate_content try: return await func(model=self._model_name, contents=contents, config=config) # type: ignore - except errors.APIError as e: # pragma: no cover + except errors.APIError as e: if (status_code := e.code) >= 400: raise ModelHTTPError(status_code=status_code, model_name=self._model_name, body=e.details) from e raise # pragma: lax no cover From 35787d44a7593d3c9df8a11c96ca5b41f444b174 Mon Sep 17 00:00:00 2001 From: timothy-jeong Date: Sat, 15 Nov 2025 16:49:37 +0900 Subject: [PATCH 3/4] fix: Silence pyright unknown type errors for APIError details (#3088) --- pydantic_ai_slim/pydantic_ai/models/google.py | 6 +++--- tests/models/test_google.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 6820479687..4829e1aa36 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -397,9 +397,9 @@ async def _generate_content( try: return await func(model=self._model_name, contents=contents, config=config) # type: ignore except errors.APIError as e: - if (status_code := e.code) >= 400: - raise ModelHTTPError(status_code=status_code, model_name=self._model_name, body=e.details) from e - raise # pragma: lax no cover + if (status_code := e.code) >= 400: + raise ModelHTTPError(status_code=status_code, model_name=self._model_name, body=e.details) from e # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType] + raise # pragma: lax no cover async def _build_content_and_config( self, diff --git a/tests/models/test_google.py b/tests/models/test_google.py index 363fa6d5a3..1e34932b02 100644 --- a/tests/models/test_google.py +++ b/tests/models/test_google.py @@ -3221,7 +3221,6 @@ async def test_cache_point_filtering(): assert content[1] == {'text': 'text after'} -# API 에러 테스트 데이터 @pytest.mark.parametrize( 'error_class,error_response,expected_status', [ @@ -3260,4 +3259,4 @@ async def test_google_api_errors_are_handled( await agent.run('This prompt will trigger the mocked error.') assert exc_info.value.status_code == expected_status - assert error_response['error']['message'] in str(exc_info.value.body) \ No newline at end of file + assert error_response['error']['message'] in str(exc_info.value.body) From 94768e01f7b694ad6734526e3fdca0b77f1c5d2c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 18 Nov 2025 23:44:36 +0000 Subject: [PATCH 4/4] improve typing --- pydantic_ai_slim/pydantic_ai/models/google.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 4829e1aa36..1ff9653411 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -398,7 +398,11 @@ async def _generate_content( return await func(model=self._model_name, contents=contents, config=config) # type: ignore except errors.APIError as e: if (status_code := e.code) >= 400: - raise ModelHTTPError(status_code=status_code, model_name=self._model_name, body=e.details) from e # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType] + raise ModelHTTPError( + status_code=status_code, + model_name=self._model_name, + body=cast(Any, e.details), # pyright: ignore[reportUnknownMemberType] + ) from e raise # pragma: lax no cover async def _build_content_and_config(