Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 20 additions & 65 deletions app/hermes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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")
Expand All @@ -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


Expand All @@ -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)


Expand Down
2 changes: 1 addition & 1 deletion bridge/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 16 additions & 47 deletions bridge/bedrock_provider.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
)
4 changes: 2 additions & 2 deletions bridge/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand Down
2 changes: 1 addition & 1 deletion cdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down