mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Add ProviderTransport ABC (4 abstract methods: convert_messages, convert_tools, build_kwargs, normalize_response) plus optional hooks (validate_response, extract_cache_stats, map_finish_reason). Add transport registry with lazy discovery — get_transport() auto-imports transport modules on first call. Add AnthropicTransport — delegates to existing anthropic_adapter.py functions, wired to ALL Anthropic code paths in run_agent.py: - Main normalize loop (L10775) - Main build_kwargs (L6673) - Response validation (L9366) - Finish reason mapping (L9534) - Cache stats extraction (L9827) - Truncation normalize (L9565) - Memory flush build_kwargs + normalize (L7363, L7395) - Iteration-limit summary + retry (L8465, L8498) Zero direct adapter imports remain for transport methods. Client lifecycle, streaming, auth, and credential management stay on AIAgent. 20 new tests (ABC contract, registry, AnthropicTransport methods). 359 anthropic-related tests pass (0 failures). PR 3 of the provider transport refactor.
129 lines
4.6 KiB
Python
129 lines
4.6 KiB
Python
"""Anthropic Messages API transport.
|
|
|
|
Delegates to the existing adapter functions in agent/anthropic_adapter.py.
|
|
This transport owns format conversion and normalization — NOT client lifecycle.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from agent.transports.base import ProviderTransport
|
|
from agent.transports.types import NormalizedResponse
|
|
|
|
|
|
class AnthropicTransport(ProviderTransport):
|
|
"""Transport for api_mode='anthropic_messages'.
|
|
|
|
Wraps the existing functions in anthropic_adapter.py behind the
|
|
ProviderTransport ABC. Each method delegates — no logic is duplicated.
|
|
"""
|
|
|
|
@property
|
|
def api_mode(self) -> str:
|
|
return "anthropic_messages"
|
|
|
|
def convert_messages(self, messages: List[Dict[str, Any]], **kwargs) -> Any:
|
|
"""Convert OpenAI messages to Anthropic (system, messages) tuple.
|
|
|
|
kwargs:
|
|
base_url: Optional[str] — affects thinking signature handling.
|
|
"""
|
|
from agent.anthropic_adapter import convert_messages_to_anthropic
|
|
|
|
base_url = kwargs.get("base_url")
|
|
return convert_messages_to_anthropic(messages, base_url=base_url)
|
|
|
|
def convert_tools(self, tools: List[Dict[str, Any]]) -> Any:
|
|
"""Convert OpenAI tool schemas to Anthropic input_schema format."""
|
|
from agent.anthropic_adapter import convert_tools_to_anthropic
|
|
|
|
return convert_tools_to_anthropic(tools)
|
|
|
|
def build_kwargs(
|
|
self,
|
|
model: str,
|
|
messages: List[Dict[str, Any]],
|
|
tools: Optional[List[Dict[str, Any]]] = None,
|
|
**params,
|
|
) -> Dict[str, Any]:
|
|
"""Build Anthropic messages.create() kwargs.
|
|
|
|
Calls convert_messages and convert_tools internally.
|
|
|
|
params (all optional):
|
|
max_tokens: int
|
|
reasoning_config: dict | None
|
|
tool_choice: str | None
|
|
is_oauth: bool
|
|
preserve_dots: bool
|
|
context_length: int | None
|
|
base_url: str | None
|
|
fast_mode: bool
|
|
"""
|
|
from agent.anthropic_adapter import build_anthropic_kwargs
|
|
|
|
return build_anthropic_kwargs(
|
|
model=model,
|
|
messages=messages,
|
|
tools=tools,
|
|
max_tokens=params.get("max_tokens", 16384),
|
|
reasoning_config=params.get("reasoning_config"),
|
|
tool_choice=params.get("tool_choice"),
|
|
is_oauth=params.get("is_oauth", False),
|
|
preserve_dots=params.get("preserve_dots", False),
|
|
context_length=params.get("context_length"),
|
|
base_url=params.get("base_url"),
|
|
fast_mode=params.get("fast_mode", False),
|
|
)
|
|
|
|
def normalize_response(self, response: Any, **kwargs) -> NormalizedResponse:
|
|
"""Normalize Anthropic response to NormalizedResponse.
|
|
|
|
kwargs:
|
|
strip_tool_prefix: bool — strip 'mcp_mcp_' prefixes from tool names.
|
|
"""
|
|
from agent.anthropic_adapter import normalize_anthropic_response_v2
|
|
|
|
strip_tool_prefix = kwargs.get("strip_tool_prefix", False)
|
|
return normalize_anthropic_response_v2(response, strip_tool_prefix=strip_tool_prefix)
|
|
|
|
def validate_response(self, response: Any) -> bool:
|
|
"""Check Anthropic response structure is valid."""
|
|
if response is None:
|
|
return False
|
|
content_blocks = getattr(response, "content", None)
|
|
if not isinstance(content_blocks, list):
|
|
return False
|
|
if not content_blocks:
|
|
return False
|
|
return True
|
|
|
|
def extract_cache_stats(self, response: Any) -> Optional[Dict[str, int]]:
|
|
"""Extract Anthropic cache_read and cache_creation token counts."""
|
|
usage = getattr(response, "usage", None)
|
|
if usage is None:
|
|
return None
|
|
cached = getattr(usage, "cache_read_input_tokens", 0) or 0
|
|
written = getattr(usage, "cache_creation_input_tokens", 0) or 0
|
|
if cached or written:
|
|
return {"cached_tokens": cached, "creation_tokens": written}
|
|
return None
|
|
|
|
# Promote the adapter's canonical mapping to module level so it's shared
|
|
_STOP_REASON_MAP = {
|
|
"end_turn": "stop",
|
|
"tool_use": "tool_calls",
|
|
"max_tokens": "length",
|
|
"stop_sequence": "stop",
|
|
"refusal": "content_filter",
|
|
"model_context_window_exceeded": "length",
|
|
}
|
|
|
|
def map_finish_reason(self, raw_reason: str) -> str:
|
|
"""Map Anthropic stop_reason to OpenAI finish_reason."""
|
|
return self._STOP_REASON_MAP.get(raw_reason, "stop")
|
|
|
|
|
|
# Auto-register on import
|
|
from agent.transports import register_transport # noqa: E402
|
|
|
|
register_transport("anthropic_messages", AnthropicTransport)
|