mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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:
parent
c1809e85e7
commit
e3ffe5b75f
12 changed files with 58 additions and 224 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
1
cli.py
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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", ""),
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue