mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 23:41:35 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd7ec0059 |
@@ -639,31 +639,47 @@ def resolve_runtime_provider(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if provider == "nous":
|
if provider == "nous":
|
||||||
creds = resolve_nous_runtime_credentials(
|
try:
|
||||||
min_key_ttl_seconds=max(60, int(os.getenv("HERMES_NOUS_MIN_KEY_TTL_SECONDS", "1800"))),
|
creds = resolve_nous_runtime_credentials(
|
||||||
timeout_seconds=float(os.getenv("HERMES_NOUS_TIMEOUT_SECONDS", "15")),
|
min_key_ttl_seconds=max(60, int(os.getenv("HERMES_NOUS_MIN_KEY_TTL_SECONDS", "1800"))),
|
||||||
)
|
timeout_seconds=float(os.getenv("HERMES_NOUS_TIMEOUT_SECONDS", "15")),
|
||||||
return {
|
)
|
||||||
"provider": "nous",
|
return {
|
||||||
"api_mode": "chat_completions",
|
"provider": "nous",
|
||||||
"base_url": creds.get("base_url", "").rstrip("/"),
|
"api_mode": "chat_completions",
|
||||||
"api_key": creds.get("api_key", ""),
|
"base_url": creds.get("base_url", "").rstrip("/"),
|
||||||
"source": creds.get("source", "portal"),
|
"api_key": creds.get("api_key", ""),
|
||||||
"expires_at": creds.get("expires_at"),
|
"source": creds.get("source", "portal"),
|
||||||
"requested_provider": requested_provider,
|
"expires_at": creds.get("expires_at"),
|
||||||
}
|
"requested_provider": requested_provider,
|
||||||
|
}
|
||||||
|
except AuthError:
|
||||||
|
if requested_provider != "auto":
|
||||||
|
raise
|
||||||
|
# Auto-detected Nous but credentials are stale/revoked —
|
||||||
|
# fall through to env-var providers (e.g. OpenRouter).
|
||||||
|
logger.info("Auto-detected Nous provider but credentials failed; "
|
||||||
|
"falling through to next provider.")
|
||||||
|
|
||||||
if provider == "openai-codex":
|
if provider == "openai-codex":
|
||||||
creds = resolve_codex_runtime_credentials()
|
try:
|
||||||
return {
|
creds = resolve_codex_runtime_credentials()
|
||||||
"provider": "openai-codex",
|
return {
|
||||||
"api_mode": "codex_responses",
|
"provider": "openai-codex",
|
||||||
"base_url": creds.get("base_url", "").rstrip("/"),
|
"api_mode": "codex_responses",
|
||||||
"api_key": creds.get("api_key", ""),
|
"base_url": creds.get("base_url", "").rstrip("/"),
|
||||||
"source": creds.get("source", "hermes-auth-store"),
|
"api_key": creds.get("api_key", ""),
|
||||||
"last_refresh": creds.get("last_refresh"),
|
"source": creds.get("source", "hermes-auth-store"),
|
||||||
"requested_provider": requested_provider,
|
"last_refresh": creds.get("last_refresh"),
|
||||||
}
|
"requested_provider": requested_provider,
|
||||||
|
}
|
||||||
|
except AuthError:
|
||||||
|
if requested_provider != "auto":
|
||||||
|
raise
|
||||||
|
# Auto-detected Codex but credentials are stale/revoked —
|
||||||
|
# fall through to env-var providers (e.g. OpenRouter).
|
||||||
|
logger.info("Auto-detected Codex provider but credentials failed; "
|
||||||
|
"falling through to next provider.")
|
||||||
|
|
||||||
if provider == "copilot-acp":
|
if provider == "copilot-acp":
|
||||||
creds = resolve_external_process_provider_credentials(provider)
|
creds = resolve_external_process_provider_credentials(provider)
|
||||||
|
|||||||
@@ -996,6 +996,89 @@ def test_custom_provider_no_key_gets_placeholder(monkeypatch):
|
|||||||
assert resolved["base_url"] == "http://localhost:8080/v1"
|
assert resolved["base_url"] == "http://localhost:8080/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_detected_nous_auth_failure_falls_through_to_openrouter(monkeypatch):
|
||||||
|
"""When auto-detect picks Nous but credentials are revoked, fall through to OpenRouter."""
|
||||||
|
from hermes_cli.auth import AuthError
|
||||||
|
|
||||||
|
monkeypatch.setenv("OPENROUTER_API_KEY", "test-or-key")
|
||||||
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
|
||||||
|
monkeypatch.setattr(rp, "load_config", lambda: {})
|
||||||
|
|
||||||
|
# resolve_provider returns "nous" (stale active_provider in auth.json)
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "nous")
|
||||||
|
# load_pool returns empty pool so we hit the direct credential resolution
|
||||||
|
monkeypatch.setattr(rp, "load_pool", lambda p: type("P", (), {
|
||||||
|
"has_credentials": lambda self: False,
|
||||||
|
})())
|
||||||
|
# Nous credential resolution fails with revoked token
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rp, "resolve_nous_runtime_credentials",
|
||||||
|
lambda **kw: (_ for _ in ()).throw(
|
||||||
|
AuthError("Refresh session has been revoked",
|
||||||
|
provider="nous", code="invalid_grant", relogin_required=True)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# With requested="auto", should fall through to OpenRouter
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="auto")
|
||||||
|
assert resolved["provider"] == "openrouter"
|
||||||
|
assert resolved["api_key"] == "test-or-key"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_detected_codex_auth_failure_falls_through_to_openrouter(monkeypatch):
|
||||||
|
"""When auto-detect picks Codex but credentials are revoked, fall through to OpenRouter."""
|
||||||
|
from hermes_cli.auth import AuthError
|
||||||
|
|
||||||
|
monkeypatch.setenv("OPENROUTER_API_KEY", "test-or-key")
|
||||||
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
|
||||||
|
monkeypatch.setattr(rp, "load_config", lambda: {})
|
||||||
|
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openai-codex")
|
||||||
|
monkeypatch.setattr(rp, "load_pool", lambda p: type("P", (), {
|
||||||
|
"has_credentials": lambda self: False,
|
||||||
|
})())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rp, "resolve_codex_runtime_credentials",
|
||||||
|
lambda **kw: (_ for _ in ()).throw(
|
||||||
|
AuthError("Codex token refresh failed: session revoked",
|
||||||
|
provider="openai-codex", code="invalid_grant", relogin_required=True)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved = rp.resolve_runtime_provider(requested="auto")
|
||||||
|
assert resolved["provider"] == "openrouter"
|
||||||
|
assert resolved["api_key"] == "test-or-key"
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_nous_auth_failure_still_raises(monkeypatch):
|
||||||
|
"""When user explicitly requests Nous and auth fails, the error should propagate."""
|
||||||
|
from hermes_cli.auth import AuthError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
monkeypatch.setenv("OPENROUTER_API_KEY", "test-or-key")
|
||||||
|
monkeypatch.setattr(rp, "load_config", lambda: {})
|
||||||
|
|
||||||
|
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "nous")
|
||||||
|
monkeypatch.setattr(rp, "load_pool", lambda p: type("P", (), {
|
||||||
|
"has_credentials": lambda self: False,
|
||||||
|
})())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
rp, "resolve_nous_runtime_credentials",
|
||||||
|
lambda **kw: (_ for _ in ()).throw(
|
||||||
|
AuthError("Refresh session has been revoked",
|
||||||
|
provider="nous", code="invalid_grant", relogin_required=True)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# With explicit "nous", should raise — don't silently switch providers
|
||||||
|
with pytest.raises(AuthError, match="Refresh session has been revoked"):
|
||||||
|
rp.resolve_runtime_provider(requested="nous")
|
||||||
|
|
||||||
|
|
||||||
def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch):
|
def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch):
|
||||||
"""Fixing custom must not change openrouter behavior."""
|
"""Fixing custom must not change openrouter behavior."""
|
||||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user