From 18cdc6b7a28b0c3a1852076dc054bf4000f1fa1f Mon Sep 17 00:00:00 2001 From: xxxigm Date: Tue, 30 Jun 2026 19:46:27 +0700 Subject: [PATCH 1/3] fix(image-gen): honor top-level image_gen.model for Nous/OpenRouter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes tools persists the selected model to image_gen.model, but the OpenRouter-compatible provider only read scoped image_gen..model and ignored the dispatch model kwarg — so Nous users always hit the default quality-first chain and fell back to Gemini. --- plugins/image_gen/openrouter/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/image_gen/openrouter/__init__.py b/plugins/image_gen/openrouter/__init__.py index d15aaabd53b0e..65bd001915fbe 100644 --- a/plugins/image_gen/openrouter/__init__.py +++ b/plugins/image_gen/openrouter/__init__.py @@ -46,8 +46,9 @@ # image model first, then fall back to Gemini 3 Pro Image if the OpenAI model # is access-gated / unavailable / times out on this endpoint. # -# Explicit override (OPENROUTER_IMAGE_MODEL or image_gen..model): -# use exactly that model (no auto fallback), so power users keep full control. +# Explicit override (OPENROUTER_IMAGE_MODEL, image_gen..model, or +# image_gen.model from ``hermes tools``): use exactly that model (no auto +# fallback), so power users keep full control. DEFAULT_MODEL = "openai/gpt-5.4-image-2" _FALLBACK_MODEL = "google/gemini-3-pro-image" _DEFAULT_MODEL_CHAIN = (DEFAULT_MODEL, _FALLBACK_MODEL) @@ -243,16 +244,18 @@ def default_model(self) -> Optional[str]: def get_setup_schema(self) -> Dict[str, Any]: return dict(self._setup_schema) - def _resolve_model(self) -> str: + def _resolve_model(self, explicit: Optional[str] = None) -> str: """Pick the image model: env override → config → :data:`DEFAULT_MODEL`.""" - return self._resolve_model_chain()[0] + return self._resolve_model_chain(explicit)[0] - def _resolve_model_chain(self) -> list[str]: + def _resolve_model_chain(self, explicit: Optional[str] = None) -> list[str]: """Ordered model attempts for this request. Explicit user/model config means "use this exact model", so no fallback. Without overrides we run the quality-first default chain. """ + if isinstance(explicit, str) and explicit.strip(): + return [explicit.strip()] env_override = os.environ.get(self._model_env_var, "").strip() if env_override: return [env_override] @@ -262,6 +265,9 @@ def _resolve_model_chain(self) -> list[str]: value = scoped.get("model") if isinstance(value, str) and value.strip(): return [value.strip()] + top = cfg.get("model") + if isinstance(top, str) and top.strip(): + return [top.strip()] return _dedupe_models(list(_DEFAULT_MODEL_CHAIN)) def generate( @@ -297,7 +303,7 @@ def generate( aspect_ratio=aspect_ratio, ) - model_chain = self._resolve_model_chain() + model_chain = self._resolve_model_chain(kwargs.get("model")) aspect = resolve_aspect_ratio(aspect_ratio) or_aspect = _ASPECT_RATIOS.get(aspect, "1:1") From fb03d869d65756e5a8c4e6ac51ef963cc226fb34 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Tue, 30 Jun 2026 19:46:27 +0700 Subject: [PATCH 2/3] test(image-gen): cover Nous/OpenRouter top-level model resolution Assert image_gen.model, explicit model kwargs, and Nous provider wiring so the config path mismatch cannot regress. --- .../test_openrouter_compat_provider.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/plugins/image_gen/test_openrouter_compat_provider.py b/tests/plugins/image_gen/test_openrouter_compat_provider.py index 654f70078d001..cef2f43941ca8 100644 --- a/tests/plugins/image_gen/test_openrouter_compat_provider.py +++ b/tests/plugins/image_gen/test_openrouter_compat_provider.py @@ -121,6 +121,26 @@ def test_model_config_override(self): with patch("plugins.image_gen.openrouter._load_image_gen_config", return_value=cfg): assert _openrouter()._resolve_model() == "google/gemini-3.1-flash-image-preview" + def test_model_top_level_config_override(self): + cfg = {"model": "openai/gpt-image-2"} + with patch("plugins.image_gen.openrouter._load_image_gen_config", return_value=cfg): + assert _openrouter()._resolve_model_chain() == ["openai/gpt-image-2"] + + def test_nous_honors_top_level_model(self): + from plugins.image_gen.openrouter import _build_providers + + cfg = {"model": "openai/gpt-image-2"} + nous = {p.name: p for p in _build_providers()}["nous"] + with patch("plugins.image_gen.openrouter._load_image_gen_config", return_value=cfg): + assert nous._resolve_model_chain() == ["openai/gpt-image-2"] + + def test_explicit_model_kwarg_wins_over_config(self): + cfg = {"model": "openai/gpt-image-2"} + with patch("plugins.image_gen.openrouter._load_image_gen_config", return_value=cfg): + assert _openrouter()._resolve_model_chain("google/gemini-3-pro-image") == [ + "google/gemini-3-pro-image" + ] + # --------------------------------------------------------------------------- # Helpers @@ -267,6 +287,17 @@ def test_auth_header(self): headers = mock_post.call_args.kwargs["headers"] assert headers["Authorization"] == "Bearer sk-or-test" + def test_generate_uses_model_kwarg_from_dispatch(self): + """image_generate passes image_gen.model as a model kwarg — honor it.""" + with patch(_RUNTIME, return_value=_runtime_ok()), \ + patch("requests.post", return_value=_mock_chat_response([_PNG_DATA_URI])) as mock_post, \ + patch("plugins.image_gen.openrouter.save_b64_image", return_value=Path("/tmp/x.png")): + result = _openrouter().generate(prompt="a pet", model="openai/gpt-image-2") + + assert result["success"] is True + assert result["model"] == "openai/gpt-image-2" + assert mock_post.call_args.kwargs["json"]["model"] == "openai/gpt-image-2" + def test_posts_to_resolved_base_url(self): """Nous routes to its own base URL — proves the same code serves both.""" nous_runtime = _runtime_ok( From a016220b792f49f8708029b4000368ed93c49fd7 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:02:45 +0530 Subject: [PATCH 3/3] docs(image-gen): align OpenRouter model-resolution docstrings with new precedence The cherry-picked fix added explicit-kwarg and top-level image_gen.model resolution but left _resolve_model / _resolve_model_chain docstrings stating the old 'env override -> config -> DEFAULT_MODEL' order. Document the full precedence (explicit kwarg -> env -> scoped -> top-level -> default chain) to match the sibling krea/openai providers. --- plugins/image_gen/openrouter/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/image_gen/openrouter/__init__.py b/plugins/image_gen/openrouter/__init__.py index 65bd001915fbe..a3cc348ccf70c 100644 --- a/plugins/image_gen/openrouter/__init__.py +++ b/plugins/image_gen/openrouter/__init__.py @@ -245,14 +245,19 @@ def get_setup_schema(self) -> Dict[str, Any]: return dict(self._setup_schema) def _resolve_model(self, explicit: Optional[str] = None) -> str: - """Pick the image model: env override → config → :data:`DEFAULT_MODEL`.""" + """Pick the image model (first of :meth:`_resolve_model_chain`).""" return self._resolve_model_chain(explicit)[0] def _resolve_model_chain(self, explicit: Optional[str] = None) -> list[str]: """Ordered model attempts for this request. - Explicit user/model config means "use this exact model", so no fallback. - Without overrides we run the quality-first default chain. + Precedence: explicit caller override (the ``model`` kwarg) → the + provider's ``*_IMAGE_MODEL`` env override → scoped + ``image_gen..model`` → top-level ``image_gen.model`` (written + by ``hermes tools``) → the quality-first default chain. + + Any explicit user/model selection means "use this exact model", so no + fallback. Only the bare default chain carries a Gemini fallback. """ if isinstance(explicit, str) and explicit.strip(): return [explicit.strip()]