mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix: clarify auth retry guidance
This commit is contained in:
@@ -110,18 +110,40 @@ def _display_source(source: str) -> str:
|
|||||||
return source.split(":", 1)[1] if source.startswith("manual:") else source
|
return source.split(":", 1)[1] if source.startswith("manual:") else source
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_exhausted_status(entry) -> tuple[str, bool]:
|
||||||
|
code = getattr(entry, "last_error_code", None)
|
||||||
|
reason = str(getattr(entry, "last_error_reason", "") or "").strip().lower()
|
||||||
|
message = str(getattr(entry, "last_error_message", "") or "").strip().lower()
|
||||||
|
|
||||||
|
if code == 429 or any(token in reason for token in ("rate_limit", "usage_limit", "quota", "exhausted")) or any(
|
||||||
|
token in message for token in ("rate limit", "usage limit", "quota", "too many requests")
|
||||||
|
):
|
||||||
|
return "rate-limited", True
|
||||||
|
|
||||||
|
if code in {401, 403} or any(token in reason for token in ("invalid_token", "invalid_grant", "unauthorized", "forbidden", "auth")) or any(
|
||||||
|
token in message for token in ("unauthorized", "forbidden", "expired", "revoked", "invalid token", "authentication")
|
||||||
|
):
|
||||||
|
return "auth failed", False
|
||||||
|
|
||||||
|
return "exhausted", True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _format_exhausted_status(entry) -> str:
|
def _format_exhausted_status(entry) -> str:
|
||||||
if entry.last_status != STATUS_EXHAUSTED:
|
if entry.last_status != STATUS_EXHAUSTED:
|
||||||
return ""
|
return ""
|
||||||
|
label, show_retry_window = _classify_exhausted_status(entry)
|
||||||
reason = getattr(entry, "last_error_reason", None)
|
reason = getattr(entry, "last_error_reason", None)
|
||||||
reason_text = f" {reason}" if isinstance(reason, str) and reason.strip() else ""
|
reason_text = f" {reason}" if isinstance(reason, str) and reason.strip() else ""
|
||||||
code = f" ({entry.last_error_code})" if entry.last_error_code else ""
|
code = f" ({entry.last_error_code})" if entry.last_error_code else ""
|
||||||
|
if not show_retry_window:
|
||||||
|
return f" {label}{reason_text}{code} (re-auth may be required)"
|
||||||
exhausted_until = _exhausted_until(entry)
|
exhausted_until = _exhausted_until(entry)
|
||||||
if exhausted_until is None:
|
if exhausted_until is None:
|
||||||
return f" exhausted{reason_text}{code}"
|
return f" {label}{reason_text}{code}"
|
||||||
remaining = max(0, int(math.ceil(exhausted_until - time.time())))
|
remaining = max(0, int(math.ceil(exhausted_until - time.time())))
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
return f" exhausted{reason_text}{code} (ready to retry)"
|
return f" {label}{reason_text}{code} (ready to retry)"
|
||||||
minutes, seconds = divmod(remaining, 60)
|
minutes, seconds = divmod(remaining, 60)
|
||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
days, hours = divmod(hours, 24)
|
days, hours = divmod(hours, 24)
|
||||||
@@ -133,7 +155,7 @@ def _format_exhausted_status(entry) -> str:
|
|||||||
wait = f"{minutes}m {seconds}s"
|
wait = f"{minutes}m {seconds}s"
|
||||||
else:
|
else:
|
||||||
wait = f"{seconds}s"
|
wait = f"{seconds}s"
|
||||||
return f" exhausted{reason_text}{code} ({wait} left)"
|
return f" {label}{reason_text}{code} ({wait} left)"
|
||||||
|
|
||||||
|
|
||||||
def auth_add_command(args) -> None:
|
def auth_add_command(args) -> None:
|
||||||
|
|||||||
@@ -654,10 +654,45 @@ def test_auth_list_shows_exhausted_cooldown(monkeypatch, capsys):
|
|||||||
auth_list_command(_Args())
|
auth_list_command(_Args())
|
||||||
|
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "exhausted (429)" in out
|
assert "rate-limited (429)" in out
|
||||||
assert "59m 30s left" in out
|
assert "59m 30s left" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_list_shows_auth_failure_when_exhausted_entry_is_unauthorized(monkeypatch, capsys):
|
||||||
|
from hermes_cli.auth_commands import auth_list_command
|
||||||
|
|
||||||
|
class _Entry:
|
||||||
|
id = "cred-1"
|
||||||
|
label = "primary"
|
||||||
|
auth_type = "oauth"
|
||||||
|
source = "manual:device_code"
|
||||||
|
last_status = "exhausted"
|
||||||
|
last_error_code = 401
|
||||||
|
last_error_reason = "invalid_token"
|
||||||
|
last_error_message = "Access token expired or revoked."
|
||||||
|
last_status_at = 1000.0
|
||||||
|
|
||||||
|
class _Pool:
|
||||||
|
def entries(self):
|
||||||
|
return [_Entry()]
|
||||||
|
|
||||||
|
def peek(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("hermes_cli.auth_commands.load_pool", lambda provider: _Pool())
|
||||||
|
monkeypatch.setattr("hermes_cli.auth_commands.time.time", lambda: 1030.0)
|
||||||
|
|
||||||
|
class _Args:
|
||||||
|
provider = "openai-codex"
|
||||||
|
|
||||||
|
auth_list_command(_Args())
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "auth failed invalid_token (401)" in out
|
||||||
|
assert "re-auth may be required" in out
|
||||||
|
assert "left" not in out
|
||||||
|
|
||||||
|
|
||||||
def test_auth_list_prefers_explicit_reset_time(monkeypatch, capsys):
|
def test_auth_list_prefers_explicit_reset_time(monkeypatch, capsys):
|
||||||
from hermes_cli.auth_commands import auth_list_command
|
from hermes_cli.auth_commands import auth_list_command
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user