Skip to content

Commit 4c8b628

Browse files
MarkDaoustcopybara-github
authored andcommitted
feat(client)!: Switch default HTTP client to httpx2 and use OS trust store by default
BREAKING CHANGE: `httpx2` is now the required dependency and default HTTP client instead of `httpx`, and the default `httpx2` SSL context now uses the OS certificate trust store (via `truststore` / `httpx2.create_ssl_context()`) when `SSL_CERT_FILE` and `SSL_CERT_DIR` are not set. The `httpx2` extra is kept as an empty no-op for backwards compatibility, and `httpx` is now an optional extra (`google-genai[httpx]`). Pinning the HTTP client backend via `GOOGLE_GENAI_HTTP_CLIENT`: - To opt back into legacy `httpx`, install `google-genai[httpx]` and set `GOOGLE_GENAI_HTTP_CLIENT=httpx` (raises `ImportError` if `httpx` is not installed). - Set `GOOGLE_GENAI_HTTP_CLIENT=httpx2` to explicitly require `httpx2` without fallback (raises `ImportError` if `httpx2` is not installed). - Leave `GOOGLE_GENAI_HTTP_CLIENT` unset (or `auto`) to use `httpx2` by default, falling back to `httpx` if only `httpx` is installed. #2680 PiperOrigin-RevId: 1006549935
1 parent cb1408b commit 4c8b628

49 files changed

Lines changed: 516 additions & 158 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -250,37 +250,37 @@ client = genai.Client(
250250
)
251251
```
252252

253-
### HTTP client option: httpx2
253+
### HTTP client option: httpx
254254

255-
By default, the SDK uses `httpx` for its HTTP transport. You can opt in to
256-
[`httpx2`](https://github.com/pydantic/httpx2) by installing `google-genai[httpx2]`
255+
By default, the SDK uses [`httpx2`](https://github.com/pydantic/httpx2) for its HTTP transport. You can opt in to
256+
legacy `httpx` by installing `google-genai[httpx]`
257257
and setting the `GOOGLE_GENAI_HTTP_CLIENT` environment variable:
258258

259259
```bash
260-
export GOOGLE_GENAI_HTTP_CLIENT=httpx2
260+
export GOOGLE_GENAI_HTTP_CLIENT=httpx
261261
```
262262

263-
Alternatively, you can directly inject an `httpx2.Client` or `httpx2.AsyncClient`
263+
Alternatively, you can directly inject an `httpx.Client` or `httpx.AsyncClient`
264264
via `HttpOptions`:
265265

266266
```python
267-
import httpx2
267+
import httpx
268268
from google import genai
269269
from google.genai import types
270270

271271
client = genai.Client(
272272
http_options=types.HttpOptions(
273-
httpx_client=httpx2.Client(),
274-
httpx_async_client=httpx2.AsyncClient(),
273+
httpx_client=httpx.Client(),
274+
httpx_async_client=httpx.AsyncClient(),
275275
)
276276
)
277277
```
278278

279279
### Faster async client option: Aiohttp
280280

281-
By default we use httpx for both sync and async client implementations. In order
281+
By default we use httpx2 for both sync and async client implementations. In order
282282
to have faster performance, you may install `google-genai[aiohttp]`. In Gen AI
283-
SDK we configure `trust_env=True` to match with the default behavior of httpx.
283+
SDK we configure `trust_env=True` to match with the default behavior of httpx2.
284284
Additional args of `aiohttp.ClientSession.request()` ([see `_RequestOptions` args](https://github.com/aio-libs/aiohttp/blob/v3.12.13/aiohttp/client.py#L170)) can be passed
285285
through the following way:
286286

@@ -295,7 +295,7 @@ client=Client(..., http_options=http_options)
295295

296296
### Proxy
297297

298-
Both httpx and aiohttp libraries use `urllib.request.getproxies` from
298+
Both httpx2 and aiohttp libraries use `urllib.request.getproxies` from
299299
environment variables. Before client initialization, you may set proxy (and
300300
optional `SSL_CERT_FILE`) by setting the environment variables:
301301

@@ -304,8 +304,8 @@ export HTTPS_PROXY='http://username:password@proxy_uri:port'
304304
export SSL_CERT_FILE='client.pem'
305305
```
306306

307-
If you need `socks5` proxy, httpx [supports](https://www.python-httpx.org/advanced/proxies/#socks) `socks5` proxy if you pass it via
308-
args to `httpx.Client()`. You may install `httpx[socks]` to use it.
307+
If you need `socks5` proxy, httpx2 [supports](https://www.python-httpx.org/advanced/proxies/#socks) `socks5` proxy if you pass it via
308+
args to `httpx2.Client()`. You may install `httpx2[socks]` to use it.
309309
Then, you can pass it through the following way:
310310

311311
```python

‎docs/source/index.rst‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ To set the API version to `v1alpha` for the Gemini Developer API:
218218
Faster async client option: Aiohttp
219219
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
220220

221-
By default we use httpx for both sync and async client implementations. In order
221+
By default we use httpx2 for both sync and async client implementations. In order
222222
to have faster performance, you may install `google-genai[aiohttp]`. In Gen AI
223-
SDK we configure `trust_env=True` to match with the default behavior of httpx.
223+
SDK we configure `trust_env=True` to match with the default behavior of httpx2.
224224
Additional args of `aiohttp.ClientSession.request()`
225225
(`see _RequestOptions args <https://github.com/aio-libs/aiohttp/blob/v3.12.13/aiohttp/client.py#L170>`_)
226226
can be passed through the following way:
@@ -237,7 +237,7 @@ can be passed through the following way:
237237
Proxy
238238
^^^^^^^
239239

240-
Both httpx and aiohttp libraries use `urllib.request.getproxies` from
240+
Both httpx2 and aiohttp libraries use `urllib.request.getproxies` from
241241
environment variables. Before client initialization, you may set proxy (and
242242
optional SSL_CERT_FILE) by setting the environment variables:
243243

@@ -248,9 +248,9 @@ optional SSL_CERT_FILE) by setting the environment variables:
248248
export SSL_CERT_FILE='client.pem'
249249
250250
251-
If you need `socks5` proxy, httpx `supports <https://www.python-httpx.org/advanced/proxies/#socks>`_
252-
`socks5` proxy if you pass it via args to httpx.Client(). You may install
253-
`httpx[socks]` to use it. Then you can pass it through the following way:
251+
If you need `socks5` proxy, httpx2 `supports <https://www.python-httpx.org/advanced/proxies/#socks>`_
252+
`socks5` proxy if you pass it via args to httpx2.Client(). You may install
253+
`httpx2[socks]` to use it. Then you can pass it through the following way:
254254

255255
.. code:: python
256256

‎google/genai/_api_client.py‎

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -809,13 +809,19 @@ def __del__(self) -> None:
809809
AsyncHttpx2Client = None # type: ignore[assignment,misc]
810810

811811

812+
# The HTTP client backend used when GOOGLE_GENAI_HTTP_CLIENT is unset or 'auto'.
813+
# If it is not installed, the other backend is used instead.
814+
_DEFAULT_HTTP_CLIENT_BACKEND = 'httpx2'
815+
816+
812817
def _get_http_client_backend() -> str:
813818
"""Determines whether to use 'httpx' or 'httpx2' as the default HTTP client.
814819
815820
Can be controlled via the GOOGLE_GENAI_HTTP_CLIENT environment variable:
816821
- 'httpx2': Use httpx2 (raises ImportError if not installed).
817822
- 'httpx': Use httpx (raises ImportError if not installed).
818-
- 'auto' or unset: Default to httpx if installed, otherwise fall back to httpx2.
823+
- 'auto' or unset: Use `_DEFAULT_HTTP_CLIENT_BACKEND` if installed,
824+
otherwise fall back to the other backend.
819825
"""
820826
env = os.environ.get('GOOGLE_GENAI_HTTP_CLIENT', '').lower().strip()
821827
if env == 'httpx2':
@@ -837,14 +843,14 @@ def _get_http_client_backend() -> str:
837843
'Unrecognized GOOGLE_GENAI_HTTP_CLIENT=%r; falling back to default.', env
838844
)
839845

840-
if httpx is not None:
841-
return 'httpx'
842-
# httpx is a required dependency so it _should_ always be present in a
843-
# standard install, but if a user has uninstalled httpx in an environment
844-
# that only has httpx2 installed, fall back to httpx2 before raising
846+
installed = {'httpx': httpx is not None, 'httpx2': httpx2 is not None}
847+
fallback = 'httpx' if _DEFAULT_HTTP_CLIENT_BACKEND == 'httpx2' else 'httpx2'
848+
# Either backend may be missing (httpx2 is an optional extra, and a user may
849+
# have uninstalled httpx), so fall back to the other one before raising
845850
# ImportError.
846-
if httpx2 is not None:
847-
return 'httpx2'
851+
for backend in (_DEFAULT_HTTP_CLIENT_BACKEND, fallback):
852+
if installed[backend]:
853+
return backend
848854
raise ImportError(
849855
'Neither httpx nor httpx2 is installed. Please install httpx2 or httpx.'
850856
)
@@ -1377,19 +1383,70 @@ def _ensure_httpx_ssl_ctx(
13771383
else None
13781384
)
13791385

1386+
default_backend = (
1387+
_get_http_client_backend()
1388+
if (client_cls is None or async_client_cls is None)
1389+
else None
1390+
)
1391+
if client_cls is not None:
1392+
sync_cls = client_cls
1393+
elif default_backend == 'httpx2' and SyncHttpx2Client is not None:
1394+
sync_cls = SyncHttpx2Client
1395+
else:
1396+
sync_cls = (
1397+
SyncHttpxClient
1398+
if SyncHttpxClient is not None
1399+
else (httpx.Client if httpx is not None else None)
1400+
)
1401+
1402+
if async_client_cls is not None:
1403+
async_cls = async_client_cls
1404+
elif default_backend == 'httpx2' and AsyncHttpx2Client is not None:
1405+
async_cls = AsyncHttpx2Client
1406+
else:
1407+
async_cls = (
1408+
AsyncHttpxClient
1409+
if AsyncHttpxClient is not None
1410+
else (httpx.AsyncClient if httpx is not None else None)
1411+
)
1412+
1413+
use_httpx2 = httpx2 is not None and (
1414+
(
1415+
sync_cls is not None
1416+
and isinstance(getattr(httpx2, 'Client', None), type)
1417+
and issubclass(sync_cls, httpx2.Client)
1418+
)
1419+
or (
1420+
async_cls is not None
1421+
and isinstance(getattr(httpx2, 'AsyncClient', None), type)
1422+
and issubclass(async_cls, httpx2.AsyncClient)
1423+
)
1424+
)
1425+
13801426
if ctx is None:
1381-
# Initialize the SSL context for the httpx client.
1427+
# Initialize the SSL context for the httpx/httpx2 client.
13821428
# Unlike requests, the httpx package does not automatically pull in the
13831429
# environment variables SSL_CERT_FILE or SSL_CERT_DIR. They need to be
1384-
# enabled explicitly.
1430+
# enabled explicitly. When using httpx2 without SSL_CERT_FILE or
1431+
# SSL_CERT_DIR, delegate to httpx2.create_ssl_context(), which uses
1432+
# truststore (OS native trust store).
13851433
if vertexai:
13861434
ctx = _get_client_cert_ssl_ctx()
13871435

13881436
if ctx is None:
1389-
ctx = ssl.create_default_context(
1390-
cafile=os.environ.get('SSL_CERT_FILE', certifi.where()),
1391-
capath=os.environ.get('SSL_CERT_DIR'),
1392-
)
1437+
if use_httpx2:
1438+
if os.environ.get('SSL_CERT_FILE') or os.environ.get('SSL_CERT_DIR'):
1439+
ctx = ssl.create_default_context(
1440+
cafile=os.environ.get('SSL_CERT_FILE'),
1441+
capath=os.environ.get('SSL_CERT_DIR'),
1442+
)
1443+
else:
1444+
ctx = httpx2.create_ssl_context()
1445+
else:
1446+
ctx = ssl.create_default_context(
1447+
cafile=os.environ.get('SSL_CERT_FILE', certifi.where()),
1448+
capath=os.environ.get('SSL_CERT_DIR'),
1449+
)
13931450

13941451
def _maybe_set(
13951452
raw_args: Optional[_common.StringDict],
@@ -1432,17 +1489,6 @@ def _maybe_set(
14321489
break
14331490
return args
14341491

1435-
sync_cls = client_cls or (
1436-
SyncHttpxClient
1437-
if SyncHttpxClient is not None
1438-
else (httpx.Client if httpx is not None else None)
1439-
)
1440-
async_cls = async_client_cls or (
1441-
AsyncHttpxClient
1442-
if AsyncHttpxClient is not None
1443-
else (httpx.AsyncClient if httpx is not None else None)
1444-
)
1445-
14461492
return (
14471493
_maybe_set(args, ctx, sync_cls),
14481494
_maybe_set(async_args, ctx, async_cls),

‎google/genai/_gaos/_hooks/adapters.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
BeforeRequestHook,
3535
)
3636
import asyncio
37-
import httpx
37+
try:
38+
import httpx
39+
except ImportError:
40+
import httpx2 as httpx
3841
from typing import Optional, Tuple, Union
3942

4043
# ============================================================================

‎google/genai/_gaos/_hooks/asyncsdkhooks.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
1919

20-
import httpx
20+
try:
21+
import httpx
22+
except ImportError:
23+
import httpx2 as httpx
2124
from .asynctypes import (
2225
AsyncBeforeRequestHook,
2326
AsyncAfterSuccessHook,

‎google/genai/_gaos/_hooks/asynctypes.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
BeforeRequestContext,
2525
)
2626
from abc import ABC, abstractmethod
27-
import httpx
27+
try:
28+
import httpx
29+
except ImportError:
30+
import httpx2 as httpx
2831
from typing import Optional, Tuple, Union
2932

3033

‎google/genai/_gaos/_hooks/google_genai_auth.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
from typing import Any, Callable, Mapping, Optional, Union, cast
2323

24-
import httpx
24+
try:
25+
import httpx
26+
except ImportError:
27+
import httpx2 as httpx
2528

2629
from .. import types, utils
2730
from .types import BeforeRequestContext, BeforeRequestHook

‎google/genai/_gaos/_hooks/sdkhooks.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
1919

20-
import httpx
20+
try:
21+
import httpx
22+
except ImportError:
23+
import httpx2 as httpx
2124
from .types import (
2225
SDKInitHook,
2326
BeforeRequestContext,

‎google/genai/_gaos/_hooks/types.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
from ..sdkconfiguration import SDKConfiguration
2121
from abc import ABC, abstractmethod
22-
import httpx
22+
try:
23+
import httpx
24+
except ImportError:
25+
import httpx2 as httpx
2326
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
2427

2528

‎google/genai/_gaos/_internal.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
from .basesdk import AsyncBaseSDK, BaseSDK
2323
from .types import OptionalNullable, UNSET
2424
from .utils import get_security_from_env, response_helpers
25-
import httpx
25+
try:
26+
import httpx
27+
except ImportError:
28+
import httpx2 as httpx
2629
from typing import Any, Mapping, Optional, Union, cast
2730

2831

0 commit comments

Comments
 (0)