From c284568b3cfe58f6e1f9cc09f0f1b1ceed39373a Mon Sep 17 00:00:00 2001 From: Wilson0511 <1795618152@qq.com> Date: Sun, 1 Nov 2026 16:09:33 +0800 Subject: [PATCH] Python: Reject candidate_count>1 in the Gemini chat client instead of silently dropping candidates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the streaming and non-streaming parsers read candidates[0] only. candidate_count flows to the API through the generic option passthrough, so a multi-candidate request is billed for every candidate while all but the first are silently discarded, and finish reasons and usage are taken from candidate 0 alone. Reject the option up front, mirroring the Ollama client's explicit rejection of unsupported tool_choice modes. 🤖 Generated with ZCode Co-Authored-By: ZCode --- .../agent_framework_gemini/_chat_client.py | 10 +++++++++ .../gemini/tests/test_gemini_client.py | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/python/packages/gemini/agent_framework_gemini/_chat_client.py b/python/packages/gemini/agent_framework_gemini/_chat_client.py index 1c41055639..85a4e2e2f6 100644 --- a/python/packages/gemini/agent_framework_gemini/_chat_client.py +++ b/python/packages/gemini/agent_framework_gemini/_chat_client.py @@ -892,6 +892,16 @@ def _prepare_config( if key == "stop" and isinstance(value, str): # ChatOptions allows a single stop string; stop_sequences takes a list. value = [value] + if key == "candidate_count" and value > 1: + # Both parsers read candidates[0] only, so more than one candidate + # would be billed but silently dropped. Reject explicitly rather + # than behave like candidate_count=1 (mirrors the Ollama client's + # unsupported-tool_choice rejection). + raise ChatClientInvalidRequestException( + "Gemini chat client supports a single candidate. " + f"candidate_count={value} would silently drop every candidate but the first; " + "request one candidate (the default) instead." + ) kwargs[_OPTION_TRANSLATIONS.get(key, key)] = value response_format = options.get("response_format") diff --git a/python/packages/gemini/tests/test_gemini_client.py b/python/packages/gemini/tests/test_gemini_client.py index 484fdd144a..7aafbac8dc 100644 --- a/python/packages/gemini/tests/test_gemini_client.py +++ b/python/packages/gemini/tests/test_gemini_client.py @@ -3173,3 +3173,25 @@ async def sdk_stream(): break assert closed + + +def test_candidate_count_above_one_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None: + """``candidate_count > 1`` is rejected instead of silently dropping candidates. + + Both the streaming and non-streaming parsers read ``candidates[0]`` only, so a + multi-candidate request would bill for every candidate while silently + discarding all but the first. Mirrors the explicit-rejection precedent for + unsupported provider options (e.g. Ollama's ``tool_choice`` handling). + """ + monkeypatch.setenv("GOOGLE_API_KEY", "test-key") + client = GeminiChatClient(model="gemini-2.5-flash") + + with pytest.raises(ChatClientInvalidRequestException, match="candidate_count"): + client._prepare_config({"candidate_count": 2}, None) + + # A single candidate (the default) flows through unchanged. + config = client._prepare_config({"candidate_count": 1}, None) + assert config.candidate_count == 1 + + config = client._prepare_config({}, None) + assert config.candidate_count is None