fix(gemini): nest OpenAI-compat thinking config under google

This commit is contained in:
Nanako0129 2026-04-29 18:21:50 +08:00 committed by Teknium
parent 5a61c116e1
commit c5a5e586d7
3 changed files with 102 additions and 24 deletions

View file

@ -101,6 +101,14 @@ from utils import base_url_host_matches, base_url_hostname, normalize_proxy_env_
logger = logging.getLogger(__name__) 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): def _extract_url_query_params(url: str):
"""Extract query params from URL, return (clean_url, default_query dict or None).""" """Extract query params from URL, return (clean_url, default_query dict or None)."""
parsed = urlparse(url) parsed = urlparse(url)
@ -861,20 +869,20 @@ def _maybe_wrap_anthropic(
- The ``anthropic`` SDK is not installed (falls back to OpenAI wire). - The ``anthropic`` SDK is not installed (falls back to OpenAI wire).
""" """
# Already wrapped — don't double-wrap. # Already wrapped — don't double-wrap.
if isinstance(client_obj, AnthropicAuxiliaryClient): if _safe_isinstance(client_obj, AnthropicAuxiliaryClient):
return client_obj return client_obj
# Other specialized adapters we should never re-dispatch. # Other specialized adapters we should never re-dispatch.
if isinstance(client_obj, CodexAuxiliaryClient): if _safe_isinstance(client_obj, CodexAuxiliaryClient):
return client_obj return client_obj
try: try:
from agent.gemini_native_adapter import GeminiNativeClient from agent.gemini_native_adapter import GeminiNativeClient
if isinstance(client_obj, GeminiNativeClient): if _safe_isinstance(client_obj, GeminiNativeClient):
return client_obj return client_obj
except ImportError: except ImportError:
pass pass
try: try:
from agent.copilot_acp_client import CopilotACPClient from agent.copilot_acp_client import CopilotACPClient
if isinstance(client_obj, CopilotACPClient): if _safe_isinstance(client_obj, CopilotACPClient):
return client_obj return client_obj
except ImportError: except ImportError:
pass pass

View file

@ -20,12 +20,7 @@ from agent.transports.types import NormalizedResponse, ToolCall, Usage
def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) -> dict | None: def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) -> dict | None:
"""Translate Hermes/OpenRouter-style reasoning config to Gemini thinkingConfig. """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): if reasoning_config is None or not isinstance(reasoning_config, dict):
return None return None
@ -71,6 +66,30 @@ def _build_gemini_thinking_config(model: str, reasoning_config: dict | None) ->
return thinking_config 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): class ChatCompletionsTransport(ProviderTransport):
"""Transport for api_mode='chat_completions'. """Transport for api_mode='chat_completions'.
@ -309,6 +328,7 @@ class ChatCompletionsTransport(ProviderTransport):
is_nous = params.get("is_nous", False) is_nous = params.get("is_nous", False)
is_github_models = params.get("is_github_models", False) is_github_models = params.get("is_github_models", False)
provider_name = str(params.get("provider_name") or "").strip().lower() provider_name = str(params.get("provider_name") or "").strip().lower()
base_url = params.get("base_url")
provider_prefs = params.get("provider_preferences") provider_prefs = params.get("provider_preferences")
if provider_prefs and is_openrouter: if provider_prefs and is_openrouter:
@ -362,7 +382,19 @@ class ChatCompletionsTransport(ProviderTransport):
if is_qwen: if is_qwen:
extra_body["vl_high_resolution_images"] = True 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) thinking_config = _build_gemini_thinking_config(model, reasoning_config)
if thinking_config: if thinking_config:
extra_body["thinking_config"] = thinking_config extra_body["thinking_config"] = thinking_config

View file

@ -122,21 +122,25 @@ class TestChatCompletionsBuildKwargs:
) )
assert kw["extra_body"]["think"] is False 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"}] msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs( kw = transport.build_kwargs(
model="gemini-3-flash-preview", model="gemini-3-flash-preview",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta",
) )
assert "thinking_config" not in kw.get("extra_body", {}) 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"}] msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs( kw = transport.build_kwargs(
model="gemini-3-flash-preview", model="gemini-3-flash-preview",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta",
reasoning_config={"enabled": True, "effort": "high"}, reasoning_config={"enabled": True, "effort": "high"},
) )
assert kw["extra_body"]["thinking_config"] == { assert kw["extra_body"]["thinking_config"] == {
@ -144,52 +148,85 @@ class TestChatCompletionsBuildKwargs:
"thinkingLevel": "high", "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"}] msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs( kw = transport.build_kwargs(
model="gemini-2.5-flash", model="gemini-2.5-flash",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta",
reasoning_config={"enabled": True, "effort": "high"}, reasoning_config={"enabled": True, "effort": "high"},
) )
assert kw["extra_body"]["thinking_config"] == { assert kw["extra_body"]["thinking_config"] == {
"includeThoughts": True, "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"}] msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs( kw = transport.build_kwargs(
model="google/gemini-3.1-pro-preview", model="google/gemini-3.1-pro-preview",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta/openai",
reasoning_config={"enabled": True, "effort": "medium"}, reasoning_config={"enabled": True, "effort": "medium"},
) )
assert kw["extra_body"]["thinking_config"] == { assert kw["extra_body"]["extra_body"]["google"]["thinking_config"] == {
"includeThoughts": True, "include_thoughts": True,
"thinkingLevel": "low", "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"}] msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs( kw = transport.build_kwargs(
model="gemini-3-flash-preview", model="gemini-3-flash-preview",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta",
reasoning_config={"enabled": False}, reasoning_config={"enabled": False},
) )
assert kw["extra_body"]["thinking_config"] == { assert kw["extra_body"]["thinking_config"] == {
"includeThoughts": False, "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"}] msgs = [{"role": "user", "content": "Hi"}]
kw = transport.build_kwargs( kw = transport.build_kwargs(
model="gemini-3-flash-preview", model="gemini-3-flash-preview",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta/openai",
reasoning_config={"enabled": True, "effort": "xhigh"}, 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): def test_gemini_flash_minimal_clamps_to_low(self, transport):
# Gemini 3 Flash documents low/medium/high; "minimal" isn't accepted, # Gemini 3 Flash documents low/medium/high; "minimal" isn't accepted,
@ -199,11 +236,12 @@ class TestChatCompletionsBuildKwargs:
model="gemini-3-flash-preview", model="gemini-3-flash-preview",
messages=msgs, messages=msgs,
provider_name="gemini", provider_name="gemini",
base_url="https://generativelanguage.googleapis.com/v1beta/openai",
reasoning_config={"enabled": True, "effort": "minimal"}, reasoning_config={"enabled": True, "effort": "minimal"},
) )
assert kw["extra_body"]["thinking_config"] == { assert kw["extra_body"]["extra_body"]["google"]["thinking_config"] == {
"includeThoughts": True, "include_thoughts": True,
"thinkingLevel": "low", "thinking_level": "low",
} }
def test_max_tokens_with_fn(self, transport): def test_max_tokens_with_fn(self, transport):