mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-04 01:37:34 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11efa75bb9 |
22
cli.py
22
cli.py
@@ -1545,6 +1545,28 @@ class HermesCLI:
|
|||||||
pass
|
pass
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
if resolved_provider in {"opencode-zen", "opencode-go"}:
|
||||||
|
try:
|
||||||
|
from hermes_cli.models import normalize_opencode_model_id, opencode_model_api_mode
|
||||||
|
|
||||||
|
canonical = normalize_opencode_model_id(resolved_provider, current_model)
|
||||||
|
if canonical and canonical != current_model:
|
||||||
|
if not self._model_is_default:
|
||||||
|
self.console.print(
|
||||||
|
f"[yellow]⚠️ Stripped provider prefix from '{current_model}'; using '{canonical}' for {resolved_provider}.[/]"
|
||||||
|
)
|
||||||
|
self.model = canonical
|
||||||
|
current_model = canonical
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
resolved_mode = opencode_model_api_mode(resolved_provider, current_model)
|
||||||
|
if resolved_mode != self.api_mode:
|
||||||
|
self.api_mode = resolved_mode
|
||||||
|
changed = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return changed
|
||||||
|
|
||||||
if resolved_provider != "openai-codex":
|
if resolved_provider != "openai-codex":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,10 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
|||||||
id="opencode-go",
|
id="opencode-go",
|
||||||
name="OpenCode Go",
|
name="OpenCode Go",
|
||||||
auth_type="api_key",
|
auth_type="api_key",
|
||||||
|
# OpenCode Go mixes API surfaces by model:
|
||||||
|
# - GLM / Kimi use OpenAI-compatible chat completions under /v1
|
||||||
|
# - MiniMax models use Anthropic Messages under /v1/messages
|
||||||
|
# Keep the provider base at /v1 and select api_mode per-model.
|
||||||
inference_base_url="https://opencode.ai/zen/go/v1",
|
inference_base_url="https://opencode.ai/zen/go/v1",
|
||||||
api_key_env_vars=("OPENCODE_GO_API_KEY",),
|
api_key_env_vars=("OPENCODE_GO_API_KEY",),
|
||||||
base_url_env_var="OPENCODE_GO_BASE_URL",
|
base_url_env_var="OPENCODE_GO_BASE_URL",
|
||||||
|
|||||||
@@ -1604,81 +1604,8 @@ def _model_flow_named_custom(config, provider_info):
|
|||||||
print(f" Provider: {name} ({base_url})")
|
print(f" Provider: {name} ({base_url})")
|
||||||
|
|
||||||
|
|
||||||
# Curated model lists for direct API-key providers
|
# Curated model lists for direct API-key providers — single source in models.py
|
||||||
_PROVIDER_MODELS = {
|
from hermes_cli.models import _PROVIDER_MODELS
|
||||||
"copilot-acp": [
|
|
||||||
"copilot-acp",
|
|
||||||
],
|
|
||||||
"copilot": [
|
|
||||||
"gpt-5.4",
|
|
||||||
"gpt-5.4-mini",
|
|
||||||
"gpt-5-mini",
|
|
||||||
"gpt-5.3-codex",
|
|
||||||
"gpt-5.2-codex",
|
|
||||||
"gpt-4.1",
|
|
||||||
"gpt-4o",
|
|
||||||
"gpt-4o-mini",
|
|
||||||
"claude-opus-4.6",
|
|
||||||
"claude-sonnet-4.6",
|
|
||||||
"claude-sonnet-4.5",
|
|
||||||
"claude-haiku-4.5",
|
|
||||||
"gemini-2.5-pro",
|
|
||||||
"grok-code-fast-1",
|
|
||||||
],
|
|
||||||
"zai": [
|
|
||||||
"glm-5",
|
|
||||||
"glm-4.7",
|
|
||||||
"glm-4.5",
|
|
||||||
"glm-4.5-flash",
|
|
||||||
],
|
|
||||||
"kimi-coding": [
|
|
||||||
"kimi-for-coding",
|
|
||||||
"kimi-k2.5",
|
|
||||||
"kimi-k2-thinking",
|
|
||||||
"kimi-k2-thinking-turbo",
|
|
||||||
"kimi-k2-turbo-preview",
|
|
||||||
"kimi-k2-0905-preview",
|
|
||||||
],
|
|
||||||
"moonshot": [
|
|
||||||
"kimi-k2.5",
|
|
||||||
"kimi-k2-thinking",
|
|
||||||
"kimi-k2-turbo-preview",
|
|
||||||
"kimi-k2-0905-preview",
|
|
||||||
],
|
|
||||||
"minimax": [
|
|
||||||
"MiniMax-M2.7",
|
|
||||||
"MiniMax-M2.7-highspeed",
|
|
||||||
"MiniMax-M2.5",
|
|
||||||
"MiniMax-M2.5-highspeed",
|
|
||||||
"MiniMax-M2.1",
|
|
||||||
],
|
|
||||||
"minimax-cn": [
|
|
||||||
"MiniMax-M2.7",
|
|
||||||
"MiniMax-M2.7-highspeed",
|
|
||||||
"MiniMax-M2.5",
|
|
||||||
"MiniMax-M2.5-highspeed",
|
|
||||||
"MiniMax-M2.1",
|
|
||||||
],
|
|
||||||
"kilocode": [
|
|
||||||
"anthropic/claude-opus-4.6",
|
|
||||||
"anthropic/claude-sonnet-4.6",
|
|
||||||
"openai/gpt-5.4",
|
|
||||||
"google/gemini-3-pro-preview",
|
|
||||||
"google/gemini-3-flash-preview",
|
|
||||||
],
|
|
||||||
# Curated HF model list — only agentic models that map to OpenRouter defaults.
|
|
||||||
# Format: HF model ID → OpenRouter equivalent noted in comment
|
|
||||||
"huggingface": [
|
|
||||||
"Qwen/Qwen3.5-397B-A17B", # ↔ qwen/qwen3.5-plus
|
|
||||||
"Qwen/Qwen3.5-35B-A3B", # ↔ qwen/qwen3.5-35b-a3b
|
|
||||||
"deepseek-ai/DeepSeek-V3.2", # ↔ deepseek/deepseek-chat
|
|
||||||
"moonshotai/Kimi-K2.5", # ↔ moonshotai/kimi-k2.5
|
|
||||||
"MiniMaxAI/MiniMax-M2.5", # ↔ minimax/minimax-m2.5
|
|
||||||
"zai-org/GLM-5", # ↔ z-ai/glm-5
|
|
||||||
"XiaomiMiMo/MiMo-V2-Flash", # ↔ xiaomi/mimo-v2-pro
|
|
||||||
"moonshotai/Kimi-K2-Thinking", # ↔ moonshotai/kimi-k2-thinking
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _current_reasoning_effort(config) -> str:
|
def _current_reasoning_effort(config) -> str:
|
||||||
@@ -2147,12 +2074,13 @@ def _model_flow_kimi(config, current_model=""):
|
|||||||
|
|
||||||
|
|
||||||
def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
||||||
"""Generic flow for API-key providers (z.ai, MiniMax)."""
|
"""Generic flow for API-key providers (z.ai, MiniMax, OpenCode, etc.)."""
|
||||||
from hermes_cli.auth import (
|
from hermes_cli.auth import (
|
||||||
PROVIDER_REGISTRY, _prompt_model_selection, _save_model_choice,
|
PROVIDER_REGISTRY, _prompt_model_selection, _save_model_choice,
|
||||||
deactivate_provider,
|
deactivate_provider,
|
||||||
)
|
)
|
||||||
from hermes_cli.config import get_env_value, save_env_value, load_config, save_config
|
from hermes_cli.config import get_env_value, save_env_value, load_config, save_config
|
||||||
|
from hermes_cli.models import fetch_api_models, opencode_model_api_mode, normalize_opencode_model_id
|
||||||
|
|
||||||
pconfig = PROVIDER_REGISTRY[provider_id]
|
pconfig = PROVIDER_REGISTRY[provider_id]
|
||||||
key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
|
key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
|
||||||
@@ -2206,7 +2134,6 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
|||||||
# Curated list is substantial — use it directly, skip live probe
|
# Curated list is substantial — use it directly, skip live probe
|
||||||
live_models = None
|
live_models = None
|
||||||
else:
|
else:
|
||||||
from hermes_cli.models import fetch_api_models
|
|
||||||
api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
||||||
live_models = fetch_api_models(api_key_for_probe, effective_base)
|
live_models = fetch_api_models(api_key_for_probe, effective_base)
|
||||||
|
|
||||||
@@ -2219,6 +2146,11 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
|||||||
print(f" Showing {len(model_list)} curated models — use \"Enter custom model name\" for others.")
|
print(f" Showing {len(model_list)} curated models — use \"Enter custom model name\" for others.")
|
||||||
# else: no defaults either, will fall through to raw input
|
# else: no defaults either, will fall through to raw input
|
||||||
|
|
||||||
|
if provider_id in {"opencode-zen", "opencode-go"}:
|
||||||
|
model_list = [normalize_opencode_model_id(provider_id, mid) for mid in model_list]
|
||||||
|
current_model = normalize_opencode_model_id(provider_id, current_model)
|
||||||
|
model_list = list(dict.fromkeys(mid for mid in model_list if mid))
|
||||||
|
|
||||||
if model_list:
|
if model_list:
|
||||||
selected = _prompt_model_selection(model_list, current_model=current_model)
|
selected = _prompt_model_selection(model_list, current_model=current_model)
|
||||||
else:
|
else:
|
||||||
@@ -2228,9 +2160,12 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
|||||||
selected = None
|
selected = None
|
||||||
|
|
||||||
if selected:
|
if selected:
|
||||||
|
if provider_id in {"opencode-zen", "opencode-go"}:
|
||||||
|
selected = normalize_opencode_model_id(provider_id, selected)
|
||||||
|
|
||||||
_save_model_choice(selected)
|
_save_model_choice(selected)
|
||||||
|
|
||||||
# Update config with provider and base URL
|
# Update config with provider, base URL, and provider-specific API mode
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
model = cfg.get("model")
|
model = cfg.get("model")
|
||||||
if not isinstance(model, dict):
|
if not isinstance(model, dict):
|
||||||
@@ -2238,7 +2173,10 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
|||||||
cfg["model"] = model
|
cfg["model"] = model
|
||||||
model["provider"] = provider_id
|
model["provider"] = provider_id
|
||||||
model["base_url"] = effective_base
|
model["base_url"] = effective_base
|
||||||
model.pop("api_mode", None) # let runtime auto-detect from URL
|
if provider_id in {"opencode-zen", "opencode-go"}:
|
||||||
|
model["api_mode"] = opencode_model_api_mode(provider_id, selected)
|
||||||
|
else:
|
||||||
|
model.pop("api_mode", None)
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
deactivate_provider()
|
deactivate_provider()
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ModelSwitchResult:
|
|||||||
provider_changed: bool = False
|
provider_changed: bool = False
|
||||||
api_key: str = ""
|
api_key: str = ""
|
||||||
base_url: str = ""
|
base_url: str = ""
|
||||||
|
api_mode: str = ""
|
||||||
persist: bool = False
|
persist: bool = False
|
||||||
error_message: str = ""
|
error_message: str = ""
|
||||||
warning_message: str = ""
|
warning_message: str = ""
|
||||||
@@ -73,6 +74,7 @@ def switch_model(
|
|||||||
detect_provider_for_model,
|
detect_provider_for_model,
|
||||||
validate_requested_model,
|
validate_requested_model,
|
||||||
_PROVIDER_LABELS,
|
_PROVIDER_LABELS,
|
||||||
|
opencode_model_api_mode,
|
||||||
)
|
)
|
||||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||||
|
|
||||||
@@ -98,11 +100,13 @@ def switch_model(
|
|||||||
# Step 4: Resolve credentials for target provider
|
# Step 4: Resolve credentials for target provider
|
||||||
api_key = current_api_key
|
api_key = current_api_key
|
||||||
base_url = current_base_url
|
base_url = current_base_url
|
||||||
|
api_mode = ""
|
||||||
if provider_changed:
|
if provider_changed:
|
||||||
try:
|
try:
|
||||||
runtime = resolve_runtime_provider(requested=target_provider)
|
runtime = resolve_runtime_provider(requested=target_provider)
|
||||||
api_key = runtime.get("api_key", "")
|
api_key = runtime.get("api_key", "")
|
||||||
base_url = runtime.get("base_url", "")
|
base_url = runtime.get("base_url", "")
|
||||||
|
api_mode = runtime.get("api_mode", "")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
provider_label = _PROVIDER_LABELS.get(target_provider, target_provider)
|
provider_label = _PROVIDER_LABELS.get(target_provider, target_provider)
|
||||||
if target_provider == "custom":
|
if target_provider == "custom":
|
||||||
@@ -130,6 +134,7 @@ def switch_model(
|
|||||||
runtime = resolve_runtime_provider(requested=current_provider)
|
runtime = resolve_runtime_provider(requested=current_provider)
|
||||||
api_key = runtime.get("api_key", "")
|
api_key = runtime.get("api_key", "")
|
||||||
base_url = runtime.get("base_url", "")
|
base_url = runtime.get("base_url", "")
|
||||||
|
api_mode = runtime.get("api_mode", "")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -166,6 +171,12 @@ def switch_model(
|
|||||||
and ("localhost" in (base_url or "") or "127.0.0.1" in (base_url or ""))
|
and ("localhost" in (base_url or "") or "127.0.0.1" in (base_url or ""))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if target_provider in {"opencode-zen", "opencode-go"}:
|
||||||
|
# Recompute against the requested new model, not the currently-configured
|
||||||
|
# model used during runtime resolution. OpenCode mixes API surfaces by
|
||||||
|
# model family, so a same-provider model switch can change api_mode.
|
||||||
|
api_mode = opencode_model_api_mode(target_provider, new_model)
|
||||||
|
|
||||||
return ModelSwitchResult(
|
return ModelSwitchResult(
|
||||||
success=True,
|
success=True,
|
||||||
new_model=new_model,
|
new_model=new_model,
|
||||||
@@ -173,6 +184,7 @@ def switch_model(
|
|||||||
provider_changed=provider_changed,
|
provider_changed=provider_changed,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
api_mode=api_mode,
|
||||||
persist=bool(validation.get("persist")),
|
persist=bool(validation.get("persist")),
|
||||||
warning_message=validation.get("message") or "",
|
warning_message=validation.get("message") or "",
|
||||||
is_custom_target=is_custom_target,
|
is_custom_target=is_custom_target,
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||||||
"kimi-k2-turbo-preview",
|
"kimi-k2-turbo-preview",
|
||||||
"kimi-k2-0905-preview",
|
"kimi-k2-0905-preview",
|
||||||
],
|
],
|
||||||
|
"moonshot": [
|
||||||
|
"kimi-k2.5",
|
||||||
|
"kimi-k2-thinking",
|
||||||
|
"kimi-k2-turbo-preview",
|
||||||
|
"kimi-k2-0905-preview",
|
||||||
|
],
|
||||||
"minimax": [
|
"minimax": [
|
||||||
"MiniMax-M2.7",
|
"MiniMax-M2.7",
|
||||||
"MiniMax-M2.7-highspeed",
|
"MiniMax-M2.7-highspeed",
|
||||||
@@ -948,6 +954,53 @@ def copilot_model_api_mode(
|
|||||||
return "chat_completions"
|
return "chat_completions"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_opencode_model_id(provider_id: Optional[str], model_id: Optional[str]) -> str:
|
||||||
|
"""Normalize OpenCode config IDs to the bare model slug used in API requests."""
|
||||||
|
provider = normalize_provider(provider_id)
|
||||||
|
current = str(model_id or "").strip()
|
||||||
|
if not current or provider not in {"opencode-zen", "opencode-go"}:
|
||||||
|
return current
|
||||||
|
|
||||||
|
prefix = f"{provider}/"
|
||||||
|
if current.lower().startswith(prefix):
|
||||||
|
return current[len(prefix):]
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def opencode_model_api_mode(provider_id: Optional[str], model_id: Optional[str]) -> str:
|
||||||
|
"""Determine the API mode for an OpenCode Zen / Go model.
|
||||||
|
|
||||||
|
OpenCode routes different models behind different API surfaces:
|
||||||
|
|
||||||
|
- GPT-5 / Codex models on Zen use ``/v1/responses``
|
||||||
|
- Claude models on Zen use ``/v1/messages``
|
||||||
|
- MiniMax models on Go use ``/v1/messages``
|
||||||
|
- GLM / Kimi on Go use ``/v1/chat/completions``
|
||||||
|
- Other Zen models (Gemini, GLM, Kimi, MiniMax, Qwen, etc.) use
|
||||||
|
``/v1/chat/completions``
|
||||||
|
|
||||||
|
This follows the published OpenCode docs for Zen and Go endpoints.
|
||||||
|
"""
|
||||||
|
provider = normalize_provider(provider_id)
|
||||||
|
normalized = normalize_opencode_model_id(provider_id, model_id).lower()
|
||||||
|
if not normalized:
|
||||||
|
return "chat_completions"
|
||||||
|
|
||||||
|
if provider == "opencode-go":
|
||||||
|
if normalized.startswith("minimax-"):
|
||||||
|
return "anthropic_messages"
|
||||||
|
return "chat_completions"
|
||||||
|
|
||||||
|
if provider == "opencode-zen":
|
||||||
|
if normalized.startswith("claude-"):
|
||||||
|
return "anthropic_messages"
|
||||||
|
if normalized.startswith("gpt-"):
|
||||||
|
return "codex_responses"
|
||||||
|
return "chat_completions"
|
||||||
|
|
||||||
|
return "chat_completions"
|
||||||
|
|
||||||
|
|
||||||
def github_model_reasoning_efforts(
|
def github_model_reasoning_efforts(
|
||||||
model_id: Optional[str],
|
model_id: Optional[str],
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -82,9 +82,27 @@ def _get_model_config() -> Dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _provider_supports_explicit_api_mode(provider: Optional[str], configured_provider: Optional[str] = None) -> bool:
|
||||||
|
"""Check whether a persisted api_mode should be honored for a given provider.
|
||||||
|
|
||||||
|
Prevents stale api_mode from a previous provider leaking into a
|
||||||
|
different one after a model/provider switch. Only applies the
|
||||||
|
persisted mode when the config's provider matches the runtime
|
||||||
|
provider (or when no configured provider is recorded).
|
||||||
|
"""
|
||||||
|
normalized_provider = (provider or "").strip().lower()
|
||||||
|
normalized_configured = (configured_provider or "").strip().lower()
|
||||||
|
if not normalized_configured:
|
||||||
|
return True
|
||||||
|
if normalized_provider == "custom":
|
||||||
|
return normalized_configured == "custom" or normalized_configured.startswith("custom:")
|
||||||
|
return normalized_configured == normalized_provider
|
||||||
|
|
||||||
|
|
||||||
def _copilot_runtime_api_mode(model_cfg: Dict[str, Any], api_key: str) -> str:
|
def _copilot_runtime_api_mode(model_cfg: Dict[str, Any], api_key: str) -> str:
|
||||||
|
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
||||||
if configured_mode:
|
if configured_mode and _provider_supports_explicit_api_mode("copilot", configured_provider):
|
||||||
return configured_mode
|
return configured_mode
|
||||||
|
|
||||||
model_name = str(model_cfg.get("default") or "").strip()
|
model_name = str(model_cfg.get("default") or "").strip()
|
||||||
@@ -140,9 +158,13 @@ def _resolve_runtime_from_pool_entry(
|
|||||||
elif provider == "copilot":
|
elif provider == "copilot":
|
||||||
api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", ""))
|
api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", ""))
|
||||||
else:
|
else:
|
||||||
|
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
||||||
if configured_mode:
|
if configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
|
||||||
api_mode = configured_mode
|
api_mode = configured_mode
|
||||||
|
elif provider in ("opencode-zen", "opencode-go"):
|
||||||
|
from hermes_cli.models import opencode_model_api_mode
|
||||||
|
api_mode = opencode_model_api_mode(provider, model_cfg.get("default", ""))
|
||||||
elif base_url.rstrip("/").endswith("/anthropic"):
|
elif base_url.rstrip("/").endswith("/anthropic"):
|
||||||
api_mode = "anthropic_messages"
|
api_mode = "anthropic_messages"
|
||||||
|
|
||||||
@@ -666,10 +688,14 @@ def resolve_runtime_provider(
|
|||||||
if provider == "copilot":
|
if provider == "copilot":
|
||||||
api_mode = _copilot_runtime_api_mode(model_cfg, creds.get("api_key", ""))
|
api_mode = _copilot_runtime_api_mode(model_cfg, creds.get("api_key", ""))
|
||||||
else:
|
else:
|
||||||
# Check explicit api_mode from model config first
|
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
|
# Only honor persisted api_mode when it belongs to the same provider family.
|
||||||
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
|
||||||
if configured_mode:
|
if configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
|
||||||
api_mode = configured_mode
|
api_mode = configured_mode
|
||||||
|
elif provider in ("opencode-zen", "opencode-go"):
|
||||||
|
from hermes_cli.models import opencode_model_api_mode
|
||||||
|
api_mode = opencode_model_api_mode(provider, model_cfg.get("default", ""))
|
||||||
# Auto-detect Anthropic-compatible endpoints by URL convention
|
# Auto-detect Anthropic-compatible endpoints by URL convention
|
||||||
# (e.g. https://api.minimax.io/anthropic, https://dashscope.../anthropic)
|
# (e.g. https://api.minimax.io/anthropic, https://dashscope.../anthropic)
|
||||||
elif base_url.rstrip("/").endswith("/anthropic"):
|
elif base_url.rstrip("/").endswith("/anthropic"):
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ _DEFAULT_PROVIDER_MODELS = {
|
|||||||
"minimax-cn": ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"],
|
"minimax-cn": ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"],
|
||||||
"ai-gateway": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5", "google/gemini-3-flash"],
|
"ai-gateway": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5", "google/gemini-3-flash"],
|
||||||
"kilocode": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
"kilocode": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
||||||
|
"opencode-zen": ["gpt-5.4", "gpt-5.3-codex", "claude-sonnet-4-6", "gemini-3-flash", "glm-5", "kimi-k2.5", "minimax-m2.7"],
|
||||||
|
"opencode-go": ["glm-5", "kimi-k2.5", "minimax-m2.5", "minimax-m2.7"],
|
||||||
"huggingface": [
|
"huggingface": [
|
||||||
"Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
"Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
||||||
"Qwen/Qwen3-Coder-480B-A35B-Instruct", "deepseek-ai/DeepSeek-R1-0528",
|
"Qwen/Qwen3-Coder-480B-A35B-Instruct", "deepseek-ai/DeepSeek-R1-0528",
|
||||||
@@ -183,6 +185,8 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
|
|||||||
fetch_api_models,
|
fetch_api_models,
|
||||||
fetch_github_model_catalog,
|
fetch_github_model_catalog,
|
||||||
normalize_copilot_model_id,
|
normalize_copilot_model_id,
|
||||||
|
normalize_opencode_model_id,
|
||||||
|
opencode_model_api_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
pconfig = PROVIDER_REGISTRY[provider_id]
|
pconfig = PROVIDER_REGISTRY[provider_id]
|
||||||
@@ -236,6 +240,11 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
|
|||||||
f" Use \"Custom model\" if the model you expect isn't listed."
|
f" Use \"Custom model\" if the model you expect isn't listed."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if provider_id in {"opencode-zen", "opencode-go"}:
|
||||||
|
provider_models = [normalize_opencode_model_id(provider_id, mid) for mid in provider_models]
|
||||||
|
current_model = normalize_opencode_model_id(provider_id, current_model)
|
||||||
|
provider_models = list(dict.fromkeys(mid for mid in provider_models if mid))
|
||||||
|
|
||||||
model_choices = list(provider_models)
|
model_choices = list(provider_models)
|
||||||
model_choices.append("Custom model")
|
model_choices.append("Custom model")
|
||||||
model_choices.append(f"Keep current ({current_model})")
|
model_choices.append(f"Keep current ({current_model})")
|
||||||
@@ -253,6 +262,8 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
|
|||||||
catalog=catalog,
|
catalog=catalog,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
) or selected_model
|
) or selected_model
|
||||||
|
elif provider_id in {"opencode-zen", "opencode-go"}:
|
||||||
|
selected_model = normalize_opencode_model_id(provider_id, selected_model)
|
||||||
_set_default_model(config, selected_model)
|
_set_default_model(config, selected_model)
|
||||||
elif model_idx == len(provider_models):
|
elif model_idx == len(provider_models):
|
||||||
custom = prompt_fn("Enter model name")
|
custom = prompt_fn("Enter model name")
|
||||||
@@ -263,6 +274,8 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
|
|||||||
catalog=catalog,
|
catalog=catalog,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
) or custom
|
) or custom
|
||||||
|
elif provider_id in {"opencode-zen", "opencode-go"}:
|
||||||
|
selected_model = normalize_opencode_model_id(provider_id, custom)
|
||||||
else:
|
else:
|
||||||
selected_model = custom
|
selected_model = custom
|
||||||
_set_default_model(config, selected_model)
|
_set_default_model(config, selected_model)
|
||||||
@@ -294,6 +307,10 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
|
|||||||
catalog=catalog,
|
catalog=catalog,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
|
elif provider_id in {"opencode-zen", "opencode-go"} and selected_model:
|
||||||
|
model_cfg = _model_config_dict(config)
|
||||||
|
model_cfg["api_mode"] = opencode_model_api_mode(provider_id, selected_model)
|
||||||
|
config["model"] = model_cfg
|
||||||
|
|
||||||
|
|
||||||
def _sync_model_from_disk(config: Dict[str, Any]) -> None:
|
def _sync_model_from_disk(config: Dict[str, Any]) -> None:
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from hermes_cli.models import (
|
|||||||
fetch_api_models,
|
fetch_api_models,
|
||||||
github_model_reasoning_efforts,
|
github_model_reasoning_efforts,
|
||||||
normalize_copilot_model_id,
|
normalize_copilot_model_id,
|
||||||
|
normalize_opencode_model_id,
|
||||||
normalize_provider,
|
normalize_provider,
|
||||||
|
opencode_model_api_mode,
|
||||||
parse_model_input,
|
parse_model_input,
|
||||||
probe_api_models,
|
probe_api_models,
|
||||||
provider_label,
|
provider_label,
|
||||||
@@ -339,6 +341,28 @@ class TestCopilotNormalization:
|
|||||||
}]
|
}]
|
||||||
assert copilot_model_api_mode("gpt-5.4", catalog=catalog) == "codex_responses"
|
assert copilot_model_api_mode("gpt-5.4", catalog=catalog) == "codex_responses"
|
||||||
|
|
||||||
|
def test_normalize_opencode_model_id_strips_provider_prefix(self):
|
||||||
|
assert normalize_opencode_model_id("opencode-go", "opencode-go/kimi-k2.5") == "kimi-k2.5"
|
||||||
|
assert normalize_opencode_model_id("opencode-zen", "opencode-zen/claude-sonnet-4-6") == "claude-sonnet-4-6"
|
||||||
|
assert normalize_opencode_model_id("opencode-go", "glm-5") == "glm-5"
|
||||||
|
|
||||||
|
def test_opencode_zen_api_modes_match_docs(self):
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "gpt-5.4") == "codex_responses"
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "gpt-5.3-codex") == "codex_responses"
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "opencode-zen/gpt-5.4") == "codex_responses"
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "claude-sonnet-4-6") == "anthropic_messages"
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "opencode-zen/claude-sonnet-4-6") == "anthropic_messages"
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "gemini-3-flash") == "chat_completions"
|
||||||
|
assert opencode_model_api_mode("opencode-zen", "minimax-m2.5") == "chat_completions"
|
||||||
|
|
||||||
|
def test_opencode_go_api_modes_match_docs(self):
|
||||||
|
assert opencode_model_api_mode("opencode-go", "glm-5") == "chat_completions"
|
||||||
|
assert opencode_model_api_mode("opencode-go", "opencode-go/glm-5") == "chat_completions"
|
||||||
|
assert opencode_model_api_mode("opencode-go", "kimi-k2.5") == "chat_completions"
|
||||||
|
assert opencode_model_api_mode("opencode-go", "opencode-go/kimi-k2.5") == "chat_completions"
|
||||||
|
assert opencode_model_api_mode("opencode-go", "minimax-m2.5") == "anthropic_messages"
|
||||||
|
assert opencode_model_api_mode("opencode-go", "opencode-go/minimax-m2.5") == "anthropic_messages"
|
||||||
|
|
||||||
|
|
||||||
# -- validate — format checks -----------------------------------------------
|
# -- validate — format checks -----------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,14 @@ class TestDetectProviderForModel:
|
|||||||
assert result[0] == "openrouter"
|
assert result[0] == "openrouter"
|
||||||
assert result[1] == "anthropic/claude-opus-4.6"
|
assert result[1] == "anthropic/claude-opus-4.6"
|
||||||
|
|
||||||
def test_bare_name_gets_openrouter_slug(self):
|
def test_bare_name_gets_openrouter_slug(self, monkeypatch):
|
||||||
|
for env_var in (
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"ANTHROPIC_TOKEN",
|
||||||
|
"CLAUDE_CODE_TOKEN",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
):
|
||||||
|
monkeypatch.delenv(env_var, raising=False)
|
||||||
"""Bare model names should get mapped to full OpenRouter slugs."""
|
"""Bare model names should get mapped to full OpenRouter slugs."""
|
||||||
result = detect_provider_for_model("claude-opus-4.6", "openai-codex")
|
result = detect_provider_for_model("claude-opus-4.6", "openai-codex")
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|||||||
@@ -186,6 +186,22 @@ class TestNormalizeModelForProvider:
|
|||||||
assert changed is True
|
assert changed is True
|
||||||
assert cli.model == "claude-opus-4.6"
|
assert cli.model == "claude-opus-4.6"
|
||||||
|
|
||||||
|
def test_opencode_go_prefix_stripped(self):
|
||||||
|
cli = _make_cli(model="opencode-go/kimi-k2.5")
|
||||||
|
cli.api_mode = "chat_completions"
|
||||||
|
changed = cli._normalize_model_for_provider("opencode-go")
|
||||||
|
assert changed is True
|
||||||
|
assert cli.model == "kimi-k2.5"
|
||||||
|
assert cli.api_mode == "chat_completions"
|
||||||
|
|
||||||
|
def test_opencode_zen_claude_sets_messages_mode(self):
|
||||||
|
cli = _make_cli(model="opencode-zen/claude-sonnet-4-6")
|
||||||
|
cli.api_mode = "chat_completions"
|
||||||
|
changed = cli._normalize_model_for_provider("opencode-zen")
|
||||||
|
assert changed is True
|
||||||
|
assert cli.model == "claude-sonnet-4-6"
|
||||||
|
assert cli.api_mode == "anthropic_messages"
|
||||||
|
|
||||||
def test_default_model_replaced(self):
|
def test_default_model_replaced(self):
|
||||||
"""The untouched default (anthropic/claude-opus-4.6) gets swapped."""
|
"""The untouched default (anthropic/claude-opus-4.6) gets swapped."""
|
||||||
import cli as _cli_mod
|
import cli as _cli_mod
|
||||||
|
|||||||
@@ -210,3 +210,50 @@ class TestProviderPersistsAfterModelSave:
|
|||||||
assert model.get("base_url") == "acp://copilot"
|
assert model.get("base_url") == "acp://copilot"
|
||||||
assert model.get("default") == "gpt-5.4"
|
assert model.get("default") == "gpt-5.4"
|
||||||
assert model.get("api_mode") == "chat_completions"
|
assert model.get("api_mode") == "chat_completions"
|
||||||
|
|
||||||
|
def test_opencode_go_models_are_selectable_and_persist_normalized(self, config_home, monkeypatch):
|
||||||
|
from hermes_cli.main import _model_flow_api_key_provider
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
|
||||||
|
monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-key")
|
||||||
|
|
||||||
|
with patch("hermes_cli.models.fetch_api_models", return_value=["opencode-go/kimi-k2.5", "opencode-go/minimax-m2.7"]), \
|
||||||
|
patch("hermes_cli.auth._prompt_model_selection", return_value="kimi-k2.5"), \
|
||||||
|
patch("hermes_cli.auth.deactivate_provider"), \
|
||||||
|
patch("builtins.input", return_value=""):
|
||||||
|
_model_flow_api_key_provider(load_config(), "opencode-go", "opencode-go/kimi-k2.5")
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
|
||||||
|
model = config.get("model")
|
||||||
|
assert isinstance(model, dict)
|
||||||
|
assert model.get("provider") == "opencode-go"
|
||||||
|
assert model.get("default") == "kimi-k2.5"
|
||||||
|
assert model.get("api_mode") == "chat_completions"
|
||||||
|
|
||||||
|
def test_opencode_go_same_provider_switch_recomputes_api_mode(self, config_home, monkeypatch):
|
||||||
|
from hermes_cli.main import _model_flow_api_key_provider
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
|
||||||
|
monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-key")
|
||||||
|
(config_home / "config.yaml").write_text(
|
||||||
|
"model:\n"
|
||||||
|
" default: kimi-k2.5\n"
|
||||||
|
" provider: opencode-go\n"
|
||||||
|
" base_url: https://opencode.ai/zen/go/v1\n"
|
||||||
|
" api_mode: chat_completions\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("hermes_cli.models.fetch_api_models", return_value=["opencode-go/kimi-k2.5", "opencode-go/minimax-m2.5"]), \
|
||||||
|
patch("hermes_cli.auth._prompt_model_selection", return_value="minimax-m2.5"), \
|
||||||
|
patch("hermes_cli.auth.deactivate_provider"), \
|
||||||
|
patch("builtins.input", return_value=""):
|
||||||
|
_model_flow_api_key_provider(load_config(), "opencode-go", "kimi-k2.5")
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
|
||||||
|
model = config.get("model")
|
||||||
|
assert isinstance(model, dict)
|
||||||
|
assert model.get("provider") == "opencode-go"
|
||||||
|
assert model.get("default") == "minimax-m2.5"
|
||||||
|
assert model.get("api_mode") == "anthropic_messages"
|
||||||
|
|||||||
@@ -643,6 +643,34 @@ def test_model_config_api_mode(monkeypatch):
|
|||||||
assert resolved["base_url"] == "http://127.0.0.1:9208/v1"
|
assert resolved["base_url"] == "http://127.0.0.1:9208/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_config_api_mode_ignored_when_provider_differs(monkeypatch):
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "zai")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rp,
|
||||||
|
"_get_model_config",
|
||||||
|
lambda: {
|
||||||
|
"provider": "opencode-go",
|
||||||
|
"default": "minimax-m2.5",
|
||||||
|
"api_mode": "anthropic_messages",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rp,
|
||||||
|
"resolve_api_key_provider_credentials",
|
||||||
|
lambda provider: {
|
||||||
|
"provider": provider,
|
||||||
|
"api_key": "test-key",
|
||||||
|
"base_url": "https://api.z.ai/api/paas/v4",
|
||||||
|
"source": "env",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="zai")
|
||||||
|
|
||||||
|
assert resolved["provider"] == "zai"
|
||||||
|
assert resolved["api_mode"] == "chat_completions"
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_api_mode_ignored(monkeypatch):
|
def test_invalid_api_mode_ignored(monkeypatch):
|
||||||
"""Invalid api_mode values should fall back to chat_completions."""
|
"""Invalid api_mode values should fall back to chat_completions."""
|
||||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter")
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter")
|
||||||
@@ -808,6 +836,78 @@ def test_alibaba_anthropic_endpoint_override_uses_anthropic_messages(monkeypatch
|
|||||||
assert resolved["base_url"] == "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
assert resolved["base_url"] == "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_zen_gpt_defaults_to_responses(monkeypatch):
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "opencode-zen")
|
||||||
|
monkeypatch.setattr(rp, "_get_model_config", lambda: {"default": "gpt-5.4"})
|
||||||
|
monkeypatch.setenv("OPENCODE_ZEN_API_KEY", "test-opencode-zen-key")
|
||||||
|
monkeypatch.delenv("OPENCODE_ZEN_BASE_URL", raising=False)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="opencode-zen")
|
||||||
|
|
||||||
|
assert resolved["provider"] == "opencode-zen"
|
||||||
|
assert resolved["api_mode"] == "codex_responses"
|
||||||
|
assert resolved["base_url"] == "https://opencode.ai/zen/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_zen_claude_defaults_to_messages(monkeypatch):
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "opencode-zen")
|
||||||
|
monkeypatch.setattr(rp, "_get_model_config", lambda: {"default": "claude-sonnet-4-6"})
|
||||||
|
monkeypatch.setenv("OPENCODE_ZEN_API_KEY", "test-opencode-zen-key")
|
||||||
|
monkeypatch.delenv("OPENCODE_ZEN_BASE_URL", raising=False)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="opencode-zen")
|
||||||
|
|
||||||
|
assert resolved["provider"] == "opencode-zen"
|
||||||
|
assert resolved["api_mode"] == "anthropic_messages"
|
||||||
|
assert resolved["base_url"] == "https://opencode.ai/zen/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_go_minimax_defaults_to_messages(monkeypatch):
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "opencode-go")
|
||||||
|
monkeypatch.setattr(rp, "_get_model_config", lambda: {"default": "minimax-m2.5"})
|
||||||
|
monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-opencode-go-key")
|
||||||
|
monkeypatch.delenv("OPENCODE_GO_BASE_URL", raising=False)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="opencode-go")
|
||||||
|
|
||||||
|
assert resolved["provider"] == "opencode-go"
|
||||||
|
assert resolved["api_mode"] == "anthropic_messages"
|
||||||
|
assert resolved["base_url"] == "https://opencode.ai/zen/go/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_go_glm_defaults_to_chat_completions(monkeypatch):
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "opencode-go")
|
||||||
|
monkeypatch.setattr(rp, "_get_model_config", lambda: {"default": "glm-5"})
|
||||||
|
monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-opencode-go-key")
|
||||||
|
monkeypatch.delenv("OPENCODE_GO_BASE_URL", raising=False)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="opencode-go")
|
||||||
|
|
||||||
|
assert resolved["provider"] == "opencode-go"
|
||||||
|
assert resolved["api_mode"] == "chat_completions"
|
||||||
|
assert resolved["base_url"] == "https://opencode.ai/zen/go/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_opencode_go_configured_api_mode_still_overrides_default(monkeypatch):
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "opencode-go")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rp,
|
||||||
|
"_get_model_config",
|
||||||
|
lambda: {
|
||||||
|
"provider": "opencode-go",
|
||||||
|
"default": "minimax-m2.5",
|
||||||
|
"api_mode": "chat_completions",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-opencode-go-key")
|
||||||
|
monkeypatch.delenv("OPENCODE_GO_BASE_URL", raising=False)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="opencode-go")
|
||||||
|
|
||||||
|
assert resolved["provider"] == "opencode-go"
|
||||||
|
assert resolved["api_mode"] == "chat_completions"
|
||||||
|
|
||||||
|
|
||||||
def test_named_custom_provider_anthropic_api_mode(monkeypatch):
|
def test_named_custom_provider_anthropic_api_mode(monkeypatch):
|
||||||
"""Custom providers should accept api_mode: anthropic_messages."""
|
"""Custom providers should accept api_mode: anthropic_messages."""
|
||||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-anthropic-proxy")
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-anthropic-proxy")
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ def mock_provider_registry():
|
|||||||
"kimi-coding": FakePConfig("Kimi Coding", ["KIMI_API_KEY"], "KIMI_BASE_URL", "https://api.kimi.example"),
|
"kimi-coding": FakePConfig("Kimi Coding", ["KIMI_API_KEY"], "KIMI_BASE_URL", "https://api.kimi.example"),
|
||||||
"minimax": FakePConfig("MiniMax", ["MINIMAX_API_KEY"], "MINIMAX_BASE_URL", "https://api.minimax.example"),
|
"minimax": FakePConfig("MiniMax", ["MINIMAX_API_KEY"], "MINIMAX_BASE_URL", "https://api.minimax.example"),
|
||||||
"minimax-cn": FakePConfig("MiniMax CN", ["MINIMAX_API_KEY"], "MINIMAX_CN_BASE_URL", "https://api.minimax-cn.example"),
|
"minimax-cn": FakePConfig("MiniMax CN", ["MINIMAX_API_KEY"], "MINIMAX_CN_BASE_URL", "https://api.minimax-cn.example"),
|
||||||
|
"opencode-zen": FakePConfig("OpenCode Zen", ["OPENCODE_ZEN_API_KEY"], "OPENCODE_ZEN_BASE_URL", "https://opencode.ai/zen/v1"),
|
||||||
|
"opencode-go": FakePConfig("OpenCode Go", ["OPENCODE_GO_API_KEY"], "OPENCODE_GO_BASE_URL", "https://opencode.ai/zen/go/v1"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +36,8 @@ class TestSetupProviderModelSelection:
|
|||||||
("kimi-coding", ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"]),
|
("kimi-coding", ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"]),
|
||||||
("minimax", ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]),
|
("minimax", ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]),
|
||||||
("minimax-cn", ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]),
|
("minimax-cn", ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]),
|
||||||
|
("opencode-zen", ["gpt-5.4", "gpt-5.3-codex", "claude-sonnet-4-6", "gemini-3-flash"]),
|
||||||
|
("opencode-go", ["glm-5", "kimi-k2.5", "minimax-m2.5", "minimax-m2.7"]),
|
||||||
])
|
])
|
||||||
@patch("hermes_cli.models.fetch_api_models", return_value=[])
|
@patch("hermes_cli.models.fetch_api_models", return_value=[])
|
||||||
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
||||||
@@ -122,3 +126,30 @@ class TestSetupProviderModelSelection:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert config["model"]["default"] == "my-custom-model"
|
assert config["model"]["default"] == "my-custom-model"
|
||||||
|
|
||||||
|
@patch("hermes_cli.models.fetch_api_models", return_value=["opencode-go/kimi-k2.5", "opencode-go/minimax-m2.7"])
|
||||||
|
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
||||||
|
def test_opencode_live_models_are_normalized_for_selection(
|
||||||
|
self, mock_env, mock_fetch, mock_provider_registry
|
||||||
|
):
|
||||||
|
from hermes_cli.setup import _setup_provider_model_selection
|
||||||
|
|
||||||
|
captured_choices = {}
|
||||||
|
|
||||||
|
def fake_prompt_choice(label, choices, default):
|
||||||
|
captured_choices["choices"] = choices
|
||||||
|
return len(choices) - 1
|
||||||
|
|
||||||
|
with patch("hermes_cli.auth.PROVIDER_REGISTRY", mock_provider_registry):
|
||||||
|
_setup_provider_model_selection(
|
||||||
|
config={"model": {}},
|
||||||
|
provider_id="opencode-go",
|
||||||
|
current_model="opencode-go/kimi-k2.5",
|
||||||
|
prompt_choice=fake_prompt_choice,
|
||||||
|
prompt_fn=lambda _: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
offered = captured_choices["choices"]
|
||||||
|
assert "kimi-k2.5" in offered
|
||||||
|
assert "minimax-m2.7" in offered
|
||||||
|
assert all("opencode-go/" not in choice for choice in offered)
|
||||||
|
|||||||
Reference in New Issue
Block a user