feat(provider): add OpenCode Zen and OpenCode Go providers

Add support for OpenCode Zen (pay-as-you-go, 35+ curated models) and
OpenCode Go ($10/month subscription, open models) as first-class providers.

Both are OpenAI-compatible endpoints resolved via the generic api_key
provider flow — no custom adapter needed.

Files changed:
- hermes_cli/auth.py — ProviderConfig entries + aliases
- hermes_cli/config.py — OPENCODE_ZEN/GO API key env vars
- hermes_cli/models.py — model catalogs, labels, aliases, provider order
- hermes_cli/main.py — provider labels, menu entries, model flow dispatch
- hermes_cli/setup.py — setup wizard branches (idx 10, 11)
- agent/model_metadata.py — context lengths for all OpenCode models
- agent/auxiliary_client.py — default aux models
- .env.example — documentation

Co-authored-by: DevAgarwal2 <DevAgarwal2@users.noreply.github.com>
This commit is contained in:
Teknium 2026-03-17 02:02:43 -07:00 committed by GitHub
parent 4cb6735541
commit 40e2f8d9f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 227 additions and 2 deletions

View file

@ -45,6 +45,22 @@ MINIMAX_API_KEY=
MINIMAX_CN_API_KEY= MINIMAX_CN_API_KEY=
# MINIMAX_CN_BASE_URL=https://api.minimaxi.com/v1 # Override default base URL # MINIMAX_CN_BASE_URL=https://api.minimaxi.com/v1 # Override default base URL
# =============================================================================
# LLM PROVIDER (OpenCode Zen)
# =============================================================================
# OpenCode Zen provides curated, tested models (GPT, Claude, Gemini, MiniMax, GLM, Kimi)
# Pay-as-you-go pricing. Get your key at: https://opencode.ai/auth
OPENCODE_ZEN_API_KEY=
# OPENCODE_ZEN_BASE_URL=https://opencode.ai/zen/v1 # Override default base URL
# =============================================================================
# LLM PROVIDER (OpenCode Go)
# =============================================================================
# OpenCode Go provides access to open models (GLM-5, Kimi K2.5, MiniMax M2.5)
# $10/month subscription. Get your key at: https://opencode.ai/auth
OPENCODE_GO_API_KEY=
# OPENCODE_GO_BASE_URL=https://opencode.ai/zen/go/v1 # Override default base URL
# ============================================================================= # =============================================================================
# TOOL API KEYS # TOOL API KEYS
# ============================================================================= # =============================================================================

View file

@ -58,6 +58,8 @@ _API_KEY_PROVIDER_AUX_MODELS: Dict[str, str] = {
"minimax-cn": "MiniMax-M2.5-highspeed", "minimax-cn": "MiniMax-M2.5-highspeed",
"anthropic": "claude-haiku-4-5-20251001", "anthropic": "claude-haiku-4-5-20251001",
"ai-gateway": "google/gemini-3-flash", "ai-gateway": "google/gemini-3-flash",
"opencode-zen": "gemini-3-flash",
"opencode-go": "glm-5",
} }
# OpenRouter app attribution headers # OpenRouter app attribution headers

View file

@ -80,6 +80,42 @@ DEFAULT_CONTEXT_LENGTHS = {
"MiniMax-M2.5": 204800, "MiniMax-M2.5": 204800,
"MiniMax-M2.5-highspeed": 204800, "MiniMax-M2.5-highspeed": 204800,
"MiniMax-M2.1": 204800, "MiniMax-M2.1": 204800,
# OpenCode Zen models
"gpt-5.4-pro": 128000,
"gpt-5.4": 128000,
"gpt-5.3-codex": 128000,
"gpt-5.3-codex-spark": 128000,
"gpt-5.2": 128000,
"gpt-5.2-codex": 128000,
"gpt-5.1": 128000,
"gpt-5.1-codex": 128000,
"gpt-5.1-codex-max": 128000,
"gpt-5.1-codex-mini": 128000,
"gpt-5": 128000,
"gpt-5-codex": 128000,
"gpt-5-nano": 128000,
"claude-opus-4-6": 200000,
"claude-opus-4-5": 200000,
"claude-opus-4-1": 200000,
"claude-sonnet-4-6": 200000,
"claude-sonnet-4-5": 200000,
"claude-sonnet-4": 200000,
"claude-haiku-4-5": 200000,
"claude-3-5-haiku": 200000,
"gemini-3.1-pro": 1048576,
"gemini-3-pro": 1048576,
"gemini-3-flash": 1048576,
"minimax-m2.5": 204800,
"minimax-m2.5-free": 204800,
"minimax-m2.1": 204800,
"glm-5": 202752,
"glm-4.7": 202752,
"glm-4.6": 202752,
"kimi-k2.5": 262144,
"kimi-k2-thinking": 262144,
"kimi-k2": 262144,
"qwen3-coder": 32768,
"big-pickle": 128000,
} }

View file

@ -163,6 +163,22 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
api_key_env_vars=("AI_GATEWAY_API_KEY",), api_key_env_vars=("AI_GATEWAY_API_KEY",),
base_url_env_var="AI_GATEWAY_BASE_URL", base_url_env_var="AI_GATEWAY_BASE_URL",
), ),
"opencode-zen": ProviderConfig(
id="opencode-zen",
name="OpenCode Zen",
auth_type="api_key",
inference_base_url="https://opencode.ai/zen/v1",
api_key_env_vars=("OPENCODE_ZEN_API_KEY",),
base_url_env_var="OPENCODE_ZEN_BASE_URL",
),
"opencode-go": ProviderConfig(
id="opencode-go",
name="OpenCode Go",
auth_type="api_key",
inference_base_url="https://opencode.ai/zen/go/v1",
api_key_env_vars=("OPENCODE_GO_API_KEY",),
base_url_env_var="OPENCODE_GO_BASE_URL",
),
} }
@ -541,6 +557,8 @@ def resolve_provider(
"minimax-china": "minimax-cn", "minimax_cn": "minimax-cn", "minimax-china": "minimax-cn", "minimax_cn": "minimax-cn",
"claude": "anthropic", "claude-code": "anthropic", "claude": "anthropic", "claude-code": "anthropic",
"aigateway": "ai-gateway", "vercel": "ai-gateway", "vercel-ai-gateway": "ai-gateway", "aigateway": "ai-gateway", "vercel": "ai-gateway", "vercel-ai-gateway": "ai-gateway",
"opencode": "opencode-zen", "zen": "opencode-zen",
"go": "opencode-go", "opencode-go-sub": "opencode-go",
} }
normalized = _PROVIDER_ALIASES.get(normalized, normalized) normalized = _PROVIDER_ALIASES.get(normalized, normalized)

View file

@ -485,6 +485,38 @@ OPTIONAL_ENV_VARS = {
"password": False, "password": False,
"category": "provider", "category": "provider",
}, },
"OPENCODE_ZEN_API_KEY": {
"description": "OpenCode Zen API key (pay-as-you-go access to curated models)",
"prompt": "OpenCode Zen API key",
"url": "https://opencode.ai/auth",
"password": True,
"category": "provider",
"advanced": True,
},
"OPENCODE_ZEN_BASE_URL": {
"description": "OpenCode Zen base URL override",
"prompt": "OpenCode Zen base URL (leave empty for default)",
"url": None,
"password": False,
"category": "provider",
"advanced": True,
},
"OPENCODE_GO_API_KEY": {
"description": "OpenCode Go API key ($10/month subscription for open models)",
"prompt": "OpenCode Go API key",
"url": "https://opencode.ai/auth",
"password": True,
"category": "provider",
"advanced": True,
},
"OPENCODE_GO_BASE_URL": {
"description": "OpenCode Go base URL override",
"prompt": "OpenCode Go base URL (leave empty for default)",
"url": None,
"password": False,
"category": "provider",
"advanced": True,
},
# ── Tool API keys ── # ── Tool API keys ──
"FIRECRAWL_API_KEY": { "FIRECRAWL_API_KEY": {

View file

@ -768,6 +768,8 @@ def cmd_model(args):
"kimi-coding": "Kimi / Moonshot", "kimi-coding": "Kimi / Moonshot",
"minimax": "MiniMax", "minimax": "MiniMax",
"minimax-cn": "MiniMax (China)", "minimax-cn": "MiniMax (China)",
"opencode-zen": "OpenCode Zen",
"opencode-go": "OpenCode Go",
"ai-gateway": "AI Gateway", "ai-gateway": "AI Gateway",
"custom": "Custom endpoint", "custom": "Custom endpoint",
} }
@ -788,6 +790,8 @@ def cmd_model(args):
("kimi-coding", "Kimi / Moonshot (Moonshot AI direct API)"), ("kimi-coding", "Kimi / Moonshot (Moonshot AI direct API)"),
("minimax", "MiniMax (global direct API)"), ("minimax", "MiniMax (global direct API)"),
("minimax-cn", "MiniMax China (domestic direct API)"), ("minimax-cn", "MiniMax China (domestic direct API)"),
("opencode-zen", "OpenCode Zen (35+ curated models, pay-as-you-go)"),
("opencode-go", "OpenCode Go (open models, $10/month subscription)"),
("ai-gateway", "AI Gateway (Vercel — 200+ models, pay-per-use)"), ("ai-gateway", "AI Gateway (Vercel — 200+ models, pay-per-use)"),
] ]
@ -857,7 +861,7 @@ def cmd_model(args):
_model_flow_anthropic(config, current_model) _model_flow_anthropic(config, current_model)
elif selected_provider == "kimi-coding": elif selected_provider == "kimi-coding":
_model_flow_kimi(config, current_model) _model_flow_kimi(config, current_model)
elif selected_provider in ("zai", "minimax", "minimax-cn", "ai-gateway"): elif selected_provider in ("zai", "minimax", "minimax-cn", "opencode-zen", "opencode-go", "ai-gateway"):
_model_flow_api_key_provider(config, selected_provider, current_model) _model_flow_api_key_provider(config, selected_provider, current_model)

View file

@ -83,6 +83,48 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
"deepseek-chat", "deepseek-chat",
"deepseek-reasoner", "deepseek-reasoner",
], ],
"opencode-zen": [
"gpt-5.4-pro",
"gpt-5.4",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5",
"gpt-5-codex",
"gpt-5-nano",
"claude-opus-4-6",
"claude-opus-4-5",
"claude-opus-4-1",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-sonnet-4",
"claude-haiku-4-5",
"claude-3-5-haiku",
"gemini-3.1-pro",
"gemini-3-pro",
"gemini-3-flash",
"minimax-m2.5",
"minimax-m2.5-free",
"minimax-m2.1",
"glm-5",
"glm-4.7",
"glm-4.6",
"kimi-k2.5",
"kimi-k2-thinking",
"kimi-k2",
"qwen3-coder",
"big-pickle",
],
"opencode-go": [
"glm-5",
"kimi-k2.5",
"minimax-m2.5",
],
"ai-gateway": [ "ai-gateway": [
"anthropic/claude-opus-4.6", "anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-4.6",
@ -109,6 +151,8 @@ _PROVIDER_LABELS = {
"minimax-cn": "MiniMax (China)", "minimax-cn": "MiniMax (China)",
"anthropic": "Anthropic", "anthropic": "Anthropic",
"deepseek": "DeepSeek", "deepseek": "DeepSeek",
"opencode-zen": "OpenCode Zen",
"opencode-go": "OpenCode Go",
"ai-gateway": "AI Gateway", "ai-gateway": "AI Gateway",
"custom": "Custom endpoint", "custom": "Custom endpoint",
} }
@ -125,6 +169,10 @@ _PROVIDER_ALIASES = {
"claude": "anthropic", "claude": "anthropic",
"claude-code": "anthropic", "claude-code": "anthropic",
"deep-seek": "deepseek", "deep-seek": "deepseek",
"opencode": "opencode-zen",
"zen": "opencode-zen",
"go": "opencode-go",
"opencode-go-sub": "opencode-go",
"aigateway": "ai-gateway", "aigateway": "ai-gateway",
"vercel": "ai-gateway", "vercel": "ai-gateway",
"vercel-ai-gateway": "ai-gateway", "vercel-ai-gateway": "ai-gateway",
@ -162,6 +210,7 @@ def list_available_providers() -> list[dict[str, str]]:
_PROVIDER_ORDER = [ _PROVIDER_ORDER = [
"openrouter", "nous", "openai-codex", "openrouter", "nous", "openai-codex",
"zai", "kimi-coding", "minimax", "minimax-cn", "anthropic", "zai", "kimi-coding", "minimax", "minimax-cn", "anthropic",
"opencode-zen", "opencode-go",
"ai-gateway", "deepseek", "custom", "ai-gateway", "deepseek", "custom",
] ]
# Build reverse alias map # Build reverse alias map

View file

@ -726,6 +726,8 @@ def setup_model_provider(config: dict):
"MiniMax China (mainland China endpoint)", "MiniMax China (mainland China endpoint)",
"Anthropic (Claude models — API key or Claude Code subscription)", "Anthropic (Claude models — API key or Claude Code subscription)",
"AI Gateway (Vercel — 200+ models, pay-per-use)", "AI Gateway (Vercel — 200+ models, pay-per-use)",
"OpenCode Zen (35+ curated models, pay-as-you-go)",
"OpenCode Go (open models, $10/month subscription)",
] ]
if keep_label: if keep_label:
provider_choices.append(keep_label) provider_choices.append(keep_label)
@ -1266,7 +1268,73 @@ def setup_model_provider(config: dict):
_update_config_for_provider("ai-gateway", pconfig.inference_base_url, default_model="anthropic/claude-opus-4.6") _update_config_for_provider("ai-gateway", pconfig.inference_base_url, default_model="anthropic/claude-opus-4.6")
_set_model_provider(config, "ai-gateway", pconfig.inference_base_url) _set_model_provider(config, "ai-gateway", pconfig.inference_base_url)
# else: provider_idx == 10 (Keep current) — only shown when a provider already exists elif provider_idx == 10: # OpenCode Zen
selected_provider = "opencode-zen"
print()
print_header("OpenCode Zen API Key")
pconfig = PROVIDER_REGISTRY["opencode-zen"]
print_info(f"Provider: {pconfig.name}")
print_info(f"Base URL: {pconfig.inference_base_url}")
print_info("Get your API key at: https://opencode.ai/auth")
print()
existing_key = get_env_value("OPENCODE_ZEN_API_KEY")
if existing_key:
print_info(f"Current: {existing_key[:8]}... (configured)")
if prompt_yes_no("Update API key?", False):
api_key = prompt_text("OpenCode Zen API key", password=True)
if api_key:
save_env_value("OPENCODE_ZEN_API_KEY", api_key)
print_success("OpenCode Zen API key updated")
else:
api_key = prompt_text("OpenCode Zen API key", password=True)
if api_key:
save_env_value("OPENCODE_ZEN_API_KEY", api_key)
print_success("OpenCode Zen API key saved")
else:
print_warning("Skipped - agent won't work without an API key")
# Clear custom endpoint vars if switching
if existing_custom:
save_env_value("OPENAI_BASE_URL", "")
save_env_value("OPENAI_API_KEY", "")
_set_model_provider(config, "opencode-zen", pconfig.inference_base_url)
selected_base_url = pconfig.inference_base_url
elif provider_idx == 11: # OpenCode Go
selected_provider = "opencode-go"
print()
print_header("OpenCode Go API Key")
pconfig = PROVIDER_REGISTRY["opencode-go"]
print_info(f"Provider: {pconfig.name}")
print_info(f"Base URL: {pconfig.inference_base_url}")
print_info("Get your API key at: https://opencode.ai/auth")
print()
existing_key = get_env_value("OPENCODE_GO_API_KEY")
if existing_key:
print_info(f"Current: {existing_key[:8]}... (configured)")
if prompt_yes_no("Update API key?", False):
api_key = prompt_text("OpenCode Go API key", password=True)
if api_key:
save_env_value("OPENCODE_GO_API_KEY", api_key)
print_success("OpenCode Go API key updated")
else:
api_key = prompt_text("OpenCode Go API key", password=True)
if api_key:
save_env_value("OPENCODE_GO_API_KEY", api_key)
print_success("OpenCode Go API key saved")
else:
print_warning("Skipped - agent won't work without an API key")
# Clear custom endpoint vars if switching
if existing_custom:
save_env_value("OPENAI_BASE_URL", "")
save_env_value("OPENAI_API_KEY", "")
_set_model_provider(config, "opencode-go", pconfig.inference_base_url)
selected_base_url = pconfig.inference_base_url
# else: provider_idx == 12 (Keep current) — only shown when a provider already exists
# Normalize "keep current" to an explicit provider so downstream logic # Normalize "keep current" to an explicit provider so downstream logic
# doesn't fall back to the generic OpenRouter/static-model path. # doesn't fall back to the generic OpenRouter/static-model path.
if selected_provider is None: if selected_provider is None: