fix(models): delist hy3-free and laguna-s-2.1-free — OpenCode relay dropped them (anon 401)

The OpenCode Zen relay no longer serves hy3-free (since ~2026-08-31) or
laguna-s-2.1-free (new, verified 2026-09-09): both are gone from the live
GET /zen/v1/models catalog and anonymous chat completions return
401 {"type":"ModelError","message":"Model <id> is not supported"}
(2 probes >=60s apart, x-opencode-session header present).

- hermes_cli/models_catalog_static.py: remove both slugs from the
  opencode-free offline floor and the opencode-zen discovery floor;
  document the delist dates in the catalog comment.
- plugins/model-providers/opencode-free: default_aux_model moves from the
  dead laguna-s-2.1-free to nemotron-3.5-lightning-free (fastest surviving
  anonymous model).
- tests: swap fixtures off the dead slugs; extend the floor-exclusion
  invariant to cover both.

The live revalidation path already hides them when the relay is reachable;
this fixes the OFFLINE floor and the aux default, which would otherwise
offer/route to models that 401.
This commit is contained in:
Teknium
2026-09-09 02:44:40 -07:00
parent 230ca004a7
commit cc81e436ce
6 changed files with 18 additions and 14 deletions

View File

@@ -229,15 +229,17 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
"grok-4.6", "grok-4.5", "grok-build-0.1", "muse-spark-1.2", "minimax-m3", "minimax-m2.7", "minimax-m2.5",
"glm-5.3", "glm-5.3-flash", "glm-5.2", "glm-5.1", "glm-5", "kimi-k2.7-code", "deepseek-v4-pro",
"deepseek-v4-flash", "deepseek-v4-flash-free", "qwen3.6-plus", "qwen3.5-plus", "big-pickle", "mimo-v2.5-free",
"hy3-free", "laguna-s-2.1-free", "nemotron-3-ultra-free", "nemotron-3.5-lightning-free",
"nemotron-3-ultra-free", "nemotron-3.5-lightning-free",
"muse-spark-1.2-contributor-free", "muse-spark-1.3-contributor-free",
],
# OpenCode keyless free tier — OFFLINE FLOOR only. provider_model_ids("opencode-free")
# revalidates live against GET /zen/v1/models and filters to the anonymous tier, so this list
# may lag the relay (intentional). Known-delisted models are REMOVED (the offline fallback must
# not offer a model that 401s, e.g. x-preview-f-free).
# not offer a model that 401s; x-preview-f-free delisted 2026-08-26, hy3-free and
# laguna-s-2.1-free delisted 2026-09-09 — both dropped from live /zen/v1/models and 401
# "Model … is not supported" anonymously).
"opencode-free": [
"deepseek-v4-flash-free", "hy3-free", "mimo-v2.5-free", "laguna-s-2.1-free",
"deepseek-v4-flash-free", "mimo-v2.5-free",
"nemotron-3-ultra-free", "nemotron-3.5-lightning-free", "muse-spark-1.2-contributor-free",
"muse-spark-1.3-contributor-free",
],

View File

@@ -40,9 +40,10 @@ opencode_free = OpenCodeFreeProfile(
"X-Title": "Hermes Agent",
"User-Agent": f"HermesAgent/{_HERMES_VERSION}",
},
# laguna is the fastest non-UA-gated free model; big-pickle 429s every
# laguna-s-2.1-free was delisted by the relay 2026-09-09 (anon 401); the fastest
# surviving free model is the lightning-tier Nemotron. big-pickle 429s every
# client except the opencode CLI's own User-Agent.
default_aux_model="laguna-s-2.1-free",
default_aux_model="nemotron-3.5-lightning-free",
)
register_provider(opencode_free)

View File

@@ -35,7 +35,7 @@ def _agent(provider, model, base_url, api_mode=None):
("opencode-go", "glm-5", "https://opencode.ai/zen/go/v1", None), # chat_completions
("opencode-go", "gpt-5.6-luna", "https://opencode.ai/zen/go/v1", None), # codex_responses
("opencode-go", "minimax-m2.7", "https://opencode.ai/zen/go/v1", "anthropic_messages"),
("opencode-free", "laguna-s-2.1-free", "https://opencode.ai/zen/v1", None),
("opencode-free", "nemotron-3.5-lightning-free", "https://opencode.ai/zen/v1", None),
("custom", "glm-5", "https://opencode.ai/zen/go/v1", None), # URL-only detection
],
)

View File

@@ -285,7 +285,7 @@ class TestCopilotNormalization:
assert opencode_model_api_mode("opencode-zen", "x-preview-f-free") == "chat_completions"
assert opencode_model_api_mode("opencode-zen", "opencode-zen/x-preview-f-free") == "chat_completions"
# Other free-tier Zen models are chat/completions too.
assert opencode_model_api_mode("opencode-zen", "hy3-free") == "chat_completions"
assert opencode_model_api_mode("opencode-zen", "nemotron-3.5-lightning-free") == "chat_completions"
assert opencode_model_api_mode("opencode-zen", "nemotron-3.5-lightning-free") == "chat_completions"
# Hy3 on Go is chat/completions (Go endpoint table).
assert opencode_model_api_mode("opencode-go", "hy3") == "chat_completions"

View File

@@ -32,12 +32,11 @@ from hermes_cli.models import (
_STATIC_FLOOR = list(_PROVIDER_MODELS["opencode-free"])
# The live relay's current free tier. x-preview-f-free was DELISTED 2026-08-26;
# deepseek-v4-flash-free + mimo-v2.5-free are back on the live list.
# hy3-free and laguna-s-2.1-free were DELISTED 2026-09-09 (gone from live
# /models, anon 401 "Model … is not supported").
_LIVE_FREE_MODELS = [
"deepseek-v4-flash-free",
"hy3-free",
"mimo-v2.5-free",
"laguna-s-2.1-free",
"nemotron-3-ultra-free",
"nemotron-3.5-lightning-free",
"muse-spark-1.2-contributor-free",
@@ -220,3 +219,5 @@ class TestOpencodeFreeFollowUps:
def test_static_floor_excludes_delisted_model(self):
"""The offline floor must not offer a model known to 401 (#95914)."""
assert "x-preview-f-free" not in _PROVIDER_MODELS["opencode-free"]
assert "hy3-free" not in _PROVIDER_MODELS["opencode-free"]
assert "laguna-s-2.1-free" not in _PROVIDER_MODELS["opencode-free"]

View File

@@ -32,7 +32,7 @@ from hermes_cli.models import (
class TestFreeRuntime:
def test_zen_provider_free_model(self):
rt = opencode_zen_free_runtime("opencode-zen", "hy3-free")
rt = opencode_zen_free_runtime("opencode-zen", "nemotron-3.5-lightning-free")
assert rt is not None
assert rt["base_url"] == "https://opencode.ai/zen/v1"
assert rt["api_key"] == OPENCODE_ZEN_FREE_KEYLESS_PLACEHOLDER
@@ -42,7 +42,7 @@ class TestFreeRuntime:
def test_go_provider_heals_to_zen(self):
# Free slugs only exist on the Zen relay; a Go selection must be
# routed to Zen (the Go relay rejects the model outright).
rt = opencode_zen_free_runtime("opencode-go", "hy3-free")
rt = opencode_zen_free_runtime("opencode-go", "nemotron-3.5-lightning-free")
assert rt is not None
assert rt["base_url"] == "https://opencode.ai/zen/v1"
@@ -82,13 +82,13 @@ class TestRuntimeProviderKeylessRouting:
return resolve_runtime_provider(requested=provider, target_model=model)
def test_zen_free_model_resolves_keyless(self):
rt = self._resolve("opencode-zen", "hy3-free")
rt = self._resolve("opencode-zen", "nemotron-3.5-lightning-free")
assert rt["api_key"] == OPENCODE_ZEN_FREE_KEYLESS_PLACEHOLDER
assert rt["base_url"] == "https://opencode.ai/zen/v1"
assert rt["api_mode"] == "chat_completions"
def test_go_free_model_resolves_keyless_on_zen(self):
rt = self._resolve("opencode-go", "hy3-free")
rt = self._resolve("opencode-go", "nemotron-3.5-lightning-free")
assert rt["api_key"] == OPENCODE_ZEN_FREE_KEYLESS_PLACEHOLDER
assert rt["base_url"] == "https://opencode.ai/zen/v1"