Files
hermes-agent/tests/hermes_cli/test_models_detect_credential_gate.py
teknium1 ff4399a0d7 fix: route a slug shared by several catalogs to the provider the user can use
`detect_provider_for_model` took the FIRST static-catalog hit as the only
guess. `gpt-5.6-luna` (and the rest of the gpt-5.6 family) is listed by both
`openai-api` and `openai-codex`, so a user with a Codex OAuth grant and no
OPENAI_API_KEY was routed to a keyless openai-api on a fresh (`auto`)
session, or — after the credential gate — left on the current provider with
the request silently ignored, while the grant they hold was never
considered.

`_static_catalog_matches` now yields every catalog that lists the slug in
ladder order; `detect_provider_for_model` keeps its existing credential gate
and takes the first sibling the user actually has credentials for. A fresh
session with no usable provider anywhere still fails loudly on the first
guess, and a user holding both keys keeps today's openai-api routing.

Fixes #102775
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
2026-09-19 10:22:01 -07:00

71 lines
3.6 KiB
Python

"""Auto-detection must never hand the user a provider they hold no credentials for.
Regression for the "accidental provider" class: ``/model <name>`` on provider A, where the name is
only known to provider B (static catalog or OpenRouter), used to switch the session to B even when
B had no key — an immediate 401 for most vendors, and for OpenRouter (whose runtime resolves with an
empty key instead of raising) a silent switch onto a metered aggregator.
"""
from __future__ import annotations
import pytest
from hermes_cli import models
@pytest.fixture
def no_live_catalog(monkeypatch):
monkeypatch.setattr(models, "cached_provider_model_ids", lambda provider, **_: [])
monkeypatch.setattr(models, "_find_openrouter_slug", lambda name: f"vendor/{name}")
@pytest.fixture
def authed(monkeypatch):
"""Pin which providers count as authenticated; everything else has no credentials."""
from hermes_cli import models_detect
granted: set[str] = set()
monkeypatch.setattr(models_detect, "provider_has_credentials", lambda p: p in granted)
return granted
class TestNoCredentialsNoSwitch:
def test_openrouter_only_model_stays_when_no_openrouter_key(self, no_live_catalog, authed):
assert models.detect_provider_for_model("some-model-only-openrouter-has", "deepseek") is None
def test_openrouter_remap_allowed_with_key(self, no_live_catalog, authed):
authed.add("openrouter")
assert models.detect_provider_for_model("some-model-only-openrouter-has", "deepseek") == (
"openrouter", "vendor/some-model-only-openrouter-has")
def test_static_vendor_match_requires_that_vendors_credentials(self, no_live_catalog, authed, monkeypatch):
monkeypatch.setattr(models, "detect_static_provider_for_model", lambda n, c: ("anthropic", n))
assert models.detect_provider_for_model("claude-something", "deepseek") is None
authed.add("anthropic")
assert models.detect_provider_for_model("claude-something", "deepseek") == ("anthropic", "claude-something")
def test_explicitly_named_provider_is_not_gated(self, no_live_catalog, authed, monkeypatch):
"""``/model nous`` names the provider: hand it back so the credential step can prompt/fail
loudly instead of silently ignoring the request."""
monkeypatch.setattr(models, "detect_static_provider_for_model", lambda n, c: ("nous", "hermes-4-405b"))
assert models.detect_provider_for_model("nous", "deepseek") == ("nous", "hermes-4-405b")
class TestSharedSlugTiebreak:
"""A slug listed by several first-party catalogs goes to the one the user can use (#102775):
``gpt-5.6-luna`` sits in both ``openai-api`` and ``openai-codex``, and the first catalog hit
used to be the only candidate — a Codex-only user was routed to a keyless ``openai-api``
(``auto``) or left on the current provider (explicit switch)."""
def test_shared_slug_goes_to_the_credentialed_sibling(self, no_live_catalog, authed):
authed.add("openai-codex")
assert models.detect_provider_for_model("gpt-5.6-luna", "deepseek") == ("openai-codex", "gpt-5.6-luna")
assert models.detect_provider_for_model("gpt-5.6-luna", "auto") == ("openai-codex", "gpt-5.6-luna")
def test_shared_slug_keeps_first_catalog_when_it_is_usable(self, no_live_catalog, authed):
authed.update({"openai-api", "openai-codex"})
assert models.detect_provider_for_model("gpt-5.6-luna", "deepseek") == ("openai-api", "gpt-5.6-luna")
authed.clear()
# Nothing usable anywhere: a fresh session still fails loudly on the first guess.
assert models.detect_provider_for_model("gpt-5.6-luna", "auto") == ("openai-api", "gpt-5.6-luna")