mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
Stage 301: PR #1685
This commit is contained in:
@@ -1902,6 +1902,47 @@ def _get_label_for_model(model_id: str, existing_groups: list) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _read_visible_codex_cache_model_ids() -> list[str]:
|
||||
"""Return visible model slugs from Codex's local models_cache.json.
|
||||
|
||||
The agent's provider_model_ids('openai-codex') intentionally filters IDs
|
||||
with ``supported_in_api: false``. Codex CLI still lists some of those models
|
||||
in its picker (notably ``gpt-5.3-codex-spark`` from #1680), so the WebUI
|
||||
merges this visible local catalog to stay in sync with Codex itself.
|
||||
"""
|
||||
codex_home = Path(os.getenv("CODEX_HOME", "").strip() or (HOME / ".codex")).expanduser()
|
||||
cache_path = codex_home / "models_cache.json"
|
||||
try:
|
||||
payload = json.loads(cache_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
entries = payload.get("models") if isinstance(payload, dict) else None
|
||||
if not isinstance(entries, list):
|
||||
return []
|
||||
|
||||
sortable: list[tuple[int, str]] = []
|
||||
for item in entries:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
slug = item.get("slug")
|
||||
if not isinstance(slug, str) or not slug.strip():
|
||||
continue
|
||||
visibility = item.get("visibility", "")
|
||||
if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"):
|
||||
continue
|
||||
priority = item.get("priority")
|
||||
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
||||
sortable.append((rank, slug.strip()))
|
||||
|
||||
sortable.sort(key=lambda item: (item[0], item[1]))
|
||||
ordered: list[str] = []
|
||||
for _, slug in sortable:
|
||||
if slug not in ordered:
|
||||
ordered.append(slug)
|
||||
return ordered
|
||||
|
||||
|
||||
def get_available_models() -> dict:
|
||||
"""
|
||||
Return available models grouped by provider.
|
||||
@@ -2671,6 +2712,43 @@ def get_available_models() -> dict:
|
||||
except Exception:
|
||||
logger.warning("Failed to load Ollama Cloud models from hermes_cli")
|
||||
|
||||
if raw_models:
|
||||
models = _apply_provider_prefix(raw_models, pid, active_provider)
|
||||
groups.append(
|
||||
{
|
||||
"provider": provider_name,
|
||||
"provider_id": pid,
|
||||
"models": models,
|
||||
}
|
||||
)
|
||||
elif pid == "openai-codex":
|
||||
# Codex account catalogs drift faster than WebUI releases
|
||||
# (for example gpt-5.3-codex-spark in #1680). Ask the
|
||||
# agent's Codex resolver first so /api/models inherits the
|
||||
# live Codex API / local ~/.codex cache / static fallback
|
||||
# chain instead of freezing the picker to WebUI's curated
|
||||
# _PROVIDER_MODELS snapshot.
|
||||
raw_models = []
|
||||
codex_ids = []
|
||||
try:
|
||||
from hermes_cli.models import provider_model_ids as _provider_model_ids
|
||||
|
||||
codex_ids = [mid for mid in (_provider_model_ids("openai-codex") or []) if mid]
|
||||
except Exception:
|
||||
logger.warning("Failed to load OpenAI Codex models from hermes_cli")
|
||||
|
||||
for mid in _read_visible_codex_cache_model_ids():
|
||||
if mid not in codex_ids:
|
||||
codex_ids.append(mid)
|
||||
|
||||
raw_models = [
|
||||
{"id": mid, "label": _get_label_for_model(mid, [])}
|
||||
for mid in codex_ids
|
||||
]
|
||||
|
||||
if not raw_models:
|
||||
raw_models = copy.deepcopy(_PROVIDER_MODELS.get("openai-codex", []))
|
||||
|
||||
if raw_models:
|
||||
models = _apply_provider_prefix(raw_models, pid, active_provider)
|
||||
groups.append(
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Regression tests for #1680 — Codex model picker uses live Codex discovery."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
|
||||
from api import config
|
||||
|
||||
|
||||
def _flatten_ids(groups):
|
||||
return [m.get("id") for g in groups for m in g.get("models", [])]
|
||||
|
||||
|
||||
def _install_fake_hermes_models(monkeypatch, provider_model_ids):
|
||||
hermes_cli = types.ModuleType("hermes_cli")
|
||||
hermes_cli.__path__ = []
|
||||
models = types.ModuleType("hermes_cli.models")
|
||||
models._PROVIDER_ALIASES = {}
|
||||
models.provider_model_ids = provider_model_ids
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli.models", models)
|
||||
|
||||
|
||||
def _configure_codex(monkeypatch, tmp_path, default="gpt-5.3-codex-spark"):
|
||||
monkeypatch.setattr(config, "_get_config_path", lambda: tmp_path / "missing-config.yaml")
|
||||
monkeypatch.setattr(config, "_models_cache_path", tmp_path / "models_cache.json")
|
||||
monkeypatch.setattr(config, "cfg", {
|
||||
"model": {"provider": "openai-codex", "default": default},
|
||||
"providers": {},
|
||||
"fallback_providers": [],
|
||||
})
|
||||
monkeypatch.setattr(config, "_cfg_mtime", 0.0)
|
||||
config.invalidate_models_cache()
|
||||
|
||||
|
||||
def test_openai_codex_group_uses_provider_model_ids_for_spark(monkeypatch, tmp_path):
|
||||
"""Codex-only models from the Codex catalog must surface in /api/models.
|
||||
|
||||
The static WebUI fallback chronically drifts. ``gpt-5.3-codex-spark`` is
|
||||
the regression case from #1680: it is discoverable by the Codex provider
|
||||
resolver but was missing from the picker because get_available_models()
|
||||
copied _PROVIDER_MODELS["openai-codex"] without asking hermes_cli.
|
||||
"""
|
||||
calls = []
|
||||
|
||||
def provider_model_ids(provider):
|
||||
calls.append(provider)
|
||||
assert provider == "openai-codex"
|
||||
return ["gpt-5.4", "gpt-5.3-codex-spark", "gpt-5.3-codex"]
|
||||
|
||||
_install_fake_hermes_models(monkeypatch, provider_model_ids)
|
||||
_configure_codex(monkeypatch, tmp_path)
|
||||
|
||||
result = config.get_available_models()
|
||||
|
||||
codex_groups = [g for g in result["groups"] if g.get("provider_id") == "openai-codex"]
|
||||
assert calls == ["openai-codex"]
|
||||
assert codex_groups, "OpenAI Codex group should be present"
|
||||
assert "gpt-5.3-codex-spark" in _flatten_ids(codex_groups)
|
||||
assert codex_groups[0]["models"][0]["label"] == "GPT 5.4"
|
||||
|
||||
|
||||
def test_openai_codex_group_merges_visible_codex_cache_models(monkeypatch, tmp_path):
|
||||
"""Visible Codex CLI cache models should appear even if API-filtered.
|
||||
|
||||
Michael's local Codex cache lists ``gpt-5.3-codex-spark`` with
|
||||
``supported_in_api: false``. The agent helper currently filters those IDs
|
||||
out, but the WebUI picker is a Codex-model selection surface and should
|
||||
mirror the visible Codex catalog instead of hiding Spark.
|
||||
"""
|
||||
def provider_model_ids(provider):
|
||||
assert provider == "openai-codex"
|
||||
return ["gpt-5.4", "gpt-5.3-codex"]
|
||||
|
||||
_install_fake_hermes_models(monkeypatch, provider_model_ids)
|
||||
_configure_codex(monkeypatch, tmp_path, default="gpt-5.4")
|
||||
|
||||
codex_home = tmp_path / "codex-home"
|
||||
codex_home.mkdir()
|
||||
(codex_home / "models_cache.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"models": [
|
||||
{"slug": "gpt-5.4", "visibility": "list", "priority": 0},
|
||||
{
|
||||
"slug": "gpt-5.3-codex-spark",
|
||||
"visibility": "list",
|
||||
"supported_in_api": False,
|
||||
"priority": 7,
|
||||
},
|
||||
{"slug": "hidden-test-model", "visibility": "hide", "priority": 8},
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("CODEX_HOME", str(codex_home))
|
||||
|
||||
result = config.get_available_models()
|
||||
|
||||
codex_groups = [g for g in result["groups"] if g.get("provider_id") == "openai-codex"]
|
||||
ids = _flatten_ids(codex_groups)
|
||||
assert "gpt-5.3-codex-spark" in ids
|
||||
assert "hidden-test-model" not in ids
|
||||
Reference in New Issue
Block a user