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 | |
|---|---|---|---|
|
|
34a6997f1d |
@@ -95,6 +95,10 @@ _COMMON_BETAS = [
|
|||||||
"interleaved-thinking-2025-05-14",
|
"interleaved-thinking-2025-05-14",
|
||||||
"fine-grained-tool-streaming-2025-05-14",
|
"fine-grained-tool-streaming-2025-05-14",
|
||||||
]
|
]
|
||||||
|
# MiniMax's Anthropic-compatible endpoints fail tool-use requests when
|
||||||
|
# the fine-grained tool streaming beta is present. Omit it so tool calls
|
||||||
|
# fall back to the provider's default response path.
|
||||||
|
_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14"
|
||||||
|
|
||||||
# Additional beta headers required for OAuth/subscription auth.
|
# Additional beta headers required for OAuth/subscription auth.
|
||||||
# Matches what Claude Code (and pi-ai / OpenCode) send.
|
# Matches what Claude Code (and pi-ai / OpenCode) send.
|
||||||
@@ -204,6 +208,19 @@ def _requires_bearer_auth(base_url: str | None) -> bool:
|
|||||||
return normalized.startswith(("https://api.minimax.io/anthropic", "https://api.minimaxi.com/anthropic"))
|
return normalized.startswith(("https://api.minimax.io/anthropic", "https://api.minimaxi.com/anthropic"))
|
||||||
|
|
||||||
|
|
||||||
|
def _common_betas_for_base_url(base_url: str | None) -> list[str]:
|
||||||
|
"""Return the beta headers that are safe for the configured endpoint.
|
||||||
|
|
||||||
|
MiniMax's Anthropic-compatible endpoints (Bearer-auth) reject requests
|
||||||
|
that include Anthropic's ``fine-grained-tool-streaming`` beta — every
|
||||||
|
tool-use message triggers a connection error. Strip that beta for
|
||||||
|
Bearer-auth endpoints while keeping all other betas intact.
|
||||||
|
"""
|
||||||
|
if _requires_bearer_auth(base_url):
|
||||||
|
return [b for b in _COMMON_BETAS if b != _TOOL_STREAMING_BETA]
|
||||||
|
return _COMMON_BETAS
|
||||||
|
|
||||||
|
|
||||||
def build_anthropic_client(api_key: str, base_url: str = None):
|
def build_anthropic_client(api_key: str, base_url: str = None):
|
||||||
"""Create an Anthropic client, auto-detecting setup-tokens vs API keys.
|
"""Create an Anthropic client, auto-detecting setup-tokens vs API keys.
|
||||||
|
|
||||||
@@ -222,6 +239,7 @@ def build_anthropic_client(api_key: str, base_url: str = None):
|
|||||||
}
|
}
|
||||||
if normalized_base_url:
|
if normalized_base_url:
|
||||||
kwargs["base_url"] = normalized_base_url
|
kwargs["base_url"] = normalized_base_url
|
||||||
|
common_betas = _common_betas_for_base_url(normalized_base_url)
|
||||||
|
|
||||||
if _requires_bearer_auth(normalized_base_url):
|
if _requires_bearer_auth(normalized_base_url):
|
||||||
# Some Anthropic-compatible providers (e.g. MiniMax) expect the API key in
|
# Some Anthropic-compatible providers (e.g. MiniMax) expect the API key in
|
||||||
@@ -231,21 +249,21 @@ def build_anthropic_client(api_key: str, base_url: str = None):
|
|||||||
# not use Anthropic's sk-ant-api prefix and would otherwise be misread as
|
# not use Anthropic's sk-ant-api prefix and would otherwise be misread as
|
||||||
# Anthropic OAuth/setup tokens.
|
# Anthropic OAuth/setup tokens.
|
||||||
kwargs["auth_token"] = api_key
|
kwargs["auth_token"] = api_key
|
||||||
if _COMMON_BETAS:
|
if common_betas:
|
||||||
kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)}
|
kwargs["default_headers"] = {"anthropic-beta": ",".join(common_betas)}
|
||||||
elif _is_third_party_anthropic_endpoint(base_url):
|
elif _is_third_party_anthropic_endpoint(base_url):
|
||||||
# Third-party proxies (Azure AI Foundry, AWS Bedrock, etc.) use their
|
# Third-party proxies (Azure AI Foundry, AWS Bedrock, etc.) use their
|
||||||
# own API keys with x-api-key auth. Skip OAuth detection — their keys
|
# own API keys with x-api-key auth. Skip OAuth detection — their keys
|
||||||
# don't follow Anthropic's sk-ant-* prefix convention and would be
|
# don't follow Anthropic's sk-ant-* prefix convention and would be
|
||||||
# misclassified as OAuth tokens.
|
# misclassified as OAuth tokens.
|
||||||
kwargs["api_key"] = api_key
|
kwargs["api_key"] = api_key
|
||||||
if _COMMON_BETAS:
|
if common_betas:
|
||||||
kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)}
|
kwargs["default_headers"] = {"anthropic-beta": ",".join(common_betas)}
|
||||||
elif _is_oauth_token(api_key):
|
elif _is_oauth_token(api_key):
|
||||||
# OAuth access token / setup-token → Bearer auth + Claude Code identity.
|
# OAuth access token / setup-token → Bearer auth + Claude Code identity.
|
||||||
# Anthropic routes OAuth requests based on user-agent and headers;
|
# Anthropic routes OAuth requests based on user-agent and headers;
|
||||||
# without Claude Code's fingerprint, requests get intermittent 500s.
|
# without Claude Code's fingerprint, requests get intermittent 500s.
|
||||||
all_betas = _COMMON_BETAS + _OAUTH_ONLY_BETAS
|
all_betas = common_betas + _OAUTH_ONLY_BETAS
|
||||||
kwargs["auth_token"] = api_key
|
kwargs["auth_token"] = api_key
|
||||||
kwargs["default_headers"] = {
|
kwargs["default_headers"] = {
|
||||||
"anthropic-beta": ",".join(all_betas),
|
"anthropic-beta": ",".join(all_betas),
|
||||||
@@ -255,8 +273,8 @@ def build_anthropic_client(api_key: str, base_url: str = None):
|
|||||||
else:
|
else:
|
||||||
# Regular API key → x-api-key header + common betas
|
# Regular API key → x-api-key header + common betas
|
||||||
kwargs["api_key"] = api_key
|
kwargs["api_key"] = api_key
|
||||||
if _COMMON_BETAS:
|
if common_betas:
|
||||||
kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)}
|
kwargs["default_headers"] = {"anthropic-beta": ",".join(common_betas)}
|
||||||
|
|
||||||
return _anthropic_sdk.Anthropic(**kwargs)
|
return _anthropic_sdk.Anthropic(**kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ class TestBuildAnthropicClient:
|
|||||||
build_anthropic_client("sk-ant-api03-x", base_url="https://custom.api.com")
|
build_anthropic_client("sk-ant-api03-x", base_url="https://custom.api.com")
|
||||||
kwargs = mock_sdk.Anthropic.call_args[1]
|
kwargs = mock_sdk.Anthropic.call_args[1]
|
||||||
assert kwargs["base_url"] == "https://custom.api.com"
|
assert kwargs["base_url"] == "https://custom.api.com"
|
||||||
|
assert kwargs["default_headers"] == {
|
||||||
|
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
||||||
|
}
|
||||||
|
|
||||||
def test_minimax_anthropic_endpoint_uses_bearer_auth_for_regular_api_keys(self):
|
def test_minimax_anthropic_endpoint_uses_bearer_auth_for_regular_api_keys(self):
|
||||||
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
|
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
|
||||||
@@ -92,7 +95,20 @@ class TestBuildAnthropicClient:
|
|||||||
assert kwargs["auth_token"] == "minimax-secret-123"
|
assert kwargs["auth_token"] == "minimax-secret-123"
|
||||||
assert "api_key" not in kwargs
|
assert "api_key" not in kwargs
|
||||||
assert kwargs["default_headers"] == {
|
assert kwargs["default_headers"] == {
|
||||||
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
"anthropic-beta": "interleaved-thinking-2025-05-14"
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_minimax_cn_anthropic_endpoint_omits_tool_streaming_beta(self):
|
||||||
|
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
|
||||||
|
build_anthropic_client(
|
||||||
|
"minimax-cn-secret-123",
|
||||||
|
base_url="https://api.minimaxi.com/anthropic",
|
||||||
|
)
|
||||||
|
kwargs = mock_sdk.Anthropic.call_args[1]
|
||||||
|
assert kwargs["auth_token"] == "minimax-cn-secret-123"
|
||||||
|
assert "api_key" not in kwargs
|
||||||
|
assert kwargs["default_headers"] == {
|
||||||
|
"anthropic-beta": "interleaved-thinking-2025-05-14"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Tests for MiniMax provider hardening — context lengths, thinking guard, catalog."""
|
"""Tests for MiniMax provider hardening — context lengths, thinking guard, catalog, beta headers."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
class TestMinimaxContextLengths:
|
class TestMinimaxContextLengths:
|
||||||
@@ -103,3 +105,100 @@ class TestMinimaxModelCatalog:
|
|||||||
models = _PROVIDER_MODELS[provider]
|
models = _PROVIDER_MODELS[provider]
|
||||||
assert "MiniMax-M2.7-highspeed" not in models
|
assert "MiniMax-M2.7-highspeed" not in models
|
||||||
assert "MiniMax-M2.5-highspeed" not in models
|
assert "MiniMax-M2.5-highspeed" not in models
|
||||||
|
|
||||||
|
|
||||||
|
class TestMinimaxBetaHeaders:
|
||||||
|
"""MiniMax Anthropic-compat endpoints reject fine-grained-tool-streaming beta.
|
||||||
|
|
||||||
|
Verify that build_anthropic_client omits the tool-streaming beta for MiniMax
|
||||||
|
(both global and China domains) while keeping it for native Anthropic and
|
||||||
|
other third-party endpoints. Covers the fix for #6510 / #6555.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_TOOL_BETA = "fine-grained-tool-streaming-2025-05-14"
|
||||||
|
_THINKING_BETA = "interleaved-thinking-2025-05-14"
|
||||||
|
|
||||||
|
# -- helper ----------------------------------------------------------
|
||||||
|
|
||||||
|
def _build_and_get_betas(self, api_key, base_url=None):
|
||||||
|
"""Build client, return the anthropic-beta header string."""
|
||||||
|
from agent.anthropic_adapter import build_anthropic_client
|
||||||
|
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
|
||||||
|
build_anthropic_client(api_key, base_url=base_url)
|
||||||
|
kwargs = mock_sdk.Anthropic.call_args[1]
|
||||||
|
headers = kwargs.get("default_headers", {})
|
||||||
|
return headers.get("anthropic-beta", "")
|
||||||
|
|
||||||
|
# -- MiniMax global --------------------------------------------------
|
||||||
|
|
||||||
|
def test_minimax_global_omits_tool_streaming(self):
|
||||||
|
betas = self._build_and_get_betas(
|
||||||
|
"mm-key-123", base_url="https://api.minimax.io/anthropic"
|
||||||
|
)
|
||||||
|
assert self._TOOL_BETA not in betas
|
||||||
|
assert self._THINKING_BETA in betas
|
||||||
|
|
||||||
|
def test_minimax_global_trailing_slash(self):
|
||||||
|
betas = self._build_and_get_betas(
|
||||||
|
"mm-key-123", base_url="https://api.minimax.io/anthropic/"
|
||||||
|
)
|
||||||
|
assert self._TOOL_BETA not in betas
|
||||||
|
|
||||||
|
# -- MiniMax China ---------------------------------------------------
|
||||||
|
|
||||||
|
def test_minimax_cn_omits_tool_streaming(self):
|
||||||
|
betas = self._build_and_get_betas(
|
||||||
|
"mm-cn-key-456", base_url="https://api.minimaxi.com/anthropic"
|
||||||
|
)
|
||||||
|
assert self._TOOL_BETA not in betas
|
||||||
|
assert self._THINKING_BETA in betas
|
||||||
|
|
||||||
|
def test_minimax_cn_trailing_slash(self):
|
||||||
|
betas = self._build_and_get_betas(
|
||||||
|
"mm-cn-key-456", base_url="https://api.minimaxi.com/anthropic/"
|
||||||
|
)
|
||||||
|
assert self._TOOL_BETA not in betas
|
||||||
|
|
||||||
|
# -- Non-MiniMax keeps full betas ------------------------------------
|
||||||
|
|
||||||
|
def test_native_anthropic_keeps_tool_streaming(self):
|
||||||
|
betas = self._build_and_get_betas("sk-ant-api03-real-key-here")
|
||||||
|
assert self._TOOL_BETA in betas
|
||||||
|
assert self._THINKING_BETA in betas
|
||||||
|
|
||||||
|
def test_third_party_proxy_keeps_tool_streaming(self):
|
||||||
|
betas = self._build_and_get_betas(
|
||||||
|
"custom-key", base_url="https://my-proxy.example.com/anthropic"
|
||||||
|
)
|
||||||
|
assert self._TOOL_BETA in betas
|
||||||
|
|
||||||
|
def test_custom_base_url_keeps_tool_streaming(self):
|
||||||
|
betas = self._build_and_get_betas(
|
||||||
|
"custom-key", base_url="https://custom.api.com"
|
||||||
|
)
|
||||||
|
assert self._TOOL_BETA in betas
|
||||||
|
|
||||||
|
# -- _common_betas_for_base_url unit tests ---------------------------
|
||||||
|
|
||||||
|
def test_common_betas_none_url(self):
|
||||||
|
from agent.anthropic_adapter import _common_betas_for_base_url, _COMMON_BETAS
|
||||||
|
assert _common_betas_for_base_url(None) == _COMMON_BETAS
|
||||||
|
|
||||||
|
def test_common_betas_empty_url(self):
|
||||||
|
from agent.anthropic_adapter import _common_betas_for_base_url, _COMMON_BETAS
|
||||||
|
assert _common_betas_for_base_url("") == _COMMON_BETAS
|
||||||
|
|
||||||
|
def test_common_betas_minimax_url(self):
|
||||||
|
from agent.anthropic_adapter import _common_betas_for_base_url, _TOOL_STREAMING_BETA
|
||||||
|
betas = _common_betas_for_base_url("https://api.minimax.io/anthropic")
|
||||||
|
assert _TOOL_STREAMING_BETA not in betas
|
||||||
|
assert len(betas) > 0 # still has other betas
|
||||||
|
|
||||||
|
def test_common_betas_minimax_cn_url(self):
|
||||||
|
from agent.anthropic_adapter import _common_betas_for_base_url, _TOOL_STREAMING_BETA
|
||||||
|
betas = _common_betas_for_base_url("https://api.minimaxi.com/anthropic")
|
||||||
|
assert _TOOL_STREAMING_BETA not in betas
|
||||||
|
|
||||||
|
def test_common_betas_regular_url(self):
|
||||||
|
from agent.anthropic_adapter import _common_betas_for_base_url, _COMMON_BETAS
|
||||||
|
assert _common_betas_for_base_url("https://api.anthropic.com") == _COMMON_BETAS
|
||||||
|
|||||||
Reference in New Issue
Block a user