fix(models): preserve native discovery for canonical provider URLs
(cherry picked from commit dbbe9a98e238d7c01177d0d3ac205deaa94e4675)
This commit is contained in:
@@ -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]]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user