Skip to content

Commit efc8330

Browse files
authored
Merge branch 'main' into fix/uri_params
2 parents 8eaeb45 + f3f5a5c commit efc8330

File tree

11 files changed

+184
-35
lines changed

11 files changed

+184
-35
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ See the 'Create a client' section above to initialize a client.
274274

275275
### Generate Content
276276

277-
#### with text content
277+
#### with text content input (text output)
278278

279279
```python
280280
response = client.models.generate_content(
@@ -283,6 +283,28 @@ response = client.models.generate_content(
283283
print(response.text)
284284
```
285285

286+
#### with text content input (image output)
287+
288+
```python
289+
from google.genai import types
290+
291+
response = client.models.generate_content(
292+
model='gemini-2.5-flash-image',
293+
contents='A cartoon infographic for flying sneakers',
294+
config=types.GenerateContentConfig(
295+
response_modalities=["IMAGE"],
296+
image_config=types.ImageConfig(
297+
aspect_ratio="9:16",
298+
),
299+
),
300+
)
301+
302+
for part in response.parts:
303+
if part.inline_data:
304+
generated_image = part.as_image()
305+
generated_image.show()
306+
```
307+
286308
#### with uploaded file (Gemini Developer API only)
287309
download the file in console.
288310

google/genai/_api_client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -693,8 +693,15 @@ def __init__(
693693
self._http_options
694694
)
695695
self._async_httpx_client_args = async_client_args
696-
self._httpx_client = SyncHttpxClient(**client_args)
697-
self._async_httpx_client = AsyncHttpxClient(**async_client_args)
696+
697+
if self._http_options.httpx_client:
698+
self._httpx_client = self._http_options.httpx_client
699+
else:
700+
self._httpx_client = SyncHttpxClient(**client_args)
701+
if self._http_options.httpx_async_client:
702+
self._async_httpx_client = self._http_options.httpx_async_client
703+
else:
704+
self._async_httpx_client = AsyncHttpxClient(**async_client_args)
698705
if self._use_aiohttp():
699706
# Do it once at the genai.Client level. Share among all requests.
700707
self._async_client_session_request_args = self._ensure_aiohttp_ssl_ctx(

google/genai/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6903,9 +6903,11 @@ async def async_generator(model, contents, config): # type: ignore[no-untyped-d
69036903
response = await self._generate_content_stream(
69046904
model=model, contents=contents, config=config
69056905
)
6906-
logger.info(f'AFC remote call {i} is done.')
6906+
# TODO: b/453739108 - make AFC logic more robust like the other 3 methods.
6907+
if i > 1:
6908+
logger.info(f'AFC remote call {i} is done.')
69076909
remaining_remote_calls_afc -= 1
6908-
if remaining_remote_calls_afc == 0:
6910+
if i > 1 and remaining_remote_calls_afc == 0:
69096911
logger.info(
69106912
'Reached max remote calls for automatic function calling.'
69116913
)

google/genai/tests/client/test_async_stream.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
"""Tests for async stream."""
1717

1818
import asyncio
19-
from collections.abc import Sequence
20-
import datetime
2119
from typing import List
2220
from unittest import mock
2321
from unittest.mock import AsyncMock
@@ -35,8 +33,6 @@
3533
import httpx
3634

3735
from ... import _api_client as api_client
38-
from ... import errors
39-
from ... import types
4036

4137

4238
class MockHTTPXResponse(httpx.Response):

google/genai/tests/client/test_client_close.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,6 @@ async def test_async_httpx_client_context_manager():
9090
assert async_client._api_client._async_httpx_client.is_closed
9191

9292

93-
@requires_aiohttp
94-
@pytest.mark.asyncio
95-
async def test_aclose_aiohttp_session():
96-
"""Tests that the aiohttp session is closed when the client is closed."""
97-
api_client.has_aiohttp = True
98-
async_client = Client(
99-
vertexai=True,
100-
project='test_project',
101-
location='global',
102-
).aio
103-
await async_client.aclose()
104-
assert async_client._api_client._aiohttp_session is None
105-
106-
10793
@requires_aiohttp
10894
@pytest.fixture
10995
def mock_request():
@@ -130,6 +116,47 @@ async def _aiohttp_async_response(status: int):
130116
return response
131117

132118

119+
@requires_aiohttp
120+
@mock.patch.object(aiohttp.ClientSession, 'request', autospec=True)
121+
def test_aclose_aiohttp_session(mock_request):
122+
"""Tests that the aiohttp session is closed when the client is closed."""
123+
api_client.has_aiohttp = True
124+
async def run():
125+
mock_request.side_effect = (
126+
aiohttp.ClientConnectorError(
127+
connection_key=aiohttp.client_reqrep.ConnectionKey(
128+
'localhost', 80, False, True, None, None, None
129+
),
130+
os_error=OSError,
131+
),
132+
_aiohttp_async_response(200),
133+
)
134+
with _patch_auth_default():
135+
async_client = Client(
136+
vertexai=True,
137+
project='test_project',
138+
location='global',
139+
).aio
140+
# aiohttp session is created in the first request instead of client
141+
# initialization.
142+
_ = await async_client._api_client._async_request_once(
143+
api_client.HttpRequest(
144+
method='GET',
145+
url='https://example.com',
146+
headers={},
147+
data=None,
148+
timeout=None,
149+
)
150+
)
151+
assert async_client._api_client._aiohttp_session is not None
152+
assert not async_client._api_client._aiohttp_session.closed
153+
# Close the client and check that the session is closed.
154+
await async_client.aclose()
155+
assert async_client._api_client._aiohttp_session.closed
156+
157+
asyncio.run(run())
158+
159+
133160
@requires_aiohttp
134161
@mock.patch.object(aiohttp.ClientSession, 'request', autospec=True)
135162
def test_aiohttp_session_context_manager(mock_request):
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
"""Tests for custom clients."""
17+
import asyncio
18+
from unittest import mock
19+
20+
from google.oauth2 import credentials
21+
import httpx
22+
import pytest
23+
24+
from ... import _api_client as api_client
25+
from ... import Client
26+
27+
28+
try:
29+
import aiohttp
30+
31+
AIOHTTP_NOT_INSTALLED = False
32+
except ImportError:
33+
AIOHTTP_NOT_INSTALLED = True
34+
aiohttp = mock.MagicMock()
35+
36+
requires_aiohttp = pytest.mark.skipif(
37+
AIOHTTP_NOT_INSTALLED, reason='aiohttp is not installed, skipping test.'
38+
)
39+
40+
41+
# Httpx
42+
def test_constructor_with_httpx_clients():
43+
mldev_http_options = {
44+
'httpx_client': httpx.Client(trust_env=False),
45+
'httpx_async_client': httpx.AsyncClient(trust_env=False),
46+
}
47+
vertexai_http_options = {
48+
'httpx_client': httpx.Client(trust_env=False),
49+
'httpx_async_client': httpx.AsyncClient(trust_env=False),
50+
}
51+
52+
mldev_client = Client(
53+
api_key='google_api_key', http_options=mldev_http_options
54+
)
55+
assert not mldev_client.models._api_client._httpx_client.trust_env
56+
assert not mldev_client.models._api_client._async_httpx_client.trust_env
57+
58+
vertexai_client = Client(
59+
vertexai=True,
60+
project='fake_project_id',
61+
location='fake-location',
62+
http_options=vertexai_http_options,
63+
)
64+
assert not vertexai_client.models._api_client._httpx_client.trust_env
65+
assert not vertexai_client.models._api_client._async_httpx_client.trust_env
66+

google/genai/tests/client/test_http_options.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def test_patch_http_options_with_copies_all_fields():
3737
http_options_keys = types.HttpOptions.model_fields.keys()
3838

3939
for key in http_options_keys:
40-
assert hasattr(patched, key) and getattr(patched, key) is not None
40+
assert hasattr(patched, key)
41+
if key not in ['httpx_client', 'httpx_async_client', 'aiohttp_client_session']:
42+
assert getattr(patched, key) is not None
4143
assert patched.base_url == 'https://fake-url.com/'
4244
assert patched.api_version == 'v1'
4345
assert patched.headers['X-Custom-Header'] == 'custom_value'

google/genai/tests/models/test_generate_content_tools.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,8 +522,6 @@ def divide_floats(a: float, b: float) -> float:
522522
]
523523
},
524524
),
525-
# TODO(b/450916996): Remove this once the feature is launched in Gemini.
526-
exception_if_mldev='400',
527525
),
528526
]
529527

google/genai/types.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@
8989
except ImportError:
9090
yaml = None
9191

92+
_is_httpx_imported = False
93+
if typing.TYPE_CHECKING:
94+
import httpx
95+
96+
HttpxClient = httpx.Client
97+
HttpxAsyncClient = httpx.AsyncClient
98+
_is_httpx_imported = True
99+
else:
100+
HttpxClient: typing.Type = Any
101+
HttpxAsyncClient: typing.Type = Any
102+
103+
try:
104+
import httpx
105+
106+
HttpxClient = httpx.Client
107+
HttpxAsyncClient = httpx.AsyncClient
108+
_is_httpx_imported = True
109+
except ImportError:
110+
HttpxClient = None
111+
HttpxAsyncClient = None
112+
92113
logger = logging.getLogger('google_genai.types')
93114

94115
T = typing.TypeVar('T', bound='GenerateContentResponse')
@@ -1530,6 +1551,15 @@ class HttpOptions(_common.BaseModel):
15301551
default=None, description="""HTTP retry options for the request."""
15311552
)
15321553

1554+
httpx_client: Optional['HttpxClient'] = Field(
1555+
default=None,
1556+
description="""A custom httpx client to be used for the request.""",
1557+
)
1558+
httpx_async_client: Optional['HttpxAsyncClient'] = Field(
1559+
default=None,
1560+
description="""A custom httpx async client to be used for the request.""",
1561+
)
1562+
15331563

15341564
class HttpOptionsDict(TypedDict, total=False):
15351565
"""HTTP options to be used in each of the requests."""

pyproject.toml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ name = "google-genai"
66
version = "1.45.0"
77
description = "GenAI Python SDK"
88
readme = "README.md"
9-
license = {text = "Apache-2.0"}
9+
license = "Apache-2.0"
1010
requires-python = ">=3.9"
1111
authors = [
1212
{ name = "Google LLC", email = "googleapis-packages@google.com" },
1313
]
1414
classifiers = [
1515
"Intended Audience :: Developers",
16-
"License :: OSI Approved :: Apache Software License",
1716
"Operating System :: OS Independent",
1817
"Programming Language :: Python",
1918
"Programming Language :: Python :: 3",
@@ -44,14 +43,9 @@ local-tokenizer = ["sentencepiece>=0.2.0", "protobuf"]
4443
[project.urls]
4544
Homepage = "https://github.com/googleapis/python-genai"
4645

47-
[tool.setuptools]
48-
packages = [
49-
"google",
50-
"google.genai",
51-
]
52-
include-package-data = true
46+
# [tool.setuptools] settings are in setup.cfg
5347

54-
[tools.setuptools.package_data]
48+
[tool.setuptools.package-data]
5549
"google.genai" = ["py.typed"]
5650

5751
[tool.mypy]

0 commit comments

Comments
 (0)