diff --git a/api/config.py b/api/config.py index c77e65fc..b279e0e2 100644 --- a/api/config.py +++ b/api/config.py @@ -1904,28 +1904,42 @@ _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.py, synced there against the live endpoint and + # https://opencode.ai/docs/go/ on 2026-08-20/21). `ox-alpha-free` is the + # Go-subscription twin of Zen's keyless Ox Alpha — same id shape, but it + # requires the Go key. "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.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": "ox-alpha-free", "label": "Ox Alpha (Go)"}, ], # 'gemini' is the hermes_cli provider ID for Google AI Studio # Model IDs are bare — sent directly to: diff --git a/tests/test_issue5311_opencode_go_live_models.py b/tests/test_issue5311_opencode_go_live_models.py index fccdb7f6..da925c94 100644 --- a/tests/test_issue5311_opencode_go_live_models.py +++ b/tests/test_issue5311_opencode_go_live_models.py @@ -165,3 +165,80 @@ def test_opencode_go_config_allowlist_still_wins(monkeypatch, tmp_path): assert calls == [] # probe never consulted assert _ids(group) == ["glm-5"] + + +# ── Static fallback list contract ───────────────────────────────────── +# The remaining tests pin the offline fallback list itself. It is an exact +# mirror of Hermes core's curated ``opencode-go`` list +# (hermes_cli/models.py) — 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 (kimi-k3, gpt-5.6-luna, glm-5.3, +# grok-4.5, qwen3.8-max, hy3, hy3-preview, muse-spark-1.2-contributor, +# ox-alpha-free) and ids core dropped. + +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.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", + "ox-alpha-free", +] + + +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_curated_list(): + 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["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["ox-alpha-free"] == "Ox Alpha (Go)"