From 8ead3d7eff78b47e4caa72aab1183b06e864c147 Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 22 Apr 2026 22:50:13 +0530 Subject: [PATCH 1/3] fix(gemini): apply timeout after client_params merge and handle HttpOptions object Two bugs in Gemini.get_client() caused self.timeout to be silently ignored: 1. Ordering bug: timeout was injected into http_options BEFORE client_params.update(self.client_params) was called. Any http_options key inside client_params therefore overwrote the injected timeout. 2. Type bug: the isinstance(http_options, dict) guard silently skipped timeout injection when the user passed an HttpOptions object (e.g. HttpOptions(some_option='value')) inside client_params. The object branch was simply not handled. Fix: - Move the timeout injection block AFTER client_params.update() so client_params are fully merged before the timeout is applied. - Normalise http_options to a plain dict before merging: call model_dump(exclude_none=True) for pydantic/HttpOptions objects and fall back to {} for any other non-dict type. - Only inject self.timeout when the caller has NOT already set a timeout key, preserving explicit user overrides. Fixes #7599 --- libs/agno/agno/models/google/gemini.py | 20 +++-- .../tests/unit/models/google/test_gemini.py | 80 +++++++++++++++---- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/libs/agno/agno/models/google/gemini.py b/libs/agno/agno/models/google/gemini.py index fcf52080cf..a5111318c0 100644 --- a/libs/agno/agno/models/google/gemini.py +++ b/libs/agno/agno/models/google/gemini.py @@ -187,14 +187,24 @@ 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): + 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 diff --git a/libs/agno/tests/unit/models/google/test_gemini.py b/libs/agno/tests/unit/models/google/test_gemini.py index 7e3fb992f0..f0bdb2bbb5 100644 --- a/libs/agno/tests/unit/models/google/test_gemini.py +++ b/libs/agno/tests/unit/models/google/test_gemini.py @@ -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.""" @@ -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.""" From e712285a9e427862dacbee196339ea8dbe978be8 Mon Sep 17 00:00:00 2001 From: Ayush Date: Thu, 23 Apr 2026 14:25:26 +0530 Subject: [PATCH 2/3] fix(gemini): log warning when unrecognized http_options type is dropped --- libs/agno/agno/models/google/gemini.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/agno/agno/models/google/gemini.py b/libs/agno/agno/models/google/gemini.py index a5111318c0..06b3abb363 100644 --- a/libs/agno/agno/models/google/gemini.py +++ b/libs/agno/agno/models/google/gemini.py @@ -200,6 +200,10 @@ def get_client(self) -> GeminiClient: # 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: From f5a40bf63d2239d0b26ce74327d7d38173bc3b0d Mon Sep 17 00:00:00 2001 From: Ayush Date: Thu, 30 Apr 2026 03:13:37 +0530 Subject: [PATCH 3/3] test(gemini): restore test_timeout_does_not_override_client_params_http_options Restores the deleted test as suggested by reviewer. The test still passes under the new logic and documents the caller-explicit-timeout-wins precedence rule as a positive assertion. --- libs/agno/tests/unit/models/google/test_gemini.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libs/agno/tests/unit/models/google/test_gemini.py b/libs/agno/tests/unit/models/google/test_gemini.py index f0bdb2bbb5..b6cb7d4f27 100644 --- a/libs/agno/tests/unit/models/google/test_gemini.py +++ b/libs/agno/tests/unit/models/google/test_gemini.py @@ -268,6 +268,21 @@ 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 + # Caller's explicit timeout in client_params wins over self.timeout + assert kwargs["http_options"]["timeout"] == 60000 + def test_timeout_with_vertexai(self): """Test that timeout works correctly in Vertex AI mode."""