mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: restrict fallback providers to actual hermes providers
Remove hallucinated providers (openai, deepseek, together, groq, fireworks, mistral, gemini, nous) from the fallback provider map. These don't exist in hermes-agent's provider system. The real supported providers for fallback are: openrouter (OPENROUTER_API_KEY) zai (ZAI_API_KEY) kimi-coding (KIMI_API_KEY) minimax (MINIMAX_API_KEY) minimax-cn (MINIMAX_CN_API_KEY) For any other OpenAI-compatible endpoint, users can use the base_url + api_key_env overrides in the config. Also adds Kimi User-Agent header for kimi fallback (matching the main provider system).
This commit is contained in:
parent
4cfb66bac2
commit
b3765c28d0
3 changed files with 65 additions and 46 deletions
|
|
@ -108,21 +108,17 @@ DEFAULT_CONFIG = {
|
|||
# Hermes will automatically switch to this model for the remainder of the session.
|
||||
# Set to None / omit to disable fallback.
|
||||
#
|
||||
# Built-in providers (auto-resolve base_url and API key from env):
|
||||
# openrouter (OPENROUTER_API_KEY) — best fallback, routes to any model
|
||||
# openai (OPENAI_API_KEY) — GPT-4.1, o3, etc.
|
||||
# nous (NOUS_API_KEY) — Nous inference API
|
||||
# deepseek (DEEPSEEK_API_KEY) — DeepSeek models
|
||||
# together (TOGETHER_API_KEY) — Together AI
|
||||
# groq (GROQ_API_KEY) — Groq (fast inference)
|
||||
# fireworks (FIREWORKS_API_KEY) — Fireworks AI
|
||||
# mistral (MISTRAL_API_KEY) — Mistral models
|
||||
# gemini (GEMINI_API_KEY) — Google Gemini
|
||||
# Supported providers (auto-resolve base_url and API key from env):
|
||||
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
||||
# zai (ZAI_API_KEY) — Z.AI / GLM
|
||||
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
||||
# minimax (MINIMAX_API_KEY) — MiniMax
|
||||
# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China)
|
||||
#
|
||||
# For any other OpenAI-compatible endpoint, use base_url + api_key_env.
|
||||
"fallback_model": {
|
||||
"provider": "", # provider name from the list above
|
||||
"model": "", # model slug, e.g. "anthropic/claude-sonnet-4", "gpt-4.1"
|
||||
"model": "", # model slug, e.g. "anthropic/claude-sonnet-4"
|
||||
# Optional overrides (usually auto-resolved from provider):
|
||||
# "base_url": "", # custom endpoint URL
|
||||
# "api_key_env": "", # env var name for API key (e.g. "MY_CUSTOM_KEY")
|
||||
|
|
|
|||
16
run_agent.py
16
run_agent.py
|
|
@ -2161,16 +2161,14 @@ class AIAgent:
|
|||
# ── Provider fallback ──────────────────────────────────────────────────
|
||||
|
||||
# Maps provider id → (default_base_url, [env_var_names])
|
||||
# Only includes providers that Hermes actually supports.
|
||||
# For anything else, use base_url + api_key_env in the config.
|
||||
_FALLBACK_PROVIDERS = {
|
||||
"openrouter": (OPENROUTER_BASE_URL, ["OPENROUTER_API_KEY"]),
|
||||
"openai": ("https://api.openai.com/v1", ["OPENAI_API_KEY"]),
|
||||
"nous": ("https://inference-api.nousresearch.com/v1", ["NOUS_API_KEY"]),
|
||||
"deepseek": ("https://api.deepseek.com/v1", ["DEEPSEEK_API_KEY"]),
|
||||
"together": ("https://api.together.xyz/v1", ["TOGETHER_API_KEY"]),
|
||||
"groq": ("https://api.groq.com/openai/v1", ["GROQ_API_KEY"]),
|
||||
"fireworks": ("https://api.fireworks.ai/inference/v1", ["FIREWORKS_API_KEY"]),
|
||||
"mistral": ("https://api.mistral.ai/v1", ["MISTRAL_API_KEY"]),
|
||||
"gemini": ("https://generativelanguage.googleapis.com/v1beta/openai", ["GEMINI_API_KEY", "GOOGLE_API_KEY"]),
|
||||
"zai": ("https://api.z.ai/api/paas/v4", ["ZAI_API_KEY", "Z_AI_API_KEY"]),
|
||||
"kimi-coding": ("https://api.moonshot.ai/v1", ["KIMI_API_KEY"]),
|
||||
"minimax": ("https://api.minimax.io/v1", ["MINIMAX_API_KEY"]),
|
||||
"minimax-cn": ("https://api.minimaxi.com/v1", ["MINIMAX_CN_API_KEY"]),
|
||||
}
|
||||
|
||||
def _try_activate_fallback(self) -> bool:
|
||||
|
|
@ -2224,6 +2222,8 @@ class AIAgent:
|
|||
"X-OpenRouter-Title": "Hermes Agent",
|
||||
"X-OpenRouter-Categories": "productivity,cli-agent",
|
||||
}
|
||||
elif "api.kimi.com" in fb_base_url.lower():
|
||||
client_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
|
||||
|
||||
self.client = OpenAI(**client_kwargs)
|
||||
self._client_kwargs = client_kwargs
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class TestTryActivateFallback:
|
|||
assert agent._try_activate_fallback() is False
|
||||
|
||||
def test_returns_false_for_missing_model(self):
|
||||
agent = _make_agent(fallback_model={"provider": "openai"})
|
||||
agent = _make_agent(fallback_model={"provider": "openrouter"})
|
||||
assert agent._try_activate_fallback() is False
|
||||
|
||||
def test_activates_openrouter_fallback(self):
|
||||
|
|
@ -88,33 +88,47 @@ class TestTryActivateFallback:
|
|||
# OpenRouter should get attribution headers
|
||||
assert "default_headers" in call_kwargs
|
||||
|
||||
def test_activates_openai_fallback(self):
|
||||
def test_activates_zai_fallback(self):
|
||||
agent = _make_agent(
|
||||
fallback_model={"provider": "openai", "model": "gpt-4.1"},
|
||||
fallback_model={"provider": "zai", "model": "glm-5"},
|
||||
)
|
||||
with (
|
||||
patch.dict("os.environ", {"OPENAI_API_KEY": "sk-openai-key"}),
|
||||
patch.dict("os.environ", {"ZAI_API_KEY": "sk-zai-key"}),
|
||||
patch("run_agent.OpenAI") as mock_openai,
|
||||
):
|
||||
result = agent._try_activate_fallback()
|
||||
assert result is True
|
||||
assert agent.model == "gpt-4.1"
|
||||
assert agent.provider == "openai"
|
||||
assert agent.model == "glm-5"
|
||||
assert agent.provider == "zai"
|
||||
call_kwargs = mock_openai.call_args[1]
|
||||
assert call_kwargs["api_key"] == "sk-openai-key"
|
||||
assert "openai.com" in call_kwargs["base_url"]
|
||||
assert call_kwargs["api_key"] == "sk-zai-key"
|
||||
assert "z.ai" in call_kwargs["base_url"].lower()
|
||||
|
||||
def test_activates_deepseek_fallback(self):
|
||||
def test_activates_kimi_fallback(self):
|
||||
agent = _make_agent(
|
||||
fallback_model={"provider": "deepseek", "model": "deepseek-chat"},
|
||||
fallback_model={"provider": "kimi-coding", "model": "kimi-k2.5"},
|
||||
)
|
||||
with (
|
||||
patch.dict("os.environ", {"DEEPSEEK_API_KEY": "sk-ds-key"}),
|
||||
patch.dict("os.environ", {"KIMI_API_KEY": "sk-kimi-key"}),
|
||||
patch("run_agent.OpenAI"),
|
||||
):
|
||||
assert agent._try_activate_fallback() is True
|
||||
assert agent.model == "deepseek-chat"
|
||||
assert agent.provider == "deepseek"
|
||||
assert agent.model == "kimi-k2.5"
|
||||
assert agent.provider == "kimi-coding"
|
||||
|
||||
def test_activates_minimax_fallback(self):
|
||||
agent = _make_agent(
|
||||
fallback_model={"provider": "minimax", "model": "MiniMax-M2.5"},
|
||||
)
|
||||
with (
|
||||
patch.dict("os.environ", {"MINIMAX_API_KEY": "sk-mm-key"}),
|
||||
patch("run_agent.OpenAI") as mock_openai,
|
||||
):
|
||||
assert agent._try_activate_fallback() is True
|
||||
assert agent.model == "MiniMax-M2.5"
|
||||
assert agent.provider == "minimax"
|
||||
call_kwargs = mock_openai.call_args[1]
|
||||
assert "minimax.io" in call_kwargs["base_url"]
|
||||
|
||||
def test_only_fires_once(self):
|
||||
agent = _make_agent(
|
||||
|
|
@ -131,10 +145,10 @@ class TestTryActivateFallback:
|
|||
def test_returns_false_when_no_api_key(self):
|
||||
"""Fallback should fail gracefully when the API key env var is unset."""
|
||||
agent = _make_agent(
|
||||
fallback_model={"provider": "deepseek", "model": "deepseek-chat"},
|
||||
fallback_model={"provider": "minimax", "model": "MiniMax-M2.5"},
|
||||
)
|
||||
# Ensure DEEPSEEK_API_KEY is not in the environment
|
||||
env = {k: v for k, v in os.environ.items() if k != "DEEPSEEK_API_KEY"}
|
||||
# Ensure MINIMAX_API_KEY is not in the environment
|
||||
env = {k: v for k, v in os.environ.items() if k != "MINIMAX_API_KEY"}
|
||||
with patch.dict("os.environ", env, clear=True):
|
||||
assert agent._try_activate_fallback() is False
|
||||
assert agent._fallback_activated is False
|
||||
|
|
@ -182,15 +196,28 @@ class TestTryActivateFallback:
|
|||
|
||||
def test_prompt_caching_disabled_for_non_openrouter(self):
|
||||
agent = _make_agent(
|
||||
fallback_model={"provider": "openai", "model": "gpt-4.1"},
|
||||
fallback_model={"provider": "zai", "model": "glm-5"},
|
||||
)
|
||||
with (
|
||||
patch.dict("os.environ", {"OPENAI_API_KEY": "sk-oai-key"}),
|
||||
patch.dict("os.environ", {"ZAI_API_KEY": "sk-zai-key"}),
|
||||
patch("run_agent.OpenAI"),
|
||||
):
|
||||
agent._try_activate_fallback()
|
||||
assert agent._use_prompt_caching is False
|
||||
|
||||
def test_zai_alt_env_var(self):
|
||||
"""Z.AI should also check Z_AI_API_KEY as fallback env var."""
|
||||
agent = _make_agent(
|
||||
fallback_model={"provider": "zai", "model": "glm-5"},
|
||||
)
|
||||
with (
|
||||
patch.dict("os.environ", {"Z_AI_API_KEY": "sk-alt-key"}),
|
||||
patch("run_agent.OpenAI") as mock_openai,
|
||||
):
|
||||
assert agent._try_activate_fallback() is True
|
||||
call_kwargs = mock_openai.call_args[1]
|
||||
assert call_kwargs["api_key"] == "sk-alt-key"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Fallback config init
|
||||
|
|
@ -220,18 +247,14 @@ class TestFallbackInit:
|
|||
# =============================================================================
|
||||
|
||||
class TestProviderCredentials:
|
||||
"""Verify that each known provider resolves its API key correctly."""
|
||||
"""Verify that each supported provider resolves its API key correctly."""
|
||||
|
||||
@pytest.mark.parametrize("provider,env_var,base_url_fragment", [
|
||||
("openrouter", "OPENROUTER_API_KEY", "openrouter"),
|
||||
("openai", "OPENAI_API_KEY", "openai.com"),
|
||||
("deepseek", "DEEPSEEK_API_KEY", "deepseek.com"),
|
||||
("together", "TOGETHER_API_KEY", "together.xyz"),
|
||||
("groq", "GROQ_API_KEY", "groq.com"),
|
||||
("fireworks", "FIREWORKS_API_KEY", "fireworks.ai"),
|
||||
("mistral", "MISTRAL_API_KEY", "mistral.ai"),
|
||||
("gemini", "GEMINI_API_KEY", "googleapis.com"),
|
||||
("nous", "NOUS_API_KEY", "nousresearch.com"),
|
||||
("zai", "ZAI_API_KEY", "z.ai"),
|
||||
("kimi-coding", "KIMI_API_KEY", "moonshot.ai"),
|
||||
("minimax", "MINIMAX_API_KEY", "minimax.io"),
|
||||
("minimax-cn", "MINIMAX_CN_API_KEY", "minimaxi.com"),
|
||||
])
|
||||
def test_provider_resolves(self, provider, env_var, base_url_fragment):
|
||||
agent = _make_agent(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue