fix(providers): thread the session effort knob to Gemini

_GOOGLE_DEFAULT declared no reasoning_effort_values, so
resolve_reasoning_effort returned None and the session effort knob was
silently dropped for every Gemini model — the same bug class this
branch fixed on the local lanes. Gemini's OpenAI-compat surface
documents a flat reasoning_effort (2.5: thinking_budget mapping; 3.x:
thinking_level), so declaring values lights up the inherited
chat-completions path.

Values are the safe cross-model set (minimal/low/medium/high): "none"
is excluded because 2.5 Pro and the 3.x family reject disabling
thinking — and the resolver never forwards the knob's none anyway (the
param is omitted, server default applies). Off-list xhigh/max snap to
the declared default high. Encoded from the official compatibility
docs per the static-caps pattern; not live-verified.
This commit is contained in:
Patrick Buckley
2026-07-04 01:22:43 -07:00
parent 110ea6ba11
commit ee9e9c1fe3
2 changed files with 46 additions and 0 deletions
+33
View File
@@ -1802,6 +1802,39 @@ class TestProviderFactory:
# ===========================================================================
class TestGoogleEffortKnob:
"""The session effort knob reaches Gemini as a flat reasoning_effort."""
def _create_kwargs(self, reasoning_effort: str) -> dict[str, Any]:
from turnstone.core.providers._google import GoogleProvider
prov = GoogleProvider()
client = MagicMock()
client.chat.completions.create.return_value = iter([])
list(
prov.create_streaming(
client=client,
model="gemini-3-flash",
messages=[{"role": "user", "content": "hi"}],
reasoning_effort=reasoning_effort,
)
)
return client.chat.completions.create.call_args[1]
def test_knob_values_forward_verbatim(self) -> None:
for knob in ("minimal", "low", "medium", "high"):
assert self._create_kwargs(knob)["reasoning_effort"] == knob
def test_off_list_knob_snaps_to_high(self) -> None:
"""xhigh/max are not in Gemini's vocabulary — snap down to high."""
for knob in ("xhigh", "max"):
assert self._create_kwargs(knob)["reasoning_effort"] == "high"
def test_none_omits_the_param(self) -> None:
"""Knob none never sends "none" — 2.5 Pro / 3.x reject disabling."""
assert "reasoning_effort" not in self._create_kwargs("none")
class TestGoogleProviderFidelity:
"""Tests for thought_signature round-trip via provider_blocks."""
+13
View File
@@ -43,6 +43,19 @@ _GOOGLE_DEFAULT = ModelCapabilities(
# Gemini's OpenAI-compat endpoint accepts max_tokens (not
# max_completion_tokens which is OpenAI Responses-specific).
token_param="max_tokens",
# Gemini's OpenAI-compat endpoint accepts a flat ``reasoning_effort``
# (2.5 family: thinking_budget 1024/1024/8192/24576 for minimal/low/
# medium/high; 3.x family: thinking_level of the same name). The
# declared values are the safe set across ALL current Gemini models:
# "none" is excluded because 2.5 Pro and the 3.x family reject
# disabling thinking — and ``resolve_reasoning_effort`` never
# forwards the knob's "none" anyway (the param is omitted and the
# server default applies). Off-list knob values (xhigh, max) snap
# to the default "high". Encoded from
# ai.google.dev/gemini-api/docs/openai (2026-07); not live-verified —
# the static-caps pattern for commercial providers.
reasoning_effort_values=("minimal", "low", "medium", "high"),
default_reasoning_effort="high",
)