fix: remove legacy compression.summary_* config and env var fallbacks (#8992)

Remove the backward-compat code paths that read compression provider/model
settings from legacy config keys and env vars, which caused silent failures
when auto-detection resolved to incompatible backends.

What changed:
- Remove compression.summary_model, summary_provider, summary_base_url from
  DEFAULT_CONFIG and cli.py defaults
- Remove backward-compat block in _resolve_task_provider_model() that read
  from the legacy compression section
- Remove _get_auxiliary_provider() and _get_auxiliary_env_override() helper
  functions (AUXILIARY_*/CONTEXT_* env var readers)
- Remove env var fallback chain for per-task overrides
- Update hermes config show to read from auxiliary.compression
- Add config migration (v16→17) that moves non-empty legacy values to
  auxiliary.compression and strips the old keys
- Update example config and openclaw migration script
- Remove/update tests for deleted code paths

Compression model/provider is now configured exclusively via:
  auxiliary.compression.provider / auxiliary.compression.model

Closes #8923
This commit is contained in:
Teknium 2026-04-13 04:59:26 -07:00 committed by GitHub
parent c1809e85e7
commit e3ffe5b75f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 58 additions and 224 deletions

View file

@ -27,10 +27,6 @@ Per-task overrides are configured in config.yaml under the ``auxiliary:`` sectio
(e.g. ``auxiliary.vision.provider``, ``auxiliary.compression.model``). (e.g. ``auxiliary.vision.provider``, ``auxiliary.compression.model``).
Default "auto" follows the chains above. Default "auto" follows the chains above.
Legacy env var overrides (AUXILIARY_{TASK}_PROVIDER, AUXILIARY_{TASK}_MODEL,
AUXILIARY_{TASK}_BASE_URL, etc.) are still read as a backward-compat fallback
but config.yaml takes priority. New configuration should always use config.yaml.
Payment / credit exhaustion fallback: Payment / credit exhaustion fallback:
When a resolved provider returns HTTP 402 or a credit-related error, When a resolved provider returns HTTP 402 or a credit-related error,
call_llm() automatically retries with the next available provider in the call_llm() automatically retries with the next available provider in the
@ -753,30 +749,6 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
# ── Provider resolution helpers ───────────────────────────────────────────── # ── Provider resolution helpers ─────────────────────────────────────────────
def _get_auxiliary_provider(task: str = "") -> str:
"""Read the provider override for a specific auxiliary task.
Checks AUXILIARY_{TASK}_PROVIDER first (e.g. AUXILIARY_VISION_PROVIDER),
then CONTEXT_{TASK}_PROVIDER (for the compression section's summary_provider),
then falls back to "auto". Returns one of: "auto", "openrouter", "nous", "main".
"""
if task:
for prefix in ("AUXILIARY_", "CONTEXT_"):
val = os.getenv(f"{prefix}{task.upper()}_PROVIDER", "").strip().lower()
if val and val != "auto":
return val
return "auto"
def _get_auxiliary_env_override(task: str, suffix: str) -> Optional[str]:
"""Read an auxiliary env override from AUXILIARY_* or CONTEXT_* prefixes."""
if not task:
return None
for prefix in ("AUXILIARY_", "CONTEXT_"):
val = os.getenv(f"{prefix}{task.upper()}_{suffix}", "").strip()
if val:
return val
return None
def _try_openrouter() -> Tuple[Optional[OpenAI], Optional[str]]: def _try_openrouter() -> Tuple[Optional[OpenAI], Optional[str]]:
@ -1591,8 +1563,8 @@ def get_text_auxiliary_client(
task: Optional task name ("compression", "web_extract") to check task: Optional task name ("compression", "web_extract") to check
for a task-specific provider override. for a task-specific provider override.
Callers may override the returned model with a per-task env var Callers may override the returned model via config.yaml
(e.g. CONTEXT_COMPRESSION_MODEL, AUXILIARY_WEB_EXTRACT_MODEL). (e.g. auxiliary.compression.model, auxiliary.web_extract.model).
""" """
provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(task or None) provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(task or None)
return resolve_provider_client( return resolve_provider_client(
@ -2011,9 +1983,8 @@ def _resolve_task_provider_model(
Priority: Priority:
1. Explicit provider/model/base_url/api_key args (always win) 1. Explicit provider/model/base_url/api_key args (always win)
2. Config file (auxiliary.{task}.* or compression.*) 2. Config file (auxiliary.{task}.provider/model/base_url)
3. Env var overrides (backward-compat: AUXILIARY_{TASK}_*, CONTEXT_{TASK}_*) 3. "auto" (full auto-detection chain)
4. "auto" (full auto-detection chain)
Returns (provider, model, base_url, api_key, api_mode) where model may Returns (provider, model, base_url, api_key, api_mode) where model may
be None (use provider default). When base_url is set, provider is forced be None (use provider default). When base_url is set, provider is forced
@ -2044,22 +2015,8 @@ def _resolve_task_provider_model(
cfg_api_key = str(task_config.get("api_key", "")).strip() or None cfg_api_key = str(task_config.get("api_key", "")).strip() or None
cfg_api_mode = str(task_config.get("api_mode", "")).strip() or None cfg_api_mode = str(task_config.get("api_mode", "")).strip() or None
# Backwards compat: compression section has its own keys. resolved_model = model or cfg_model
# The auxiliary.compression defaults to provider="auto", so treat resolved_api_mode = cfg_api_mode
# both None and "auto" as "not explicitly configured".
if task == "compression" and (not cfg_provider or cfg_provider == "auto"):
comp = config.get("compression", {}) if isinstance(config, dict) else {}
if isinstance(comp, dict):
cfg_provider = comp.get("summary_provider", "").strip() or None
cfg_model = cfg_model or comp.get("summary_model", "").strip() or None
_sbu = comp.get("summary_base_url") or ""
cfg_base_url = cfg_base_url or _sbu.strip() or None
# Env vars are backward-compat fallback only — config.yaml is primary.
env_model = _get_auxiliary_env_override(task, "MODEL") if task else None
env_api_mode = _get_auxiliary_env_override(task, "API_MODE") if task else None
resolved_model = model or cfg_model or env_model
resolved_api_mode = cfg_api_mode or env_api_mode
if base_url: if base_url:
return "custom", resolved_model, base_url, api_key, resolved_api_mode return "custom", resolved_model, base_url, api_key, resolved_api_mode
@ -2073,17 +2030,6 @@ def _resolve_task_provider_model(
if cfg_provider and cfg_provider != "auto": if cfg_provider and cfg_provider != "auto":
return cfg_provider, resolved_model, None, None, resolved_api_mode return cfg_provider, resolved_model, None, None, resolved_api_mode
# Env vars are backward-compat fallback for users who haven't
# migrated to config.yaml yet.
env_base_url = _get_auxiliary_env_override(task, "BASE_URL")
env_api_key = _get_auxiliary_env_override(task, "API_KEY")
if env_base_url:
return "custom", resolved_model, env_base_url, env_api_key, resolved_api_mode
env_provider = _get_auxiliary_provider(task)
if env_provider != "auto":
return env_provider, resolved_model, None, None, resolved_api_mode
return "auto", resolved_model, None, None, resolved_api_mode return "auto", resolved_model, None, None, resolved_api_mode
return "auto", resolved_model, None, None, resolved_api_mode return "auto", resolved_model, None, None, resolved_api_mode

View file

@ -309,15 +309,8 @@ compression:
# compression of older turns. # compression of older turns.
protect_last_n: 20 protect_last_n: 20
# Model to use for generating summaries (fast/cheap recommended) # To pin a specific model/provider for compression summaries, use the
# This model compresses the middle turns into a concise summary. # auxiliary section below (auxiliary.compression.provider / model).
# IMPORTANT: it receives the full middle section of the conversation, so it
# MUST support a context length at least as large as your main model's.
summary_model: "google/gemini-3-flash-preview"
# Provider for the summary model (default: "auto")
# Options: "auto", "openrouter", "nous", "main"
# summary_provider: "auto"
# ============================================================================= # =============================================================================
# Auxiliary Models (Advanced — Experimental) # Auxiliary Models (Advanced — Experimental)

1
cli.py
View file

@ -237,7 +237,6 @@ def load_cli_config() -> Dict[str, Any]:
"compression": { "compression": {
"enabled": True, # Auto-compress when approaching context limit "enabled": True, # Auto-compress when approaching context limit
"threshold": 0.50, # Compress at 50% of model's context limit "threshold": 0.50, # Compress at 50% of model's context limit
"summary_model": "", # Model for summaries (empty = use main model)
}, },
"smart_model_routing": { "smart_model_routing": {
"enabled": False, "enabled": False,

View file

@ -414,9 +414,7 @@ DEFAULT_CONFIG = {
"threshold": 0.50, # compress when context usage exceeds this ratio "threshold": 0.50, # compress when context usage exceeds this ratio
"target_ratio": 0.20, # fraction of threshold to preserve as recent tail "target_ratio": 0.20, # fraction of threshold to preserve as recent tail
"protect_last_n": 20, # minimum recent messages to keep uncompressed "protect_last_n": 20, # minimum recent messages to keep uncompressed
"summary_model": "", # empty = use main configured model
"summary_provider": "auto",
"summary_base_url": None,
}, },
"smart_model_routing": { "smart_model_routing": {
"enabled": False, "enabled": False,
@ -702,7 +700,7 @@ DEFAULT_CONFIG = {
}, },
# Config schema version - bump this when adding new required fields # Config schema version - bump this when adding new required fields
"_config_version": 16, "_config_version": 17,
} }
# ============================================================================= # =============================================================================
@ -1975,6 +1973,43 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
print(f" ✓ Migrated tool_progress_overrides → display.platforms: {migrated}") print(f" ✓ Migrated tool_progress_overrides → display.platforms: {migrated}")
results["config_added"].append("display.platforms (migrated from tool_progress_overrides)") results["config_added"].append("display.platforms (migrated from tool_progress_overrides)")
# ── Version 16 → 17: remove legacy compression.summary_* keys ──
if current_ver < 17:
config = read_raw_config()
comp = config.get("compression", {})
if isinstance(comp, dict):
s_model = comp.pop("summary_model", None)
s_provider = comp.pop("summary_provider", None)
s_base_url = comp.pop("summary_base_url", None)
migrated_keys = []
# Migrate non-empty, non-default values to auxiliary.compression
if s_model and str(s_model).strip():
aux = config.setdefault("auxiliary", {})
aux_comp = aux.setdefault("compression", {})
if not aux_comp.get("model"):
aux_comp["model"] = str(s_model).strip()
migrated_keys.append(f"model={s_model}")
if s_provider and str(s_provider).strip() not in ("", "auto"):
aux = config.setdefault("auxiliary", {})
aux_comp = aux.setdefault("compression", {})
if not aux_comp.get("provider") or aux_comp.get("provider") == "auto":
aux_comp["provider"] = str(s_provider).strip()
migrated_keys.append(f"provider={s_provider}")
if s_base_url and str(s_base_url).strip():
aux = config.setdefault("auxiliary", {})
aux_comp = aux.setdefault("compression", {})
if not aux_comp.get("base_url"):
aux_comp["base_url"] = str(s_base_url).strip()
migrated_keys.append(f"base_url={s_base_url}")
if migrated_keys or s_model is not None or s_provider is not None or s_base_url is not None:
config["compression"] = comp
save_config(config)
if not quiet:
if migrated_keys:
print(f" ✓ Migrated compression.summary_* → auxiliary.compression: {', '.join(migrated_keys)}")
else:
print(" ✓ Removed unused compression.summary_* keys")
if current_ver < latest_ver and not quiet: if current_ver < latest_ver and not quiet:
print(f"Config version: {current_ver}{latest_ver}") print(f"Config version: {current_ver}{latest_ver}")
@ -2790,10 +2825,11 @@ def show_config():
print(f" Threshold: {compression.get('threshold', 0.50) * 100:.0f}%") print(f" Threshold: {compression.get('threshold', 0.50) * 100:.0f}%")
print(f" Target ratio: {compression.get('target_ratio', 0.20) * 100:.0f}% of threshold preserved") print(f" Target ratio: {compression.get('target_ratio', 0.20) * 100:.0f}% of threshold preserved")
print(f" Protect last: {compression.get('protect_last_n', 20)} messages") print(f" Protect last: {compression.get('protect_last_n', 20)} messages")
_sm = compression.get('summary_model', '') or '(main model)' _aux_comp = config.get('auxiliary', {}).get('compression', {})
_sm = _aux_comp.get('model', '') or '(auto)'
print(f" Model: {_sm}") print(f" Model: {_sm}")
comp_provider = compression.get('summary_provider', 'auto') comp_provider = _aux_comp.get('provider', 'auto')
if comp_provider != 'auto': if comp_provider and comp_provider != 'auto':
print(f" Provider: {comp_provider}") print(f" Provider: {comp_provider}")
# Auxiliary models # Auxiliary models

View file

@ -1995,7 +1995,9 @@ class Migrator:
if compaction.get("timeout"): if compaction.get("timeout"):
pass # No direct mapping pass # No direct mapping
if compaction.get("model"): if compaction.get("model"):
compression["summary_model"] = compaction["model"] aux = hermes_cfg.setdefault("auxiliary", {})
aux_comp = aux.setdefault("compression", {})
aux_comp["model"] = compaction["model"]
hermes_cfg["compression"] = compression hermes_cfg["compression"] = compression
changes = True changes = True

View file

@ -1212,7 +1212,6 @@ class AIAgent:
_compression_cfg = {} _compression_cfg = {}
compression_threshold = float(_compression_cfg.get("threshold", 0.50)) compression_threshold = float(_compression_cfg.get("threshold", 0.50))
compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in ("true", "1", "yes") compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in ("true", "1", "yes")
compression_summary_model = _compression_cfg.get("summary_model") or None
compression_target_ratio = float(_compression_cfg.get("target_ratio", 0.20)) compression_target_ratio = float(_compression_cfg.get("target_ratio", 0.20))
compression_protect_last = int(_compression_cfg.get("protect_last_n", 20)) compression_protect_last = int(_compression_cfg.get("protect_last_n", 20))
@ -1301,7 +1300,7 @@ class AIAgent:
protect_first_n=3, protect_first_n=3,
protect_last_n=compression_protect_last, protect_last_n=compression_protect_last,
summary_target_ratio=compression_target_ratio, summary_target_ratio=compression_target_ratio,
summary_model_override=compression_summary_model, summary_model_override=None,
quiet_mode=self.quiet_mode, quiet_mode=self.quiet_mode,
base_url=self.base_url, base_url=self.base_url,
api_key=getattr(self, "api_key", ""), api_key=getattr(self, "api_key", ""),

View file

@ -17,7 +17,6 @@ from agent.auxiliary_client import (
call_llm, call_llm,
async_call_llm, async_call_llm,
_read_codex_access_token, _read_codex_access_token,
_get_auxiliary_provider,
_get_provider_chain, _get_provider_chain,
_is_payment_error, _is_payment_error,
_try_payment_fallback, _try_payment_fallback,
@ -32,12 +31,6 @@ def _clean_env(monkeypatch):
"OPENROUTER_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_KEY",
"OPENAI_MODEL", "LLM_MODEL", "NOUS_INFERENCE_BASE_URL", "OPENAI_MODEL", "LLM_MODEL", "NOUS_INFERENCE_BASE_URL",
"ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN",
# Per-task provider/model/direct-endpoint overrides
"AUXILIARY_VISION_PROVIDER", "AUXILIARY_VISION_MODEL",
"AUXILIARY_VISION_BASE_URL", "AUXILIARY_VISION_API_KEY",
"AUXILIARY_WEB_EXTRACT_PROVIDER", "AUXILIARY_WEB_EXTRACT_MODEL",
"AUXILIARY_WEB_EXTRACT_BASE_URL", "AUXILIARY_WEB_EXTRACT_API_KEY",
"CONTEXT_COMPRESSION_PROVIDER", "CONTEXT_COMPRESSION_MODEL",
): ):
monkeypatch.delenv(key, raising=False) monkeypatch.delenv(key, raising=False)
@ -568,29 +561,6 @@ class TestGetTextAuxiliaryClient:
call_kwargs = mock_openai.call_args call_kwargs = mock_openai.call_args
assert call_kwargs.kwargs["base_url"] == "http://localhost:1234/v1" assert call_kwargs.kwargs["base_url"] == "http://localhost:1234/v1"
def test_task_direct_endpoint_override(self, monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_BASE_URL", "http://localhost:2345/v1")
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_API_KEY", "task-key")
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_MODEL", "task-model")
with patch("agent.auxiliary_client.OpenAI") as mock_openai:
client, model = get_text_auxiliary_client("web_extract")
assert model == "task-model"
assert mock_openai.call_args.kwargs["base_url"] == "http://localhost:2345/v1"
assert mock_openai.call_args.kwargs["api_key"] == "task-key"
def test_task_direct_endpoint_without_openai_key_uses_placeholder(self, monkeypatch):
"""Local endpoints without an API key should use 'no-key-required' placeholder."""
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_BASE_URL", "http://localhost:2345/v1")
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_MODEL", "task-model")
with patch("agent.auxiliary_client.OpenAI") as mock_openai:
client, model = get_text_auxiliary_client("web_extract")
assert client is not None
assert model == "task-model"
assert mock_openai.call_args.kwargs["api_key"] == "no-key-required"
assert mock_openai.call_args.kwargs["base_url"] == "http://localhost:2345/v1"
def test_custom_endpoint_uses_config_saved_base_url(self, monkeypatch): def test_custom_endpoint_uses_config_saved_base_url(self, monkeypatch):
config = { config = {
"model": { "model": {
@ -879,73 +849,9 @@ class TestAuxiliaryPoolAwareness:
class TestGetAuxiliaryProvider:
"""Tests for _get_auxiliary_provider env var resolution."""
def test_no_task_returns_auto(self):
assert _get_auxiliary_provider() == "auto"
assert _get_auxiliary_provider("") == "auto"
def test_auxiliary_prefix_takes_priority(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_VISION_PROVIDER", "openrouter")
assert _get_auxiliary_provider("vision") == "openrouter"
def test_context_prefix_fallback(self, monkeypatch):
monkeypatch.setenv("CONTEXT_COMPRESSION_PROVIDER", "nous")
assert _get_auxiliary_provider("compression") == "nous"
def test_auxiliary_prefix_over_context_prefix(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_COMPRESSION_PROVIDER", "openrouter")
monkeypatch.setenv("CONTEXT_COMPRESSION_PROVIDER", "nous")
assert _get_auxiliary_provider("compression") == "openrouter"
def test_auto_value_treated_as_auto(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_VISION_PROVIDER", "auto")
assert _get_auxiliary_provider("vision") == "auto"
def test_whitespace_stripped(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_VISION_PROVIDER", " openrouter ")
assert _get_auxiliary_provider("vision") == "openrouter"
def test_case_insensitive(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_VISION_PROVIDER", "OpenRouter")
assert _get_auxiliary_provider("vision") == "openrouter"
def test_main_provider(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_PROVIDER", "main")
assert _get_auxiliary_provider("web_extract") == "main"
class TestTaskSpecificOverrides: class TestTaskSpecificOverrides:
"""Integration tests for per-task provider routing via get_text_auxiliary_client(task=...).""" """Integration tests for per-task provider routing via get_text_auxiliary_client(task=...)."""
def test_text_with_vision_provider_override(self, monkeypatch):
"""AUXILIARY_VISION_PROVIDER should not affect text tasks."""
monkeypatch.setenv("AUXILIARY_VISION_PROVIDER", "nous")
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
with patch("agent.auxiliary_client.OpenAI"):
client, model = get_text_auxiliary_client() # no task → auto
assert model == "google/gemini-3-flash-preview" # OpenRouter, not Nous
def test_compression_task_reads_context_prefix(self, monkeypatch):
"""Compression task should check CONTEXT_COMPRESSION_PROVIDER env var."""
monkeypatch.setenv("CONTEXT_COMPRESSION_PROVIDER", "nous")
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") # would win in auto
with patch("agent.auxiliary_client._read_nous_auth") as mock_nous, \
patch("agent.auxiliary_client.OpenAI"):
mock_nous.return_value = {"access_token": "***"}
client, model = get_text_auxiliary_client("compression")
# Config-first: model comes from config.yaml summary_model default,
# but provider is forced to Nous via env var
assert client is not None
def test_web_extract_task_override(self, monkeypatch):
monkeypatch.setenv("AUXILIARY_WEB_EXTRACT_PROVIDER", "openrouter")
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
with patch("agent.auxiliary_client.OpenAI"):
client, model = get_text_auxiliary_client("web_extract")
assert model == "google/gemini-3-flash-preview"
def test_task_direct_endpoint_from_config(self, monkeypatch, tmp_path): def test_task_direct_endpoint_from_config(self, monkeypatch, tmp_path):
hermes_home = tmp_path / "hermes" hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True) hermes_home.mkdir(parents=True, exist_ok=True)
@ -979,8 +885,6 @@ class TestTaskSpecificOverrides:
"""model: """model:
default: glm-5.1 default: glm-5.1
provider: opencode-go provider: opencode-go
compression:
summary_provider: auto
""" """
) )
monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setenv("HERMES_HOME", str(hermes_home))
@ -1039,25 +943,6 @@ model:
"model": "gpt-5.4", "model": "gpt-5.4",
} }
def test_compression_summary_base_url_from_config(self, monkeypatch, tmp_path):
"""compression.summary_base_url should produce a custom-endpoint client."""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True)
(hermes_home / "config.yaml").write_text(
"""compression:
summary_provider: custom
summary_model: glm-4.7
summary_base_url: https://api.z.ai/api/coding/paas/v4
"""
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Custom endpoints need an API key to build the client
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
with patch("agent.auxiliary_client.OpenAI") as mock_openai:
client, model = get_text_auxiliary_client("compression")
assert model == "glm-4.7"
assert mock_openai.call_args.kwargs["base_url"] == "https://api.z.ai/api/coding/paas/v4"
class TestAuxiliaryMaxTokensParam: class TestAuxiliaryMaxTokensParam:
def test_codex_fallback_uses_max_tokens(self, monkeypatch): def test_codex_fallback_uses_max_tokens(self, monkeypatch):

View file

@ -273,18 +273,6 @@ class TestDefaultConfigShape:
assert web["provider"] == "auto" assert web["provider"] == "auto"
assert web["model"] == "" assert web["model"] == ""
def test_compression_provider_default(self):
from hermes_cli.config import DEFAULT_CONFIG
compression = DEFAULT_CONFIG["compression"]
assert "summary_provider" in compression
assert compression["summary_provider"] == "auto"
def test_compression_base_url_default(self):
from hermes_cli.config import DEFAULT_CONFIG
compression = DEFAULT_CONFIG["compression"]
assert "summary_base_url" in compression
assert compression["summary_base_url"] is None
# ── CLI defaults parity ───────────────────────────────────────────────────── # ── CLI defaults parity ─────────────────────────────────────────────────────

View file

@ -12,17 +12,6 @@ def _isolate(tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes" hermes_home = tmp_path / ".hermes"
hermes_home.mkdir() hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setenv("HERMES_HOME", str(hermes_home))
for env_var in (
"AUXILIARY_VISION_PROVIDER",
"AUXILIARY_VISION_MODEL",
"AUXILIARY_VISION_BASE_URL",
"AUXILIARY_VISION_API_KEY",
"CONTEXT_VISION_PROVIDER",
"CONTEXT_VISION_MODEL",
"CONTEXT_VISION_BASE_URL",
"CONTEXT_VISION_API_KEY",
):
monkeypatch.delenv(env_var, raising=False)
# Write a minimal config so load_config doesn't fail # Write a minimal config so load_config doesn't fail
(hermes_home / "config.yaml").write_text("model:\n default: test-model\n") (hermes_home / "config.yaml").write_text("model:\n default: test-model\n")

View file

@ -51,10 +51,10 @@ class TestSaveConfigValueAtomic:
def test_creates_nested_keys(self, config_env): def test_creates_nested_keys(self, config_env):
"""Dot-separated paths create intermediate dicts as needed.""" """Dot-separated paths create intermediate dicts as needed."""
from cli import save_config_value from cli import save_config_value
save_config_value("compression.summary_model", "google/gemini-3-flash-preview") save_config_value("auxiliary.compression.model", "google/gemini-3-flash-preview")
result = yaml.safe_load(config_env.read_text()) result = yaml.safe_load(config_env.read_text())
assert result["compression"]["summary_model"] == "google/gemini-3-flash-preview" assert result["auxiliary"]["compression"]["model"] == "google/gemini-3-flash-preview"
def test_overwrites_existing_value(self, config_env): def test_overwrites_existing_value(self, config_env):
"""Updating an existing key replaces the value.""" """Updating an existing key replaces the value."""

View file

@ -119,8 +119,7 @@ def test_toolset_has_keys_for_vision_accepts_codex_auth(tmp_path, monkeypatch):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False) monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False) monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("AUXILIARY_VISION_PROVIDER", raising=False)
monkeypatch.delenv("CONTEXT_VISION_PROVIDER", raising=False)
monkeypatch.setattr( monkeypatch.setattr(
"agent.auxiliary_client.resolve_vision_provider_client", "agent.auxiliary_client.resolve_vision_provider_client",
lambda: ("openai-codex", object(), "gpt-4.1"), lambda: ("openai-codex", object(), "gpt-4.1"),

View file

@ -463,8 +463,6 @@ class TestVisionRequirements:
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False) monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False) monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("AUXILIARY_VISION_PROVIDER", raising=False)
monkeypatch.delenv("CONTEXT_VISION_PROVIDER", raising=False)
assert check_vision_requirements() is True assert check_vision_requirements() is True