fix(models): preserve native discovery for canonical provider URLs

(cherry picked from commit dbbe9a98e238d7c01177d0d3ac205deaa94e4675)
This commit is contained in:
tarkilhk
2026-09-26 14:01:24 +08:00
committed by kshitij
parent ef811bcea5
commit 6e74191b87
3 changed files with 73 additions and 3 deletions

View File

@@ -1605,8 +1605,8 @@ def _chat_catalog_rows(models):
def _configured_relay_base_url(provider: str) -> str:
"""``model.base_url`` when it points the *configured* provider at a relay/proxy, else "".
Discovery must probe the same endpoint inference uses (#121387): with ``model.base_url``
set for the configured provider, the vendor's canonical host is NOT the catalog to list.
Discovery must probe the same endpoint inference uses (#121387): when ``model.base_url``
differs from the registered endpoint, the vendor's canonical host is NOT the catalog to list.
Mirrors the ``$OPENAI_BASE_URL`` -> ``model.base_url`` -> canonical precedence of
``_openai_discovery_base_url`` for every built-in provider, not just OpenAI.
"""
@@ -1622,7 +1622,15 @@ def _configured_relay_base_url(provider: str) -> str:
return ""
except Exception:
return ""
return str(model_cfg.get("base_url") or "").strip().rstrip("/")
base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/")
from hermes_cli.auth import PROVIDER_REGISTRY
registered = PROVIDER_REGISTRY.get(normalize_provider(provider))
# Setup persists canonical URLs too. They still need the provider's native discovery
# (notably Codex OAuth); the generic relay probe only supports API-key providers.
if registered and base_url == registered.inference_base_url.rstrip("/"):
return ""
return base_url
def _relay_model_catalog(normalized: str, relay: str) -> Optional[list[str]]:

View File

@@ -203,6 +203,44 @@ def test_picker_direct_chatgpt_positive_control(monkeypatch, picker_http):
assert _authorized_hosts(picker_http) == {"chatgpt.com"}
@pytest.mark.parametrize("suffix", ["", "/"])
def test_full_picker_discovers_codex_models_with_pinned_canonical_url(monkeypatch, suffix):
"""A saved canonical URL must not bypass account discovery via the relay-only API-key path."""
import httpx
from hermes_cli.inventory import build_model_options_payload, load_picker_context
home = _home(monkeypatch)
_write_config(home, base_url=CHATGPT + suffix)
_write_singleton(home, JWT)
tokens_before = json.loads((home / "auth.json").read_bytes())["providers"]["openai-codex"]["tokens"]
config_before = (home / "config.yaml").read_bytes()
discovered = "gpt-6-astra"
seen = []
def catalog_get(url, headers=None, **kwargs):
seen.append((url, headers))
return SimpleNamespace(status_code=200, json=lambda: {
"models": [{"slug": discovered, "visibility": "list", "priority": 0}],
})
monkeypatch.setattr(httpx, "get", catalog_get)
# Unrelated metadata sources stay offline; config, auth, discovery, cache and picker are real.
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
monkeypatch.setattr("hermes_cli.models.get_curated_nous_model_ids", lambda: [])
monkeypatch.setattr("hermes_cli.models.fetch_ollama_cloud_models", lambda **kw: [])
monkeypatch.setattr("hermes_cli.models_pricing.get_pricing_for_provider", lambda *a, **kw: {})
payload = build_model_options_payload(load_picker_context(), refresh=True)
row = next(p for p in payload["providers"] if p["slug"] == "openai-codex")
assert discovered in row["models"]
assert seen
assert all(urlparse(url).hostname == "chatgpt.com" for url, _ in seen)
assert all(headers["Authorization"] == f"Bearer {JWT}" for _, headers in seen)
assert json.loads((home / "auth.json").read_bytes())["providers"]["openai-codex"]["tokens"] == tokens_before
assert (home / "config.yaml").read_bytes() == config_before
def test_picker_refuses_opaque_key_aimed_at_chatgpt(picker_http):
"""Defense in depth: a non-JWT key composed with chatgpt.com is never sent there."""
from hermes_cli.codex_models import get_codex_model_ids

View File

@@ -19,6 +19,30 @@ class _RecordingProfile:
return ["relay-only-model"]
@pytest.mark.parametrize("provider", ["openai-codex", "copilot", "nous", "opencode-zen"])
def test_canonical_url_preserves_native_catalog_but_other_paths_stay_relays(monkeypatch, provider):
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_cli.config import atomic_config_write
from hermes_constants import get_hermes_home
canonical = PROVIDER_REGISTRY[provider].inference_base_url
config_path = get_hermes_home() / "config.yaml"
calls = []
def native_catalog(slug, force_refresh):
calls.append(slug)
return ["native-catalog-model"]
monkeypatch.setitem(models._PROVIDER_CATALOG_FETCHERS, provider, native_catalog)
monkeypatch.setattr(models, "_relay_model_catalog", lambda *args: None)
for base_url, expected in [(canonical, [provider]), (canonical + "/relay", [])]:
calls.clear()
atomic_config_write(config_path, {"model": {"provider": provider, "base_url": base_url}})
catalog = models.provider_model_ids(provider, force_refresh=True)
assert calls == expected
assert ("native-catalog-model" in catalog) == bool(expected)
def test_relay_base_url_is_probed_for_configured_provider(monkeypatch):
monkeypatch.setattr(
models,