fix(llm): forward reasoning effort to GPT-6 and gate minimal thinking

- reasoning_effort was forwarded only to IDs matching gpt-5 or the o-series, so
  GPT-6 models silently dropped the configured effort; match GPT-5 and later,
  with a version boundary so unrelated IDs do not match
- Gemini Pro, 3.8+ and the -latest aliases reject thinking_level "minimal"
  with a 400; send it only to numbered Flash models before 3.8 and map it to
  "low" elsewhere, since aliases move between generations
This commit is contained in:
Yijia-Xiao
2026-09-14 18:09:33 +00:00
parent 62d3479217
commit 9b4c741d33
4 changed files with 42 additions and 11 deletions

View File

@@ -1,8 +1,8 @@
"""Gemini thinking_level forwarding (Gemini 3.x). """Gemini thinking_level forwarding (Gemini 3.x).
The catalog is Gemini 3.x only, which takes the string ``thinking_level`` The catalog is Gemini 3.x only, which takes the string ``thinking_level``
directly. Pro accepts low/high; Flash also accepts minimal/medium — an directly. Pro, Gemini 3.8+ and the -latest aliases reject "minimal" with a 400,
unsupported "minimal" on Pro is mapped to "low". so it is mapped to "low" there; numbered Flash models before 3.8 accept it.
""" """
from unittest import mock from unittest import mock
@@ -35,6 +35,22 @@ def test_pro_remaps_minimal_to_low():
assert kw["thinking_level"] == "low" # Pro doesn't accept "minimal" assert kw["thinking_level"] == "low" # Pro doesn't accept "minimal"
def test_flash_38_remaps_minimal_to_low():
kw = _captured_kwargs("gemini-3.8-flash", thinking_level="minimal")
assert kw["thinking_level"] == "low" # 3.8 Flash 400s on "minimal"
def test_flash_38_keeps_supported_levels():
kw = _captured_kwargs("gemini-3.8-flash", thinking_level="high")
assert kw["thinking_level"] == "high"
@pytest.mark.parametrize("alias", ["gemini-flash-latest", "gemini-pro-latest"])
def test_latest_alias_remaps_minimal_to_low(alias):
# Aliases move between generations; gemini-flash-latest 400s on "minimal".
assert _captured_kwargs(alias, thinking_level="minimal")["thinking_level"] == "low"
def test_pro_keeps_high(): def test_pro_keeps_high():
kw = _captured_kwargs("gemini-3.1-pro-preview", thinking_level="high") kw = _captured_kwargs("gemini-3.1-pro-preview", thinking_level="high")
assert kw["thinking_level"] == "high" assert kw["thinking_level"] == "high"

View File

@@ -17,9 +17,10 @@ from tradingagents.llm_clients.openai_client import (
"model,expected", "model,expected",
[ [
("gpt-5.5", True), ("gpt-5.4", True), ("gpt-5.4-mini", True), ("gpt-5.5", True), ("gpt-5.4", True), ("gpt-5.4-mini", True),
("gpt-5.5-pro", True), ("o1", True), ("o3-mini", True), ("gpt-5.5-pro", True), ("gpt-6-astra", True), ("o1", True), ("o3-mini", True),
("gpt-4.1", False), ("gpt-4o", False), ("gpt-4o-mini", False), ("gpt-4.1", False), ("gpt-4o", False), ("gpt-4o-mini", False),
("gpt-3.5-turbo", False), ("gpt-3.5-turbo", False), ("gpt-10", True),
("gpt-5foo", False), ("gpt-60x", False), ("o3rd-party", False),
], ],
) )
def test_supports_reasoning_effort(model, expected): def test_supports_reasoning_effort(model, expected):

View File

@@ -1,3 +1,4 @@
import re
from typing import Any from typing import Any
from langchain_google_genai import ChatGoogleGenerativeAI from langchain_google_genai import ChatGoogleGenerativeAI
@@ -5,6 +6,19 @@ from langchain_google_genai import ChatGoogleGenerativeAI
from .base_client import BaseLLMClient, normalize_content from .base_client import BaseLLMClient, normalize_content
from .validators import validate_model from .validators import validate_model
_GEMINI_VERSION = re.compile(r"^gemini-(\d+)\.(\d+)")
def _accepts_minimal_thinking(model: str) -> bool:
"""Whether ``thinking_level="minimal"`` is accepted: numbered Flash models
before 3.8. Pro, 3.8+ and version-less aliases (which move between
generations) are treated as rejecting it."""
model_lc = model.lower()
match = _GEMINI_VERSION.match(model_lc)
return bool(match) and "pro" not in model_lc and (
(int(match.group(1)), int(match.group(2))) < (3, 8)
)
class NormalizedChatGoogleGenerativeAI(ChatGoogleGenerativeAI): class NormalizedChatGoogleGenerativeAI(ChatGoogleGenerativeAI):
"""ChatGoogleGenerativeAI with normalized content output. """ChatGoogleGenerativeAI with normalized content output.
@@ -42,12 +56,12 @@ class GoogleClient(BaseLLMClient):
llm_kwargs["google_api_key"] = google_api_key llm_kwargs["google_api_key"] = google_api_key
# Gemini 3.x takes the string ``thinking_level`` (the integer # Gemini 3.x takes the string ``thinking_level`` (the integer
# ``thinking_budget`` was for the now-retired 2.5 line). Pro accepts # ``thinking_budget`` was for the now-retired 2.5 line). Pro, Gemini
# low/high; Flash also accepts minimal/medium — so map an unsupported # 3.8+ and the -latest aliases reject "minimal" with a 400; "low" is
# "minimal" on Pro to the nearest level it does accept. # accepted everywhere, so it is the fallback.
thinking_level = self.kwargs.get("thinking_level") thinking_level = self.kwargs.get("thinking_level")
if thinking_level: if thinking_level:
if "pro" in self.model.lower() and thinking_level == "minimal": if thinking_level == "minimal" and not _accepts_minimal_thinking(self.model):
thinking_level = "low" thinking_level = "low"
llm_kwargs["thinking_level"] = thinking_level llm_kwargs["thinking_level"] = thinking_level

View File

@@ -168,11 +168,11 @@ _PASSTHROUGH_KWARGS = (
"api_key", "callbacks", "http_client", "http_async_client", "api_key", "callbacks", "http_client", "http_async_client",
) )
# OpenAI's ``reasoning_effort`` is only accepted by reasoning models — the GPT-5 # OpenAI's ``reasoning_effort`` is only accepted by reasoning models — GPT-5 and
# family and the o-series. Non-reasoning models (gpt-4.1, gpt-4o, ...) 400 with # later, and the o-series. Non-reasoning models (gpt-4.1, gpt-4o, ...) 400 with
# "Unsupported parameter: 'reasoning.effort' is not supported with this model". # "Unsupported parameter: 'reasoning.effort' is not supported with this model".
# Drop the kwarg for those rather than crash the run. # Drop the kwarg for those rather than crash the run.
_OPENAI_REASONING_MODEL = re.compile(r"^(gpt-5|o[1-9])") _OPENAI_REASONING_MODEL = re.compile(r"^(?:gpt-(?:[5-9]|[1-9]\d)|o[1-9])(?:[.-]|$)")
def _supports_reasoning_effort(model: str) -> bool: def _supports_reasoning_effort(model: str) -> bool: