diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 2012d3df1..521c583dc 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -504,6 +504,7 @@ class Config(BaseSettings): model_normalized = model_lower.replace("-", "_") model_prefix = model_lower.split("/", 1)[0] if "/" in model_lower else "" normalized_prefix = model_prefix.replace("-", "_") + prefixed_provider = find_by_name(model_prefix) if model_prefix else None def _kw_matches(kw: str) -> bool: kw = kw.lower() @@ -540,8 +541,15 @@ class Config(BaseSettings): # honor a local keyword match when the user has actually # configured that local endpoint via `api_base` — mirrors the # gate already used by the local-fallback loop below. - if spec.is_local and not p.api_base: - continue + if spec.is_local: + # A qualified model belongs to its explicit provider or a + # gateway fallback, never to a different local provider + # whose model-family keyword happens to match. + foreign_prefix = bool( + prefixed_provider is not None and prefixed_provider.name != spec.name + ) + if not p.api_base or foreign_prefix: + continue if spec.is_oauth or spec.is_local or spec.is_direct or p.api_key: return p, spec.name @@ -550,16 +558,17 @@ class Config(BaseSettings): # Prefer providers whose detect_by_base_keyword matches the configured api_base # (e.g. Ollama's "11434" in "http://localhost:11434") over plain registry order. local_fallback: tuple[ProviderConfig, str] | None = None - for spec in PROVIDERS: - if not spec.is_local: - continue - p = getattr(self.providers, spec.name, None) - if not (p and p.api_base): - continue - if spec.detect_by_base_keyword and spec.detect_by_base_keyword in p.api_base: - return p, spec.name - if local_fallback is None: - local_fallback = (p, spec.name) + if prefixed_provider is None: + for spec in PROVIDERS: + if not spec.is_local: + continue + p = getattr(self.providers, spec.name, None) + if not (p and p.api_base): + continue + if spec.detect_by_base_keyword and spec.detect_by_base_keyword in p.api_base: + return p, spec.name + if local_fallback is None: + local_fallback = (p, spec.name) if local_fallback: return local_fallback diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index 647dbb166..bc4bc6a70 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -1196,6 +1196,27 @@ def test_config_bare_nemotron_still_auto_routes_to_configured_ollama(): assert config.get_api_base() == "http://localhost:11434/v1" +def test_config_cloud_nemotron_is_not_hijacked_by_configured_ollama(): + """An explicit cloud namespace takes precedence over local keywords.""" + config = Config.model_validate( + { + "agents": { + "defaults": { + "provider": "auto", + "model": "nvidia/nemotron-3-super-120b-a12b", + } + }, + "providers": { + "ollama": {"apiBase": "http://localhost:11434/v1"}, + "openrouter": {"apiKey": "sk-or-test"}, + }, + } + ) + + assert config.get_provider_name() == "openrouter" + assert config.get_api_base() == "https://openrouter.ai/api/v1" + + def test_openai_compat_provider_passes_model_through(): from nanobot.providers.openai_compat_provider import OpenAICompatProvider