Gemini reasoning config mapping and visibility bugs
Cluster 16941 · agent. Data as of
3 open items. Fix merged in #16999. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #16999. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #16999. That is separate from the fix event. - Still open
- 1 issue and 2 PRs.
Next step: read #16999, then check whether your report is one of the 1 still open.
Read #16999Pick up a Lab taskThese issues and fixes address how reasoning/thinking configuration is bridged and mapped for Gemini and Vertex routes, including thought summaries incorrectly appearing in user-visible content and reasoning effort being incorrectly coupled with thought-summary visibility.
Open issues1
- Issue#64035[Bug]: Gemini/Vertex thought summaries leak into user-visible content with display.show_reasoning: false (effort and visibility coupled via include_thoughts)
Open PRs2
Fixes on record
-
#16999 fix(gemini): bridge reasoning_config into thinking_config for chat-completions routes
files:
agent/transports/chat_completions.py,run_agent.py,tests/agent/transports/test_chat_completions.pyShow the diff
-
agent/transports/chat_completions.py@@ -18,6 +18,58 @@ from agent.transports.types import NormalizedResponse, ToolCall, Usage +def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) -> dict | None: + """Translate Hermes/OpenRouter-style reasoning config to Gemini thinkingConfig. + + Gemini native/cloud-code adapters do not read ``extra_body.reasoning``. + They only inspect ``extra_body.thinking_config`` / ``thinkingConfig`` and + then request thought parts with ``includeThoughts`` enabled. + """ + if reasoning_config is None or not isinstance(reasoning_config, dict): + return None + + if reasoning_config.get("enabled") is False: + # Gemini can hide thought parts even when internal thinking still + # happens; omit thinkingLevel to avoid model-specific validation quirks. + return {"includeThoughts": False} + + effort = str(reasoning_config.get("effort", "medium") or "medium").strip().lower() + if effort == "none": + return {"includeThoughts": False} + + thinking_config: Dict[str, Any] = {"includeThoughts": True} + normalized_model = (model or "").strip().lower() + if normalized_model.startswith("google/"): + normalized_model = normalized_model.split("/", 1)[1] + + # Gemini 2.5 accepts thinkingBudget; don't guess a budget from Hermes' + # coarse effort levels. ``includeThoughts`` alone is enough to surface + # thought parts without risking request validation errors. + if normalized_model.startswith("gemini-2.5-"): + return thinking_config + + if effort not in {"minimal", "low", "medium", "high", "xhigh"}: + effort = "medium" + + # Gemini 3 Flash documents low/medium/high thinking levels; Gemini 3 Pro + # is stricter (low/high). Clamp Hermes' wider effort set to what each + # family accepts so we never forward an undocumented level verbatim. + if normalized_model.startswith(("gemini-3", "gemini-3.1")): + if "flash" in normalized_model: + if effort in {"minimal", "low"}: + thinking_config["thinkingLevel"] = "low" + elif effort in {"high", "xhigh"}: + thinking_config["thinkingLevel"] = "high" + else: + thinking_config["thinkingLevel"] = "medium" + elif "pro" in normalized_model: + thinking_config["thinkingLevel"] = ( + "high" if effort in {"high", "xhigh"} else "low" + ) + + return thinking_config + + class ChatCompletionsTransport(ProviderTransport): """Transport for api_mode='chat_completions'. @@ -241,6 +293,7 @@ def build_kwargs( is_openrouter = params.get("is_openrouter", False) is_nous = params.get("is_nous", False) is_github_models = params.get("is_github_models", False) + provider_name = str(params.get("provider_name") or "").strip().lower() provider_prefs = params.get("provider_preferences") if provider_prefs and is_openrouter: @@ -293,6 +346,11 @@ def build_kwargs( if is_qwen: extra_body["vl_high_resolution_images"] = True + if provider_name in {"gemini", "google-gemini-cli"}: + thinking_config = _build_gemini_thinking_config(model, reasoning_config) + if thinking_config: + extra_body["thinking_config"] = thinking_config + # Merge any pre-built extra_body additions additions = params.get("extra_body_additions") if additions: -
run_agent.py@@ -8094,6 +8094,7 @@ def _build_api_kwargs(self, api_messages: list) -> dict: supports_reasoning=self._supports_reasoning_extra_body(), github_reasoning_extra=self._github_models_reasoning_extra_body() if _is_gh else None, anthropic_max_output=_ant_max, + provider_name=self.provider, ) def _supports_reasoning_extra_body(self) -> bool: -
tests/agent/transports/test_chat_completions.py@@ -4,7 +4,7 @@ from types import SimpleNamespace from agent.transports import get_transport -from agent.transports.types import NormalizedResponse, ToolCall +from agent.transports.types import NormalizedResponse @pytest.fixture @@ -122,6 +122,90 @@ def test_custom_think_false(self, transport): ) assert kw["extra_body"]["think"] is False + def test_gemini_without_explicit_reasoning_config_keeps_existing_behavior(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="gemini", + ) + assert "thinking_config" not in kw.get("extra_body", {}) + + def test_gemini_flash_reasoning_maps_to_thinking_config(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="gemini", + reasoning_config={"enabled": True, "effort": "high"}, + ) + assert kw["extra_body"]["thinking_config"] == { + "includeThoughts": True, + "thinkingLevel": "high", + } + + def test_gemini_25_reasoning_only_enables_visible_thoughts(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-2.5-flash", + messages=msgs, + provider_name="gemini", + reasoning_config={"enabled": True, "effort": "high"}, + ) + assert kw["extra_body"]["thinking_config"] == { + "includeThoughts": True, + } + + def test_gemini_pro_reasoning_clamps_to_supported_levels(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="google/gemini-3.1-pro-preview", + messages=msgs, + provider_name="gemini", + reasoning_config={"enabled": True, "effort": "medium"}, + ) + assert kw["extra_body"]["thinking_config"] == { + "includeThoughts": True, + "thinkingLevel": "low", + } + + def test_gemini_disabled_reasoning_hides_thoughts(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="gemini", + reasoning_config={"enabled": False}, + ) + assert kw["extra_body"]["thinking_config"] == { + "includeThoughts": False, + } + + def test_gemini_xhigh_clamps_to_high(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="gemini", + reasoning_config={"enabled": True, "effort": "xhigh"}, + ) + assert kw["extra_body"]["thinking_config"]["thinkingLevel"] == "high" + + def test_gemini_flash_minimal_clamps_to_low(self, transport): + # Gemini 3 Flash documents low/medium/high; "minimal" isn't accepted, + # so clamp it down to "low" rather than forwarding it verbatim. + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="gemini", + reasoning_config={"enabled": True, "effort": "minimal"}, + ) + assert kw["extra_body"]["thinking_config"] == { + "includeThoughts": True, + "thinkingLevel": "low", + } + def test_max_tokens_with_fn(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs(
-
-
#17605 fix(gemini): nest OpenAI-compat thinking config under google (salvage #17389)
files:
agent/auxiliary_client.py,agent/transports/chat_completions.py,run_agent.py,scripts/release.py,tests/agent/transports/test_chat_completions.py,tests/run_agent/test_provider_parity.pyShow the diff
-
agent/auxiliary_client.py@@ -101,6 +101,14 @@ def __repr__(self): logger = logging.getLogger(__name__) +def _safe_isinstance(obj: Any, maybe_type: Any) -> bool: + """Return False instead of raising when a patched symbol is not a type.""" + try: + return isinstance(obj, maybe_type) + except TypeError: + return False + + def _extract_url_query_params(url: str): """Extract query params from URL, return (clean_url, default_query dict or None).""" parsed = urlparse(url) @@ -861,20 +869,20 @@ def _maybe_wrap_anthropic( - The ``anthropic`` SDK is not installed (falls back to OpenAI wire). """ # Already wrapped — don't double-wrap. - if isinstance(client_obj, AnthropicAuxiliaryClient): + if _safe_isinstance(client_obj, AnthropicAuxiliaryClient): return client_obj # Other specialized adapters we should never re-dispatch. - if isinstance(client_obj, CodexAuxiliaryClient): + if _safe_isinstance(client_obj, CodexAuxiliaryClient): return client_obj try: from agent.gemini_native_adapter import GeminiNativeClient - if isinstance(client_obj, GeminiNativeClient): + if _safe_isinstance(client_obj, GeminiNativeClient): return client_obj except ImportError: pass try: from agent.copilot_acp_client import CopilotACPClient - if isinstance(client_obj, CopilotACPClient): + if _safe_isinstance(client_obj, CopilotACPClient): return client_obj except ImportError: pass -
agent/transports/chat_completions.py@@ -20,12 +20,7 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) -> dict | None: - """Translate Hermes/OpenRouter-style reasoning config to Gemini thinkingConfig. - - Gemini native/cloud-code adapters do not read ``extra_body.reasoning``. - They only inspect ``extra_body.thinking_config`` / ``thinkingConfig`` and - then request thought parts with ``includeThoughts`` enabled. - """ + """Translate Hermes/OpenRouter-style reasoning config to Gemini thinkingConfig.""" if reasoning_config is None or not isinstance(reasoning_config, dict): return None @@ -71,6 +66,30 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) -> return thinking_config +def _snake_case_gemini_thinking_config(config: dict | None) -> dict | None: + """Convert Gemini thinking config keys to the OpenAI-compat field names.""" + if not isinstance(config, dict) or not config: + return None + + translated: Dict[str, Any] = {} + if isinstance(config.get("includeThoughts"), bool): + translated["include_thoughts"] = config["includeThoughts"] + if isinstance(config.get("thinkingLevel"), str) and config["thinkingLevel"].strip(): + translated["thinking_level"] = config["thinkingLevel"].strip().lower() + if isinstance(config.get("thinkingBudget"), (int, float)): + translated["thinking_budget"] = int(config["thinkingBudget"]) + return translated or None + + +def _is_gemini_openai_compat_base_url(base_url: Any) -> bool: + normalized = str(base_url or "").strip().rstrip("/").lower() + if not normalized: + return False + if "generativelanguage.googleapis.com" not in normalized: + return False + return normalized.endswith("/openai") + + class ChatCompletionsTransport(ProviderTransport): """Transport for api_mode='chat_completions'. @@ -309,6 +328,7 @@ def build_kwargs( is_nous = params.get("is_nous", False) is_github_models = params.get("is_github_models", False) provider_name = str(params.get("provider_name") or "").strip().lower() + base_url = params.get("base_url") provider_prefs = params.get("provider_preferences") if provider_prefs and is_openrouter: @@ -362,7 +382,19 @@ def build_kwargs( if is_qwen: extra_body["vl_high_resolution_images"] = True - if provider_name in {"gemini", "google-gemini-cli"}: + if provider_name == "gemini": + raw_thinking_config = _build_gemini_thinking_config(model, reasoning_config) + if _is_gemini_openai_compat_base_url(base_url): + thinking_config = _snake_case_gemini_thinking_config(raw_thinking_config) + if thinking_config: + openai_compat_extra = extra_body.get("extra_body", {}) + google_extra = openai_compat_extra.get("google", {}) + google_extra["thinking_config"] = thinking_config + openai_compat_extra["google"] = google_extra + extra_body["extra_body"] = openai_compat_extra + elif raw_thinking_config: + extra_body["thinking_config"] = raw_thinking_config + elif provider_name == "google-gemini-cli": thinking_config = _build_gemini_thinking_config(model, reasoning_config) if thinking_config: extra_body["thinking_config"] = thinking_config -
run_agent.py@@ -8266,6 +8266,7 @@ def _build_api_kwargs(self, api_messages: list) -> dict: model=self.model, messages=_msgs_for_chat, tools=self.tools, + base_url=self.base_url, timeout=self._resolved_api_call_timeout(), max_tokens=[redacted:secret-assignment] ephemeral_max_output_tokens=[redacted:secret-assignment] -
scripts/release.py@@ -66,6 +66,7 @@ "[redacted:email]": "dlkakbs", "[redacted:email]": "BeliefanX", "[redacted:email]": "Mind-Dragon", + "[redacted:email]": "Nanako0129", "[redacted:email]": "westers", "[redacted:email]": "yeyitech", "[redacted:email]": "beenherebefore", -
tests/agent/transports/test_chat_completions.py@@ -122,21 +122,25 @@ def test_custom_think_false(self, transport): ) assert kw["extra_body"]["think"] is False - def test_gemini_without_explicit_reasoning_config_keeps_existing_behavior(self, transport): + def test_gemini_native_without_explicit_reasoning_config_keeps_existing_behavior(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="gemini-3-flash-preview", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta", ) assert "thinking_config" not in kw.get("extra_body", {}) + assert "google" not in kw.get("extra_body", {}) + assert "extra_body" not in kw.get("extra_body", {}) - def test_gemini_flash_reasoning_maps_to_thinking_config(self, transport): + def test_gemini_native_flash_reasoning_maps_to_top_level_thinking_config(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="gemini-3-flash-preview", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta", reasoning_config={"enabled": True, "effort": "high"}, ) assert kw["extra_body"]["thinking_config"] == { @@ -144,52 +148,85 @@ def test_gemini_flash_reasoning_maps_to_thinking_config(self, transport): "thinkingLevel": "high", } - def test_gemini_25_reasoning_only_enables_visible_thoughts(self, transport): + def test_gemini_openai_compat_flash_reasoning_maps_to_nested_google_thinking_config(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta/openai", + reasoning_config={"enabled": True, "effort": "high"}, + ) + assert "thinking_config" not in kw["extra_body"] + assert kw["extra_body"]["extra_body"]["google"]["thinking_config"] == { + "include_thoughts": True, + "thinking_level": "high", + } + + def test_gemini_native_25_reasoning_only_enables_visible_thoughts(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="gemini-2.5-flash", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta", reasoning_config={"enabled": True, "effort": "high"}, ) assert kw["extra_body"]["thinking_config"] == { "includeThoughts": True, } - def test_gemini_pro_reasoning_clamps_to_supported_levels(self, transport): + def test_gemini_openai_compat_pro_reasoning_clamps_to_supported_levels(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="google/gemini-3.1-pro-preview", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta/openai", reasoning_config={"enabled": True, "effort": "medium"}, ) - assert kw["extra_body"]["thinking_config"] == { - "includeThoughts": True, - "thinkingLevel": "low", + assert kw["extra_body"]["extra_body"]["google"]["thinking_config"] == { + "include_thoughts": True, + "thinking_level": "low", } - def test_gemini_disabled_reasoning_hides_thoughts(self, transport): + def test_gemini_native_disabled_reasoning_hides_thoughts(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="gemini-3-flash-preview", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta", reasoning_config={"enabled": False}, ) assert kw["extra_body"]["thinking_config"] == { "includeThoughts": False, } - def test_gemini_xhigh_clamps_to_high(self, transport): + def test_gemini_openai_compat_xhigh_clamps_to_high(self, transport): msgs = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( model="gemini-3-flash-preview", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta/openai", reasoning_config={"enabled": True, "effort": "xhigh"}, ) - assert kw["extra_body"]["thinking_config"]["thinkingLevel"] == "high" + assert kw["extra_body"]["extra_body"]["google"]["thinking_config"]["thinking_level"] == "high" + + def test_google_gemini_cli_keeps_top_level_thinking_config(self, transport): + msgs = [{"role": "user", "content": "Hi"}] + kw = transport.build_kwargs( + model="gemini-3-flash-preview", + messages=msgs, + provider_name="google-gemini-cli", + reasoning_config={"enabled": True, "effort": "high"}, + ) + assert kw["extra_body"]["thinking_config"] == { + "includeThoughts": True, + "thinkingLevel": "high", + } + assert "google" not in kw["extra_body"] def test_gemini_flash_minimal_clamps_to_low(self, transport): # Gemini 3 Flash documents low/medium/high; "minimal" isn't accepted, @@ -199,11 +236,12 @@ def test_gemini_flash_minimal_clamps_to_low(self, transport): model="gemini-3-flash-preview", messages=msgs, provider_name="gemini", + base_url="https://generativelanguage.googleapis.com/v1beta/openai", reasoning_config={"enabled": True, "effort": "minimal"}, ) - assert kw["extra_body"]["thinking_config"] == { - "includeThoughts": True, - "thinkingLevel": "low", + assert kw["extra_body"]["extra_body"]["google"]["thinking_config"] == { + "include_thoughts": True, + "thinking_level": "low", } def test_max_tokens_with_fn(self, transport): -
tests/run_agent/test_provider_parity.py@@ -144,6 +144,36 @@ def test_strips_codex_only_tool_call_fields_from_chat_messages(self, monkeypatch assert messages[1]["tool_calls"][0]["response_item_id"] == "fc_123" assert "codex_reasoning_items" in messages[1] + def test_gemini_native_passes_base_url_for_top_level_thinking_config(self, monkeypatch): + agent = _make_agent( + monkeypatch, + "gemini", + base_url="https://generativelanguage.googleapis.com/v1beta", + model="gemini-3-flash-preview", + ) + agent.reasoning_config = {"enabled": True, "effort": "high"} + kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}]) + assert kwargs["extra_body"]["thinking_config"] == { + "includeThoughts": True, + "thinkingLevel": "high", + } + assert "extra_body" not in kwargs["extra_body"] + + def test_gemini_openai_compat_passes_base_url_for_nested_google_thinking_config(self, monkeypatch): + agent = _make_agent( + monkeypatch, + "gemini", + base_url="https://generativelanguage.googleapis.com/v1beta/openai", + model="gemini-3.1-pro-preview", + ) + agent.reasoning_config = {"enabled": True, "effort": "high"} + kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}]) + assert "thinking_config" not in kwargs["extra_body"] + assert kwargs["extra_body"]["extra_body"]["google"]["thinking_config"] == { + "include_thoughts": True, + "thinking_level": "high", + } + def test_should_sanitize_tool_calls_codex_vs_chat(self, monkeypatch): """Codex API should NOT sanitize, all other APIs should sanitize.""" # Codex mode should NOT need sanitization
-
Closed issues · 1
- Closed#16941[Bug]: Gemini chat-completions route does not bridge reasoning_config into thinking_config, so Discord show_reasoning stays empty
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.29: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab