Merge pull request #7706 from nesquena/stage/7220-opencode-discovery
Release: opencode-go live model discovery (#7220)
This commit is contained in:
5 files changed
+413
-99
No files matched your search
@@ -5,6 +5,14 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The opencode-go provider lists its real models again instead of a frozen snapshot.** Model
|
||||
discovery had fallen back to a hard-coded catalog, so models added or removed upstream never
|
||||
appeared. The provider now queries live, with the lookup scoped to opencode-go alone: a slow
|
||||
or unreachable endpoint falls back to the static catalog rather than blocking the picker for
|
||||
anyone else, older Agent builds that lack the discovery API keep the previous behaviour, and a
|
||||
configured model allowlist still wins over whatever discovery returns. Thanks
|
||||
@shameez-struggles-to-commit. (#7220)
|
||||
|
||||
- **Denying a gateway approval now retires the request instead of leaving the run waiting.**
|
||||
When an approval routed through the gateway was denied, the deny reached the agent but the
|
||||
local producer was never retired, so the run could sit waiting on a decision that had already
|
||||
|
||||
+65
-27
@@ -1904,28 +1904,44 @@ _PROVIDER_MODELS = {
|
||||
{"id": "big-pickle", "label": "Big Pickle"},
|
||||
],
|
||||
# OpenCode Go — flat-rate models via opencode.ai/go ($10/month).
|
||||
# Synced 2026-07-08 from the public Go docs and documented models endpoint.
|
||||
# Keep preview/free-only Zen models out of this Go picker snapshot.
|
||||
# Fallback only: the live Hermes CLI catalog (Go-specific
|
||||
# /zen/go/v1/models probe, core v0.20.5+) leads (#1240, #5311).
|
||||
# Mirrors Hermes core's curated opencode-go list
|
||||
# (hermes_cli/models_catalog_static.py, core main as of 2026-09-10).
|
||||
# Core's 2026-09-09 sync dropped `ox-alpha-free` (Go relay delisted it:
|
||||
# GET /zen/go/v1/models omits it, POST → 401) and added `glm-5.3-flash`
|
||||
# and `muse-spark-1.3-contributor`. Core owns the sync duty against the
|
||||
# live endpoint; WebUI mirrors.
|
||||
"opencode-go": [
|
||||
{"id": "minimax-m3", "label": "MiniMax M3"},
|
||||
{"id": "minimax-m2.7", "label": "MiniMax M2.7"},
|
||||
{"id": "minimax-m2.5", "label": "MiniMax M2.5"},
|
||||
{"id": "kimi-k2.7-code", "label": "Kimi K2.7 Code"},
|
||||
{"id": "kimi-k2.6", "label": "Kimi K2.6"},
|
||||
{"id": "kimi-k2.5", "label": "Kimi K2.5"},
|
||||
{"id": "glm-5.2", "label": "GLM-5.2"},
|
||||
{"id": "glm-5.1", "label": "GLM-5.1"},
|
||||
{"id": "glm-5", "label": "GLM-5"},
|
||||
{"id": "deepseek-v4-pro", "label": "DeepSeek V4 Pro"},
|
||||
{"id": "deepseek-v4-flash","label": "DeepSeek V4 Flash"},
|
||||
{"id": "qwen3.7-max", "label": "Qwen3.7 Max"},
|
||||
{"id": "qwen3.7-plus", "label": "Qwen3.7 Plus"},
|
||||
{"id": "qwen3.6-plus", "label": "Qwen3.6 Plus"},
|
||||
{"id": "qwen3.5-plus", "label": "Qwen3.5 Plus"},
|
||||
{"id": "mimo-v2-pro", "label": "MiMo V2 Pro"},
|
||||
{"id": "mimo-v2-omni", "label": "MiMo V2 Omni"},
|
||||
{"id": "mimo-v2.5-pro", "label": "MiMo V2.5 Pro"},
|
||||
{"id": "mimo-v2.5", "label": "MiMo V2.5"},
|
||||
{"id": "kimi-k3", "label": "Kimi K3"},
|
||||
{"id": "kimi-k2.7-code", "label": "Kimi K2.7 Code"},
|
||||
{"id": "kimi-k2.6", "label": "Kimi K2.6"},
|
||||
{"id": "kimi-k2.5", "label": "Kimi K2.5"},
|
||||
{"id": "gpt-5.6-luna", "label": "GPT 5.6 Luna"},
|
||||
{"id": "grok-4.5", "label": "Grok 4.5"},
|
||||
{"id": "glm-5.3", "label": "GLM-5.3"},
|
||||
{"id": "glm-5.3-flash", "label": "GLM-5.3 Flash"},
|
||||
{"id": "glm-5.2", "label": "GLM-5.2"},
|
||||
{"id": "glm-5.1", "label": "GLM-5.1"},
|
||||
{"id": "glm-5", "label": "GLM-5"},
|
||||
{"id": "mimo-v2.5-pro", "label": "MiMo V2.5 Pro"},
|
||||
{"id": "mimo-v2.5", "label": "MiMo V2.5"},
|
||||
{"id": "mimo-v2-pro", "label": "MiMo V2 Pro"},
|
||||
{"id": "mimo-v2-omni", "label": "MiMo V2 Omni"},
|
||||
{"id": "minimax-m3", "label": "MiniMax M3"},
|
||||
{"id": "minimax-m2.7", "label": "MiniMax M2.7"},
|
||||
{"id": "minimax-m2.5", "label": "MiniMax M2.5"},
|
||||
{"id": "deepseek-v4-pro", "label": "DeepSeek V4 Pro"},
|
||||
{"id": "deepseek-v4-flash", "label": "DeepSeek V4 Flash"},
|
||||
{"id": "qwen3.8-max", "label": "Qwen3.8 Max"},
|
||||
{"id": "qwen3.7-max", "label": "Qwen3.7 Max"},
|
||||
{"id": "qwen3.7-plus", "label": "Qwen3.7 Plus"},
|
||||
{"id": "qwen3.6-plus", "label": "Qwen3.6 Plus"},
|
||||
{"id": "qwen3.5-plus", "label": "Qwen3.5 Plus"},
|
||||
{"id": "hy3", "label": "HY3"},
|
||||
{"id": "hy3-preview", "label": "HY3 Preview"},
|
||||
{"id": "muse-spark-1.2-contributor", "label": "Muse Spark 1.2 Contributor"},
|
||||
{"id": "muse-spark-1.3-contributor", "label": "Muse Spark 1.3 Contributor"},
|
||||
],
|
||||
# 'gemini' is the hermes_cli provider ID for Google AI Studio
|
||||
# Model IDs are bare — sent directly to:
|
||||
@@ -8374,6 +8390,26 @@ def _read_live_provider_model_ids(provider_id: str) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
def _hermes_cli_supports_opencode_go_live_catalog() -> bool:
|
||||
"""Whether the installed Agent has Go-specific model discovery.
|
||||
|
||||
Hermes core versions before 0.20.5 route ``opencode-go`` through a
|
||||
generic public catalog. That lookup can return a convincing non-empty
|
||||
list containing models the Go relay rejects with 404, so absence or an
|
||||
unparseable/prerelease version must fail closed to WebUI's static Go list.
|
||||
"""
|
||||
try:
|
||||
import hermes_cli
|
||||
|
||||
version = str(getattr(hermes_cli, "__version__", "")).strip()
|
||||
except Exception:
|
||||
return False
|
||||
match = re.fullmatch(r"v?(\d+)\.(\d+)\.(\d+)", version)
|
||||
if not match:
|
||||
return False
|
||||
return tuple(int(part) for part in match.groups()) >= (0, 20, 5)
|
||||
|
||||
|
||||
def _models_from_live_provider_ids(provider_id: str, live_ids: list[str]) -> list[dict]:
|
||||
"""Convert Hermes CLI model ids into WebUI picker model entries."""
|
||||
formatter = _format_ollama_label if provider_id in ("ollama", "ollama-cloud") else None
|
||||
@@ -9771,12 +9807,14 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa
|
||||
if not raw_models:
|
||||
if pid == "moa":
|
||||
raw_models = _moa_preset_models_from_config(cfg)
|
||||
elif pid == "opencode-go":
|
||||
# Skip live /v1/models probe for OpenCode Go — it
|
||||
# returns models from the public catalog that are
|
||||
# not enabled on the Go tier, causing 404 when
|
||||
# selected. Use the curated static list only. (#5311)
|
||||
pass
|
||||
elif (
|
||||
pid == "opencode-go"
|
||||
and not _hermes_cli_supports_opencode_go_live_catalog()
|
||||
):
|
||||
# Before core v0.20.5 this resolver returned the
|
||||
# generic public catalog, including models that
|
||||
# 404 on the Go tier. Use the curated Go fallback.
|
||||
raw_models = []
|
||||
else:
|
||||
raw_models = _models_from_live_provider_ids(
|
||||
pid,
|
||||
|
||||
@@ -241,6 +241,44 @@ For a foreground `python3 bootstrap.py`, stop it with Ctrl-C and start it again.
|
||||
|
||||
---
|
||||
|
||||
## "OpenCode Go model picker shows a model that errors when you send" (or is missing newly released models)
|
||||
|
||||
**Symptom.** One of two directions:
|
||||
|
||||
1. A model selected from the OpenCode Go group fails on the first message (`model not found`, `Model is unavailable`, or a region error), even though the picker offered it.
|
||||
2. A newly released OpenCode Go model does not appear in the picker at all, and must be typed into the Custom Model ID box.
|
||||
|
||||
**Why.** The Go picker follows the **live** Go-tier catalog (`https://opencode.ai/zen/go/v1/models`) whenever the installed Hermes Agent is v0.20.5 or newer. That endpoint advertises a superset of what a given tier, key, or region can actually serve — an id can be listed and still fail on send. Conversely, an id the endpoint serves but the WebUI's static fallback list predates is missing when the live path is unavailable (Agent older than v0.20.5, probe failure, or offline); the static list mirrors Hermes core's curated Go catalog and can lag new releases by design.
|
||||
|
||||
**Diagnostic.** Check which path is feeding your picker:
|
||||
|
||||
```bash
|
||||
hermes --version # live path requires >= 0.20.5
|
||||
curl -sS https://opencode.ai/zen/go/v1/models \
|
||||
-H "Authorization: Bearer $OPENCODE_GO_API_KEY" | head -50
|
||||
```
|
||||
|
||||
Interpret the two together:
|
||||
|
||||
- **Failing model absent from the `curl` output** → it was delisted upstream (for example `ox-alpha-free`, removed from the relay on 2026-09-09). A picker can still offer it from a **stale catalog merge**: Hermes core merges its own curated Go list into the live result, and every Agent release *through v0.21.1 (tag `v2026.9.7`)* still carries the delisted id in that list — verified at runtime against v0.21.0, where the live path serves 37 ids including `ox-alpha-free`. The removal is committed on core `main` (2026-09-09 sync) but **no released Agent version includes it yet** — and `main` reports the same `0.21.1` version string as the stale tag, so a version number alone cannot tell you whether the fix is in. Until a release notes the 2026-09-09 catalog sync, the **verified workaround is the config allowlist** (below); selecting the dead entry is harmless to other models (it errors on send, nothing else).
|
||||
- **Failing model present in the `curl` output** → the relay lists it but your tier/region cannot serve it; the picker is behaving correctly. Pin the models you actually use with an explicit allowlist in `config.yaml`, which takes precedence over both the live catalog and the fallback:
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
opencode-go:
|
||||
models:
|
||||
- kimi-k3
|
||||
- glm-5.3
|
||||
```
|
||||
|
||||
A lighter per-provider exclude capability is tracked in #7507.
|
||||
- **Missing new model, Agent ≥ v0.20.5** → the live catalog is the source; refresh or check the endpoint with the `curl` above (cold rebuilds are also bounded by a 4-second foreground budget — the first picker open after a restart can serve the last-known list while the live rebuild finishes in the background, so re-open the picker once before concluding it's stale).
|
||||
- **Missing new model, Agent older than v0.20.5** → the static fallback is serving by design; upgrade the Agent to ≥ v0.20.5 so the picker reads the live catalog.
|
||||
|
||||
**When to file a bug.** File a WebUI bug if a model fails on send *and* appears in the `curl` output for your key (a routing problem), or if a model is missing with Agent ≥ v0.20.5 and the live catalog reachable (fallback used when it should not be).
|
||||
|
||||
---
|
||||
|
||||
## Other troubleshooting
|
||||
|
||||
This document grows over time. If a recurring failure mode isn't covered here yet, add it via PR. The format for each entry: **Symptom → Why → Diagnostic commands → Fix → When to file a bug**.
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
"""#5311 follow-up: OpenCode Go uses the live Hermes CLI catalog again.
|
||||
|
||||
Background: #5611 made WebUI skip the live ``/v1/models`` probe for OpenCode Go
|
||||
because the probe returned public-catalog models not enabled on the Go tier.
|
||||
That premise no longer holds: Hermes core v0.20.5 (commit fcbd1076, tag
|
||||
v2026.8.19) ships a Go-specific provider profile that probes
|
||||
``https://opencode.ai/zen/go/v1/models`` directly and merges results
|
||||
live-first with its curated Go list (hermes_cli/models.py, #49129). With the
|
||||
correct Go-tier endpoint in core, WebUI should delegate like every other
|
||||
provider (#1240): live catalog first, the curated static ``_PROVIDER_MODELS``
|
||||
list as offline fallback only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
|
||||
import api.config as config
|
||||
import api.profiles as profiles
|
||||
|
||||
|
||||
_PROVIDER_ENV_VARS = (
|
||||
"OPENCODE_ZEN_API_KEY",
|
||||
"OPENCODE_GO_API_KEY",
|
||||
"OPENCODE_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
def _scrub_provider_env(monkeypatch):
|
||||
for name in _PROVIDER_ENV_VARS:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
|
||||
|
||||
def _install_fake_hermes_cli(
|
||||
monkeypatch,
|
||||
*,
|
||||
provider_id: str,
|
||||
live_ids,
|
||||
core_version: str = "0.20.5",
|
||||
raise_on_lookup: bool = False,
|
||||
):
|
||||
"""Install a hermes_cli stub that reports one authenticated provider."""
|
||||
fake_pkg = types.ModuleType("hermes_cli")
|
||||
fake_pkg.__path__ = []
|
||||
fake_pkg.__version__ = core_version
|
||||
|
||||
fake_models = types.ModuleType("hermes_cli.models")
|
||||
fake_models.list_available_providers = lambda: [
|
||||
{"id": provider_id, "authenticated": True}
|
||||
]
|
||||
|
||||
calls: list[str] = []
|
||||
|
||||
def provider_model_ids(pid):
|
||||
calls.append(pid)
|
||||
if raise_on_lookup:
|
||||
raise RuntimeError("simulated provider_model_ids failure")
|
||||
return list(live_ids) if pid == provider_id else []
|
||||
|
||||
fake_models.provider_model_ids = provider_model_ids
|
||||
|
||||
fake_auth = types.ModuleType("hermes_cli.auth")
|
||||
|
||||
def get_auth_status(pid):
|
||||
if pid == provider_id:
|
||||
return {"logged_in": True, "key_source": ""}
|
||||
return {"logged_in": False, "key_source": ""}
|
||||
|
||||
fake_auth.get_auth_status = get_auth_status
|
||||
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", fake_pkg)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli.models", fake_models)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli.auth", fake_auth)
|
||||
monkeypatch.delitem(sys.modules, "agent.credential_pool", raising=False)
|
||||
monkeypatch.delitem(sys.modules, "agent", raising=False)
|
||||
config.invalidate_models_cache()
|
||||
return calls
|
||||
|
||||
|
||||
def _configure(monkeypatch, tmp_path, *, provider: str, providers: dict | None = None):
|
||||
hermes_home = tmp_path / "hermes-home"
|
||||
hermes_home.mkdir()
|
||||
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: hermes_home)
|
||||
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": provider, "default": ""},
|
||||
"providers": providers if providers is not None else {},
|
||||
"fallback_providers": [],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(config, "_cfg_mtime", 0.0)
|
||||
monkeypatch.setattr(config, "_cfg_path", config._get_config_path(), raising=False)
|
||||
config.invalidate_models_cache()
|
||||
|
||||
|
||||
def _provider_group(result: dict, provider_id: str) -> dict:
|
||||
return next(g for g in result["groups"] if g.get("provider_id") == provider_id)
|
||||
|
||||
|
||||
def _ids(group: dict) -> list[str]:
|
||||
return [m.get("id") for m in group.get("models", [])]
|
||||
|
||||
|
||||
def test_opencode_go_probes_live_catalog(monkeypatch, tmp_path):
|
||||
"""The Go picker group must come from the live Hermes CLI catalog.
|
||||
|
||||
``sentinel-go-model`` is intentionally absent from every static list. If
|
||||
WebUI special-cases OpenCode Go away from the live probe again (the #5611
|
||||
stopgap), the sentinel disappears and this test fails.
|
||||
"""
|
||||
_scrub_provider_env(monkeypatch)
|
||||
calls = _install_fake_hermes_cli(
|
||||
monkeypatch,
|
||||
provider_id="opencode-go",
|
||||
live_ids=["sentinel-go-model", "kimi-k3"],
|
||||
)
|
||||
_configure(monkeypatch, tmp_path, provider="opencode-go")
|
||||
|
||||
result = config.get_available_models()
|
||||
group = _provider_group(result, "opencode-go")
|
||||
|
||||
assert calls == ["opencode-go"]
|
||||
ids = _ids(group)
|
||||
assert "sentinel-go-model" in ids
|
||||
assert "kimi-k3" in ids
|
||||
assert all(m.get("label") for m in group["models"])
|
||||
|
||||
|
||||
def test_opencode_go_old_core_ignores_nonempty_generic_catalog(monkeypatch, tmp_path):
|
||||
"""Pre-v0.20.5 cores must not leak their generic public catalog into Go."""
|
||||
_scrub_provider_env(monkeypatch)
|
||||
calls = _install_fake_hermes_cli(
|
||||
monkeypatch,
|
||||
provider_id="opencode-go",
|
||||
live_ids=["go-ineligible-old-core-sentinel"],
|
||||
core_version="0.20.4",
|
||||
)
|
||||
_configure(monkeypatch, tmp_path, provider="opencode-go")
|
||||
|
||||
result = config.get_available_models()
|
||||
group = _provider_group(result, "opencode-go")
|
||||
|
||||
assert calls == []
|
||||
ids = _ids(group)
|
||||
assert "go-ineligible-old-core-sentinel" not in ids
|
||||
assert "kimi-k3" in ids
|
||||
|
||||
|
||||
def test_opencode_go_static_fallback_when_probe_fails(monkeypatch, tmp_path):
|
||||
"""Offline / CLI failure must fall back to the curated static list."""
|
||||
_scrub_provider_env(monkeypatch)
|
||||
calls = _install_fake_hermes_cli(
|
||||
monkeypatch,
|
||||
provider_id="opencode-go",
|
||||
live_ids=[],
|
||||
raise_on_lookup=True,
|
||||
)
|
||||
_configure(monkeypatch, tmp_path, provider="opencode-go")
|
||||
|
||||
result = config.get_available_models()
|
||||
group = _provider_group(result, "opencode-go")
|
||||
|
||||
assert calls == ["opencode-go"]
|
||||
ids = _ids(group)
|
||||
assert "sentinel-go-model" not in ids
|
||||
assert "glm-5" in ids
|
||||
assert "kimi-k2.5" in ids
|
||||
|
||||
|
||||
def test_opencode_go_config_allowlist_still_wins(monkeypatch, tmp_path):
|
||||
"""An explicit ``providers.opencode-go.models`` allowlist remains the
|
||||
local source of truth (#644) and is not bypassed by the live probe."""
|
||||
_scrub_provider_env(monkeypatch)
|
||||
calls = _install_fake_hermes_cli(
|
||||
monkeypatch,
|
||||
provider_id="opencode-go",
|
||||
live_ids=["sentinel-go-model"],
|
||||
)
|
||||
_configure(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
provider="opencode-go",
|
||||
providers={"opencode-go": {"models": ["glm-5"]}},
|
||||
)
|
||||
|
||||
result = config.get_available_models()
|
||||
group = _provider_group(result, "opencode-go")
|
||||
|
||||
assert calls == [] # probe never consulted
|
||||
assert _ids(group) == ["glm-5"]
|
||||
|
||||
|
||||
# ── Static fallback list contract ─────────────────────────────────────
|
||||
# The remaining tests pin the offline fallback list itself to Hermes core's
|
||||
# current curated contract (hermes_cli/models_catalog_static.py, core main
|
||||
# 2026-09-10; the list originated in v0.20.5, commit fcbd1076). Core owns
|
||||
# the sync duty against the live
|
||||
# https://opencode.ai/zen/go/v1/models endpoint and
|
||||
# https://opencode.ai/docs/go/, and WebUI only mirrors. This catches drift
|
||||
# in either direction: ids core added and ids core dropped (e.g.
|
||||
# ox-alpha-free, delisted from the Go relay on 2026-09-09).
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG_PATH = ROOT / "api" / "config.py"
|
||||
CONFIG = CONFIG_PATH.read_text(encoding="utf-8")
|
||||
|
||||
EXPECTED_OPENCODE_GO_MODEL_IDS = [
|
||||
"kimi-k3",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
"gpt-5.6-luna",
|
||||
"grok-4.5",
|
||||
"glm-5.3",
|
||||
"glm-5.3-flash",
|
||||
"glm-5.2",
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"mimo-v2.5-pro",
|
||||
"mimo-v2.5",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
"minimax-m3",
|
||||
"minimax-m2.7",
|
||||
"minimax-m2.5",
|
||||
"deepseek-v4-pro",
|
||||
"deepseek-v4-flash",
|
||||
"qwen3.8-max",
|
||||
"qwen3.7-max",
|
||||
"qwen3.7-plus",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.5-plus",
|
||||
"hy3",
|
||||
"hy3-preview",
|
||||
"muse-spark-1.2-contributor",
|
||||
"muse-spark-1.3-contributor",
|
||||
]
|
||||
|
||||
|
||||
def _opencode_go_static_models():
|
||||
tree = ast.parse(CONFIG, filename=str(CONFIG_PATH))
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
if not any(isinstance(target, ast.Name) and target.id == "_PROVIDER_MODELS" for target in node.targets):
|
||||
continue
|
||||
provider_models = ast.literal_eval(node.value)
|
||||
return provider_models["opencode-go"]
|
||||
raise AssertionError("_PROVIDER_MODELS assignment not found")
|
||||
|
||||
|
||||
def test_opencode_go_static_models_match_core_catalog_snapshot():
|
||||
models = _opencode_go_static_models()
|
||||
assert [model["id"] for model in models] == EXPECTED_OPENCODE_GO_MODEL_IDS
|
||||
|
||||
|
||||
def test_opencode_go_static_models_match_installed_core_curated_list():
|
||||
"""Exercise the actual Agent contract for the supported release pair.
|
||||
|
||||
The snapshot mirrors core main's curated catalog. A released (installed)
|
||||
core can lag that sync by a few days — e.g. v2026.9.7 still lists
|
||||
``ox-alpha-free``, which core removed on 2026-09-09 after the Go relay
|
||||
delisted it. Equality binds only once the installed core carries the
|
||||
mirrored sync; the marker for "lags the sync" is the delisted id still
|
||||
being present. Anything else that differs is real drift and must fail.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
hermes_cli = pytest.importorskip(
|
||||
"hermes_cli", reason="hermes-agent is not a WebUI test dependency"
|
||||
)
|
||||
if not config._hermes_cli_supports_opencode_go_live_catalog():
|
||||
pytest.skip(f"installed hermes-agent {hermes_cli.__version__} predates v0.20.5")
|
||||
models_mod = pytest.importorskip("hermes_cli.models")
|
||||
installed = list(models_mod._PROVIDER_MODELS["opencode-go"])
|
||||
if "ox-alpha-free" in installed:
|
||||
pytest.skip(
|
||||
"installed hermes-agent catalog predates core's 2026-09-09 sync "
|
||||
"(still lists relay-delisted 'ox-alpha-free'); equality binds "
|
||||
"once a release carrying that sync is installed"
|
||||
)
|
||||
assert [model["id"] for model in _opencode_go_static_models()] == installed
|
||||
|
||||
|
||||
def test_opencode_go_recent_additions_have_human_labels():
|
||||
labels = {model["id"]: model["label"] for model in _opencode_go_static_models()}
|
||||
assert labels["gpt-5.6-luna"] == "GPT 5.6 Luna"
|
||||
assert labels["kimi-k3"] == "Kimi K3"
|
||||
assert labels["glm-5.3"] == "GLM-5.3"
|
||||
assert labels["grok-4.5"] == "Grok 4.5"
|
||||
assert labels["qwen3.8-max"] == "Qwen3.8 Max"
|
||||
assert labels["muse-spark-1.2-contributor"] == "Muse Spark 1.2 Contributor"
|
||||
assert labels["muse-spark-1.3-contributor"] == "Muse Spark 1.3 Contributor"
|
||||
assert labels["glm-5.3-flash"] == "GLM-5.3 Flash"
|
||||
@@ -1,72 +0,0 @@
|
||||
"""#5311 / #5611: OpenCode Go's generic live probe used to return public-catalog
|
||||
models not enabled on the Go tier, so the picker intentionally falls back to a curated
|
||||
static _PROVIDER_MODELS list. Keep that curated list in sync with the public Go docs
|
||||
and documented Go models endpoint while excluding preview/free-only Zen models.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG_PATH = ROOT / "api" / "config.py"
|
||||
CONFIG = CONFIG_PATH.read_text(encoding="utf-8")
|
||||
|
||||
EXPECTED_OPENCODE_GO_MODEL_IDS = [
|
||||
"minimax-m3",
|
||||
"minimax-m2.7",
|
||||
"minimax-m2.5",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
"glm-5.2",
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"deepseek-v4-pro",
|
||||
"deepseek-v4-flash",
|
||||
"qwen3.7-max",
|
||||
"qwen3.7-plus",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.5-plus",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
"mimo-v2.5-pro",
|
||||
"mimo-v2.5",
|
||||
]
|
||||
|
||||
|
||||
def _opencode_go_static_models():
|
||||
tree = ast.parse(CONFIG, filename=str(CONFIG_PATH))
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
if not any(isinstance(target, ast.Name) and target.id == "_PROVIDER_MODELS" for target in node.targets):
|
||||
continue
|
||||
provider_models = ast.literal_eval(node.value)
|
||||
return provider_models["opencode-go"]
|
||||
raise AssertionError("_PROVIDER_MODELS assignment not found")
|
||||
|
||||
|
||||
def test_opencode_go_skips_live_models_probe():
|
||||
# The provider-loop must special-case opencode-go to skip the old generic
|
||||
# live probe and fall through to this curated static list.
|
||||
body = CONFIG[CONFIG.index("def get_available_models"):]
|
||||
body = body[: body.index("\ndef ", 1)]
|
||||
assert 'elif pid == "opencode-go":' in body
|
||||
idx = body.index('elif pid == "opencode-go":')
|
||||
branch = body[idx: idx + 400]
|
||||
assert "_models_from_live_provider_ids" not in branch.split("else:")[0]
|
||||
|
||||
|
||||
def test_opencode_go_static_models_match_documented_endpoint_snapshot():
|
||||
# Snapshot from https://opencode.ai/zen/go/v1/models on 2026-07-07.
|
||||
# This catches stale-list regressions such as missing GLM-5.2 / MiniMax M3.
|
||||
models = _opencode_go_static_models()
|
||||
assert [model["id"] for model in models] == EXPECTED_OPENCODE_GO_MODEL_IDS
|
||||
|
||||
|
||||
def test_opencode_go_recent_additions_have_human_labels():
|
||||
labels = {model["id"]: model["label"] for model in _opencode_go_static_models()}
|
||||
assert labels["glm-5.2"] == "GLM-5.2"
|
||||
assert labels["minimax-m3"] == "MiniMax M3"
|
||||
assert labels["kimi-k2.7-code"] == "Kimi K2.7 Code"
|
||||
assert labels["qwen3.7-max"] == "Qwen3.7 Max"
|
||||
Reference in new issue
Block a user