mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 07:21:37 +08:00
Compare commits
3 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea53b8eed | ||
|
|
496ed0e78b | ||
|
|
3304bc93a4 |
@@ -74,9 +74,13 @@ def _is_oauth_token(key: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def build_anthropic_client(api_key: str, base_url: str = None):
|
||||
def build_anthropic_client(api_key: str, base_url: str = None, third_party: bool = False):
|
||||
"""Create an Anthropic client, auto-detecting setup-tokens vs API keys.
|
||||
|
||||
When *third_party* is True (e.g. MiniMax's Anthropic-compatible endpoint),
|
||||
skips OAuth detection, beta headers, and Claude Code user-agent — uses
|
||||
plain api_key auth only.
|
||||
|
||||
Returns an anthropic.Anthropic instance.
|
||||
"""
|
||||
if _anthropic_sdk is None:
|
||||
@@ -92,7 +96,11 @@ def build_anthropic_client(api_key: str, base_url: str = None):
|
||||
if base_url:
|
||||
kwargs["base_url"] = base_url
|
||||
|
||||
if _is_oauth_token(api_key):
|
||||
if third_party:
|
||||
# Third-party Anthropic-compatible endpoint (e.g. MiniMax) —
|
||||
# plain api_key, no Anthropic-specific betas or Claude Code headers.
|
||||
kwargs["api_key"] = api_key
|
||||
elif _is_oauth_token(api_key):
|
||||
# OAuth access token / setup-token → Bearer auth + Claude Code identity.
|
||||
# Anthropic routes OAuth requests based on user-agent and headers;
|
||||
# without Claude Code's fingerprint, requests get intermittent 500s.
|
||||
@@ -408,13 +416,17 @@ def normalize_model_name(model: str) -> str:
|
||||
- Strips 'anthropic/' prefix (OpenRouter format, case-insensitive)
|
||||
- Converts dots to hyphens in version numbers (OpenRouter uses dots,
|
||||
Anthropic uses hyphens: claude-opus-4.6 → claude-opus-4-6)
|
||||
|
||||
Only applies transforms to Claude models. Third-party models (e.g.
|
||||
MiniMax-M2.5) are returned as-is to preserve their naming convention.
|
||||
"""
|
||||
lower = model.lower()
|
||||
if lower.startswith("anthropic/"):
|
||||
model = model[len("anthropic/"):]
|
||||
# OpenRouter uses dots for version separators (claude-opus-4.6),
|
||||
# Anthropic uses hyphens (claude-opus-4-6). Convert dots to hyphens.
|
||||
model = model.replace(".", "-")
|
||||
# Only convert dots to hyphens for Claude models — third-party models
|
||||
# like MiniMax-M2.5 use dots as part of their canonical name.
|
||||
if "claude" in model.lower():
|
||||
model = model.replace(".", "-")
|
||||
return model
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ DEFAULT_CONTEXT_LENGTHS = {
|
||||
"MiniMax-M2.5": 204800,
|
||||
"MiniMax-M2.5-highspeed": 204800,
|
||||
"MiniMax-M2.1": 204800,
|
||||
"MiniMax-M2.1-highspeed": 204800,
|
||||
"MiniMax-M2": 204800,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
||||
id="minimax",
|
||||
name="MiniMax",
|
||||
auth_type="api_key",
|
||||
inference_base_url="https://api.minimax.io/v1",
|
||||
inference_base_url="https://api.minimax.io/anthropic",
|
||||
api_key_env_vars=("MINIMAX_API_KEY",),
|
||||
base_url_env_var="MINIMAX_BASE_URL",
|
||||
),
|
||||
@@ -143,7 +143,7 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
||||
id="minimax-cn",
|
||||
name="MiniMax (China)",
|
||||
auth_type="api_key",
|
||||
inference_base_url="https://api.minimaxi.com/v1",
|
||||
inference_base_url="https://api.minimaxi.com/anthropic",
|
||||
api_key_env_vars=("MINIMAX_CN_API_KEY",),
|
||||
base_url_env_var="MINIMAX_CN_BASE_URL",
|
||||
),
|
||||
|
||||
@@ -63,11 +63,15 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||
"MiniMax-M2.5",
|
||||
"MiniMax-M2.5-highspeed",
|
||||
"MiniMax-M2.1",
|
||||
"MiniMax-M2.1-highspeed",
|
||||
"MiniMax-M2",
|
||||
],
|
||||
"minimax-cn": [
|
||||
"MiniMax-M2.5",
|
||||
"MiniMax-M2.5-highspeed",
|
||||
"MiniMax-M2.1",
|
||||
"MiniMax-M2.1-highspeed",
|
||||
"MiniMax-M2",
|
||||
],
|
||||
"anthropic": [
|
||||
"claude-opus-4-6",
|
||||
|
||||
@@ -270,12 +270,15 @@ def resolve_runtime_provider(
|
||||
}
|
||||
|
||||
# API-key providers (z.ai/GLM, Kimi, MiniMax, MiniMax-CN)
|
||||
# MiniMax uses Anthropic Messages API format for reliable tool calling.
|
||||
_ANTHROPIC_COMPAT_PROVIDERS = {"minimax", "minimax-cn"}
|
||||
pconfig = PROVIDER_REGISTRY.get(provider)
|
||||
if pconfig and pconfig.auth_type == "api_key":
|
||||
creds = resolve_api_key_provider_credentials(provider)
|
||||
api_mode = "anthropic_messages" if provider in _ANTHROPIC_COMPAT_PROVIDERS else "chat_completions"
|
||||
return {
|
||||
"provider": provider,
|
||||
"api_mode": "chat_completions",
|
||||
"api_mode": api_mode,
|
||||
"base_url": creds.get("base_url", "").rstrip("/"),
|
||||
"api_key": creds.get("api_key", ""),
|
||||
"source": creds.get("source", "env"),
|
||||
|
||||
56
run_agent.py
56
run_agent.py
@@ -543,17 +543,30 @@ class AIAgent:
|
||||
|
||||
if self.api_mode == "anthropic_messages":
|
||||
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
|
||||
effective_key = api_key or resolve_anthropic_token() or ""
|
||||
# Third-party Anthropic-compatible providers (e.g. MiniMax) use
|
||||
# plain api_key auth — no OAuth detection or Anthropic env vars.
|
||||
_THIRD_PARTY_ANTHROPIC = {"minimax", "minimax-cn"}
|
||||
self._is_third_party_anthropic = self.provider in _THIRD_PARTY_ANTHROPIC
|
||||
if self._is_third_party_anthropic:
|
||||
effective_key = api_key or ""
|
||||
else:
|
||||
effective_key = api_key or resolve_anthropic_token() or ""
|
||||
self._anthropic_api_key = effective_key
|
||||
self._anthropic_base_url = base_url
|
||||
from agent.anthropic_adapter import _is_oauth_token as _is_oat
|
||||
self._is_anthropic_oauth = _is_oat(effective_key)
|
||||
self._anthropic_client = build_anthropic_client(effective_key, base_url)
|
||||
if self._is_third_party_anthropic:
|
||||
self._is_anthropic_oauth = False
|
||||
else:
|
||||
from agent.anthropic_adapter import _is_oauth_token as _is_oat
|
||||
self._is_anthropic_oauth = _is_oat(effective_key)
|
||||
self._anthropic_client = build_anthropic_client(
|
||||
effective_key, base_url, third_party=self._is_third_party_anthropic,
|
||||
)
|
||||
# No OpenAI client needed for Anthropic mode
|
||||
self.client = None
|
||||
self._client_kwargs = {}
|
||||
if not self.quiet_mode:
|
||||
print(f"🤖 AI Agent initialized with model: {self.model} (Anthropic native)")
|
||||
label = f"{self.provider} (Anthropic compat)" if self._is_third_party_anthropic else "Anthropic native"
|
||||
print(f"🤖 AI Agent initialized with model: {self.model} ({label})")
|
||||
if effective_key and len(effective_key) > 12:
|
||||
print(f"🔑 Using token: {effective_key[:8]}...{effective_key[-4:]}")
|
||||
else:
|
||||
@@ -2766,6 +2779,10 @@ class AIAgent:
|
||||
def _try_refresh_anthropic_client_credentials(self) -> bool:
|
||||
if self.api_mode != "anthropic_messages" or not hasattr(self, "_anthropic_api_key"):
|
||||
return False
|
||||
# Third-party Anthropic-compatible providers (e.g. MiniMax) manage
|
||||
# their own credentials — don't try to resolve Anthropic tokens.
|
||||
if getattr(self, "_is_third_party_anthropic", False):
|
||||
return False
|
||||
|
||||
try:
|
||||
from agent.anthropic_adapter import resolve_anthropic_token, build_anthropic_client
|
||||
@@ -5370,23 +5387,28 @@ class AIAgent:
|
||||
and not anthropic_auth_retry_attempted
|
||||
):
|
||||
anthropic_auth_retry_attempted = True
|
||||
from agent.anthropic_adapter import _is_oauth_token
|
||||
if self._try_refresh_anthropic_client_credentials():
|
||||
print(f"{self.log_prefix}🔐 Anthropic credentials refreshed after 401. Retrying request...")
|
||||
continue
|
||||
# Credential refresh didn't help — show diagnostic info
|
||||
key = self._anthropic_api_key
|
||||
auth_method = "Bearer (OAuth/setup-token)" if _is_oauth_token(key) else "x-api-key (API key)"
|
||||
print(f"{self.log_prefix}🔐 Anthropic 401 — authentication failed.")
|
||||
print(f"{self.log_prefix} Auth method: {auth_method}")
|
||||
print(f"{self.log_prefix} Token prefix: {key[:12]}..." if key and len(key) > 12 else f"{self.log_prefix} Token: (empty or short)")
|
||||
print(f"{self.log_prefix} Troubleshooting:")
|
||||
print(f"{self.log_prefix} • Check ANTHROPIC_TOKEN in ~/.hermes/.env for Hermes-managed OAuth/setup tokens")
|
||||
print(f"{self.log_prefix} • Check ANTHROPIC_API_KEY in ~/.hermes/.env for API keys or legacy token values")
|
||||
print(f"{self.log_prefix} • For API keys: verify at https://console.anthropic.com/settings/keys")
|
||||
print(f"{self.log_prefix} • For Claude Code: run 'claude /login' to refresh, then retry")
|
||||
print(f"{self.log_prefix} • Clear stale keys: hermes config set ANTHROPIC_TOKEN \"\"")
|
||||
print(f"{self.log_prefix} • Legacy cleanup: hermes config set ANTHROPIC_API_KEY \"\"")
|
||||
if getattr(self, "_is_third_party_anthropic", False):
|
||||
print(f"{self.log_prefix}🔐 {self.provider} 401 — authentication failed.")
|
||||
print(f"{self.log_prefix} Token prefix: {key[:12]}..." if key and len(key) > 12 else f"{self.log_prefix} Token: (empty or short)")
|
||||
print(f"{self.log_prefix} Troubleshooting: check your API key in ~/.hermes/.env")
|
||||
else:
|
||||
from agent.anthropic_adapter import _is_oauth_token
|
||||
auth_method = "Bearer (OAuth/setup-token)" if _is_oauth_token(key) else "x-api-key (API key)"
|
||||
print(f"{self.log_prefix}🔐 Anthropic 401 — authentication failed.")
|
||||
print(f"{self.log_prefix} Auth method: {auth_method}")
|
||||
print(f"{self.log_prefix} Token prefix: {key[:12]}..." if key and len(key) > 12 else f"{self.log_prefix} Token: (empty or short)")
|
||||
print(f"{self.log_prefix} Troubleshooting:")
|
||||
print(f"{self.log_prefix} • Check ANTHROPIC_TOKEN in ~/.hermes/.env for Hermes-managed OAuth/setup tokens")
|
||||
print(f"{self.log_prefix} • Check ANTHROPIC_API_KEY in ~/.hermes/.env for API keys or legacy token values")
|
||||
print(f"{self.log_prefix} • For API keys: verify at https://console.anthropic.com/settings/keys")
|
||||
print(f"{self.log_prefix} • For Claude Code: run 'claude /login' to refresh, then retry")
|
||||
print(f"{self.log_prefix} • Clear stale keys: hermes config set ANTHROPIC_TOKEN \"\"")
|
||||
print(f"{self.log_prefix} • Legacy cleanup: hermes config set ANTHROPIC_API_KEY \"\"")
|
||||
|
||||
retry_count += 1
|
||||
elapsed_time = time.time() - api_start_time
|
||||
|
||||
@@ -107,7 +107,7 @@ class _FakeAnthropicClient:
|
||||
pass
|
||||
|
||||
|
||||
def _fake_build_anthropic_client(key, base_url=None):
|
||||
def _fake_build_anthropic_client(key, base_url=None, **kwargs):
|
||||
return _FakeAnthropicClient()
|
||||
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ class TestProviderRegistry:
|
||||
def test_base_urls(self):
|
||||
assert PROVIDER_REGISTRY["zai"].inference_base_url == "https://api.z.ai/api/paas/v4"
|
||||
assert PROVIDER_REGISTRY["kimi-coding"].inference_base_url == "https://api.moonshot.ai/v1"
|
||||
assert PROVIDER_REGISTRY["minimax"].inference_base_url == "https://api.minimax.io/v1"
|
||||
assert PROVIDER_REGISTRY["minimax-cn"].inference_base_url == "https://api.minimaxi.com/v1"
|
||||
assert PROVIDER_REGISTRY["minimax"].inference_base_url == "https://api.minimax.io/anthropic"
|
||||
assert PROVIDER_REGISTRY["minimax-cn"].inference_base_url == "https://api.minimaxi.com/anthropic"
|
||||
|
||||
def test_oauth_providers_unchanged(self):
|
||||
"""Ensure we didn't break the existing OAuth providers."""
|
||||
@@ -239,14 +239,14 @@ class TestResolveApiKeyProviderCredentials:
|
||||
creds = resolve_api_key_provider_credentials("minimax")
|
||||
assert creds["provider"] == "minimax"
|
||||
assert creds["api_key"] == "mm-secret-key"
|
||||
assert creds["base_url"] == "https://api.minimax.io/v1"
|
||||
assert creds["base_url"] == "https://api.minimax.io/anthropic"
|
||||
|
||||
def test_resolve_minimax_cn_with_key(self, monkeypatch):
|
||||
monkeypatch.setenv("MINIMAX_CN_API_KEY", "mmcn-secret-key")
|
||||
creds = resolve_api_key_provider_credentials("minimax-cn")
|
||||
assert creds["provider"] == "minimax-cn"
|
||||
assert creds["api_key"] == "mmcn-secret-key"
|
||||
assert creds["base_url"] == "https://api.minimaxi.com/v1"
|
||||
assert creds["base_url"] == "https://api.minimaxi.com/anthropic"
|
||||
|
||||
def test_resolve_with_custom_base_url(self, monkeypatch):
|
||||
monkeypatch.setenv("GLM_API_KEY", "glm-key")
|
||||
|
||||
@@ -395,6 +395,7 @@ def execute_code(
|
||||
tool_call_log: list = []
|
||||
tool_call_counter = [0] # mutable so the RPC thread can increment
|
||||
exec_start = time.monotonic()
|
||||
server_sock = None
|
||||
|
||||
try:
|
||||
# Write the auto-generated hermes_tools module
|
||||
@@ -598,7 +599,14 @@ def execute_code(
|
||||
|
||||
except Exception as exc:
|
||||
duration = round(time.monotonic() - exec_start, 2)
|
||||
logging.exception("execute_code failed")
|
||||
logger.error(
|
||||
"execute_code failed after %ss with %d tool calls: %s: %s",
|
||||
duration,
|
||||
tool_call_counter[0],
|
||||
type(exc).__name__,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
return json.dumps({
|
||||
"status": "error",
|
||||
"error": str(exc),
|
||||
@@ -608,19 +616,17 @@ def execute_code(
|
||||
|
||||
finally:
|
||||
# Cleanup temp dir and socket
|
||||
try:
|
||||
server_sock.close()
|
||||
except Exception as e:
|
||||
logger.debug("Server socket close error: %s", e)
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
except Exception as e:
|
||||
logger.debug("Could not clean temp dir: %s", e, exc_info=True)
|
||||
if server_sock is not None:
|
||||
try:
|
||||
server_sock.close()
|
||||
except OSError as e:
|
||||
logger.debug("Server socket close error: %s", e)
|
||||
import shutil
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
try:
|
||||
os.unlink(sock_path)
|
||||
except OSError as e:
|
||||
logger.debug("Could not remove socket file: %s", e, exc_info=True)
|
||||
except OSError:
|
||||
pass # already cleaned up or never created
|
||||
|
||||
|
||||
def _kill_process_group(proc, escalate: bool = False):
|
||||
|
||||
Reference in New Issue
Block a user