Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,15 +790,21 @@ def list_authenticated_providers(
})
seen_slugs.add(slug)

# --- 2. Check Hermes-only providers (nous, openai-codex, copilot) ---
# --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---
from hermes_cli.providers import HERMES_OVERLAYS
from hermes_cli.auth import PROVIDER_REGISTRY as _auth_registry
for pid, overlay in HERMES_OVERLAYS.items():
if pid in seen_slugs:
continue
# Check if credentials exist
has_creds = False
if overlay.extra_env_vars:
has_creds = any(os.environ.get(ev) for ev in overlay.extra_env_vars)
# Also check api_key_env_vars from PROVIDER_REGISTRY for api_key auth_type
if not has_creds and overlay.auth_type == "api_key":
pcfg = _auth_registry.get(pid)
if pcfg and pcfg.api_key_env_vars:
has_creds = any(os.environ.get(ev) for ev in pcfg.api_key_env_vars)
if overlay.auth_type in ("oauth_device_code", "oauth_external", "external_process"):
# These use auth stores, not env vars — check for auth.json entries
try:
Expand Down
4 changes: 4 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,9 @@ def __init__(
except (TypeError, ValueError):
_config_context_length = None

# Store for reuse in switch_model (so config override persists across model switches)
self._config_context_length = _config_context_length

# Check custom_providers per-model context_length
if _config_context_length is None:
_custom_providers = _agent_cfg.get("custom_providers")
Expand Down Expand Up @@ -1383,6 +1386,7 @@ def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mod
base_url=self.base_url,
api_key=self.api_key,
provider=self.provider,
config_context_length=getattr(self, "_config_context_length", None),
)
self.context_compressor.model = self.model
self.context_compressor.base_url = self.base_url
Expand Down
33 changes: 33 additions & 0 deletions tests/hermes_cli/test_opencode_go_in_model_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Test that opencode-go appears in /model list when credentials are set."""

import os
from unittest.mock import patch

from hermes_cli.model_switch import list_authenticated_providers


@patch.dict(os.environ, {"OPENCODE_GO_API_KEY": "test-key"}, clear=False)
def test_opencode_go_appears_when_api_key_set():
"""opencode-go should appear in list_authenticated_providers when OPENCODE_GO_API_KEY is set."""
providers = list_authenticated_providers(current_provider="openrouter")

# Find opencode-go in results
opencode_go = next((p for p in providers if p["slug"] == "opencode-go"), None)

assert opencode_go is not None, "opencode-go should appear when OPENCODE_GO_API_KEY is set"
assert opencode_go["models"] == ["glm-5", "kimi-k2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5"]
# opencode-go is in PROVIDER_TO_MODELS_DEV, so it appears as "built-in" (Part 1)
assert opencode_go["source"] == "built-in"


def test_opencode_go_not_appears_when_no_creds():
"""opencode-go should NOT appear when no credentials are set."""
# Ensure OPENCODE_GO_API_KEY is not set
env_without_key = {k: v for k, v in os.environ.items() if k != "OPENCODE_GO_API_KEY"}

with patch.dict(os.environ, env_without_key, clear=True):
providers = list_authenticated_providers(current_provider="openrouter")

# opencode-go should not be in results
opencode_go = next((p for p in providers if p["slug"] == "opencode-go"), None)
assert opencode_go is None, "opencode-go should not appear without credentials"
74 changes: 74 additions & 0 deletions tests/run_agent/test_switch_model_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Tests that switch_model preserves config_context_length."""

from unittest.mock import MagicMock, patch

from run_agent import AIAgent
from agent.context_compressor import ContextCompressor


def _make_agent_with_compressor(config_context_length=None) -> AIAgent:
"""Build a minimal AIAgent with a context_compressor, skipping __init__."""
agent = AIAgent.__new__(AIAgent)

# Primary model settings
agent.model = "primary-model"
agent.provider = "openrouter"
agent.base_url = "https://openrouter.ai/api/v1"
agent.api_key = "sk-primary"
agent.api_mode = "chat_completions"
agent.client = MagicMock()
agent.quiet_mode = True

# Store config_context_length for later use in switch_model
agent._config_context_length = config_context_length

# Context compressor with primary model values
compressor = ContextCompressor(
model="primary-model",
threshold_percent=0.50,
base_url="https://openrouter.ai/api/v1",
api_key="sk-primary",
provider="openrouter",
quiet_mode=True,
config_context_length=config_context_length,
)
agent.context_compressor = compressor

# For switch_model
agent._primary_runtime = {}

return agent


@patch("agent.model_metadata.get_model_context_length", return_value=131_072)
def test_switch_model_preserves_config_context_length(mock_ctx_len):
"""When switching models, config_context_length should be passed to get_model_context_length."""
agent = _make_agent_with_compressor(config_context_length=32_768)

assert agent.context_compressor.model == "primary-model"
assert agent.context_compressor.context_length == 32_768 # From config override

# Switch model
agent.switch_model("new-model", "openrouter", api_key="sk-new", base_url="https://openrouter.ai/api/v1")

# Verify get_model_context_length was called with config_context_length
mock_ctx_len.assert_called_once()
call_kwargs = mock_ctx_len.call_args.kwargs
assert call_kwargs.get("config_context_length") == 32_768

# Verify compressor was updated
assert agent.context_compressor.model == "new-model"


def test_switch_model_without_config_context_length():
"""When switching models without config override, config_context_length should be None."""
agent = _make_agent_with_compressor(config_context_length=None)

with patch("agent.model_metadata.get_model_context_length", return_value=128_000) as mock_ctx_len:
# Switch model
agent.switch_model("new-model", "openrouter", api_key="sk-new", base_url="https://openrouter.ai/api/v1")

# Verify get_model_context_length was called with None
mock_ctx_len.assert_called_once()
call_kwargs = mock_ctx_len.call_args.kwargs
assert call_kwargs.get("config_context_length") is None