From 4a51ab61eb42a6b26268f38647f52fa032ea0d6a Mon Sep 17 00:00:00 2001 From: XieNBi Date: Fri, 24 Apr 2026 05:41:01 +0800 Subject: [PATCH] fix(cli): non-zero /model counts for native OpenAI and direct API rows --- hermes_cli/model_switch.py | 9 ++++ hermes_cli/models.py | 23 ++++++++ .../test_user_providers_model_switch.py | 52 +++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 1bb8592a6..6402fa469 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1240,6 +1240,15 @@ def list_authenticated_providers( if m and m not in models_list: models_list.append(m) + # Official OpenAI API rows in providers: often have base_url but no + # explicit models: dict — avoid a misleading zero count in /model. + if not models_list: + url_lower = str(api_url).strip().lower() + if "api.openai.com" in url_lower: + fb = curated.get("openai") or [] + if fb: + models_list = list(fb) + # Try to probe /v1/models if URL is set (but don't block on it) # For now just show what we know from config results.append({ diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 691742a4e..bc79b10f4 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -142,6 +142,18 @@ _PROVIDER_MODELS: dict[str, list[str]] = { "openai/gpt-5.4-pro", "openai/gpt-5.4-nano", ], + # Native OpenAI Chat Completions (api.openai.com). Used by /model counts and + # provider_model_ids fallback when /v1/models is unavailable. + "openai": [ + "gpt-5.4", + "gpt-5.4-mini", + "gpt-5-mini", + "gpt-5.3-codex", + "gpt-5.2-codex", + "gpt-4.1", + "gpt-4o", + "gpt-4o-mini", + ], "openai-codex": _codex_curated_models(), "copilot-acp": [ "copilot-acp", @@ -1748,6 +1760,17 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) live = fetch_ollama_cloud_models(force_refresh=force_refresh) if live: return live + if normalized == "openai": + api_key = os.getenv("OPENAI_API_KEY", "").strip() + if api_key: + base_raw = os.getenv("OPENAI_BASE_URL", "").strip().rstrip("/") + base = base_raw or "https://api.openai.com/v1" + try: + live = fetch_api_models(api_key, base) + if live: + return live + except Exception: + pass if normalized == "custom": base_url = _get_custom_base_url() if base_url: diff --git a/tests/hermes_cli/test_user_providers_model_switch.py b/tests/hermes_cli/test_user_providers_model_switch.py index 989a6cbed..00ccf701c 100644 --- a/tests/hermes_cli/test_user_providers_model_switch.py +++ b/tests/hermes_cli/test_user_providers_model_switch.py @@ -197,6 +197,58 @@ def test_list_authenticated_providers_dict_models_dedupe_with_default(monkeypatc assert user_prov["models"].count("model-a") == 1 +def test_openai_native_curated_catalog_is_non_empty(): + """Regression: built-in openai must have a static catalog for picker totals.""" + from hermes_cli.models import _PROVIDER_MODELS + + assert _PROVIDER_MODELS.get("openai") + assert len(_PROVIDER_MODELS["openai"]) >= 4 + + +def test_list_authenticated_providers_openai_built_in_nonzero_total(monkeypatch): + """Built-in openai row must not report total_models=0 when creds exist.""" + monkeypatch.setenv("OPENAI_API_KEY", "sk-test") + monkeypatch.setattr( + "agent.models_dev.fetch_models_dev", + lambda: {"openai": {"env": ["OPENAI_API_KEY"]}}, + ) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + providers = list_authenticated_providers( + current_provider="", + current_base_url="", + user_providers={}, + custom_providers=[], + max_models=50, + ) + row = next((p for p in providers if p.get("slug") == "openai"), None) + assert row is not None + assert row["total_models"] > 0 + + +def test_list_authenticated_providers_user_openai_official_url_fallback(monkeypatch): + """User providers: api.openai.com with no models list uses native curated fallback.""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {}) + + user_providers = { + "openai-direct": { + "name": "OpenAI Direct", + "api": "https://api.openai.com/v1", + } + } + providers = list_authenticated_providers( + current_provider="", + current_base_url="", + user_providers=user_providers, + custom_providers=[], + max_models=50, + ) + row = next((p for p in providers if p.get("slug") == "openai-direct"), None) + assert row is not None + assert row["total_models"] > 0 + + def test_list_authenticated_providers_fallback_to_default_only(monkeypatch): """When no models array is provided, should fall back to default_model.""" monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})