Move provider adapters (anthropic, bedrock, azure), platform adapters (telegram, slack, discord, feishu, dingtalk, matrix), and terminal backends (modal, daytona) out of core into plugins/ workspace members. Core references them via the plugin registries (get_provider_namespace / get_provider_service / get_tool_provider / get_credential_pool_hook) instead of direct imports. - Provider/platform/terminal adapters relocated under plugins/; pyproject extras reference workspace members; nix variants aggregate per-platform extras. - Anthropic credential discovery + OAuth-masquerade guard live in the plugin's credential_pool_hook; browser-open guarded by _can_open_graphical_browser. - Vercel AI Gateway + Vercel Sandbox removed (upstream deletion); get_bedrock_model_ids removed (replaced by bedrock_model_ids_or_none + discover_bedrock_models). - Terminal backends resolve ModalEnvironment / DaytonaEnvironment lazily from the plugin registry. - uv.lock regenerated against the pluginified workspace. Plugin test suites updated for the relocation: imports point at hermes_agent_<plat>.adapter, caplog logger-name filters and monkeypatch targets use the new module paths, and credential/rollback tests patch registries.get_provider_service rather than the removed agent.*_adapter modules. Verified: zero dead imports of relocated modules in core (import smoke test + rename-map grep); nix develop succeeds; targeted plugin suites green (bedrock, anthropic-auxiliary, matrix, dingtalk, feishu, credential_pool, switch_model_rollback). Remaining full-suite failures are pre-existing on the pre-merge tree (telegram setUpModule __code__) or environmental (voice/media/ PTY/network-dependent), not introduced here.
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
"""Tests for agent.auxiliary_client._try_custom_endpoint's anthropic_messages branch.
|
|
|
|
When a user configures a custom endpoint with ``api_mode: anthropic_messages``
|
|
(e.g. MiniMax, Zhipu GLM, LiteLLM in Anthropic-proxy mode), auxiliary tasks
|
|
(compression, web_extract, session_search, title generation) must use the
|
|
native Anthropic transport rather than being silently downgraded to an
|
|
OpenAI-wire client that speaks the wrong protocol.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env(monkeypatch):
|
|
for key in (
|
|
"OPENAI_API_KEY", "OPENAI_BASE_URL",
|
|
"ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN",
|
|
):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
def _make_fake_anthropic_namespace(build_side_effect=None, build_return=None):
|
|
"""Return a fake provider namespace dict and fake client for registry patching.
|
|
|
|
_try_custom_endpoint() resolves build_anthropic_client through
|
|
registries.get_provider_namespace("anthropic"), not via a direct module
|
|
import, so we must patch that path (not hermes_agent_anthropic.adapter).
|
|
"""
|
|
fake_client = MagicMock(name="anthropic_client")
|
|
if build_side_effect is not None:
|
|
mock_build = MagicMock(side_effect=build_side_effect)
|
|
else:
|
|
mock_build = MagicMock(return_value=build_return or fake_client)
|
|
|
|
from agent.anthropic_aux import AnthropicAuxiliaryClient as _AC
|
|
fake_ns = {
|
|
"build_anthropic_client": mock_build,
|
|
"AnthropicAuxiliaryClient": _AC,
|
|
"AsyncAnthropicAuxiliaryClient": MagicMock(),
|
|
}
|
|
return fake_ns, fake_client, mock_build
|
|
|
|
|
|
def test_custom_endpoint_anthropic_messages_builds_anthropic_wrapper():
|
|
"""api_mode=anthropic_messages → returns AnthropicAuxiliaryClient, not OpenAI."""
|
|
from agent.auxiliary_client import _try_custom_endpoint
|
|
from agent.anthropic_aux import AnthropicAuxiliaryClient
|
|
|
|
fake_ns, fake_client, _ = _make_fake_anthropic_namespace()
|
|
|
|
with patch(
|
|
"agent.auxiliary_client._resolve_custom_runtime",
|
|
return_value=(
|
|
"https://api.minimax.io/anthropic",
|
|
"minimax-key",
|
|
"anthropic_messages",
|
|
),
|
|
), patch(
|
|
"agent.auxiliary_client._read_main_model",
|
|
return_value="claude-sonnet-4-6",
|
|
), patch(
|
|
"agent.plugin_registries.registries",
|
|
) as mock_reg:
|
|
mock_reg.get_provider_namespace.return_value = fake_ns
|
|
mock_reg.get_provider_service.side_effect = lambda p, n: fake_ns.get(n) if p == "anthropic" else None
|
|
client, model = _try_custom_endpoint()
|
|
|
|
assert isinstance(client, AnthropicAuxiliaryClient), (
|
|
"Custom endpoint with api_mode=anthropic_messages must return the "
|
|
f"native Anthropic wrapper, got {type(client).__name__}"
|
|
)
|
|
assert model == "claude-sonnet-4-6"
|
|
# Wrapper should NOT be marked as OAuth — third-party endpoints are
|
|
# always API-key authenticated.
|
|
assert client.api_key == "minimax-key"
|
|
assert client.base_url == "https://api.minimax.io/anthropic"
|
|
|
|
|
|
def test_custom_endpoint_anthropic_messages_falls_back_when_sdk_missing():
|
|
"""Graceful degradation when anthropic SDK is unavailable."""
|
|
from agent.auxiliary_client import _try_custom_endpoint
|
|
|
|
import_error = ImportError("anthropic package not installed")
|
|
fake_ns, _, _ = _make_fake_anthropic_namespace(build_side_effect=import_error)
|
|
|
|
with patch(
|
|
"agent.auxiliary_client._resolve_custom_runtime",
|
|
return_value=("https://api.minimax.io/anthropic", "k", "anthropic_messages"),
|
|
), patch(
|
|
"agent.auxiliary_client._read_main_model",
|
|
return_value="claude-sonnet-4-6",
|
|
), patch(
|
|
"agent.plugin_registries.registries",
|
|
) as mock_reg:
|
|
mock_reg.get_provider_namespace.return_value = fake_ns
|
|
mock_reg.get_provider_service.side_effect = lambda p, n: fake_ns.get(n) if p == "anthropic" else None
|
|
client, model = _try_custom_endpoint()
|
|
|
|
# Should fall back to an OpenAI-wire client rather than returning
|
|
# (None, None) — the tool still needs to do *something*.
|
|
assert client is not None
|
|
assert model == "claude-sonnet-4-6"
|
|
# OpenAI client, not AnthropicAuxiliaryClient.
|
|
from agent.anthropic_aux import AnthropicAuxiliaryClient
|
|
assert not isinstance(client, AnthropicAuxiliaryClient)
|
|
|
|
|
|
def test_custom_endpoint_chat_completions_still_uses_openai_wire():
|
|
"""Regression: default path (no api_mode) must remain OpenAI client."""
|
|
from agent.auxiliary_client import _try_custom_endpoint
|
|
from agent.anthropic_aux import AnthropicAuxiliaryClient
|
|
|
|
with patch(
|
|
"agent.auxiliary_client._resolve_custom_runtime",
|
|
return_value=("https://api.example.com/v1", "key", None),
|
|
), patch(
|
|
"agent.auxiliary_client._read_main_model",
|
|
return_value="my-model",
|
|
):
|
|
client, model = _try_custom_endpoint()
|
|
|
|
assert client is not None
|
|
assert model == "my-model"
|
|
assert not isinstance(client, AnthropicAuxiliaryClient)
|