# NousResearch/hermes-agent · OpenCode provider model routing and normalization fixes

## Open now

- Nothing open on GitHub.

A cluster of bugs, fixes, and a feature addition centered on OpenCode provider support, including incorrect API surface routing, model name normalization, base URL handling, and 404 errors affecting specific models like GLM, Kimi, and Zen/ZAI providers.

## TL;DR

- 7 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 2 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #4508. Verify it addresses your case.
- **Recommended:** read PR #4508 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.27`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #3014: Fix OpenCode model selection, normalization, and mixed API-surface routing
- #13194: \[Bug\]: OpenCode Zen routes kimi-k2.5-free to /messages instead of /chat/completions
- #57288: \[Bug\]: Some OpenCode models (like GLM 5.2) fail with \`HTTP 404 — Not Found | opencode\`

## Prior & competing PRs

- **#3017: fix: repair OpenCode model routing and selection**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#4508: fix: repair OpenCode model routing and selection**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `hermes_cli/auth.py`, `hermes_cli/main.py`, `hermes_cli/model_switch.py`, `hermes_cli/models.py`, `hermes_cli/runtime_provider.py`, `hermes_cli/setup.py`, `tests/hermes_cli/test_model_validation.py`, `tests/hermes_cli/test_models.py`, `tests/test_codex_models.py`, `tests/test_model_provider_persistence.py`, `tests/test_runtime_provider_resolution.py`, `tests/test_setup_model_selection.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -1545,6 +1545,28 @@ def _normalize_model_for_provider(self, resolved_provider: str) -> bool:
                     pass
                 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":
                 return False
     
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -200,6 +200,10 @@ class ProviderConfig:
             id="opencode-go",
             name="OpenCode Go",
             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",
             api_key_env_vars=("OPENCODE_GO_API_KEY",),
             base_url_env_var="OPENCODE_GO_BASE_URL",
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -1604,81 +1604,8 @@ def _model_flow_named_custom(config, provider_info):
         print(f"   Provider: {name} ({base_url})")
     
     
    -# Curated model lists for direct API-key providers
    -_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
    -    ],
    -}
    +# Curated model lists for direct API-key providers — single source in models.py
    +from hermes_cli.models import _PROVIDER_MODELS
     
     
     def _current_reasoning_effort(config) -> str:
    ```

    ```diff
    @@ -2147,12 +2074,13 @@ def _model_flow_kimi(config, 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 (
             PROVIDER_REGISTRY, _prompt_model_selection, _save_model_choice,
             deactivate_provider,
         )
         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]
         key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
    ```

    ```diff
    @@ -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
             live_models = None
         else:
    -        from hermes_cli.models import fetch_api_models
             api_key_for_probe = [redacted:secret-assignment] or (get_env_value(key_env) if key_env else "")
             live_models = fetch_api_models(api_key_for_probe, effective_base)
     
    ```

    ```diff
    @@ -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.")
             # 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:
             selected = _prompt_model_selection(model_list, current_model=current_model)
         else:
    ```

    ```diff
    @@ -2228,9 +2160,12 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
                 selected = None
     
         if selected:
    +        if provider_id in {"opencode-zen", "opencode-go"}:
    +            selected = normalize_opencode_model_id(provider_id, 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()
             model = cfg.get("model")
             if not isinstance(model, dict):
    ```

    ```diff
    @@ -2238,7 +2173,10 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
                 cfg["model"] = model
             model["provider"] = provider_id
             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)
             deactivate_provider()
     
    ```
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -26,6 +26,7 @@ class ModelSwitchResult:
         provider_changed: bool = False
         api_key: str = ""
         base_url: str = ""
    +    api_mode: str = ""
         persist: bool = False
         error_message: str = ""
         warning_message: str = ""
    ```

    ```diff
    @@ -73,6 +74,7 @@ def switch_model(
             detect_provider_for_model,
             validate_requested_model,
             _PROVIDER_LABELS,
    +        opencode_model_api_mode,
         )
         from hermes_cli.runtime_provider import resolve_runtime_provider
     
    ```

    ```diff
    @@ -98,11 +100,13 @@ def switch_model(
         # Step 4: Resolve credentials for target provider
         api_key = [redacted:secret-assignment]
         base_url = current_base_url
    +    api_mode = ""
         if provider_changed:
             try:
                 runtime = resolve_runtime_provider(requested=target_provider)
                 api_key = [redacted:secret-assignment]"api_key", "")
                 base_url = runtime.get("base_url", "")
    +            api_mode = runtime.get("api_mode", "")
             except Exception as e:
                 provider_label = _PROVIDER_LABELS.get(target_provider, target_provider)
                 if target_provider == "custom":
    ```

    ```diff
    @@ -130,6 +134,7 @@ def switch_model(
                 runtime = resolve_runtime_provider(requested=current_provider)
                 api_key = [redacted:secret-assignment]"api_key", "")
                 base_url = runtime.get("base_url", "")
    +            api_mode = runtime.get("api_mode", "")
             except Exception:
                 pass
     
    ```

    ```diff
    @@ -166,6 +171,12 @@ def switch_model(
             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(
             success=True,
             new_model=new_model,
    ```

    ```diff
    @@ -173,6 +184,7 @@ def switch_model(
             provider_changed=provider_changed,
             api_key=[redacted:secret-assignment]
             base_url=base_url,
    +        api_mode=api_mode,
             persist=bool(validation.get("persist")),
             warning_message=validation.get("message") or "",
             is_custom_target=is_custom_target,
    ```
  - `hermes_cli/models.py`

    ```diff
    @@ -125,6 +125,12 @@
             "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",
    ```

    ```diff
    @@ -948,6 +954,53 @@ def copilot_model_api_mode(
         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(
         model_id: Optional[str],
         *,
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -82,9 +82,27 @@ def _get_model_config() -> Dict[str, Any]:
         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:
    +    configured_provider = str(model_cfg.get("provider") or "").strip().lower()
         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
     
         model_name = str(model_cfg.get("default") or "").strip()
    ```

    ```diff
    @@ -140,9 +158,13 @@ def _resolve_runtime_from_pool_entry(
         elif provider == "copilot":
             api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", ""))
         else:
    +        configured_provider = str(model_cfg.get("provider") or "").strip().lower()
             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
    +        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"):
                 api_mode = "anthropic_messages"
     
    ```

    ```diff
    @@ -666,10 +688,14 @@ def resolve_runtime_provider(
             if provider == "copilot":
                 api_mode = _copilot_runtime_api_mode(model_cfg, creds.get("api_key", ""))
             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"))
    -            if configured_mode:
    +            if configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
                     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
                 # (e.g. https://api.minimax.io/anthropic, https://dashscope.../anthropic)
                 elif base_url.rstrip("/").endswith("/anthropic"):
    ```
  - `hermes_cli/setup.py`

    ```diff
    @@ -108,6 +108,8 @@ def _supports_same_provider_pool_setup(provider: str) -> bool:
         "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"],
         "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": [
             "Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B-Thinking-2507",
             "Qwen/Qwen3-Coder-480B-A35B-Instruct", "deepseek-ai/DeepSeek-R1-0528",
    ```

    ```diff
    @@ -183,6 +185,8 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
             fetch_api_models,
             fetch_github_model_catalog,
             normalize_copilot_model_id,
    +        normalize_opencode_model_id,
    +        opencode_model_api_mode,
         )
     
         pconfig = PROVIDER_REGISTRY[provider_id]
    ```

    ```diff
    @@ -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."
                 )
     
    +    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.append("Custom model")
         model_choices.append(f"Keep current ({current_model})")
    ```

    ```diff
    @@ -253,6 +262,8 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
                     catalog=catalog,
                     api_key=[redacted:secret-assignment]
                 ) 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)
         elif model_idx == len(provider_models):
             custom = prompt_fn("Enter model name")
    ```

    ```diff
    @@ -263,6 +274,8 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
                         catalog=catalog,
                         api_key=[redacted:secret-assignment]
                     ) or custom
    +            elif provider_id in {"opencode-zen", "opencode-go"}:
    +                selected_model = normalize_opencode_model_id(provider_id, custom)
                 else:
                     selected_model = custom
                 _set_default_model(config, selected_model)
    ```

    ```diff
    @@ -294,6 +307,10 @@ def _setup_provider_model_selection(config, provider_id, current_model, prompt_c
                 catalog=catalog,
                 api_key=[redacted:secret-assignment]
             )
    +    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:
    ```
  - `tests/hermes_cli/test_model_validation.py`

    ```diff
    @@ -9,7 +9,9 @@
         fetch_api_models,
         github_model_reasoning_efforts,
         normalize_copilot_model_id,
    +    normalize_opencode_model_id,
         normalize_provider,
    +    opencode_model_api_mode,
         parse_model_input,
         probe_api_models,
         provider_label,
    ```

    ```diff
    @@ -339,6 +341,28 @@ def test_copilot_api_mode_with_catalog_only_responses(self):
             }]
             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 -----------------------------------------------
     
    ```
  - `tests/hermes_cli/test_models.py`

    ```diff
    @@ -101,7 +101,14 @@ def test_openrouter_slug_match(self):
             assert result[0] == "openrouter"
             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."""
             result = detect_provider_for_model("claude-opus-4.6", "openai-codex")
             assert result is not None
    ```
  - `tests/test_codex_models.py`

    ```diff
    @@ -186,6 +186,22 @@ def test_any_provider_prefix_stripped(self):
             assert changed is True
             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):
             """The untouched default (anthropic/claude-opus-4.6) gets swapped."""
             import cli as _cli_mod
    ```
  - `tests/test_model_provider_persistence.py`

    ```diff
    @@ -210,3 +210,50 @@ def test_copilot_acp_provider_saved_when_selected(self, config_home):
             assert model.get("base_url") == "acp://copilot"
             assert model.get("default") == "gpt-5.4"
             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"
    ```
  - `tests/test_runtime_provider_resolution.py`

    ```diff
    @@ -643,6 +643,34 @@ def test_model_config_api_mode(monkeypatch):
         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):
         """Invalid api_mode values should fall back to chat_completions."""
         monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter")
    ```

    ```diff
    @@ -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"
     
     
    +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):
         """Custom providers should accept api_mode: anthropic_messages."""
         monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-anthropic-proxy")
    ```
  - `tests/test_setup_model_selection.py`

    ```diff
    @@ -22,6 +22,8 @@ def __init__(self, name, env_vars, base_url_env, inference_url):
             "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-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"),
         }
     
     
    ```

    ```diff
    @@ -34,6 +36,8 @@ class TestSetupProviderModelSelection:
             ("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-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.config.get_env_value", return_value="fake-key")
    ```

    ```diff
    @@ -122,3 +126,30 @@ def fake_prompt_choice(label, choices, default):
                 )
     
             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)
    ```
- **#15106: fix(opencode): derive api_mode from target model, not stale config default**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/model_switch.py`, `hermes_cli/runtime_provider.py`, `tests/hermes_cli/test_model_switch_custom_providers.py`, `tests/hermes_cli/test_model_switch_opencode_anthropic.py`, `tests/hermes_cli/test_ollama_cloud_auth.py`
  - fix hunks:
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -771,7 +771,10 @@ def switch_model(
     
         if provider_changed or explicit_provider:
             try:
    -            runtime = resolve_runtime_provider(requested=target_provider)
    +            runtime = resolve_runtime_provider(
    +                requested=target_provider,
    +                target_model=new_model,
    +            )
                 api_key = [redacted:secret-assignment]"api_key", "")
                 base_url = runtime.get("base_url", "")
                 api_mode = runtime.get("api_mode", "")
    ```

    ```diff
    @@ -788,7 +791,10 @@ def switch_model(
                 )
         else:
             try:
    -            runtime = resolve_runtime_provider(requested=current_provider)
    +            runtime = resolve_runtime_provider(
    +                requested=current_provider,
    +                target_model=new_model,
    +            )
                 api_key = [redacted:secret-assignment]"api_key", "")
                 base_url = runtime.get("base_url", "")
                 api_mode = runtime.get("api_mode", "")
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -160,8 +160,16 @@ def _resolve_runtime_from_pool_entry(
         requested_provider: str,
         model_cfg: Optional[Dict[str, Any]] = None,
         pool: Optional[CredentialPool] = None,
    +    target_model: Optional[str] = None,
     ) -> Dict[str, Any]:
         model_cfg = model_cfg or _get_model_config()
    +    # When the caller is resolving for a specific target model (e.g. a /model
    +    # mid-session switch), prefer that over the persisted model.default. This
    +    # prevents api_mode being computed from a stale config default that no
    +    # longer matches the model actually being used — the bug that caused
    +    # opencode-zen /v1 to be stripped for chat_completions requests when
    +    # config.default was still a Claude model.
    +    effective_model = (target_model or model_cfg.get("default") or "")
         base_url = (getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) or "").rstrip("/")
         api_key = [redacted:secret-assignment] "runtime_api_key", None) or getattr(entry, "access_token", "")
         api_mode = "chat_completions"
    ```

    ```diff
    @@ -207,7 +215,7 @@ def _resolve_runtime_from_pool_entry(
                 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", ""))
    +            api_mode = opencode_model_api_mode(provider, effective_model)
             else:
                 # Auto-detect Anthropic-compatible endpoints (/anthropic suffix,
                 # Kimi /coding, api.openai.com → codex_responses, api.x.ai →
    ```

    ```diff
    @@ -697,8 +705,18 @@ def resolve_runtime_provider(
         requested: Optional[str] = None,
         explicit_api_key: [redacted:secret-assignment] = None,
         explicit_base_url: Optional[str] = None,
    +    target_model: Optional[str] = None,
     ) -> Dict[str, Any]:
    -    """Resolve runtime provider credentials for agent execution."""
    +    """Resolve runtime provider credentials for agent execution.
    +
    +    target_model: Optional override for model_cfg.get("default") when
    +    computing provider-specific api_mode (e.g. OpenCode Zen/Go where different
    +    models route through different API surfaces). Callers performing an
    +    explicit mid-session model switch should pass the new model here so
    +    api_mode is derived from the model they are switching TO, not the stale
    +    persisted default. Other callers can leave it None to preserve existing
    +    behavior (api_mode derived from config).
    +    """
         requested_provider = resolve_requested_provider(requested)
     
         custom_runtime = _resolve_named_custom_runtime(
    ```

    ```diff
    @@ -780,6 +798,7 @@ def resolve_runtime_provider(
                     requested_provider=requested_provider,
                     model_cfg=model_cfg,
                     pool=pool,
    +                target_model=target_model,
                 )
     
         if provider == "nous":
    ```

    ```diff
    @@ -998,7 +1017,11 @@ def resolve_runtime_provider(
                     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", ""))
    +                # Prefer the target_model from the caller (explicit mid-session
    +                # switch) over the stale model.default; see _resolve_runtime_from_pool_entry
    +                # for the same rationale.
    +                _effective = target_model or model_cfg.get("default", "")
    +                api_mode = opencode_model_api_mode(provider, _effective)
                 else:
                     # Auto-detect Anthropic-compatible endpoints by URL convention
                     # (e.g. https://api.minimax.io/anthropic, https://dashscope.../anthropic)
    ```
  - `tests/hermes_cli/test_model_switch_custom_providers.py`

    ```diff
    @@ -69,7 +69,7 @@ def test_switch_model_accepts_explicit_named_custom_provider(monkeypatch):
         """Shared /model switch pipeline should accept --provider for custom_providers."""
         monkeypatch.setattr(
             "hermes_cli.runtime_provider.resolve_runtime_provider",
    -        lambda requested: {
    +        lambda **kwargs: {
                 "api_key": "no-key-required",
                 "base_url": "http://127.0.0.1:4141/v1",
                 "api_mode": "chat_completions",
    ```
  - `tests/hermes_cli/test_model_switch_opencode_anthropic.py`

    ```diff
    @@ -250,3 +250,126 @@ def _raise_after_capture(api_key, base_url, **kwargs):
                 f"agent.switch_model did not strip /v1; passed {captured.get('base_url')} "
                 "to build_anthropic_client"
             )
    +
    +
    +
    +class TestStaleConfigDefaultDoesNotWedgeResolver:
    +    """Regression for the real bug Quentin hit.
    +
    +    When ``model.default`` in config.yaml is an OpenCode Anthropic-routed model
    +    (e.g. ``claude-sonnet-4-6`` on opencode-zen) and the user does ``/model
    +    kimi-k2.6 --provider opencode-zen`` session-only, the resolver must derive
    +    api_mode from the model being requested, not the persisted default. The
    +    earlier bug computed api_mode from ``model_cfg.get("default")``, flipped it
    +    to ``anthropic_messages`` based on the stale Claude default, and stripped
    +    ``/v1``. The chat_completions override in switch_model() fixed api_mode but
    +    never re-added ``/v1``, so requests landed on ``https://opencode.ai/zen``
    +    and got OpenCode's website 404 HTML page.
    +
    +    These tests use the REAL ``resolve_runtime_provider`` (not a mock) so a
    +    regression in the target_model plumbing surfaces immediately.
    +    """
    +
    +    def test_kimi_switch_keeps_v1_despite_claude_config_default(self, tmp_path, monkeypatch):
    +        import yaml
    +        import importlib
    +
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +        monkeypatch.setenv("OPENCODE_ZEN_API_KEY", "test-key")
    +        (tmp_path / "config.yaml").write_text(yaml.safe_dump({
    +            "model": {"provider": "opencode-zen", "default": "claude-sonnet-4-6"},
    +        }))
    +
    +        # Re-import with the new HERMES_HOME so config cache is fresh.
    +        import hermes_cli.config as _cfg_mod
    +        importlib.reload(_cfg_mod)
    +        import hermes_cli.runtime_provider as _rp_mod
    +        importlib.reload(_rp_mod)
    +        import hermes_cli.model_switch as _ms_mod
    +        importlib.reload(_ms_mod)
    +
    +        result = _ms_mod.switch_model(
    +            raw_input="kimi-k2.6",
    +            current_provider="opencode-zen",
    +            current_model="claude-sonnet-4-6",
    +            current_base_url="https://opencode.ai/zen",  # stripped from prior claude turn
    +            current_api_key="[redacted:secret-assignment]",
    +            is_global=False,
    +            explicit_provider="opencode-zen",
    +        )
    +
    +        assert result.success, f"switch failed: {result.error_message}"
    +        assert result.base_url == "https://opencode.ai/zen/v1", (
    +            f"base_url wedged at {result.base_url!r} - stale Claude config.default "
    +            "caused api_mode to be computed as anthropic_messages, stripping /v1, "
    +            "and chat_completions override never re-added it."
    +        )
    +        assert result.api_mode == "chat_completions"
    +
    +    def test_go_glm_switch_keeps_v1_despite_minimax_config_default(self, tmp_path, monkeypatch):
    +        import yaml
    +        import importlib
    +
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +        monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-key")
    +        monkeypatch.delenv("OPENCODE_ZEN_API_KEY", raising=False)
    +        (tmp_path / "config.yaml").write_text(yaml.safe_dump({
    +            "model": {"provider": "opencode-go", "default": "minimax-m2.7"},
    +        }))
    +
    +        import hermes_cli.config as _cfg_mod
    +        importlib.reload(_cfg_mod)
    +        import hermes_cli.runtime_provider as _rp_mod
    +        importlib.reload(_rp_mod)
    +        import hermes_cli.model_switch as _ms_mod
    +        importlib.reload(_ms_mod)
    +
    +        result = _ms_mod.switch_model(
    +            raw_input="glm-5.1",
    +            current_provider="opencode-go",
    +            current_model="minimax-m2.7",
    +            current_base_url="https://opencode.ai/zen/go",  # stripped from prior minimax turn
    +            current_api_key="[redacted:secret-assignment]",
    +            is_global=False,
    +            explicit_provider="opencode-go",
    +        )
    +
    +        assert result.success, f"switch failed: {result.error_message}"
    +        assert result.base_url == "https://opencode.ai/zen/go/v1"
    +        assert result.api_mode == "chat_completions"
    +
    +    def test_claude_switch_still_strips_v1_with_kimi_config_default(self, tmp_path, monkeypatch):
    +        """Inverse case: config default is chat_completions, switch TO anthropic_messages.
    +
    +        Guards that the target_model plumbing does not break the original
    +        strip-for-anthropic behavior.
    +        """
    +        import yaml
    +        import importlib
    +
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +        monkeypatch.setenv("OPENCODE_ZEN_API_KEY", "test-key")
    +        (tmp_path / "config.yaml").write_text(yaml.safe_dump({
    +            "model": {"provider": "opencode-zen", "default": "kimi-k2.6"},
    +        }))
    +
    +        import hermes_cli.config as _cfg_mod
    +        importlib.reload(_cfg_mod)
    +        import hermes_cli.runtime_provider as _rp_mod
    +        importlib.reload(_rp_mod)
    +        import hermes_cli.model_switch as _ms_mod
    +        importlib.reload(_ms_mod)
    +
    +        result = _ms_mod.switch_model(
    +            raw_input="claude-sonnet-4-6",
    +            current_provider="opencode-zen",
    +            current_model="kimi-k2.6",
    +            current_base_url="https://opencode.ai/zen/v1",
    +            current_api_key="[redacted:secret-assignment]",
    +            is_global=False,
    +            explicit_provider="opencode-zen",
    +        )
    +
    +        assert result.success, f"switch failed: {result.error_message}"
    +        assert result.base_url == "https://opencode.ai/zen"
    +        assert result.api_mode == "anthropic_messages"
    ```
  - `tests/hermes_cli/test_ollama_cloud_auth.py`

    ```diff
    @@ -518,7 +518,7 @@ def test_switch_model_uses_alias_base_url(self, monkeypatch):
     
             monkeypatch.setattr(
                 "hermes_cli.runtime_provider.resolve_runtime_provider",
    -            lambda requested: {"api_key": "", "base_url": "", "api_mode": "openai_compat", "provider": "custom"},
    +            lambda **kwargs: {"api_key": "", "base_url": "", "api_mode": "openai_compat", "provider": "custom"},
             )
     
             monkeypatch.setattr("hermes_cli.models.validate_requested_model",
    ```

    ```diff
    @@ -544,7 +544,7 @@ def test_switch_model_alias_no_api_key_gets_default(self, monkeypatch):
                 lambda raw, prov: ("custom", "local-model", "local"))
             monkeypatch.setattr(
                 "hermes_cli.runtime_provider.resolve_runtime_provider",
    -            lambda requested: {"api_key": "", "base_url": "", "api_mode": "openai_compat", "provider": "custom"},
    +            lambda **kwargs: {"api_key": "", "base_url": "", "api_mode": "openai_compat", "provider": "custom"},
             )
             monkeypatch.setattr("hermes_cli.models.validate_requested_model",
                 lambda *a, **kw: {"accepted": True, "persist": True, "recognized": True, "message": None})
    ```
- **#13334: fix(opencode): route kimi through chat completions**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `1eb29e645`, `#15106`, `#15106`

## Fix status

- Merged to main in PR #4508 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-24`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `agent`
