From db3d22912e5994747668d74693fca9b9628b601b Mon Sep 17 00:00:00 2001 From: b3nw Date: Sun, 14 Jun 2026 04:44:47 +0000 Subject: [PATCH] fix(config): reasoning toggle for nested Gemini custom provider routes (#3431) Version-gated the nested-gateway allow to 2.5-series/3-era (reviewer fix: gemini-1.5/1.0 have no thinking controls -> selector would fail on send) + pre-2.5 exclusion regression tests. Stamped v0.51.408 (Release NU). Co-authored-by: b3nw --- CHANGELOG.md | 6 ++ api/config.py | 48 ++++++++++++++++ ...st_custom_provider_bare_model_reasoning.py | 55 +++++++++++++++++++ 3 files changed, 109 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b37c88e..92da87514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.408] — 2026-06-14 — Release NU (reasoning selector for nested Gemini custom-provider routes, #3431) + +### Fixed + +- **Custom provider model ids with nested Gemini gateway routes (e.g. `vertex/gemini-2.5-…`, `gemini_cli/gemini-2.5-…`) now expose the reasoning effort selector when the underlying model supports thinking.** Extends the heuristic fallback used for bare and dot-separated custom names; embedding and image-only Gemini routes stay excluded, and the allow is version-gated to the reasoning-capable families (Gemini 2.5 series and 3-era) so pre-2.5 routes like `vertex/gemini-1.5-pro` — which have no thinking controls — don't get a selector that would fail on send. OpenRouter-style `x-ai/…` ids continue to use the existing slash-prefix list. (#3431) + ## [v0.51.407] — 2026-06-14 — Release NT (nest forked sessions under their parent in the sidebar, #3224) ### Added diff --git a/api/config.py b/api/config.py index f6058dd0a..b0783d07b 100644 --- a/api/config.py +++ b/api/config.py @@ -2482,6 +2482,49 @@ def _candidate_supports_reasoning(candidate: str) -> bool: return False +def _nested_route_reasoning_denied(model: str) -> bool: + """Hard deny for nested Gemini gateway routes that must never show a reasoning toggle.""" + lower = str(model or "").strip().lower() + if not lower: + return False + for prefix in ("vertex/gemini-", "gemini_cli/gemini-"): + if lower.startswith(prefix): + tail = lower[len(prefix) :] + if tail.startswith("embedding") or "image" in tail or "imagine" in tail: + return True + return False + + +def _nested_gateway_route_reasoning(model: str) -> bool: + """Recognize nested ``vertex/gemini-`` and ``gemini_cli/gemini-`` routes on custom providers. + + The slash-prefix heuristic list includes ``google/gemini-2`` but not gateway-prefixed + Gemini ids, so capable models behind custom aggregators stayed hidden. + """ + lower = str(model or "").strip().lower() + if not lower: + return False + for prefix in ("vertex/gemini-", "gemini_cli/gemini-"): + if lower.startswith(prefix): + tail = lower[len(prefix) :] + if tail.startswith("embedding") or "image" in tail or "imagine" in tail: + return False + # Gemini thinking/reasoning controls are documented for the 2.5 + # series and 3-era models only — 1.5 (and earlier) have no thinking + # support, so a reasoning selector on e.g. ``vertex/gemini-1.5-pro`` + # would let a user pick an effort that the route then rejects. + # Version-gate the allow to the reasoning-capable families. + if ( + tail == "2.5" + or tail.startswith(("2.5-", "2.5.", "3-", "3.")) + or "thinking" in tail + or "reasoning" in tail + ): + return True + return False + return False + + def _filter_reasoning_efforts_for_provider( efforts: list[str], model_id: str, @@ -2535,6 +2578,8 @@ def _heuristic_reasoning_efforts(model_id: str, provider_id: str) -> list[str]: ) if any(model.startswith(prefix) for prefix in prefixes): return list(VALID_REASONING_EFFORTS) + if _nested_gateway_route_reasoning(model): + return list(VALID_REASONING_EFFORTS) # Named custom providers often rewrite model ids with dots, underscores, or # extra vendor namespaces. Normalize those shapes before applying family-level # reasoning heuristics so "deepseek.v3.2", "deepseek_v4_flash", and @@ -2602,6 +2647,9 @@ def resolve_model_reasoning_efforts( hinted_model = _strip_provider_hint_for_reasoning(model) + if _nested_route_reasoning_denied(hinted_model): + return [] + try: from hermes_cli.models import ( github_model_reasoning_efforts, diff --git a/tests/test_custom_provider_bare_model_reasoning.py b/tests/test_custom_provider_bare_model_reasoning.py index 2efce2955..7bcb157f5 100644 --- a/tests/test_custom_provider_bare_model_reasoning.py +++ b/tests/test_custom_provider_bare_model_reasoning.py @@ -248,3 +248,58 @@ def test_deepseek_non_reasoning_variants_excluded(model_id): ) +# ── custom provider nested Gemini routes (vertex/, gemini_cli/) ─────────────── + + +@pytest.mark.parametrize( + "model_id", + [ + "vertex/gemini-3.1-pro-preview", + "vertex/gemini-3-pro-preview", + "gemini_cli/gemini-3-pro-preview", + ], +) +def test_custom_nested_gemini_routes_expose_reasoning(model_id): + efforts = cfg.resolve_model_reasoning_efforts( + model_id, + provider_id="custom:newapi", + ) + assert set(efforts) >= {"low", "medium", "high"}, ( + f"{model_id} via custom:newapi should expose reasoning efforts" + ) + + +@pytest.mark.parametrize( + "model_id", + [ + "vertex/gemini-embedding-001", + "vertex/gemini-3-pro-image-preview", + ], +) +def test_custom_nested_gemini_routes_exclude_non_reasoning(model_id): + assert cfg.resolve_model_reasoning_efforts( + model_id, + provider_id="custom:newapi", + ) == [], f"{model_id} must not expose reasoning efforts" + + +@pytest.mark.parametrize( + "model_id", + [ + "vertex/gemini-1.5-pro", + "vertex/gemini-1.5-flash", + "gemini_cli/gemini-1.5-pro", + "vertex/gemini-1.0-pro", + ], +) +def test_custom_nested_gemini_pre_2_5_routes_exclude_reasoning(model_id): + """Gemini thinking/reasoning controls are documented for the 2.5 series and + 3-era models only — 1.5 (and earlier) have no thinking support, so the + nested-gateway detection must not expose a reasoning selector for them + (a user could otherwise pick an effort the route then rejects).""" + assert cfg.resolve_model_reasoning_efforts( + model_id, + provider_id="custom:newapi", + ) == [], f"{model_id} (pre-2.5 Gemini) must not expose reasoning efforts" + +