feat(errors): actionable guidance for Nous OAuth 401s (#32082)
Nous Portal is OAuth-only (auth_type=oauth_device_code, no API key path), but the non-retryable-401 guidance branch only covered openai-codex and xai-oauth. A Nous 401 fell through to the generic 'Your API key was rejected... run hermes setup' message, which is wrong advice — the user needs hermes auth add nous --type oauth, not an API key. Also flag the case where the failing model slug ends in :free (OpenRouter syntax) while provider is nous. Without that hint, users re-OAuth successfully and then hit the same 401 on the next message because Nous Portal doesn't carry the OpenRouter free-tier slug. Reported by ashh — debug dump showed Nous device_code exhausted + deepseek/deepseek-v4-flash:free as the model.
This commit is contained in:
parent
dbe5d84972
commit
0d137f1039
@ -2889,15 +2889,26 @@ def run_conversation(
|
|||||||
agent._vprint(f"{agent.log_prefix} 🌐 Endpoint: {_base}", force=True)
|
agent._vprint(f"{agent.log_prefix} 🌐 Endpoint: {_base}", force=True)
|
||||||
# Actionable guidance for common auth errors
|
# Actionable guidance for common auth errors
|
||||||
if classified.is_auth or classified.reason == FailoverReason.billing:
|
if classified.is_auth or classified.reason == FailoverReason.billing:
|
||||||
if _provider in {"openai-codex", "xai-oauth"} and status_code == 401:
|
if _provider in {"openai-codex", "xai-oauth", "nous"} and status_code == 401:
|
||||||
if _provider == "openai-codex":
|
if _provider == "openai-codex":
|
||||||
agent._vprint(f"{agent.log_prefix} 💡 Codex OAuth token was rejected (HTTP 401). Your token may have been", force=True)
|
agent._vprint(f"{agent.log_prefix} 💡 Codex OAuth token was rejected (HTTP 401). Your token may have been", force=True)
|
||||||
agent._vprint(f"{agent.log_prefix} refreshed by another client (Codex CLI, VS Code). To fix:", force=True)
|
agent._vprint(f"{agent.log_prefix} refreshed by another client (Codex CLI, VS Code). To fix:", force=True)
|
||||||
agent._vprint(f"{agent.log_prefix} 1. Run `codex` in your terminal to generate fresh tokens.", force=True)
|
agent._vprint(f"{agent.log_prefix} 1. Run `codex` in your terminal to generate fresh tokens.", force=True)
|
||||||
agent._vprint(f"{agent.log_prefix} 2. Then run `hermes auth` to re-authenticate.", force=True)
|
agent._vprint(f"{agent.log_prefix} 2. Then run `hermes auth` to re-authenticate.", force=True)
|
||||||
else:
|
elif _provider == "xai-oauth":
|
||||||
agent._vprint(f"{agent.log_prefix} 💡 xAI OAuth token was rejected (HTTP 401). To fix:", force=True)
|
agent._vprint(f"{agent.log_prefix} 💡 xAI OAuth token was rejected (HTTP 401). To fix:", force=True)
|
||||||
agent._vprint(f"{agent.log_prefix} re-authenticate with xAI Grok OAuth (SuperGrok / Premium+) from `hermes model`.", force=True)
|
agent._vprint(f"{agent.log_prefix} re-authenticate with xAI Grok OAuth (SuperGrok / Premium+) from `hermes model`.", force=True)
|
||||||
|
else: # nous
|
||||||
|
agent._vprint(f"{agent.log_prefix} 💡 Nous Portal OAuth token was rejected (HTTP 401). Your token may be", force=True)
|
||||||
|
agent._vprint(f"{agent.log_prefix} expired, revoked, or your account may be out of credits. To fix:", force=True)
|
||||||
|
agent._vprint(f"{agent.log_prefix} 1. Re-authenticate: hermes auth add nous --type oauth", force=True)
|
||||||
|
agent._vprint(f"{agent.log_prefix} 2. Check your portal account: https://portal.nousresearch.com", force=True)
|
||||||
|
# ``:free`` is OpenRouter slug syntax; Nous Portal will reject
|
||||||
|
# the model name even after a successful re-auth.
|
||||||
|
if isinstance(_model, str) and _model.endswith(":free"):
|
||||||
|
agent._vprint(f"{agent.log_prefix} ⚠️ Note: `{_model}` looks like an OpenRouter slug (`:free` suffix).", force=True)
|
||||||
|
agent._vprint(f"{agent.log_prefix} Nous Portal won't recognize that model name. Either switch to a", force=True)
|
||||||
|
agent._vprint(f"{agent.log_prefix} Nous catalog model, or run `/model openrouter:{_model}` to use OpenRouter.", force=True)
|
||||||
else:
|
else:
|
||||||
agent._vprint(f"{agent.log_prefix} 💡 Your API key was rejected by the provider. Check:", force=True)
|
agent._vprint(f"{agent.log_prefix} 💡 Your API key was rejected by the provider. Check:", force=True)
|
||||||
agent._vprint(f"{agent.log_prefix} • Is the key valid? Run: hermes setup", force=True)
|
agent._vprint(f"{agent.log_prefix} • Is the key valid? Run: hermes setup", force=True)
|
||||||
|
|||||||
71
tests/agent/test_nous_oauth_401_guidance.py
Normal file
71
tests/agent/test_nous_oauth_401_guidance.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""Tests for the Nous OAuth 401 actionable-guidance branch in
|
||||||
|
``agent.conversation_loop.run_conversation``.
|
||||||
|
|
||||||
|
Source-inspection style (matches ``test_gemini_fast_fallback.py``): we assert
|
||||||
|
that the guidance strings exist in the function body so that the user-facing
|
||||||
|
hint cannot be silently removed by a future refactor.
|
||||||
|
|
||||||
|
Regression context: ashh hit a Nous 401 (OAuth token expired / portal said
|
||||||
|
account out of credits) plus a model slug ``deepseek/deepseek-v4-flash:free``
|
||||||
|
that's OpenRouter syntax, not a Nous catalog name. The previous guidance
|
||||||
|
branch only covered ``openai-codex`` and ``xai-oauth``; ``nous`` fell through
|
||||||
|
to a generic "Your API key was rejected... run hermes setup" message, which is
|
||||||
|
the wrong advice for a pure-OAuth provider.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from agent import conversation_loop
|
||||||
|
|
||||||
|
|
||||||
|
def test_nous_provider_is_in_oauth_401_set():
|
||||||
|
"""The provider-set gate that selects OAuth-specific guidance must
|
||||||
|
include ``nous`` alongside ``openai-codex`` and ``xai-oauth``.
|
||||||
|
"""
|
||||||
|
source = inspect.getsource(conversation_loop.run_conversation)
|
||||||
|
|
||||||
|
# Be flexible about set element ordering — assert all three are listed
|
||||||
|
# near each other in the gating expression.
|
||||||
|
assert "\"openai-codex\"" in source
|
||||||
|
assert "\"xai-oauth\"" in source
|
||||||
|
assert "\"nous\"" in source
|
||||||
|
|
||||||
|
# And the gate string itself must mention all three so future refactors
|
||||||
|
# that split nous off into its own gate still get caught.
|
||||||
|
needle = "_provider in {\"openai-codex\", \"xai-oauth\", \"nous\"}"
|
||||||
|
assert needle in source, (
|
||||||
|
"Expected nous to be co-gated with the other OAuth providers in the "
|
||||||
|
"actionable-401-guidance branch of run_conversation."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nous_401_guidance_strings_present():
|
||||||
|
"""User-facing remediation strings for Nous OAuth 401s must exist."""
|
||||||
|
source = inspect.getsource(conversation_loop.run_conversation)
|
||||||
|
|
||||||
|
# Must tell the user it's an OAuth token problem, NOT an API key problem
|
||||||
|
# (Nous Portal has no API key path — auth_type=oauth_device_code only).
|
||||||
|
assert "Nous Portal OAuth token was rejected" in source
|
||||||
|
|
||||||
|
# Must give the exact re-auth command, not a generic "hermes setup".
|
||||||
|
assert "hermes auth add nous --type oauth" in source
|
||||||
|
|
||||||
|
# Must point at the portal so users can check account/credit status.
|
||||||
|
assert "portal.nousresearch.com" in source
|
||||||
|
|
||||||
|
|
||||||
|
def test_free_slug_hint_for_nous_provider():
|
||||||
|
"""When the failing model slug ends with ``:free`` and the provider is
|
||||||
|
``nous``, the guidance must flag that ``:free`` is OpenRouter syntax and
|
||||||
|
suggest switching providers via ``/model openrouter:<slug>``.
|
||||||
|
|
||||||
|
Without this hint, users re-OAuth successfully and then hit the same 401
|
||||||
|
on the next message because Nous Portal doesn't carry the OpenRouter
|
||||||
|
free-tier slug.
|
||||||
|
"""
|
||||||
|
source = inspect.getsource(conversation_loop.run_conversation)
|
||||||
|
|
||||||
|
assert "endswith(\":free\")" in source
|
||||||
|
assert "OpenRouter slug" in source
|
||||||
|
assert "/model openrouter:" in source
|
||||||
Loading…
Reference in New Issue
Block a user