Skip to content
Closed
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
40 changes: 39 additions & 1 deletion agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,32 @@ def _merge_custom_provider_extra_body(agent, custom_providers: List[Dict[str, An
agent.request_overrides = overrides


def _bedrock_invokemodel_guardrail_headers(gr: Dict[str, Any]) -> Optional[Dict[str, str]]:
"""Build the ``X-Amzn-Bedrock-Guardrail*`` InvokeModel headers from the
raw ``bedrock.guardrail`` config dict, or ``None`` if incomplete.

Extracted as a pure function so the trace-enum handling is unit
testable without spinning up a full ``init_agent`` (the Converse path
builds its ``guardrailConfig`` body param the same way, just without a
header — see the ``bedrock_converse`` branch below).
"""
if not (gr.get("guardrail_identifier") and gr.get("guardrail_version")):
return None
headers = {
"X-Amzn-Bedrock-GuardrailIdentifier": gr["guardrail_identifier"],
"X-Amzn-Bedrock-GuardrailVersion": str(gr["guardrail_version"]),
}
trace = gr.get("trace")
if trace:
# Preserve the configured enum verbatim (uppercased to match the
# header's expected casing) — "disabled" and "enabled_full" are
# documented values too, not just "enabled"; collapsing all of them
# to "ENABLED" would unexpectedly turn tracing on for "disabled" and
# lose the "enabled_full" verbosity level.
headers["X-Amzn-Bedrock-Trace"] = str(trace).upper()
return headers


def init_agent(
agent,
base_url: str = None,
Expand Down Expand Up @@ -787,8 +813,20 @@ def init_agent(
agent.api_key = "aws-sdk"
agent.client = None
agent._client_kwargs = {}
# Guardrail config for Bedrock Claude via InvokeModel headers.
# The Converse API uses guardrailConfig body param; InvokeModel uses
# X-Amzn-Bedrock-Guardrail* HTTP headers — same enforcement, same
# guarantee, preserves prompt caching / thinking / 1M context.
agent._bedrock_guardrail_headers = None
try:
from hermes_cli.config import load_config as _load_gr_cfg
_gr = _load_gr_cfg().get("bedrock", {}).get("guardrail", {})
agent._bedrock_guardrail_headers = _bedrock_invokemodel_guardrail_headers(_gr)
except Exception:
pass
if not agent.quiet_mode:
print(f"🤖 AI Agent initialized with model: {agent.model} (AWS Bedrock + AnthropicBedrock SDK, {_br_region})")
_gr_label = " + Guardrails" if agent._bedrock_guardrail_headers else ""
print(f"🤖 AI Agent initialized with model: {agent.model} (AWS Bedrock + AnthropicBedrock SDK, {_br_region}{_gr_label})")
else:
# Only fall back to ANTHROPIC_TOKEN when the provider is actually Anthropic.
# Other anthropic_messages providers (MiniMax, Alibaba, etc.) must use their own API key.
Expand Down
10 changes: 10 additions & 0 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2466,6 +2466,7 @@ def build_anthropic_kwargs(
base_url: str | None = None,
fast_mode: bool = False,
drop_context_1m_beta: bool = False,
bedrock_guardrail_headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""Build kwargs for anthropic.messages.create().

Expand Down Expand Up @@ -2694,6 +2695,15 @@ def _to_oauth_wire_name(name: str) -> str:
betas.append(_FAST_MODE_BETA)
kwargs["extra_headers"] = {"anthropic-beta": ",".join(betas)}

if bedrock_guardrail_headers:
# Merge Bedrock guardrail headers without overwriting an existing
# extra_headers dict (fast_mode may have already set anthropic-beta).
# Header keys are disjoint: X-Amzn-Bedrock-Guardrail* vs anthropic-beta.
kwargs["extra_headers"] = {
**kwargs.get("extra_headers", {}),
**bedrock_guardrail_headers,
}

return kwargs


Expand Down
1 change: 1 addition & 0 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
base_url=getattr(agent, "_anthropic_base_url", None),
fast_mode=(agent.request_overrides or {}).get("speed") == "fast",
drop_context_1m_beta=bool(getattr(agent, "_oauth_1m_beta_disabled", False)),
bedrock_guardrail_headers=getattr(agent, "_bedrock_guardrail_headers", None),
)

# AWS Bedrock native Converse API — bypasses the OpenAI client entirely.
Expand Down
7 changes: 6 additions & 1 deletion agent/transports/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def build_kwargs(
base_url=params.get("base_url"),
fast_mode=params.get("fast_mode", False),
drop_context_1m_beta=params.get("drop_context_1m_beta", False),
bedrock_guardrail_headers=params.get("bedrock_guardrail_headers"),
)

def normalize_response(self, response: Any, **kwargs) -> NormalizedResponse:
Expand Down Expand Up @@ -216,7 +217,9 @@ def validate_response(self, response: Any) -> bool:
if not isinstance(content_blocks, list):
return False
if not content_blocks:
return getattr(response, "stop_reason", None) in {"end_turn", "refusal"}
return getattr(response, "stop_reason", None) in {
"end_turn", "refusal", "guardrail_intervened",
}
return True

def extract_cache_stats(self, response: Any) -> Optional[Dict[str, int]]:
Expand All @@ -238,6 +241,8 @@ def extract_cache_stats(self, response: Any) -> Optional[Dict[str, int]]:
"stop_sequence": "stop",
"refusal": "content_filter",
"model_context_window_exceeded": "length",
# Bedrock guardrail blocked the request via InvokeModel headers
"guardrail_intervened": "content_filter",
}

def map_finish_reason(self, raw_reason: str) -> str:
Expand Down
13 changes: 7 additions & 6 deletions hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1972,9 +1972,12 @@ def resolve_runtime_provider(
guardrail_config["streamProcessingMode"] = _gr["stream_processing_mode"]
if _gr.get("trace"):
guardrail_config["trace"] = _gr["trace"]
# Dual-path routing: Claude models use AnthropicBedrock SDK for full
# feature parity (prompt caching, thinking budgets, adaptive thinking).
# Non-Claude models use the Converse API for multi-model support.
# Dual-path routing:
# - Claude models → AnthropicBedrock SDK (InvokeModel) → anthropic_messages path.
# Full feature parity: prompt caching, thinking budgets, 1M context.
# Guardrails are enforced via X-Amzn-Bedrock-Guardrail* HTTP headers injected
# into every InvokeModel request (see agent_init.py + anthropic_adapter.py).
# - Non-Claude models → boto3 Converse API → bedrock_converse path.
_current_model = str(target_model or model_cfg.get("default") or "").strip()
if is_anthropic_bedrock_model(_current_model):
# Claude on Bedrock → AnthropicBedrock SDK → anthropic_messages path
Expand All @@ -1989,7 +1992,7 @@ def resolve_runtime_provider(
"requested_provider": requested_provider,
}
else:
# Non-Claude (Nova, DeepSeek, Llama, etc.) → Converse API
# Non-Claude (Nova, DeepSeek, Llama, etc.) → Converse API.
runtime = {
"provider": "bedrock",
"api_mode": "bedrock_converse",
Expand All @@ -1999,8 +2002,6 @@ def resolve_runtime_provider(
"region": region,
"requested_provider": requested_provider,
}
if guardrail_config:
runtime["guardrail_config"] = guardrail_config
return runtime

# API-key providers (z.ai/GLM, Kimi, MiniMax, MiniMax-CN)
Expand Down
14 changes: 11 additions & 3 deletions hermes_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,17 @@
# module (class declaration, ``isinstance`` checks, docstring) working
# unchanged. See #44873.
if sys.platform == "win32":
from concurrent_log_handler import ( # noqa: E402
ConcurrentRotatingFileHandler as RotatingFileHandler,
)
try:
from concurrent_log_handler import ( # noqa: E402
ConcurrentRotatingFileHandler as RotatingFileHandler,
)
except ImportError:
# concurrent-log-handler is a declared core dependency on Windows
# (see pyproject.toml #44873). If it is somehow absent (e.g. a
# partial install or a stripped dev environment), fall back to the
# stdlib handler. Log rotation may fail with WinError 32 under
# concurrent writers, but the rest of the application stays functional.
from logging.handlers import RotatingFileHandler # noqa: E402
else:
from logging.handlers import RotatingFileHandler # noqa: E402

Expand Down
Loading
Loading