Files
hermes-webui/tests/test_issue2177_nvidia_prefix_preservation.py
nesquena-hermes 9b1d786459 fix(config): preserve nvidia/ prefix on NVIDIA NIM (closes #2177)
Move the `_PORTAL_PROVIDERS` guard in `resolve_model_provider()` to run
BEFORE the `prefix == config_provider` strip branch. The guard was added
for NVIDIA (along with the Nous portal cases in #854 / #894) but was
placed after the strip, so it never fired when `config_provider == "nvidia"`
and the model id started with `nvidia/`.

For `model_id="nvidia/nemotron-3-super-120b-a12b"`,
`config_provider="nvidia"`:
  - prefix = "nvidia", bare = "nemotron-3-super-120b-a12b"
  - prefix == config_provider → True → strip branch returned bare name
  - `_PORTAL_PROVIDERS` guard never reached
  - bare "nemotron-3-super-120b-a12b" sent to NVIDIA NIM → HTTP 404

NIM requires the full namespaced path. The fix moves the portal guard
to run first, so all portal providers (Nous, OpenCode-Zen, OpenCode-Go,
NVIDIA NIM) always preserve the full `provider/model` id regardless of
whether the prefix happens to equal the provider name.

This also closes a latent symmetric bug for the Nous case if a
`nous/<model>` id ever existed in the catalog.

Test plan:
- New `tests/test_issue2177_nvidia_prefix_preservation.py` covers:
  - nvidia/nemotron-... under nvidia (the reported case)
  - cross-namespace qwen/ and meta/ under nvidia (regression pin)
  - every static nvidia model in `_PROVIDER_MODELS` resolves to itself
  - latent nous/<model> under nous (structural ordering pin)
  - non-portal providers (anthropic) still strip — fix doesn't over-correct
- Existing portal-routing suites (test_nous_portal_routing.py,
  test_issue895_894_nous_prefix.py) continue to pass.
- Full test suite: 5320 passed, 4 skipped, 3 xpassed.

Reported on Discord by @vishnu (Nathan forwarded as #2177).
2026-05-13 07:05:57 +00:00

119 lines
5.3 KiB
Python

"""Regression tests for #2177 — resolve_model_provider() strips nvidia/ prefix on NVIDIA NIM.
The bug: when ``config_provider == "nvidia"`` and the model id is
``nvidia/nemotron-3-super-120b-a12b``, the resolver hit the
``prefix == config_provider`` strip BEFORE the ``_PORTAL_PROVIDERS`` guard and
returned the bare name ``nemotron-3-super-120b-a12b``. NVIDIA NIM's inference
endpoint then 404'd because it requires the full namespaced model id.
Same bug class as #854 / #894 for the Nous portal. The fix moves the
``_PORTAL_PROVIDERS`` guard to run BEFORE the prefix-strip so portal providers
always preserve the full ``provider/model`` path regardless of whether the
prefix happens to equal the config_provider name.
"""
import api.config as config
def _resolve(model_id: str, provider: str):
"""Resolve a model under a synthesized config_provider, isolated from disk state."""
old = dict(config.cfg)
old_mtime = config._cfg_mtime
config.cfg.clear()
config.cfg["model"] = {"provider": provider}
try:
config._cfg_mtime = config.Path(config._get_config_path()).stat().st_mtime
except Exception:
config._cfg_mtime = 0.0
try:
return config.resolve_model_provider(model_id)
finally:
config.cfg.clear()
config.cfg.update(old)
config._cfg_mtime = old_mtime
class TestNvidiaPortalPrefixPreservation:
"""NVIDIA NIM requires the full ``namespace/model`` id — prefix must never be stripped."""
def test_nvidia_self_namespace_prefix_preserved(self):
"""``nvidia/nemotron-...`` under nvidia provider must keep the nvidia/ prefix.
This is the exact case in #2177 — prefix == config_provider would otherwise
trigger the strip branch and emit a bare ``nemotron-...`` id that NIM 404s.
"""
model, provider, _ = _resolve("nvidia/nemotron-3-super-120b-a12b", "nvidia")
assert model == "nvidia/nemotron-3-super-120b-a12b", (
f"prefix was stripped: {model!r} — NVIDIA NIM requires the full namespaced id"
)
assert provider == "nvidia"
def test_nvidia_cross_namespace_qwen_prefix_preserved(self):
"""``qwen/...`` under nvidia provider keeps the qwen/ prefix (regression pin).
Cross-namespace already worked because ``prefix != config_provider`` — pinning
the test so a later refactor doesn't regress it.
"""
model, provider, _ = _resolve("qwen/qwen2.5-coder-32b-instruct", "nvidia")
assert model == "qwen/qwen2.5-coder-32b-instruct"
assert provider == "nvidia"
def test_nvidia_cross_namespace_meta_prefix_preserved(self):
"""``meta/llama-...`` under nvidia provider keeps the meta/ prefix."""
model, provider, _ = _resolve("meta/llama-3.1-70b-instruct", "nvidia")
assert model == "meta/llama-3.1-70b-instruct"
assert provider == "nvidia"
def test_nvidia_static_models_all_resolve_with_prefix_intact(self):
"""Every static nvidia model in ``_PROVIDER_MODELS`` must resolve to itself.
Pins the contract that the static dropdown list and the resolver agree on
the wire format — same invariant ``test_nous_portal_routing.py`` enforces
for the Nous portal.
"""
nvidia_models = config._PROVIDER_MODELS.get("nvidia", [])
assert nvidia_models, "nvidia must have at least one static model"
for entry in nvidia_models:
mid = entry["id"]
resolved, provider, _ = _resolve(mid, "nvidia")
assert resolved == mid, (
f"static model {mid!r} resolved to {resolved!r}"
f"portal must preserve full namespaced id"
)
assert provider == "nvidia"
class TestPortalGuardOrdering:
"""The ``_PORTAL_PROVIDERS`` guard must run BEFORE the prefix-strip branch.
Otherwise, any portal user whose model id starts with the literal provider
name (nvidia/nvidia-..., nous/nous-..., etc.) hits the strip branch and
breaks at the upstream API. This is structural — pinning the ordering so a
future refactor cannot quietly reintroduce the bug.
"""
def test_hypothetical_nous_self_prefix_preserved(self):
"""``nous/<model>`` under nous provider must keep the prefix.
Latent bug shape — there's no shipped ``nous/<model>`` id today, but if
Nous ever serves a self-prefixed model the resolver must handle it the
same way it handles nvidia/.
"""
model, provider, _ = _resolve("nous/hermes-3-something", "nous")
assert model == "nous/hermes-3-something", (
f"prefix stripped: {model!r} — portal guard must preserve full id"
)
assert provider == "nous"
def test_anthropic_self_prefix_still_strips_for_anthropic(self):
"""Non-portal providers (anthropic) keep the existing strip behavior.
``anthropic/claude-...`` under the anthropic provider should still
resolve to bare ``claude-...`` — anthropic's API doesn't want the
``anthropic/`` prefix. Pinning so the fix doesn't over-correct.
"""
model, provider, _ = _resolve("anthropic/claude-opus-4.6", "anthropic")
assert model == "claude-opus-4.6", (
f"anthropic strip-prefix path broken: got {model!r}, expected 'claude-opus-4.6'"
)
assert provider == "anthropic"