diff --git a/docs/docs/usage-guide/changing_a_model.md b/docs/docs/usage-guide/changing_a_model.md index 2e0e9bd563..1f07827bef 100644 --- a/docs/docs/usage-guide/changing_a_model.md +++ b/docs/docs/usage-guide/changing_a_model.md @@ -519,7 +519,7 @@ key = "..." # your openrouter api key #### Openrouter provider routing, reasoning and output cap -For `openrouter/...` models you can optionally restrict which upstream providers Openrouter uses, control reasoning, and cap the completion length. All keys live in the `[openrouter]` section of `configuration.toml` and default to unset (no change to Openrouter's default behavior): +For `openrouter/...` models you can optionally restrict which upstream providers Openrouter uses, control reasoning, and cap the completion length. All keys live in the `[openrouter]` section of `configuration.toml`. Models listed in [`SUPPORT_REASONING_EFFORT_MODELS`](https://github.com/the-pr-agent/pr-agent/blob/main/pr_agent/algo/__init__.py) inherit `config.reasoning_effort` unless an Openrouter-specific effort or token budget is set. ```toml [openrouter] @@ -527,12 +527,12 @@ For `openrouter/...` models you can optionally restrict which upstream providers # provider_only = ["z-ai"] # hard allowlist of upstream providers; empty = default routing # provider_order = ["z-ai", "novita"] # preferred order instead of an allowlist; ignored when provider_only is set # allow_fallbacks = true # when provider_order is set, allow routing beyond the list -# reasoning_effort = "low" # "none" disables reasoning; otherwise "low", "medium" or "high" -# reasoning_max_tokens = 2048 # cap the reasoning budget in tokens +# reasoning_effort = "low" # override global effort: "none", "minimal", "low", "medium", "high", "xhigh" or "max" +# reasoning_max_tokens = 2048 # explicit budget; takes precedence over effort unless effort is "none" # max_tokens = 16000 # hard cap on completion tokens for the request ``` -`provider_only` and `reasoning_effort = "none"` are useful to pin a specific provider and to bound the cost of reasoning models. See the Openrouter [provider routing](https://openrouter.ai/docs/features/provider-routing) and [reasoning tokens](https://openrouter.ai/docs/use-cases/reasoning-tokens) docs. +`provider_only` and `reasoning_effort = "none"` are useful to pin a specific provider and to bound the cost of reasoning models. Because Openrouter treats effort and token budgets as mutually exclusive, an explicit Openrouter-specific `"none"` keeps reasoning disabled; otherwise a positive `reasoning_max_tokens` value takes precedence over the global effort and other Openrouter-specific values. Invalid Openrouter-specific effort values are warned about and treated as unset, so registered reasoning models fall back to `config.reasoning_effort`. Openrouter normalizes `"max"` to `"xhigh"` in this path to match LiteLLM 1.98.0. Supported effort values vary by model, and models whose metadata marks reasoning as mandatory reject `"none"`. For Anthropic models using a reasoning budget, set the effective output `max_tokens` higher than `reasoning_max_tokens` so the final answer has output headroom. See the Openrouter [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection) and [reasoning tokens](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens) docs. ### Neon AI Gateway @@ -595,7 +595,7 @@ custom_model_max_tokens= ... ```toml [config] -reasoning_effort = "medium" # "none", "minimal", "low", "medium", "high", "xhigh" +reasoning_effort = "medium" # "none", "minimal", "low", "medium", "high", "xhigh", "max" ``` With the OpenAI models that support reasoning effort (eg: gpt-5.6-terra), you can specify its reasoning effort via `config` section. The default value is `medium`. You can change it to any supported value based on your usage. Available values depend on the model and provider. diff --git a/pr_agent/algo/__init__.py b/pr_agent/algo/__init__.py index d99ef8ebf9..c7e694664e 100644 --- a/pr_agent/algo/__init__.py +++ b/pr_agent/algo/__init__.py @@ -394,12 +394,11 @@ "o3-2025-04-16", "o4-mini", "o4-mini-2025-04-16", - # Gemini 2.5 exposes a thinking budget that LiteLLM maps from reasoning_effort - # (low/medium/high -> thinkingConfig.thinkingBudget). Without these entries a - # configured reasoning_effort is silently dropped for Gemini, so a runaway - # thinking trace can consume the whole output budget and return an empty - # completion. Matched provider-prefix-insensitively in litellm_ai_handler so - # prefixed forms (e.g. "openrouter/google/gemini-2.5-pro") are covered too. + # Gemini 2.5 exposes a thinking budget controlled by reasoning_effort. Without + # these entries a configured effort is silently dropped, so a runaway thinking + # trace can consume the whole output budget and return an empty completion. + # LiteLLM maps native provider paths to thinkingConfig.thinkingBudget, while + # LiteLLMAIHandler routes OpenRouter-prefixed forms through extra_body.reasoning. "gemini-2.5-pro", "gemini-2.5-flash", ] diff --git a/pr_agent/algo/ai_handlers/litellm_ai_handler.py b/pr_agent/algo/ai_handlers/litellm_ai_handler.py index 5230581e51..4754e3f601 100644 --- a/pr_agent/algo/ai_handlers/litellm_ai_handler.py +++ b/pr_agent/algo/ai_handlers/litellm_ai_handler.py @@ -749,12 +749,18 @@ async def chat_completion(self, model: str, system: str, user: str, temperature: if 'temperature' in kwargs: del kwargs['temperature'] + openrouter_reasoning_effort = None + reasoning_model = model.rsplit(":", 1)[0] if model.startswith("openrouter/") else model # Add reasoning_effort if model supports it. Match the bare model # id as well as any provider-prefixed form (e.g. # "openrouter/google/gemini-2.5-pro", "gemini/gemini-2.5-pro"), so a # configured reasoning_effort is not silently dropped for models the - # user references with a provider prefix. - if any(model == m or model.endswith("/" + m) for m in self.support_reasoning_models): + # user references with a provider prefix. OpenRouter routing variants + # such as :nitro and :floor are stripped only for this membership test. + if any( + reasoning_model == m or reasoning_model.endswith("/" + m) + for m in self.support_reasoning_models + ): config_effort = get_settings().config.reasoning_effort try: ReasoningEffort(config_effort) @@ -767,8 +773,14 @@ async def chat_completion(self, model: str, system: str, user: str, temperature: f"Using default '{reasoning_effort}'. Valid values: {[e.value for e in ReasoningEffort]}" ) - get_logger().info(f"Adding reasoning_effort with value {reasoning_effort} to model {model}.") - kwargs["reasoning_effort"] = reasoning_effort + if model.startswith("openrouter/"): + # LiteLLM 1.98.0 rejects top-level reasoning_effort for some + # OpenRouter model IDs it does not mark as reasoning-capable; + # defer to OpenRouter's unified reasoning object below. + openrouter_reasoning_effort = reasoning_effort + else: + get_logger().info(f"Adding reasoning_effort with value {reasoning_effort} to model {model}.") + kwargs["reasoning_effort"] = reasoning_effort # https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking if (model in self.claude_extended_thinking_models) and get_settings().config.get("enable_claude_extended_thinking", False): @@ -863,9 +875,8 @@ async def chat_completion(self, model: str, system: str, user: str, temperature: get_logger().info(f"Using Bedrock custom inference profile: {model_id}") # OpenRouter provider routing, reasoning control and output cap. - # Applied only to "openrouter/*" models. Every key defaults to unset in - # the [openrouter] section of configuration.toml, so this block is a - # no-op unless explicitly configured, and never affects other providers. + # Registered reasoning models inherit config.reasoning_effort when + # no OpenRouter-specific effort or token budget is configured. if isinstance(model, str) and model.startswith("openrouter/"): openrouter_settings = get_settings().get("openrouter", {}) extra_body = kwargs.get("extra_body") or {} @@ -903,20 +914,52 @@ def _as_int(value): provider["allow_fallbacks"] = _as_bool(openrouter_settings.get("allow_fallbacks", True)) reasoning = {} - reasoning_effort = str(openrouter_settings.get("reasoning_effort", "") or "").strip().lower() - if reasoning_effort == "none": - reasoning["enabled"] = False - elif reasoning_effort in ("low", "medium", "high"): - reasoning["effort"] = reasoning_effort - elif reasoning_effort: - get_logger().warning( - f"Ignoring invalid openrouter.reasoning_effort '{reasoning_effort}'. " - "Valid values: none, low, medium, high." - ) + effective_reasoning_effort = str( + openrouter_settings.get("reasoning_effort", "") or "" + ).strip().lower() reasoning_max_tokens = _as_int(openrouter_settings.get("reasoning_max_tokens", 0)) - if reasoning_max_tokens > 0 and reasoning.get("enabled") is not False: + if effective_reasoning_effort: + try: + ReasoningEffort(effective_reasoning_effort) + except (TypeError, ValueError): + get_logger().warning( + f"Ignoring invalid openrouter.reasoning_effort '{effective_reasoning_effort}'. " + f"Valid values: {[effort.value for effort in ReasoningEffort]}." + ) + effective_reasoning_effort = "" + if not effective_reasoning_effort: + if reasoning_max_tokens > 0 and openrouter_reasoning_effort: + get_logger().warning( + f"Ignoring config.reasoning_effort='{openrouter_reasoning_effort}' because " + "openrouter.reasoning_max_tokens takes precedence." + ) + elif reasoning_max_tokens <= 0: + effective_reasoning_effort = openrouter_reasoning_effort or "" + + # Preserve explicit disablement; otherwise keep effort and + # max_tokens mutually exclusive by preferring the token budget. + if effective_reasoning_effort == "none": + if reasoning_max_tokens > 0: + get_logger().warning( + "Ignoring openrouter.reasoning_max_tokens because " + "openrouter.reasoning_effort='none' disables reasoning." + ) + reasoning["enabled"] = False + elif reasoning_max_tokens > 0: + if effective_reasoning_effort: + get_logger().warning( + f"Ignoring openrouter.reasoning_effort='{effective_reasoning_effort}' because " + "openrouter.reasoning_max_tokens takes precedence." + ) reasoning["max_tokens"] = reasoning_max_tokens + elif effective_reasoning_effort: + # OpenRouter uses xhigh for the max alias; extra_body bypasses + # LiteLLM's OpenRouter parameter mapping. + reasoning["effort"] = ( + "xhigh" if effective_reasoning_effort == "max" else effective_reasoning_effort + ) if reasoning: + get_logger().info(f"Adding OpenRouter reasoning {reasoning} to model {model}.") extra_body["reasoning"] = reasoning if extra_body: @@ -926,6 +969,17 @@ def _as_int(value): if max_tokens > 0: existing = _as_int(kwargs.get("max_tokens", 0)) kwargs["max_tokens"] = min(existing, max_tokens) if existing > 0 else max_tokens + effective_max_tokens = _as_int(kwargs.get("max_tokens", 0)) + effective_reasoning_max_tokens = _as_int(reasoning.get("max_tokens", 0)) + if ( + model.startswith("openrouter/anthropic/") + and effective_reasoning_max_tokens > 0 + and 0 < effective_max_tokens <= effective_reasoning_max_tokens + ): + get_logger().warning( + f"OpenRouter Anthropic max_tokens ({effective_max_tokens}) must be greater than " + f"reasoning_max_tokens ({effective_reasoning_max_tokens}) to leave output headroom." + ) get_logger().debug("Prompts", artifact={"system": system, "user": user}) diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index 3c9a1d78fe..4d133183e9 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -407,13 +407,21 @@ cache_control_injection_points = [] # Optional: enable Anthropic prompt caching # OpenRouter provider routing, reasoning control and output cap. These apply only # to models addressed as "openrouter/...". The API key and api_base live in # .secrets.toml (or the openrouter__key env var); the keys below are non-secret. -# Refs: https://openrouter.ai/docs/features/provider-routing -# https://openrouter.ai/docs/use-cases/reasoning-tokens +# Refs: https://openrouter.ai/docs/guides/routing/provider-selection +# https://openrouter.ai/docs/guides/best-practices/reasoning-tokens provider_only = [] # restrict routing to these upstream providers only, a hard allowlist (e.g. ["z-ai"]); empty = OpenRouter default routing provider_order = [] # preferred provider order; ignored when provider_only is set; empty = unset allow_fallbacks = true # when provider_order is set, allow routing beyond the listed providers -reasoning_effort = "" # "" leaves reasoning to the model default; "none" disables it; otherwise "low", "medium" or "high" -reasoning_max_tokens = 0 # cap the reasoning budget in tokens; 0 = unset +# Invalid reasoning_effort values are warned about and treated as unset. +# Empty inherits config.reasoning_effort for models in SUPPORT_REASONING_EFFORT_MODELS. +# Valid values: "none", "minimal", "low", "medium", "high", "xhigh", "max". +# OpenRouter normalizes "max" to "xhigh" to match LiteLLM 1.98.0. +# Model-specific support varies; mandatory reasoning models reject "none". +reasoning_effort = "" +# A positive value overrides global effort and non-none OpenRouter-specific efforts. +# Explicit openrouter.reasoning_effort = "none" keeps reasoning disabled. +# Some providers require max_tokens to be greater than the reasoning budget. +reasoning_max_tokens = 0 max_tokens = 0 # hard cap on completion tokens for the request; 0 = unset [pr_similar_issue] diff --git a/tests/unittest/test_litellm_openrouter_controls.py b/tests/unittest/test_litellm_openrouter_controls.py index 82583445e0..3b26a088a9 100644 --- a/tests/unittest/test_litellm_openrouter_controls.py +++ b/tests/unittest/test_litellm_openrouter_controls.py @@ -5,8 +5,8 @@ The [openrouter] settings (provider_only, provider_order, allow_fallbacks, reasoning_effort, reasoning_max_tokens, max_tokens) are injected into the request as `extra_body.provider`, `extra_body.reasoning` and `max_tokens`, but only for -models addressed as "openrouter/...". When nothing is configured the block is a -no-op, and non-openrouter models are never touched. +models addressed as "openrouter/...". Registered reasoning models inherit the +global effort when no OpenRouter effort or budget is set; other models are no-op. """ import os from unittest.mock import AsyncMock, MagicMock, patch @@ -14,6 +14,7 @@ import litellm import openai import pytest +from litellm.utils import get_optional_params import pr_agent.algo.ai_handlers.litellm_ai_handler as litellm_handler @@ -33,17 +34,24 @@ @pytest.fixture(autouse=True) def _restore_litellm_globals(): """LiteLLMAIHandler.__init__ mutates global litellm/openai state and, when - AWS_USE_IMDS is set, os.environ; snapshot and restore both, and drop - AWS_USE_IMDS so the AWS credential path never runs in these tests.""" - saved = (litellm.api_key, getattr(litellm, "openai_key", None), openai.api_key) + AWS_USE_IMDS is set, os.environ; snapshot and restore both, and isolate + drop_params so parameter-validation tests are deterministic.""" + saved = ( + litellm.api_key, + getattr(litellm, "openai_key", None), + openai.api_key, + litellm.drop_params, + ) saved_env = {name: os.environ.get(name) for name in _HANDLER_ENV_VARS} os.environ.pop("AWS_USE_IMDS", None) + litellm.drop_params = False try: yield finally: litellm.api_key = saved[0] litellm.openai_key = saved[1] openai.api_key = saved[2] + litellm.drop_params = saved[3] for name, value in saved_env.items(): if value is None: os.environ.pop(name, None) @@ -51,12 +59,12 @@ def _restore_litellm_globals(): os.environ[name] = value -def _make_settings(openrouter=None): +def _make_settings(openrouter=None, reasoning_effort="medium"): """Minimal settings whose `.get("openrouter", ...)` returns the given dict.""" openrouter = openrouter or {} return type("Settings", (), { "config": type("Config", (), { - "reasoning_effort": None, + "reasoning_effort": reasoning_effort, "ai_timeout": 30, "custom_reasoning_model": False, "max_model_tokens": 32000, @@ -79,8 +87,12 @@ def _mock_response(): return mock -async def _run(monkeypatch, model, openrouter): - monkeypatch.setattr(litellm_handler, "get_settings", lambda: _make_settings(openrouter)) +async def _run(monkeypatch, model, openrouter, reasoning_effort="medium"): + monkeypatch.setattr( + litellm_handler, + "get_settings", + lambda: _make_settings(openrouter, reasoning_effort), + ) with patch("pr_agent.algo.ai_handlers.litellm_ai_handler.acompletion", new_callable=AsyncMock) as mock_call: mock_call.return_value = _mock_response() @@ -124,11 +136,12 @@ async def test_reasoning_none_disables(self, monkeypatch): @pytest.mark.asyncio async def test_reasoning_max_tokens(self, monkeypatch): + """Verify that a token budget suppresses the mutually exclusive effort control.""" kwargs = await _run(monkeypatch, "openrouter/z-ai/glm-5.2", { "reasoning_effort": "high", "reasoning_max_tokens": 2048, }) - assert kwargs["extra_body"]["reasoning"] == {"effort": "high", "max_tokens": 2048} + assert kwargs["extra_body"]["reasoning"] == {"max_tokens": 2048} @pytest.mark.asyncio async def test_no_config_is_noop(self, monkeypatch): @@ -151,13 +164,169 @@ async def test_invalid_reasoning_effort_ignored(self, monkeypatch): assert "extra_body" not in kwargs @pytest.mark.asyncio - async def test_reasoning_max_tokens_dropped_when_disabled(self, monkeypatch): + async def test_reasoning_none_overrides_budget(self, monkeypatch): kwargs = await _run(monkeypatch, "openrouter/z-ai/glm-5.2", { "reasoning_effort": "none", "reasoning_max_tokens": 2048, }) assert kwargs["extra_body"]["reasoning"] == {"enabled": False} + @pytest.mark.asyncio + async def test_reasoning_budget_overrides_global_none(self, monkeypatch): + logger = MagicMock() + monkeypatch.setattr(litellm_handler, "get_logger", lambda: logger) + kwargs = await _run( + monkeypatch, + "openrouter/google/gemini-2.5-pro", + {"reasoning_max_tokens": 2048}, + reasoning_effort="none", + ) + assert kwargs["extra_body"]["reasoning"] == {"max_tokens": 2048} + assert any( + "Ignoring config.reasoning_effort='none'" in call.args[0] + for call in logger.warning.call_args_list + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "model", + [ + "openrouter/google/gemini-2.5-pro", + "openrouter/google/gemini-2.5-pro:nitro", + "openrouter/google/gemini-2.5-pro:floor", + "openrouter/google/gemini-2.5-flash", + ], + ) + async def test_global_reasoning_effort_uses_openrouter_body(self, monkeypatch, model): + kwargs = await _run( + monkeypatch, + model, + {}, + reasoning_effort="low", + ) + assert "reasoning_effort" not in kwargs + assert kwargs["extra_body"]["reasoning"] == {"effort": "low"} + assert kwargs["model"] == model + + @pytest.mark.asyncio + async def test_openrouter_effort_overrides_global_effort(self, monkeypatch): + kwargs = await _run( + monkeypatch, + "openrouter/google/gemini-2.5-pro", + {"reasoning_effort": "high"}, + reasoning_effort="low", + ) + assert kwargs["extra_body"]["reasoning"] == {"effort": "high"} + + @pytest.mark.asyncio + async def test_invalid_openrouter_effort_falls_back_to_global_effort(self, monkeypatch): + kwargs = await _run( + monkeypatch, + "openrouter/google/gemini-2.5-pro", + {"reasoning_effort": "hgh"}, + reasoning_effort="high", + ) + assert kwargs["extra_body"]["reasoning"] == {"effort": "high"} + + @pytest.mark.asyncio + async def test_registered_model_inherits_default_global_effort(self, monkeypatch): + kwargs = await _run(monkeypatch, "openrouter/google/gemini-2.5-pro", {}) + assert kwargs["extra_body"]["reasoning"] == {"effort": "medium"} + + @pytest.mark.asyncio + async def test_global_none_disables_reasoning(self, monkeypatch): + kwargs = await _run( + monkeypatch, + "openrouter/google/gemini-2.5-flash", + {}, + reasoning_effort="none", + ) + assert kwargs["extra_body"]["reasoning"] == {"enabled": False} + + @pytest.mark.asyncio + @pytest.mark.parametrize( + ("global_effort", "openrouter", "expected"), + [ + ("max", {}, "xhigh"), + ("medium", {"reasoning_effort": "max"}, "xhigh"), + ("minimal", {}, "minimal"), + ], + ) + async def test_openrouter_effort_normalization( + self, monkeypatch, global_effort, openrouter, expected + ): + kwargs = await _run( + monkeypatch, + "openrouter/google/gemini-2.5-pro", + openrouter, + reasoning_effort=global_effort, + ) + assert kwargs["extra_body"]["reasoning"] == {"effort": expected} + + @pytest.mark.asyncio + async def test_reasoning_budget_suppresses_global_effort(self, monkeypatch): + kwargs = await _run( + monkeypatch, + "openrouter/google/gemini-2.5-pro", + {"reasoning_max_tokens": 2048}, + reasoning_effort="high", + ) + assert kwargs["extra_body"]["reasoning"] == {"max_tokens": 2048} + + def test_litellm_requires_openrouter_reasoning_in_extra_body(self): + """Pin the LiteLLM 1.98.0 workaround boundary so upgrades expose when it can be removed.""" + with pytest.raises(litellm.UnsupportedParamsError): + get_optional_params( + model="google/gemini-2.5-pro", + custom_llm_provider="openrouter", + reasoning_effort="low", + ) + + params = get_optional_params( + model="google/gemini-2.5-pro", + custom_llm_provider="openrouter", + extra_body={"reasoning": {"effort": "low"}}, + ) + assert params["extra_body"]["reasoning"] == {"effort": "low"} + + disabled_params = get_optional_params( + model="google/gemini-2.5-flash", + custom_llm_provider="openrouter", + extra_body={"reasoning": {"enabled": False}}, + ) + assert disabled_params["extra_body"]["reasoning"] == {"enabled": False} + + @pytest.mark.asyncio + async def test_anthropic_reasoning_budget_warns_without_output_headroom(self, monkeypatch): + logger = MagicMock() + monkeypatch.setattr(litellm_handler, "get_logger", lambda: logger) + kwargs = await _run( + monkeypatch, + "openrouter/anthropic/claude-3.7-sonnet", + {"reasoning_max_tokens": 2048, "max_tokens": 1024}, + ) + assert kwargs["extra_body"]["reasoning"] == {"max_tokens": 2048} + assert kwargs["max_tokens"] == 1024 + assert any( + "must be greater than reasoning_max_tokens" in call.args[0] + for call in logger.warning.call_args_list + ) + + @pytest.mark.asyncio + async def test_anthropic_disabled_reasoning_skips_headroom_warning(self, monkeypatch): + logger = MagicMock() + monkeypatch.setattr(litellm_handler, "get_logger", lambda: logger) + kwargs = await _run( + monkeypatch, + "openrouter/anthropic/claude-3.7-sonnet", + {"reasoning_effort": "none", "reasoning_max_tokens": 2048, "max_tokens": 1024}, + ) + assert kwargs["extra_body"]["reasoning"] == {"enabled": False} + assert not any( + "must be greater than reasoning_max_tokens" in call.args[0] + for call in logger.warning.call_args_list + ) + @pytest.mark.asyncio async def test_string_overrides_are_coerced(self, monkeypatch): # Dynaconf/env overrides can arrive as strings; they must not crash or diff --git a/tests/unittest/test_litellm_reasoning_effort.py b/tests/unittest/test_litellm_reasoning_effort.py index 49e28f1108..12f7f53900 100644 --- a/tests/unittest/test_litellm_reasoning_effort.py +++ b/tests/unittest/test_litellm_reasoning_effort.py @@ -860,9 +860,9 @@ class TestLiteLLMReasoningEffortGemini: """Gemini 2.5 reasoning_effort handling via the SUPPORT_REASONING_EFFORT_MODELS path. Gemini 2.5 exposes a thinking budget that LiteLLM maps from reasoning_effort. The - membership test in chat_completion matches the bare model id as well as any - provider-prefixed form (e.g. "openrouter/google/gemini-2.5-pro"), so a configured - reasoning_effort is not silently dropped for models referenced with a prefix. + membership test in chat_completion matches bare and provider-prefixed ids such as + "vertex_ai/gemini-2.5-pro". OpenRouter models use extra_body.reasoning instead and + are covered by test_litellm_openrouter_controls.py. """ def _isolate_env(self, monkeypatch): @@ -883,8 +883,6 @@ async def test_gemini_prefixed_forms_get_reasoning_effort(self, monkeypatch, moc "gemini-2.5-flash", "gemini/gemini-2.5-pro", "vertex_ai/gemini-2.5-pro", - "openrouter/google/gemini-2.5-pro", - "openrouter/google/gemini-2.5-flash", ] for model in gemini_models: