From d02a8a62fd31a83d2b7a1d5da5f8478f417aab5b Mon Sep 17 00:00:00 2001 From: JiaDe-Wu Date: Tue, 14 Apr 2026 00:54:12 +0000 Subject: [PATCH] feat: replace Anthropic monkey-patch with native Bedrock Converse API provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the anthropic.Anthropic -> AnthropicBedrock monkey-patch with Hermes Agent native Bedrock provider (provider="bedrock"), which uses the Converse API directly via boto3. Benefits: - Supports ALL Bedrock models (Claude, Nova, DeepSeek, Llama, Mistral) not just Claude (Anthropic SDK limitation) - No monkey-patching — clean provider="bedrock" configuration - Native streaming, tool calling, error classification, Guardrails - Dynamic model discovery via ListFoundationModels Changes: - app/hermes/main.py: Delete monkey-patch, use provider="bedrock" - bridge/contract.py: Default provider anthropic -> bedrock - bridge/bedrock_provider.py: Simplified (Converse API handles routing) - bridge/Dockerfile: Add [bedrock] extra for boto3 - cdk.json: Use inference profile ID as default model Depends on: NousResearch/hermes-agent#7920 (native Bedrock provider PR) Ref: https://github.com/JiaDe-Wu/sample-hermes-agent-on-aws-with-bedrock --- app/hermes/main.py | 85 +++++++++----------------------------- bridge/Dockerfile | 2 +- bridge/bedrock_provider.py | 63 +++++++--------------------- bridge/contract.py | 4 +- cdk.json | 2 +- 5 files changed, 40 insertions(+), 116 deletions(-) diff --git a/app/hermes/main.py b/app/hermes/main.py index 2ba2025..e076e8a 100644 --- a/app/hermes/main.py +++ b/app/hermes/main.py @@ -4,10 +4,10 @@ /ping and /invocations HTTP contract automatically. Architecture: - - Monkey-patches the anthropic SDK so that any Anthropic() client - creation returns an AnthropicBedrock() client instead — this - transparently routes all API calls through Bedrock with SigV4 auth. - - hermes-agent code is unmodified; it thinks it's talking to Anthropic. + - Hermes Agent's native Bedrock provider (provider="bedrock") routes + all API calls through the Converse API with SigV4 authentication. + - No monkey-patching needed — Hermes handles Bedrock natively. + - Supports ALL Bedrock models (Claude, Nova, DeepSeek, Llama, etc.) """ from __future__ import annotations @@ -19,15 +19,17 @@ import traceback from typing import Any +from bedrock_agentcore.runtime import BedrockAgentCoreApp + +logger = logging.getLogger("hermes.agentcore") +app = BedrockAgentCoreApp() +log = app.logger + # --------------------------------------------------------------------------- -# Monkey-patch anthropic SDK BEFORE importing hermes-agent. -# This makes all Anthropic() client creation use Bedrock SigV4 auth. +# Cached agent singleton # --------------------------------------------------------------------------- -import httpx # noqa: E402 -import anthropic # noqa: E402 - -_OrigAnthropic = anthropic.Anthropic +_agent = None def _get_region() -> str: @@ -38,54 +40,13 @@ def _get_region() -> str: ) -class _PatchedAnthropic: - """Drop-in replacement for anthropic.Anthropic that uses Bedrock.""" - - _bedrock_client = None - - def __new__(cls, *args, **kwargs): - # If called with a real Anthropic API key, use original client. - api_key = kwargs.get("api_key", "") - if api_key and api_key.startswith("sk-ant-"): - return _OrigAnthropic(*args, **kwargs) - - # Otherwise, route through Bedrock. - if cls._bedrock_client is None: - region = _get_region() - client = anthropic.AnthropicBedrock( - aws_region=region, - timeout=httpx.Timeout(600.0, connect=10.0), - ) - - cls._bedrock_client = client - return cls._bedrock_client - - -# Apply the patch. -anthropic.Anthropic = _PatchedAnthropic # type: ignore[misc] - -# --------------------------------------------------------------------------- - -from bedrock_agentcore.runtime import BedrockAgentCoreApp # noqa: E402 - -logger = logging.getLogger("hermes.agentcore") -app = BedrockAgentCoreApp() -log = app.logger - -# --------------------------------------------------------------------------- -# Cached agent singleton -# --------------------------------------------------------------------------- - -_agent = None - - def get_or_create_agent(): """Lazy-init the full hermes-agent. Blocks on first call (~5-15s).""" global _agent if _agent is not None: return _agent - log.info("Initializing hermes-agent (first request) …") + log.info("Initializing hermes-agent (first request) ...") os.environ["HERMES_HEADLESS"] = "1" os.environ.setdefault("AGENTCORE_MODE", "1") @@ -96,25 +57,19 @@ def get_or_create_agent(): from run_agent import AIAgent - # Patch the class method BEFORE creating the agent instance. - # This ensures preserve_dots=True during __init__ normalization. - AIAgent._anthropic_preserve_dots = lambda self: True - - # Use Bedrock model ID directly. The monkey-patched anthropic SDK - # routes everything through Bedrock automatically. model = os.environ.get("BEDROCK_MODEL_ID", "us.anthropic.claude-sonnet-4-6") + # Use Hermes's native Bedrock provider — Converse API with SigV4 auth. + # No monkey-patching needed. Supports all Bedrock models. _agent = AIAgent( model=model, - provider="anthropic", + provider="bedrock", + base_url=f"https://bedrock-runtime.{region}.amazonaws.com", + api_key="aws-sdk", quiet_mode=True, ) - # Force-restore the dotted Bedrock model ID — hermes-agent's __init__ - # normalises dots to dashes (us.anthropic... → us-anthropic...) which - # Bedrock rejects as an invalid model identifier. - _agent.model = model - log.info("hermes-agent ready (model=%s, region=%s, backend=bedrock)", model, region) + log.info("hermes-agent ready (model=%s, region=%s, provider=bedrock)", model, region) return _agent @@ -123,7 +78,7 @@ def get_or_create_agent(): # --------------------------------------------------------------------------- def _sigterm_handler(signum: int, frame: Any) -> None: - log.info("SIGTERM received — shutting down") + log.info("SIGTERM received -- shutting down") sys.exit(0) diff --git a/bridge/Dockerfile b/bridge/Dockerfile index f19044f..2aeb8df 100644 --- a/bridge/Dockerfile +++ b/bridge/Dockerfile @@ -21,7 +21,7 @@ COPY hermes-agent/ /build/hermes-agent/ # Install hermes-agent + bridge deps into a prefix we can copy later. RUN pip install --no-cache-dir --prefix=/install \ - -e "/build/hermes-agent[cron,mcp]" \ + -e "/build/hermes-agent[bedrock,cron,mcp]" \ boto3 botocore litellm \ && pip install --no-cache-dir --prefix=/install \ boto3 botocore litellm diff --git a/bridge/bedrock_provider.py b/bridge/bedrock_provider.py index 199be5b..cb5346b 100644 --- a/bridge/bedrock_provider.py +++ b/bridge/bedrock_provider.py @@ -1,13 +1,11 @@ -"""Bedrock LLM provider configuration for hermes-agent. +"""Bedrock model configuration for hermes-agent. -This module configures litellm as a transparent OpenAI-compatible proxy that -routes model calls to Amazon Bedrock ConverseStream. hermes-agent's existing -multi-provider architecture only needs a ``base_url`` override to use it. +With the native Bedrock provider (provider="bedrock"), hermes-agent handles +Converse API routing directly via boto3. This module provides model ID +resolution helpers for the bridge layer. -Usage in contract.py: - from bridge.bedrock_provider import configure_bedrock - configure_bedrock() - # Then start litellm proxy or configure hermes-agent to point at it. +The native provider supports ALL Bedrock models (Claude, Nova, DeepSeek, +Llama, Mistral, etc.) - not just Anthropic models. """ from __future__ import annotations @@ -17,45 +15,16 @@ logger = logging.getLogger("agentcore.bedrock_provider") -# ---- Model mapping ------------------------------------------------------- -# hermes-agent model name → litellm / Bedrock model identifier -# The "bedrock/" prefix tells litellm to use the Bedrock provider. -MODEL_MAP: dict[str, str] = { - # Anthropic — cross-region inference profiles (global.*) - "claude-opus-4": "bedrock/global.anthropic.claude-opus-4-6-v1", - "claude-opus-4-6": "bedrock/global.anthropic.claude-opus-4-6-v1", - "claude-sonnet-4": "bedrock/global.anthropic.claude-sonnet-4-6-v1", - "claude-sonnet-4-6": "bedrock/global.anthropic.claude-sonnet-4-6-v1", - "claude-haiku-3.5": "bedrock/global.anthropic.claude-haiku-4-5-20251001-v1", - "claude-haiku-4-5": "bedrock/global.anthropic.claude-haiku-4-5-20251001-v1", - # Direct region model IDs (no global prefix) - "anthropic.claude-opus-4-6-v1": "bedrock/anthropic.claude-opus-4-6-v1", - "anthropic.claude-sonnet-4-6-v1": "bedrock/anthropic.claude-sonnet-4-6-v1", - "anthropic.claude-haiku-4-5-20251001-v1": "bedrock/anthropic.claude-haiku-4-5-20251001-v1", -} +def get_default_model() -> str: + """Return the configured Bedrock model ID.""" + return os.environ.get("BEDROCK_MODEL_ID", "us.anthropic.claude-sonnet-4-6") -def resolve_model(hermes_model: str) -> str: - """Resolve a hermes-agent model name to a litellm/Bedrock model ID. - - If the model is not in the map, return it unchanged (allows passthrough - for non-Bedrock models accessed via NAT gateway). - """ - return MODEL_MAP.get(hermes_model, hermes_model) - - -def configure_bedrock() -> None: - """Set environment variables so litellm uses Bedrock by default. - - Call once at startup, before any litellm import. - """ - # litellm reads AWS credentials from the IAM role attached to the - # AgentCore runtime — no explicit keys needed. - default_model = os.environ.get("BEDROCK_MODEL_ID", "anthropic.claude-sonnet-4-6-v1") - os.environ.setdefault("LITELLM_MODEL", f"bedrock/{default_model}") - - # Suppress litellm telemetry in production. - os.environ.setdefault("LITELLM_TELEMETRY", "False") - - logger.info("Bedrock provider configured (default_model=%s)", default_model) +def get_region() -> str: + """Return the configured AWS region.""" + return ( + os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "us-west-2" + ) diff --git a/bridge/contract.py b/bridge/contract.py index 5307c4e..3df68d5 100644 --- a/bridge/contract.py +++ b/bridge/contract.py @@ -245,8 +245,8 @@ def _init_full_agent() -> None: from run_agent import AIAgent # noqa: WPS433 - model = os.environ.get("BEDROCK_MODEL_ID", "anthropic.claude-sonnet-4-6-v1") - provider = os.environ.get("HERMES_PROVIDER", "anthropic") + model = os.environ.get("BEDROCK_MODEL_ID", "us.anthropic.claude-sonnet-4-6") + provider = os.environ.get("HERMES_PROVIDER", "bedrock") base_url = os.environ.get("HERMES_BASE_URL", "") kwargs: dict[str, Any] = { diff --git a/cdk.json b/cdk.json index 97d943f..98b054b 100644 --- a/cdk.json +++ b/cdk.json @@ -20,7 +20,7 @@ "@aws-cdk/aws-lambda:recognizeLayerVersion": true, "@aws-cdk/core:stackRelativeExports": true, - "default_model_id": "global.anthropic.claude-opus-4-6-v1", + "default_model_id": "us.anthropic.claude-sonnet-4-6", "warmup_model_id": "global.anthropic.claude-sonnet-4-6-v1", "session_idle_timeout": 1800,