fix: make Nous Portal access token resolution resilient

- Track auth store source path on Nous state reads and write rotated
  OAuth refresh tokens back to the same store, preventing stale-token
  replays when Hermes falls back to a global/root auth.json.
- Skip Nous fallback entries locally when no access/refresh token is
  present, suppressing repeated failed resolution attempts within a
  session.
- Sync session model metadata after fallback switches so the gateway
  DB reflects the backend that actually served the latest turn.
This commit is contained in:
HODLCLONE 2026-06-19 20:20:01 -04:00 committed by Teknium
parent cfbc7ed1f9
commit 6ed2f5d76f
4 changed files with 276 additions and 32 deletions

View file

@ -0,0 +1,100 @@
"""Tests for Nous fallback local-availability suppression.
Blocker if Nous token material is missing locally: the fallback chain
should not repeatedly attempt Nous resolution; it must skip and continue
to the next provider.
"""
from __future__ import annotations
from unittest.mock import patch
from run_agent import AIAgent
def _make_agent(fallback_model=None):
with (
patch("run_agent.get_tool_definitions", return_value=[]),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
):
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
fallback_model=fallback_model,
)
agent.client = None
return agent
def _mock_client(base_url="https://openrouter.ai/api/v1", api_key="fb-key"):
mock = type("Client", (), {})()
mock.base_url = base_url
mock.api_key = api_key
mock.chat = type("Chat", (), {})()
mock.chat.completions = type("Completions", (), {})()
mock.chat.completions.create = lambda *args, **kwargs: None
return mock
class TestNousFallbackLocalAvailability:
def test_missing_nous_token_is_skipped_once(self):
"""Nous fallback is skipped when no access/refresh token is stored."""
agent = _make_agent(
fallback_model=[
{"provider": "nous", "model": "anthropic/claude-sonnet-4.6"},
{"provider": "openai", "model": "gpt-4o"},
]
)
with patch(
"hermes_cli.auth.get_provider_auth_state",
return_value={},
), patch(
"agent.auxiliary_client.resolve_provider_client",
return_value=(_mock_client(api_key="fb"), "gpt-4o"),
):
activated = agent._try_activate_fallback(None)
assert activated is True
assert agent.model == "gpt-4o"
def test_nous_unavailable_not_retried_in_same_session(self):
"""After Nous is skipped once, subsequent activations continue further."""
agent = _make_agent(
fallback_model=[
{"provider": "nous", "model": "anthropic/claude-sonnet-4.6"},
{"provider": "openai", "model": "gpt-4o"},
]
)
with patch(
"hermes_cli.auth.get_provider_auth_state",
return_value={},
):
agent._try_activate_fallback(None)
key = (
"nous",
"anthropic/claude-sonnet-4.6",
"",
)
assert key in getattr(agent, "_unavailable_fallback_keys", set())
def test_present_nous_token_allows_activation(self):
"""Nous is considered when token material exists."""
agent = _make_agent(
fallback_model=[
{"provider": "nous", "model": "anthropic/claude-sonnet-4.6"},
{"provider": "openai", "model": "gpt-4o"},
]
)
with patch(
"hermes_cli.auth.get_provider_auth_state",
return_value={"access_token": "abc", "refresh_token": "xyz"},
), patch(
"agent.auxiliary_client.resolve_provider_client",
return_value=(_mock_client(api_key="fb"), "anthropic/claude-sonnet-4.6"),
):
activated = agent._try_activate_fallback(None)
assert activated is True
assert agent.provider == "nous"