mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(gemini): nest OpenAI-compat thinking config under google
This commit is contained in:
parent
5a61c116e1
commit
c5a5e586d7
3 changed files with 102 additions and 24 deletions
|
|
@ -101,6 +101,14 @@ from utils import base_url_host_matches, base_url_hostname, normalize_proxy_env_
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""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 @@ class ChatCompletionsTransport(ProviderTransport):
|
|||
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 @@ class ChatCompletionsTransport(ProviderTransport):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -122,21 +122,25 @@ class TestChatCompletionsBuildKwargs:
|
|||
)
|
||||
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 @@ class TestChatCompletionsBuildKwargs:
|
|||
"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 @@ class TestChatCompletionsBuildKwargs:
|
|||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue