Skip to content

fix(gemini): let 4xx fall through the structured-output ladder and gate the no_think strategy by model - #3613

Closed
konlue wants to merge 1 commit into
agentscope-ai:mainfrom
konlue:fix/gemini-structured-fallback
Closed

konlue wants to merge 1 commit into
agentscope-ai:mainfrom
konlue:fix/gemini-structured-fallback

Conversation

@konlue

@konlue konlue commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

AgentScope Version

2.0.12dev (main @ 2350af9)

Description

GeminiChatModel is the only mainline provider without a _get_structured_output_fallback_exceptions override, and its _get_retryable_exceptions returns the wide (google.genai.errors.APIError,) (model/_gemini/_model.py:250-258) — the parent of both ClientError (4xx) and ServerError (5xx). In the structured-output ladder, the retryable check runs before the fallback check, and retry-exhaustion raises unconditionally (model/_base.py:582). So any 4xx aborts the whole ladder — the auto / no_think / none strategies are never tried. Every other provider declares a narrow retryable set whose members never intersect its fallback tuple, so 4xx falls through for them.

Changes

  1. model/_base.py (give-up branch): when a retryable exception exhausts its retries but is also fallback-eligible, walk the ladder instead of raising. Providers whose retryable and fallback sets are disjoint (all but Gemini today) are unaffected — the new branch is unreachable for them. A purely transient error that is not fallback-eligible still raises as before.
  2. model/_gemini/_model.py: declare _get_structured_output_fallback_exceptions → (errors.ClientError,) so 4xx switches strategies (5xx stay retryable).
  3. model/_gemini/_model.py: implement _get_disable_thinking_kwargs, the same gap [Bug]: Ollama structured-output fallback never tries no_think — _get_disable_thinking_kwargs is not overridden #3583/fix(ollama): implement _get_disable_thinking_kwargs so structured-output fallback can disable thinking #3588 fixed for Ollama, gated by model — 2.5 Flash / Flash-Lite accept thinking_budget: 0, but the pro models reject it with 400 and Gemini 3 deprecates thinking_budget, so those return {} and keep the server default.

Complementary to #3523 (tri-state thinking_enable): that fix stopped out-of-the-box budget-0 failures for pro models; this adds the structured-output no_think step for the models that support it.

Open question for maintainers: with the give-up branch consulting the fallback tuple, a 429 (also a ClientError) now falls to the next strategy after its retries instead of aborting — bounded extra calls to a rate-limited endpoint. If that is undesirable, the alternative is gating the no_think kwargs more narrowly and leaving the base branch untouched, at the cost of keeping the abort for schema-type 4xx.

How to test

Regression cases added to the existing test files:

  • tests/model_base_test.py::StructuredOutputRetryableFallbackTest — an exception that is both retryable and fallback-eligible walks the ladder after exhausting retries (fails on main: the ladder aborts);
  • tests/model_gemini_test.py::GeminiStructuredFallbackTest — a mocked 400 walks all four strategies (len(attempts) > (max_retries + 1) * 2; fails on main with 4 not greater than 8), plus assertions pinning the fallback-exception tuple and the model-gated no_think kwargs.
python -m pytest tests/model_base_test.py tests/model_gemini_test.py
# 24 + 43 passed.

Fixes #3612

Checklist

  • An issue has been created for this PR
  • I have read the CONTRIBUTING.md
  • Docstrings are in Google style
  • Related documentation has been updated (e.g. links, examples, etc.) in documentation repository
  • Code is ready for review

@qbc2016

qbc2016 commented Nov 2, 2026

Copy link
Copy Markdown
Member

Thanks for the contribution. Before we change the fallback behavior, could you provide a real-API before/after reproduction showing that this PR turns a failing request into a successful structured response?

The current Gemini regression test increases the number of failed attempts from 4 to 16, but the request still fails. This verifies control flow, rather than demonstrating that the additional fallback attempts are useful.

Please also clarify the need for the no_think strategy. Our gemini-2.5-flash test succeeded with thinking enabled for both native structured output and forced function calling. Is there a specific model or request configuration that fails with thinking enabled but succeeds when thinking is disabled?

Finally, making all ClientError exceptions fallback-eligible also extends retries for errors such as 429 and 403, which changing the output strategy cannot resolve. Please distinguish recoverable compatibility errors from these cases and add tests covering both successful recovery and retry limits.

@konlue

konlue commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Withdrawing this PR — see #3612 for the full reply.

You're right that the regression test only shows the ladder continuing (4 → 16 attempts), not that any fallback actually succeeds. I can't produce a real-API before/after from my environment (no Gemini credential, and generativelanguage.googleapis.com is unreachable there), and your gemini-2.5-flash result shows the initial strategy working with thinking enabled, so neither the ClientError fall-through nor the no_think gate is justified by evidence.

The third point is fair as well: treating all ClientErrors as fallback-eligible would extend retries for 429/403, which a strategy change cannot fix.

Closing both. Thanks for the review.

@konlue konlue closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr/awaiting-review Waiting for maintainers to review this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gemini structured-output fallback aborts on 4xx — ClientError is treated as retryable before the fallback check

2 participants