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.
144 lines
6.1 KiB
Python
144 lines
6.1 KiB
Python
"""OpenCode Zen free-tier keyless routing (x-preview-f-free / "Ox Alpha").
|
|
|
|
The Zen relay serves ``*-free`` models ANONYMOUSLY: a request with no
|
|
Authorization header succeeds, while any non-empty bearer the relay doesn't
|
|
recognize — including our historical "no-key-required" placeholder and valid
|
|
OpenCode GO subscription keys — is rejected with 401 "Invalid API key".
|
|
The Go relay doesn't serve the free tier at all ("Model x is not supported").
|
|
|
|
These tests pin the keyless routing added for the community report where the
|
|
free Ox Alpha model failed under an OpenCode subscription:
|
|
|
|
1. ``opencode_zen_free_runtime`` pins free slugs to the Zen relay with the
|
|
keyless placeholder + empty-Authorization headers, for BOTH family
|
|
providers (Go selections heal to Zen).
|
|
2. ``resolve_runtime_provider`` routes free slugs keylessly with no
|
|
OPENCODE_* credential present, and still fails closed for paid models.
|
|
3. The keyless placeholder never reaches the wire: client default_headers
|
|
carry ``Authorization: ""`` overriding the SDK bearer.
|
|
"""
|
|
|
|
import os
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.models import (
|
|
OPENCODE_ZEN_FREE_KEYLESS_PLACEHOLDER,
|
|
opencode_zen_free_headers,
|
|
opencode_zen_free_runtime,
|
|
)
|
|
|
|
|
|
class TestFreeRuntime:
|
|
def test_zen_provider_free_model(self):
|
|
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
|
|
assert rt["api_mode"] == "chat_completions"
|
|
assert rt["default_headers"]["Authorization"] == ""
|
|
|
|
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", "nemotron-3.5-lightning-free")
|
|
assert rt is not None
|
|
assert rt["base_url"] == "https://opencode.ai/zen/v1"
|
|
|
|
def test_go_ox_alpha_free_does_not_heal_to_zen(self):
|
|
"""ox-alpha-free is a KEYED Go-subscription model despite its -free
|
|
suffix (Zen doesn't serve it; Go 401s anonymous). Membership in the
|
|
verified keyless catalog — not the suffix — gates the heal."""
|
|
assert opencode_zen_free_runtime("opencode-go", "ox-alpha-free") is None
|
|
assert opencode_zen_free_runtime("opencode-zen", "ox-alpha-free") is None
|
|
|
|
def test_paid_model_returns_none(self):
|
|
assert opencode_zen_free_runtime("opencode-zen", "claude-sonnet-5") is None
|
|
|
|
def test_non_opencode_provider_returns_none(self):
|
|
assert opencode_zen_free_runtime("openrouter", "x-preview-f-free") is None
|
|
assert opencode_zen_free_runtime(None, "x-preview-f-free") is None
|
|
|
|
def test_headers_override_sdk_bearer(self):
|
|
headers = opencode_zen_free_headers()
|
|
assert headers["Authorization"] == ""
|
|
assert headers["X-Title"] == "Hermes Agent"
|
|
|
|
|
|
class TestRuntimeProviderKeylessRouting:
|
|
@pytest.fixture(autouse=True)
|
|
def _no_opencode_creds(self, monkeypatch):
|
|
for var in ("OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY"):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
def _resolve(self, provider, model):
|
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
|
|
|
with mock.patch(
|
|
"hermes_cli.runtime_provider._get_model_config",
|
|
return_value={"provider": provider, "model": model, "default": model},
|
|
):
|
|
return resolve_runtime_provider(requested=provider, target_model=model)
|
|
|
|
def test_zen_free_model_resolves_keyless(self):
|
|
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", "nemotron-3.5-lightning-free")
|
|
assert rt["api_key"] == OPENCODE_ZEN_FREE_KEYLESS_PLACEHOLDER
|
|
assert rt["base_url"] == "https://opencode.ai/zen/v1"
|
|
|
|
def test_paid_model_still_fails_closed_without_key(self):
|
|
from hermes_cli.auth import AuthError
|
|
|
|
with pytest.raises(AuthError):
|
|
self._resolve("opencode-zen", "claude-sonnet-5")
|
|
|
|
|
|
class TestKeylessProviderAlwaysAuthenticated:
|
|
"""opencode-free counts as authenticated everywhere, with zero keys.
|
|
|
|
The provider is keyless: there is no credential to configure, so every
|
|
surface that gates on auth (get_auth_status, provider:model listing,
|
|
the /model picker source, the desktop explicit-only filter) must treat
|
|
every install as logged in.
|
|
"""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_creds(self, monkeypatch):
|
|
for var in ("OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY"):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
def test_auth_status_logged_in(self):
|
|
from hermes_cli.auth import get_auth_status
|
|
|
|
st = get_auth_status("opencode-free")
|
|
assert st["logged_in"] is True
|
|
assert st["configured"] is True
|
|
assert st["key_source"] == "keyless"
|
|
|
|
def test_list_available_providers_authenticated(self):
|
|
from hermes_cli.models import list_available_providers
|
|
|
|
rows = {r["id"]: r["authenticated"] for r in list_available_providers()}
|
|
assert rows.get("opencode-free") is True
|
|
|
|
def test_picker_source_includes_provider_with_models(self):
|
|
import model_tools # noqa: F401 — plugin discovery
|
|
from hermes_cli.model_switch import list_authenticated_providers
|
|
|
|
provs = list_authenticated_providers(for_picker=True)
|
|
free = [p for p in provs if p["slug"] == "opencode-free"]
|
|
assert free, "opencode-free must appear in the picker with zero keys"
|
|
assert free[0]["models"], "picker row must carry the curated models"
|
|
|
|
def test_explicit_only_filter_keeps_keyless(self):
|
|
from hermes_cli.inventory import _provider_is_keyless
|
|
|
|
assert _provider_is_keyless("opencode-free") is True
|
|
assert _provider_is_keyless("opencode-zen") is False
|