fix(providers): require api_base before local provider wins on keyword match

Ollama's spec keeps "nemotron" as a keyword so bare `nemotron-3-nano`
auto-routes to a configured Ollama install (PR #1863). NVIDIA NIM was
later registered with the same "nemotron" keyword (commit 046d0831),
creating the only keyword collision in the registry.

In `_match_provider`, the keyword loop accepted any local provider on
`spec.is_local` alone — no api_base check. Models like
`nvidia/nemotron-3-super-120b-a12b` (intended for OpenRouter or NVIDIA
NIM) were therefore hijacked to http://localhost:11434/v1 even when the
user had never configured Ollama, causing silent connection errors at
runtime.

Add the same api_base gate the local-fallback loop already uses: a local
provider only wins by keyword when the user has actually set its
api_base. Preserves PR #1863's intent for users who configured Ollama;
fixes the silent hijack for everyone else.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
NearlCrews 2026-05-10 23:36:09 -04:00 committed by Xubin Ren
parent 4c387f6633
commit 5eb818e800
2 changed files with 45 additions and 0 deletions

View File

@ -533,6 +533,15 @@ class Config(BaseSettings):
continue
p = getattr(self.providers, spec.name, None)
if p and any(_kw_matches(kw) for kw in spec.keywords):
# Local providers (Ollama, vLLM, …) keep model-family keywords
# like "nemotron" or "llama" to enable bare-model auto-routing,
# but those keywords collide with cloud-hosted variants of the
# same family (e.g. `nvidia/nemotron-...` via OpenRouter). Only
# 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_oauth or spec.is_local or spec.is_direct or p.api_key:
return p, spec.name

View File

@ -1160,6 +1160,42 @@ def test_config_falls_back_to_vllm_when_ollama_not_configured():
assert config.get_api_base() == "http://localhost:8000"
def test_config_cloud_nemotron_is_not_hijacked_by_unconfigured_ollama():
"""`nvidia/nemotron-*` via a gateway must not route to Ollama when no
Ollama endpoint is configured. Ollama keeps "nemotron" in its keywords
for bare-model auto-routing (PR #1863), which previously hijacked
cloud-hosted nemotron variants and silently sent traffic to
http://localhost:11434/v1."""
config = Config.model_validate(
{
"agents": {
"defaults": {
"provider": "auto",
"model": "nvidia/nemotron-3-super-120b-a12b",
}
},
"providers": {"openrouter": {"apiKey": "sk-or-test"}},
}
)
assert config.get_provider_name() == "openrouter"
assert config.get_api_base() == "https://openrouter.ai/api/v1"
def test_config_bare_nemotron_still_auto_routes_to_configured_ollama():
"""Preserves PR #1863 intent: when the user has actually configured an
Ollama endpoint, a bare nemotron model still auto-routes there."""
config = Config.model_validate(
{
"agents": {"defaults": {"provider": "auto", "model": "nemotron-3-nano"}},
"providers": {"ollama": {"apiBase": "http://localhost:11434/v1"}},
}
)
assert config.get_provider_name() == "ollama"
assert config.get_api_base() == "http://localhost:11434/v1"
def test_openai_compat_provider_passes_model_through():
from nanobot.providers.openai_compat_provider import OpenAICompatProvider