Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions google/genai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from . import _common

from ._interactions import AsyncGeminiNextGenAPIClient, DEFAULT_MAX_RETRIES, GeminiNextGenAPIClient
from ._interactions._types import NOT_GIVEN
from . import _interactions

from ._interactions.resources import AsyncInteractionsResource as AsyncNextGenInteractionsResource, InteractionsResource as NextGenInteractionsResource
Expand Down Expand Up @@ -175,7 +176,9 @@ def _nextgen_client(self) -> AsyncGeminiNextGenAPIClient:
default_headers=http_opts.headers,
http_client=http_client,
# uSDk expects ms, nextgen uses a httpx Timeout -> expects seconds.
timeout=http_opts.timeout / 1000 if http_opts.timeout else None,
# Pass NOT_GIVEN (not None) when unset so the Stainless client uses
# its DEFAULT_TIMEOUT. httpx treats None as "no timeout".
timeout=http_opts.timeout / 1000 if http_opts.timeout else NOT_GIVEN,
max_retries=max_retries,
client_adapter=AsyncGeminiNextGenAPIClientAdapter(self._api_client)
)
Expand Down Expand Up @@ -552,7 +555,9 @@ def _nextgen_client(self) -> GeminiNextGenAPIClient:
default_headers=http_opts.headers,
http_client=self._api_client._httpx_client,
# uSDk expects ms, nextgen uses a httpx Timeout -> expects seconds.
timeout=http_opts.timeout / 1000 if http_opts.timeout else None,
# Pass NOT_GIVEN (not None) when unset so the Stainless client uses
# its DEFAULT_TIMEOUT. httpx treats None as "no timeout".
timeout=http_opts.timeout / 1000 if http_opts.timeout else NOT_GIVEN,
max_retries=max_retries,
client_adapter=GeminiNextGenAPIClientAdapter(self._api_client),
)
Expand Down
131 changes: 131 additions & 0 deletions google/genai/tests/interactions/test_default_timeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for default timeout behavior of the interactions client.

When no timeout is configured in http_options, the wrapper must pass
NOT_GIVEN to the Stainless client so it falls back to DEFAULT_TIMEOUT
(60s). Previously it passed None, which httpx interprets as "no timeout"
causing requests to hang indefinitely.
"""

from unittest import mock

import pytest

from ... import client as client_lib
from ..._interactions._types import NotGiven

pytest_plugins = ("pytest_asyncio",)


class TestSyncDefaultTimeout:
"""Sync client default timeout tests."""

def test_default_timeout_is_not_given(self):
"""When no timeout is set, NOT_GIVEN (not None) should be passed."""
with mock.patch.object(
client_lib, "GeminiNextGenAPIClient", spec_set=True
) as mock_nextgen_client:
client = client_lib.Client(
api_key="placeholder",
http_options={"api_version": "v1alpha"},
)
_ = client.interactions

timeout_arg = mock_nextgen_client.call_args.kwargs["timeout"]
assert isinstance(timeout_arg, NotGiven), (
f"Expected timeout to be NOT_GIVEN but got {timeout_arg!r}. "
f"None would disable httpx timeouts entirely."
)

def test_explicit_timeout_passes_through(self):
"""When timeout is set, it should pass through as seconds (ms / 1000)."""
with mock.patch.object(
client_lib, "GeminiNextGenAPIClient", spec_set=True
) as mock_nextgen_client:
client = client_lib.Client(
api_key="placeholder",
http_options={"api_version": "v1alpha", "timeout": 30000},
)
_ = client.interactions

mock_nextgen_client.assert_called_once_with(
base_url=mock.ANY,
api_key="placeholder",
api_version="v1alpha",
default_headers=mock.ANY,
http_client=mock.ANY,
timeout=30.0,
max_retries=mock.ANY,
client_adapter=mock.ANY,
)

def test_no_http_options_uses_not_given(self):
"""When no http_options at all, timeout should still be NOT_GIVEN."""
with mock.patch.object(
client_lib, "GeminiNextGenAPIClient", spec_set=True
) as mock_nextgen_client:
client = client_lib.Client(api_key="placeholder")
_ = client.interactions

timeout_arg = mock_nextgen_client.call_args.kwargs["timeout"]
assert isinstance(timeout_arg, NotGiven), (
f"Expected timeout to be NOT_GIVEN but got {timeout_arg!r}."
)


class TestAsyncDefaultTimeout:
"""Async client default timeout tests."""

@pytest.mark.asyncio
async def test_default_timeout_is_not_given(self):
"""When no timeout is set, NOT_GIVEN (not None) should be passed."""
with mock.patch.object(
client_lib, "AsyncGeminiNextGenAPIClient", spec_set=True
) as mock_nextgen_client:
client = client_lib.Client(
api_key="placeholder",
http_options={"api_version": "v1alpha"},
)
_ = client.aio.interactions

timeout_arg = mock_nextgen_client.call_args.kwargs["timeout"]
assert isinstance(timeout_arg, NotGiven), (
f"Expected timeout to be NOT_GIVEN but got {timeout_arg!r}. "
f"None would disable httpx timeouts entirely."
)

@pytest.mark.asyncio
async def test_explicit_timeout_passes_through(self):
"""When timeout is set, it should pass through as seconds (ms / 1000)."""
with mock.patch.object(
client_lib, "AsyncGeminiNextGenAPIClient", spec_set=True
) as mock_nextgen_client:
client = client_lib.Client(
api_key="placeholder",
http_options={"api_version": "v1alpha", "timeout": 30000},
)
_ = client.aio.interactions

mock_nextgen_client.assert_called_once_with(
base_url=mock.ANY,
api_key="placeholder",
api_version="v1alpha",
default_headers=mock.ANY,
http_client=mock.ANY,
timeout=30.0,
max_retries=mock.ANY,
client_adapter=mock.ANY,
)
Loading