mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge 116f8b6c29 into 05d8f11085
This commit is contained in:
commit
4d184def8b
2 changed files with 80 additions and 0 deletions
12
run_agent.py
12
run_agent.py
|
|
@ -5606,6 +5606,18 @@ class AIAgent:
|
||||||
)
|
)
|
||||||
self._swap_credential(next_entry)
|
self._swap_credential(next_entry)
|
||||||
return True, False
|
return True, False
|
||||||
|
# All credentials for this provider are exhausted due to an auth
|
||||||
|
# failure. Emit a plain-language notification so gateway users
|
||||||
|
# (Telegram, Discord, etc.) know their primary AI is unavailable
|
||||||
|
# and what to do about it. Same-provider key rotation (above)
|
||||||
|
# remains silent — the message only fires when rotation fails.
|
||||||
|
_provider_label = getattr(self, "provider", "unknown")
|
||||||
|
self._emit_status(
|
||||||
|
f"⚠️ Primary AI ({_provider_label}) is unavailable — the API key "
|
||||||
|
f"may be invalid or expired (HTTP {rotate_status}). Switched to "
|
||||||
|
f"fallback if one is configured. To restore: check your API key "
|
||||||
|
f"and run `hermes auth reset {_provider_label}`."
|
||||||
|
)
|
||||||
|
|
||||||
return False, has_retried_429
|
return False, has_retried_429
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,3 +232,71 @@ class TestPoolRotationCycle:
|
||||||
)
|
)
|
||||||
assert recovered is False
|
assert recovered is False
|
||||||
assert has_retried is False
|
assert has_retried is False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. Auth exhaustion notification via _emit_status
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestAuthExhaustionNotification:
|
||||||
|
"""Verify _emit_status is called when all credentials are 401-exhausted.
|
||||||
|
|
||||||
|
Same-provider key rotation should remain silent — the notification only
|
||||||
|
fires when mark_exhausted_and_rotate() returns None (no credentials left).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _make_agent(self, *, next_entry=None):
|
||||||
|
"""Minimal AIAgent with a credential pool that returns next_entry on rotate."""
|
||||||
|
from run_agent import AIAgent
|
||||||
|
from agent.error_classifier import FailoverReason
|
||||||
|
|
||||||
|
with patch.object(AIAgent, "__init__", lambda self, **kw: None):
|
||||||
|
agent = AIAgent()
|
||||||
|
|
||||||
|
pool = MagicMock()
|
||||||
|
pool.has_credentials.return_value = True
|
||||||
|
pool.try_refresh_current.return_value = None # refresh always fails
|
||||||
|
pool.mark_exhausted_and_rotate.return_value = next_entry
|
||||||
|
agent._credential_pool = pool
|
||||||
|
agent._swap_credential = MagicMock()
|
||||||
|
agent.log_prefix = ""
|
||||||
|
agent.provider = "kimi-coding"
|
||||||
|
agent._emit_status = MagicMock()
|
||||||
|
|
||||||
|
return agent, pool
|
||||||
|
|
||||||
|
def test_emits_notification_when_all_credentials_exhausted(self):
|
||||||
|
"""When pool is fully exhausted on 401, _emit_status must fire."""
|
||||||
|
from agent.error_classifier import FailoverReason
|
||||||
|
|
||||||
|
agent, _ = self._make_agent(next_entry=None) # no credentials left
|
||||||
|
|
||||||
|
recovered, _ = agent._recover_with_credential_pool(
|
||||||
|
status_code=401,
|
||||||
|
has_retried_429=False,
|
||||||
|
classified_reason=FailoverReason.auth,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert recovered is False
|
||||||
|
agent._emit_status.assert_called_once()
|
||||||
|
msg = agent._emit_status.call_args[0][0]
|
||||||
|
assert "kimi-coding" in msg
|
||||||
|
assert "401" in msg
|
||||||
|
assert "hermes auth reset" in msg
|
||||||
|
|
||||||
|
def test_silent_when_rotation_to_next_credential_succeeds(self):
|
||||||
|
"""When a next credential is available, rotation is silent — no _emit_status."""
|
||||||
|
from agent.error_classifier import FailoverReason
|
||||||
|
|
||||||
|
next_entry = MagicMock()
|
||||||
|
next_entry.id = "cred-2"
|
||||||
|
agent, _ = self._make_agent(next_entry=next_entry)
|
||||||
|
|
||||||
|
recovered, _ = agent._recover_with_credential_pool(
|
||||||
|
status_code=401,
|
||||||
|
has_retried_429=False,
|
||||||
|
classified_reason=FailoverReason.auth,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert recovered is True
|
||||||
|
agent._emit_status.assert_not_called()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue