Skip to content

Python: Reject candidate_count>1 in the Gemini chat client instead of silently dropping candidates - #9929

Open
Wilson_Jiang (Wilson0511) wants to merge 1 commit into
microsoft:mainfrom
Wilson0511:fix/gemini-multi-candidate-silent-drop
Open

Wilson_Jiang (Wilson0511) wants to merge 1 commit into
microsoft:mainfrom
Wilson0511:fix/gemini-multi-candidate-silent-drop

Conversation

@Wilson0511

Copy link
Copy Markdown
Contributor

Fixes #9928

Description

candidate_count flows to the Gemini API through the generic option passthrough, but both the streaming and non-streaming parsers read candidates[0] only — so candidate_count > 1 is billed for every candidate while all but the first are silently discarded, and finish reasons and usage are attributed to candidate 0 alone. Reject the option up front in _prepare_config with ChatClientInvalidRequestException, mirroring the Ollama client's explicit rejection of unsupported tool_choice modes. candidate_count=1 and absent flow through unchanged.

RegressionCheck

  • test_candidate_count_above_one_is_rejected: {"candidate_count": 2} raises before any request is built; {"candidate_count": 1} still forwards (config.candidate_count == 1); absent stays None.
  • Full tests/ for the gemini package on this branch: failure set identical to main (pre-existing environment failures only; zero new).

🤖 Generated with ZCode
Co-Authored-By: ZCode noreply@zcode.dev

… silently dropping candidates

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 <noreply@zcode.dev>
@Wilson0511

Copy link
Copy Markdown
Contributor Author

The initial push of this branch was mis-based (it accidentally contained an unrelated worktree snapshot) and was force-pushed to a single clean commit (c284568, gemini files only). The Copilot review above was submitted against that stale commit — its summary ("shell API change", "Redis migration test") references files that are not part of this PR. The current diff is exactly two gemini files (+32 lines).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The shell API change breaks existing callers, and the Redis migration test leaves a test-owned key behind during cleanup.

No open issues 🎉

What changed in this PR

This PR addresses #9928 by rejecting Gemini requests for multiple candidates rather than silently discarding responses. It also includes broader changes to Python Agent Framework context management, resource limits, and DevUI file handling.

Changes:

  • Reject unsupported Gemini candidate counts and preserve both MCP approval lists in OpenAI and Foundry clients.
  • Add experimental agent-triggered compaction, skill unloading, and identity-preserving Redis scope filters.
  • Bound shell output and Monty attachment capture, and restrict DevUI file URLs, with supporting tests and documentation.
File Description
python/​packages/​tools/​tests/​test_shell_truncate_and_quote.py Test bounded streaming capture and decoding.
python/​packages/​tools/​tests/​test_shell_parse_rc.py Test incremental completion-marker parsing.
python/​packages/​tools/​tests/​test_local_shell_tool.py Cover output limits, cancellation, and pipe cleanup.
python/​packages/​tools/​tests/​test_docker_shell_tool.py Cover bounded Docker output and termination cleanup.
python/​packages/​tools/​README.md Document independent output limits.
python/​packages/​tools/​agent_framework_tools/​shell/​_types.py Update truncation field documentation.
python/​packages/​tools/​agent_framework_tools/​shell/​_truncate.py Add bounded byte and text buffers.
python/​packages/​tools/​agent_framework_tools/​shell/​_tool.py Expose separate stdout and stderr limits.
python/​packages/​tools/​agent_framework_tools/​shell/​_session.py Bound persistent capture and parse markers incrementally.
python/​packages/​tools/​agent_framework_tools/​shell/​_executor.py Use concurrent bounded stateless capture.
python/​packages/​tools/​agent_framework_tools/​shell/​_docker.py Apply per-stream limits and revise cleanup.
python/​packages/​tools/​agent_framework_tools/​shell/​_capture.py Add shared subprocess capture and pipe cleanup.
python/​packages/​redis/​tests/​test_providers.py Update scope-schema expectations and mocks.
python/​packages/​redis/​tests/​test_context_provider_sdk.py Verify scope tokens and client ownership.
python/​packages/​redis/​tests/​test_context_provider_integration.py Add live isolation and migration tests.
python/​packages/​redis/​tests/​test_context_provider_edges.py Cover scope encoding and schema validation.
python/​packages/​redis/​README.md Document incompatible-schema migration.
python/​packages/​redis/​agent_framework_redis/​_context_provider.py Add hashed scope filters and compatibility checks.
python/​packages/​openai/​tests/​openai/​test_openai_chat_client.py Test continuation rejection and combined approval lists.
python/​packages/​openai/​agent_framework_openai/​_chat_client.py Declare continuation aliases and merge approval lists.
python/​packages/​monty/​tests/​monty/​test_monty_codeact.py Test attachment budgets and bounded reads.
python/​packages/​monty/​README.md Document per-execution capture limits.
python/​packages/​monty/​agent_framework_monty/​_instructions.py Describe capture limits in agent instructions.
python/​packages/​monty/​agent_framework_monty/​_execute_code_tool.py Enforce attachment count and byte budgets.
python/​packages/​gemini/​tests/​test_gemini_client.py Test candidate-count rejection and defaults.
python/​packages/​gemini/​agent_framework_gemini/​_chat_client.py Reject requests for multiple candidates.
python/​packages/​foundry/​tests/​foundry/​test_foundry_chat_client.py Verify both MCP approval lists survive.
python/​packages/​foundry/​agent_framework_foundry/​_chat_client.py Merge MCP approval lists without overwriting.
python/​packages/​devui/​frontend/​yarn.lock Lock added browser-test dependencies.
python/​packages/​devui/​frontend/​test/​file-urls.test.mjs Test URL restrictions and embedded-file lifecycle.
python/​packages/​devui/​frontend/​src/​lib/​file-urls.ts Validate file URLs and embedded formats.
python/​packages/​devui/​frontend/​src/​components/​features/​agent/​message-renderers/​OpenAIContentRenderer.tsx Use validated links and managed blob URLs.
python/​packages/​devui/​frontend/​package.json Add file-URL tests and Happy DOM.
python/​packages/​devui/​frontend/​package-lock.json Lock updated frontend dependencies.
python/​packages/​core/​tests/​core/​test_function_invocation_logic.py Verify rejection-result metadata.
python/​packages/​core/​tests/​core/​test_compaction.py Cover trigger semantics and skill unloading.
python/​packages/​core/​tests/​core/​test_clients.py Test client-managed-history requirements.
python/​packages/​core/​tests/​core/​test_agents.py Test compaction within agent execution.
python/​packages/​core/​AGENTS.md Document compaction architecture and constraints.
python/​packages/​core/​agent_framework/​_types.py Define rejection and skill-provenance keys.
python/​packages/​core/​agent_framework/​_tools.py Mark rejected approval results.
python/​packages/​core/​agent_framework/​_skills.py Record resolved skill identity on results.
python/​packages/​core/​agent_framework/​_feature_stage.py Register the experimental compaction feature.
python/​packages/​core/​agent_framework/​_compaction.py Add trigger-driven compaction and skill unloading.
python/​packages/​core/​agent_framework/​_clients.py Reject incompatible service-managed history options.
python/​packages/​core/​agent_framework/​__init__.pyi Export new strategy types.
python/​packages/​core/​agent_framework/​__init__.py Expose new strategies through lazy exports.

Review effort: Balanced

# 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; "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default should be that folks remove it altogether, but if passed, it needs to be 1

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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 as value would also be wrong, so let's make it:

Suggested change
if key == "candidate_count" and value > 1:
if key == "candidate_count" and value != "1":

This branch was previously deployed

1 inactive deployment
github-app-auth — c284568b Deployed Nov 1, 2026 by Wilson0511 via add_label #26782
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: Gemini chat client silently drops all but the first candidate when candidate_count > 1

3 participants