diff --git a/tests/test_providers.py b/tests/test_providers.py index 0b35eb68..b37cc1c3 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -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.""" diff --git a/turnstone/core/providers/_google.py b/turnstone/core/providers/_google.py index 1ea819c5..2b30d739 100644 --- a/turnstone/core/providers/_google.py +++ b/turnstone/core/providers/_google.py @@ -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", )