From 7176b3a756e1d1c6130b5b16ecf95bb1c8a6c8ee Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 11 Jul 2026 05:18:27 +0000 Subject: [PATCH] fix(config): route plugin providers before config_provider bare-passthrough Gate found the plugin-routing branch ran AFTER the provider==config_provider early-return, so when an ACTIVE plugin provider was also the configured default, model_with_provider_context returned a bare model and dropped the '@plugin:' hint -> wrong backend. Moved the _is_plugin_model_provider() branch above the config_provider passthrough. Regression test (GMI + anthropic/claude-sonnet-4.6 as the active plugin+configured provider) added. Co-authored-by: alexfoxtm. --- api/config.py | 22 ++++++++------- tests/test_plugin_model_providers.py | 42 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/api/config.py b/api/config.py index 173e3e93..0dce3e1e 100644 --- a/api/config.py +++ b/api/config.py @@ -2960,6 +2960,16 @@ def model_with_provider_context(model_id: str, model_provider: str | None = None if provider in _ACP_SUBPROCESS_PROVIDERS: return f"@{provider}:{model}" + # Plugin-only model providers (e.g. 9router, and other model plugins whose + # slugs are not in the static provider tables) route through the plugin, not + # the default provider. This MUST come before the `provider == config_provider` + # bare-passthrough below: when a plugin provider is ALSO the configured + # provider, returning a bare model would drop the '@plugin:' hint and the model + # would be sent to the wrong backend. Emit the explicit hint so it stays + # routable to the plugin that surfaced it. (#5909 gate finding) + if _is_plugin_model_provider(provider): + return f"@{provider}:{model}" + # If the selected provider is already the configured provider, leaving the # model bare preserves provider-specific base_url/proxy settings. if provider == config_provider: @@ -2979,16 +2989,8 @@ def model_with_provider_context(model_id: str, model_provider: str | None = None if isinstance(providers_cfg, dict) and provider in providers_cfg: return f"@{provider}:{model}" - # Plugin-only model providers (e.g. 9router, and other model plugins whose - # slugs are not in the static provider tables) route through the plugin, - # not the default provider. A surfaced plugin-only model such as - # 'gh/claude-sonnet-4.5' carries a '/' in its ID, so without this branch it - # would fall through to the bare-passthrough below and inherit the default - # provider. Emit the explicit '@plugin:model' hint so the model stays - # routable to the plugin that surfaced it. This mirrors the explicit - # configured-provider handling above. - if _is_plugin_model_provider(provider): - return f"@{provider}:{model}" + # (Plugin-only provider routing handled above, before the config_provider + # bare-passthrough.) # For non-OpenRouter slash IDs without an explicit configured provider, # keep the ID intact so existing custom/proxy base_url routing and diff --git a/tests/test_plugin_model_providers.py b/tests/test_plugin_model_providers.py index 0239f9b0..4cc6316f 100644 --- a/tests/test_plugin_model_providers.py +++ b/tests/test_plugin_model_providers.py @@ -430,3 +430,45 @@ class TestPluginModelProviderRouting: config.cfg.clear() config.cfg.update(old_cfg) invalidate_plugin_model_provider_cache() + + def test_plugin_provider_routes_even_when_it_is_the_configured_provider(self, monkeypatch): + # #5909 gate finding: the plugin-routing branch must run BEFORE the + # `provider == config_provider` bare-passthrough. When the ACTIVE plugin + # provider is also the configured default, returning a bare model would + # drop the '@plugin:' hint and route to the wrong backend. + profile = SimpleNamespace( + name="gmi", + display_name="GMI", + env_vars=("GMI_API_KEY",), + auth_type="api_key", + aliases=(), + fallback_models=("anthropic/claude-sonnet-4.6",), + ) + + def _fake_list_providers(): + return [profile] + + fake_providers = types.ModuleType("providers") + fake_providers.list_providers = _fake_list_providers + monkeypatch.setitem(sys.modules, "providers", fake_providers) + invalidate_plugin_model_provider_cache() + + old_cfg = dict(config.cfg) + config.cfg.clear() + # The plugin IS the configured provider now. + config.cfg["model"] = {"provider": "gmi", "default": "anthropic/claude-sonnet-4.6"} + config.cfg["providers"] = {} + try: + assert config._is_plugin_model_provider("gmi") + routed = config.model_with_provider_context( + "anthropic/claude-sonnet-4.6", "gmi" + ) + assert routed == "@gmi:anthropic/claude-sonnet-4.6", ( + "an active plugin provider must keep its '@plugin:' routing hint " + "even when it equals the configured provider, got " + f"{routed!r}" + ) + finally: + config.cfg.clear() + config.cfg.update(old_cfg) + invalidate_plugin_model_provider_cache()