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
24 changes: 19 additions & 5 deletions libs/agno/agno/models/google/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,28 @@ def get_client(self) -> GeminiClient:

client_params = {k: v for k, v in client_params.items() if v is not None}

if self.client_params:
client_params.update(self.client_params)

if self.timeout is not None:
# Normalize http_options to a dict so we can safely merge the timeout.
# client_params may supply http_options as either a plain dict or an
# HttpOptions object; both are normalised here to avoid silently
# skipping the timeout when the object form is used.
http_options = client_params.get("http_options", {})
if isinstance(http_options, dict):
if hasattr(http_options, "model_dump"):
# HttpOptions (or any pydantic model) → plain dict
http_options = http_options.model_dump(exclude_none=True)
elif not isinstance(http_options, dict):
log_warning(
f"Unrecognized http_options type {type(http_options).__name__!r} in client_params; "
"falling back to empty dict. Use a plain dict or HttpOptions object instead."
)
http_options = {}
# Only inject timeout when the caller has not already set one
if "timeout" not in http_options:
http_options["timeout"] = int(self.timeout * 1000)
client_params["http_options"] = http_options

if self.client_params:
client_params.update(self.client_params)
client_params["http_options"] = http_options

self.client = genai.Client(**client_params)
return self.client
Expand Down
80 changes: 66 additions & 14 deletions libs/agno/tests/unit/models/google/test_gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,20 +268,6 @@ def test_timeout_fractional_seconds(self):
_, kwargs = mock_client_cls.call_args
assert kwargs["http_options"]["timeout"] == 1500

def test_timeout_does_not_override_client_params_http_options(self):
"""Test that client_params http_options take precedence over timeout."""
model = Gemini(
api_key="test-key",
timeout=30.0,
client_params={"http_options": {"timeout": 60000}},
)

with patch("agno.models.google.gemini.genai.Client") as mock_client_cls:
model.get_client()

_, kwargs = mock_client_cls.call_args
# client_params is applied after timeout, so it should override
assert kwargs["http_options"]["timeout"] == 60000

def test_timeout_with_vertexai(self):
"""Test that timeout works correctly in Vertex AI mode."""
Expand All @@ -299,6 +285,72 @@ def test_timeout_with_vertexai(self):
assert kwargs["http_options"]["timeout"] == 10000
assert kwargs["vertexai"] is True

def test_timeout_survives_client_params_update(self):
"""Regression test for #7599: timeout must be applied AFTER client_params.update()
so that extra client_params keys don't silently overwrite http_options."""
model = Gemini(
api_key="test-key",
timeout=5.0,
# client_params carries an unrelated key — must not clobber timeout
client_params={"some_other_param": "value"},
)

with patch("agno.models.google.gemini.genai.Client") as mock_client_cls:
model.get_client()

_, kwargs = mock_client_cls.call_args
assert "http_options" in kwargs
assert kwargs["http_options"]["timeout"] == 5000

def test_timeout_with_http_options_object_in_client_params(self):
"""Regression test for #7599: when client_params supplies http_options as an
HttpOptions-like object (not a plain dict), timeout must still be injected."""

class FakeHttpOptions:
"""Minimal stand-in for google.genai.types.HttpOptions."""

def __init__(self, **kw):
self._data = kw

def model_dump(self, exclude_none=False):
if exclude_none:
return {k: v for k, v in self._data.items() if v is not None}
return dict(self._data)

model = Gemini(
api_key="test-key",
timeout=7.0,
client_params={"http_options": FakeHttpOptions(some_option="value")},
)

with patch("agno.models.google.gemini.genai.Client") as mock_client_cls:
model.get_client()

_, kwargs = mock_client_cls.call_args
http_opts = kwargs["http_options"]
# Timeout must be present even though http_options was an object
assert isinstance(http_opts, dict)
assert http_opts["timeout"] == 7000
# Pre-existing options must be preserved
assert http_opts["some_option"] == "value"

def test_explicit_timeout_in_client_params_http_options_takes_precedence(self):
"""When the user explicitly sets a timeout inside client_params http_options,
that value must not be overwritten by self.timeout."""
model = Gemini(
api_key="test-key",
timeout=30.0,
client_params={"http_options": {"timeout": 60000}},
)

with patch("agno.models.google.gemini.genai.Client") as mock_client_cls:
model.get_client()

_, kwargs = mock_client_cls.call_args
# client_params explicit timeout wins over self.timeout
assert kwargs["http_options"]["timeout"] == 60000



def test_parallel_search_requires_vertexai():
"""Test that parallel_search raises an error when vertexai is not enabled."""
Expand Down