fix: CI test failures — metadata key, cli console, docker env, vision order

Fixes 9 test failures on current main, incorporating ideas from PR stack
#6219-#6222 by xinbenlv with corrections:

- model_metadata: sync HF context length key casing
  (minimaxai/minimax-m2.5 → MiniMaxAI/MiniMax-M2.5)

- cli.py: route quick command error output through self.console
  instead of creating a new ChatConsole() instance

- docker.py: explicit docker_forward_env entries now bypass the
  Hermes secret blocklist (intentional opt-in wins over generic filter)

- auxiliary_client: revert _read_main_provider() to simple
  provider.strip().lower() — the _normalize_aux_provider() call
  introduced in 5c03f2e7 stripped the custom: prefix, breaking
  named custom provider resolution

- auxiliary_client: flip vision auto-detection order to
  active provider → OpenRouter → Nous → stop (was OR → Nous → active)

- test: update vision priority test to match new order

Based on PR #6219-#6222 by xinbenlv.
This commit is contained in:
Teknium
2026-04-08 14:04:00 -07:00
parent 105caa001b
commit adab577812
5 changed files with 64 additions and 45 deletions

View File

@@ -834,7 +834,7 @@ def _read_main_provider() -> str:
if isinstance(model_cfg, dict):
provider = model_cfg.get("provider", "")
if isinstance(provider, str) and provider.strip():
return _normalize_aux_provider(provider)
return provider.strip().lower()
except Exception:
pass
return ""
@@ -1470,19 +1470,25 @@ def _preferred_main_vision_provider() -> Optional[str]:
def get_available_vision_backends() -> List[str]:
"""Return the currently available vision backends in auto-selection order.
Order: OpenRouter → Nous → active provider. This is the single source
of truth for setup, tool gating, and runtime auto-routing of vision tasks.
Order: active provider → OpenRouter → Nous → stop. This is the single
source of truth for setup, tool gating, and runtime auto-routing of
vision tasks.
"""
available = [p for p in _VISION_AUTO_PROVIDER_ORDER
if _strict_vision_backend_available(p)]
# Also check the user's active provider (may be DeepSeek, Alibaba, named
# custom, etc.) — resolve_provider_client handles all provider types.
available: List[str] = []
# 1. Active provider — if the user configured a provider, try it first.
main_provider = _read_main_provider()
if (main_provider and main_provider not in ("auto", "")
and main_provider not in available):
client, _ = resolve_provider_client(main_provider, _read_main_model())
if client is not None:
available.append(main_provider)
if main_provider and main_provider not in ("auto", ""):
if main_provider in _VISION_AUTO_PROVIDER_ORDER:
if _strict_vision_backend_available(main_provider):
available.append(main_provider)
else:
client, _ = resolve_provider_client(main_provider, _read_main_model())
if client is not None:
available.append(main_provider)
# 2. OpenRouter, 3. Nous — skip if already covered by main provider.
for p in _VISION_AUTO_PROVIDER_ORDER:
if p not in available and _strict_vision_backend_available(p):
available.append(p)
return available
@@ -1529,28 +1535,37 @@ def resolve_vision_provider_client(
if requested == "auto":
# Vision auto-detection order:
# 1. OpenRouter (known vision-capable default model)
# 2. Nous Portal (known vision-capable default model)
# 3. Active provider + model (user's main chat config)
# 1. Active provider + model (user's main chat config)
# 2. OpenRouter (known vision-capable default model)
# 3. Nous Portal (known vision-capable default model)
# 4. Stop
for candidate in _VISION_AUTO_PROVIDER_ORDER:
sync_client, default_model = _resolve_strict_vision_backend(candidate)
if sync_client is not None:
return _finalize(candidate, sync_client, default_model)
# Fall back to the user's active provider + model.
main_provider = _read_main_provider()
main_model = _read_main_model()
if main_provider and main_provider not in ("auto", ""):
sync_client, resolved_model = resolve_provider_client(
main_provider, main_model)
if main_provider in _VISION_AUTO_PROVIDER_ORDER:
# Known strict backend — use its defaults.
sync_client, default_model = _resolve_strict_vision_backend(main_provider)
if sync_client is not None:
return _finalize(main_provider, sync_client, default_model)
else:
# Exotic provider (DeepSeek, Alibaba, named custom, etc.)
rpc_client, rpc_model = resolve_provider_client(
main_provider, main_model)
if rpc_client is not None:
logger.info(
"Vision auto-detect: using active provider %s (%s)",
main_provider, rpc_model or main_model,
)
return _finalize(
main_provider, rpc_client, rpc_model or main_model)
# Fall back through aggregators.
for candidate in _VISION_AUTO_PROVIDER_ORDER:
if candidate == main_provider:
continue # already tried above
sync_client, default_model = _resolve_strict_vision_backend(candidate)
if sync_client is not None:
logger.info(
"Vision auto-detect: using active provider %s (%s)",
main_provider, resolved_model or main_model,
)
return _finalize(
main_provider, sync_client, resolved_model or main_model)
return _finalize(candidate, sync_client, default_model)
logger.debug("Auxiliary vision client: none available")
return None, None, None

View File

@@ -136,7 +136,7 @@ DEFAULT_CONTEXT_LENGTHS = {
"deepseek-ai/DeepSeek-V3.2": 65536,
"moonshotai/Kimi-K2.5": 262144,
"moonshotai/Kimi-K2-Thinking": 262144,
"minimaxai/minimax-m2.5": 1048576,
"MiniMaxAI/MiniMax-M2.5": 1048576,
"XiaomiMiMo/MiMo-V2-Flash": 32768,
"mimo-v2-pro": 1048576,
"mimo-v2-omni": 1048576,

12
cli.py
View File

@@ -4668,13 +4668,13 @@ class HermesCLI:
if output:
self.console.print(_rich_text_from_ansi(output))
else:
ChatConsole().print("[dim]Command returned no output[/]")
self.console.print("[dim]Command returned no output[/]")
except subprocess.TimeoutExpired:
ChatConsole().print("[bold red]Quick command timed out (30s)[/]")
self.console.print("[bold red]Quick command timed out (30s)[/]")
except Exception as e:
ChatConsole().print(f"[bold red]Quick command error: {e}[/]")
self.console.print(f"[bold red]Quick command error: {e}[/]")
else:
ChatConsole().print(f"[bold red]Quick command '{base_cmd}' has no command defined[/]")
self.console.print(f"[bold red]Quick command '{base_cmd}' has no command defined[/]")
elif qcmd.get("type") == "alias":
target = qcmd.get("target", "").strip()
if target:
@@ -4683,9 +4683,9 @@ class HermesCLI:
aliased_command = f"{target} {user_args}".strip()
return self.process_command(aliased_command)
else:
ChatConsole().print(f"[bold red]Quick command '{base_cmd}' has no target defined[/]")
self.console.print(f"[bold red]Quick command '{base_cmd}' has no target defined[/]")
else:
ChatConsole().print(f"[bold red]Quick command '{base_cmd}' has unsupported type (supported: 'exec', 'alias')[/]")
self.console.print(f"[bold red]Quick command '{base_cmd}' has unsupported type (supported: 'exec', 'alias')[/]")
# Check for plugin-registered slash commands
elif base_cmd.lstrip("/") in _get_plugin_cmd_handler_names():
from hermes_cli.plugins import get_plugin_command_handler

View File

@@ -737,8 +737,8 @@ class TestAuxiliaryPoolAwareness:
assert client is not None
assert client.__class__.__name__ == "AnthropicAuxiliaryClient"
def test_vision_auto_prefers_openrouter_over_active_provider(self, monkeypatch):
"""OpenRouter is tried before the active provider in vision auto."""
def test_vision_auto_prefers_active_provider_over_openrouter(self, monkeypatch):
"""Active provider is tried before OpenRouter in vision auto."""
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
monkeypatch.setenv("ANTHROPIC_API_KEY", "***")
@@ -746,12 +746,13 @@ class TestAuxiliaryPoolAwareness:
patch("agent.auxiliary_client._read_nous_auth", return_value=None),
patch("agent.auxiliary_client._read_main_provider", return_value="anthropic"),
patch("agent.auxiliary_client._read_main_model", return_value="claude-sonnet-4"),
patch("agent.auxiliary_client.OpenAI") as mock_openai,
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()),
patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="***"),
):
provider, client, model = resolve_vision_provider_client()
# OpenRouter should win over anthropic active provider
assert provider == "openrouter"
# Active provider should win over OpenRouter
assert provider == "anthropic"
def test_vision_auto_uses_named_custom_as_active_provider(self, monkeypatch):
"""Named custom provider works as active provider fallback in vision auto."""

View File

@@ -505,14 +505,17 @@ class DockerEnvironment(BaseEnvironment):
# (dynamic from host process). Forward values take precedence.
exec_env: dict[str, str] = dict(self._env)
forward_keys = set(self._forward_env)
explicit_forward_keys = set(self._forward_env)
passthrough_keys: set[str] = set()
try:
from tools.env_passthrough import get_all_passthrough
forward_keys |= get_all_passthrough()
passthrough_keys = set(get_all_passthrough())
except Exception:
pass
# Strip Hermes-managed secrets so they never leak into the container.
forward_keys -= _HERMES_PROVIDER_ENV_BLOCKLIST
# Explicit docker_forward_env entries are an intentional opt-in and must
# win over the generic Hermes secret blocklist. Only implicit passthrough
# keys are filtered.
forward_keys = explicit_forward_keys | (passthrough_keys - _HERMES_PROVIDER_ENV_BLOCKLIST)
hermes_env = _load_hermes_env_vars() if forward_keys else {}
for key in sorted(forward_keys):
value = os.getenv(key)