Skip to content

Commit c895124

Browse files
authored
Merge branch 'main' into gemini-3-pro-preview
2 parents bdb66f4 + 9301c84 commit c895124

File tree

4 files changed

+65
-5
lines changed

4 files changed

+65
-5
lines changed

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ class FilePart:
10701070

10711071
def has_content(self) -> bool:
10721072
"""Return `True` if the file content is non-empty."""
1073-
return bool(self.content) # pragma: no cover
1073+
return bool(self.content.data)
10741074

10751075
__repr__ = _utils.dataclasses_no_defaults_repr
10761076

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .._output import OutputObjectDefinition
1515
from .._run_context import RunContext
1616
from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool
17-
from ..exceptions import UserError
17+
from ..exceptions import ModelHTTPError, UserError
1818
from ..messages import (
1919
BinaryContent,
2020
BuiltinToolCallPart,
@@ -52,7 +52,7 @@
5252
)
5353

5454
try:
55-
from google.genai import Client
55+
from google.genai import Client, errors
5656
from google.genai.types import (
5757
BlobDict,
5858
CodeExecutionResult,
@@ -402,7 +402,16 @@ async def _generate_content(
402402
) -> GenerateContentResponse | Awaitable[AsyncIterator[GenerateContentResponse]]:
403403
contents, config = await self._build_content_and_config(messages, model_settings, model_request_parameters)
404404
func = self.client.aio.models.generate_content_stream if stream else self.client.aio.models.generate_content
405-
return await func(model=self._model_name, contents=contents, config=config) # type: ignore
405+
try:
406+
return await func(model=self._model_name, contents=contents, config=config) # type: ignore
407+
except errors.APIError as e:
408+
if (status_code := e.code) >= 400:
409+
raise ModelHTTPError(
410+
status_code=status_code,
411+
model_name=self._model_name,
412+
body=cast(Any, e.details), # pyright: ignore[reportUnknownMemberType]
413+
) from e
414+
raise # pragma: lax no cover
406415

407416
async def _build_content_and_config(
408417
self,

tests/models/test_google.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from httpx import Timeout
1111
from inline_snapshot import Is, snapshot
1212
from pydantic import BaseModel
13+
from pytest_mock import MockerFixture
1314
from typing_extensions import TypedDict
1415

1516
from pydantic_ai import (
@@ -43,7 +44,7 @@
4344
)
4445
from pydantic_ai.agent import Agent
4546
from pydantic_ai.builtin_tools import CodeExecutionTool, ImageGenerationTool, UrlContextTool, WebSearchTool
46-
from pydantic_ai.exceptions import ModelRetry, UnexpectedModelBehavior, UserError
47+
from pydantic_ai.exceptions import ModelHTTPError, ModelRetry, UnexpectedModelBehavior, UserError
4748
from pydantic_ai.messages import (
4849
BuiltinToolCallEvent, # pyright: ignore[reportDeprecated]
4950
BuiltinToolResultEvent, # pyright: ignore[reportDeprecated]
@@ -57,6 +58,7 @@
5758
from ..parts_from_messages import part_types_from_messages
5859

5960
with try_import() as imports_successful:
61+
from google.genai import errors
6062
from google.genai.types import (
6163
FinishReason as GoogleFinishReason,
6264
GenerateContentResponse,
@@ -3679,3 +3681,44 @@ def get_country() -> str:
36793681
),
36803682
]
36813683
)
3684+
@pytest.mark.parametrize(
3685+
'error_class,error_response,expected_status',
3686+
[
3687+
(
3688+
errors.ServerError,
3689+
{'error': {'code': 503, 'message': 'The service is currently unavailable.', 'status': 'UNAVAILABLE'}},
3690+
503,
3691+
),
3692+
(
3693+
errors.ClientError,
3694+
{'error': {'code': 400, 'message': 'Invalid request parameters', 'status': 'INVALID_ARGUMENT'}},
3695+
400,
3696+
),
3697+
(
3698+
errors.ClientError,
3699+
{'error': {'code': 429, 'message': 'Rate limit exceeded', 'status': 'RESOURCE_EXHAUSTED'}},
3700+
429,
3701+
),
3702+
],
3703+
)
3704+
3705+
3706+
async def test_google_api_errors_are_handled(
3707+
allow_model_requests: None,
3708+
google_provider: GoogleProvider,
3709+
mocker: MockerFixture,
3710+
error_class: type[errors.APIError],
3711+
error_response: dict[str, Any],
3712+
expected_status: int,
3713+
):
3714+
model = GoogleModel('gemini-1.5-flash', provider=google_provider)
3715+
mocked_error = error_class(expected_status, error_response)
3716+
mocker.patch.object(model.client.aio.models, 'generate_content', side_effect=mocked_error)
3717+
3718+
agent = Agent(model=model)
3719+
3720+
with pytest.raises(ModelHTTPError) as exc_info:
3721+
await agent.run('This prompt will trigger the mocked error.')
3722+
3723+
assert exc_info.value.status_code == expected_status
3724+
assert error_response['error']['message'] in str(exc_info.value.body)

tests/test_messages.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,14 @@ def test_pre_usage_refactor_messages_deserializable():
422422
)
423423

424424

425+
def test_file_part_has_content():
426+
filepart = FilePart(content=BinaryContent(data=b'', media_type='application/pdf'))
427+
assert not filepart.has_content()
428+
429+
filepart.content.data = b'not empty'
430+
assert filepart.has_content()
431+
432+
425433
def test_file_part_serialization_roundtrip():
426434
# Verify that a serialized BinaryImage doesn't come back as a BinaryContent.
427435
messages: list[ModelMessage] = [

0 commit comments

Comments
 (0)