mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat: native AWS Bedrock provider via Converse API
Salvaged from PR #7920 by JiaDe-Wu — cherry-picked Bedrock-specific additions onto current main, skipping stale-branch reverts (293 commits behind). Dual-path architecture: - Claude models → AnthropicBedrock SDK (prompt caching, thinking budgets) - Non-Claude models → Converse API via boto3 (Nova, DeepSeek, Llama, Mistral) Includes: - Core adapter (agent/bedrock_adapter.py, 1098 lines) - Full provider registration (auth, models, providers, config, runtime, main) - IAM credential chain + Bedrock API Key auth modes - Dynamic model discovery via ListFoundationModels + ListInferenceProfiles - Streaming with delta callbacks, error classification, guardrails - hermes doctor + hermes auth integration - /usage pricing for 7 Bedrock models - 130 automated tests (79 unit + 28 integration + follow-up fixes) - Documentation (website/docs/guides/aws-bedrock.md) - boto3 optional dependency (pip install hermes-agent[bedrock]) Co-authored-by: JiaDe WU <40445668+JiaDe-Wu@users.noreply.github.com>
This commit is contained in:
@@ -274,6 +274,14 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
|
||||
api_key_env_vars=("XIAOMI_API_KEY",),
|
||||
base_url_env_var="XIAOMI_BASE_URL",
|
||||
),
|
||||
"bedrock": ProviderConfig(
|
||||
id="bedrock",
|
||||
name="AWS Bedrock",
|
||||
auth_type="aws_sdk",
|
||||
inference_base_url="https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
api_key_env_vars=(),
|
||||
base_url_env_var="BEDROCK_BASE_URL",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -924,6 +932,7 @@ def resolve_provider(
|
||||
"qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth",
|
||||
"hf": "huggingface", "hugging-face": "huggingface", "huggingface-hub": "huggingface",
|
||||
"mimo": "xiaomi", "xiaomi-mimo": "xiaomi",
|
||||
"aws": "bedrock", "aws-bedrock": "bedrock", "amazon-bedrock": "bedrock", "amazon": "bedrock",
|
||||
"go": "opencode-go", "opencode-go-sub": "opencode-go",
|
||||
"kilo": "kilocode", "kilo-code": "kilocode", "kilo-gateway": "kilocode",
|
||||
# Local server aliases — route through the generic custom provider
|
||||
@@ -980,6 +989,15 @@ def resolve_provider(
|
||||
if has_usable_secret(os.getenv(env_var, "")):
|
||||
return pid
|
||||
|
||||
# AWS Bedrock — detect via boto3 credential chain (IAM roles, SSO, env vars).
|
||||
# This runs after API-key providers so explicit keys always win.
|
||||
try:
|
||||
from agent.bedrock_adapter import has_aws_credentials
|
||||
if has_aws_credentials():
|
||||
return "bedrock"
|
||||
except ImportError:
|
||||
pass # boto3 not installed — skip Bedrock auto-detection
|
||||
|
||||
raise AuthError(
|
||||
"No inference provider configured. Run 'hermes model' to choose a "
|
||||
"provider and model, or set an API key (OPENROUTER_API_KEY, "
|
||||
@@ -2446,6 +2464,13 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
pconfig = PROVIDER_REGISTRY.get(target)
|
||||
if pconfig and pconfig.auth_type == "api_key":
|
||||
return get_api_key_provider_status(target)
|
||||
# AWS SDK providers (Bedrock) — check via boto3 credential chain
|
||||
if pconfig and pconfig.auth_type == "aws_sdk":
|
||||
try:
|
||||
from agent.bedrock_adapter import has_aws_credentials
|
||||
return {"logged_in": has_aws_credentials(), "provider": target}
|
||||
except ImportError:
|
||||
return {"logged_in": False, "provider": target, "error": "boto3 not installed"}
|
||||
return {"logged_in": False}
|
||||
|
||||
|
||||
|
||||
@@ -368,6 +368,27 @@ def _interactive_auth() -> None:
|
||||
print("=" * 50)
|
||||
|
||||
auth_list_command(SimpleNamespace(provider=None))
|
||||
|
||||
# Show AWS Bedrock credential status (not in the pool — uses boto3 chain)
|
||||
try:
|
||||
from agent.bedrock_adapter import has_aws_credentials, resolve_aws_auth_env_var, resolve_bedrock_region
|
||||
if has_aws_credentials():
|
||||
auth_source = resolve_aws_auth_env_var() or "unknown"
|
||||
region = resolve_bedrock_region()
|
||||
print(f"bedrock (AWS SDK credential chain):")
|
||||
print(f" Auth: {auth_source}")
|
||||
print(f" Region: {region}")
|
||||
try:
|
||||
import boto3
|
||||
sts = boto3.client("sts", region_name=region)
|
||||
identity = sts.get_caller_identity()
|
||||
arn = identity.get("Arn", "unknown")
|
||||
print(f" Identity: {arn}")
|
||||
except Exception:
|
||||
print(f" Identity: (could not resolve — boto3 STS call failed)")
|
||||
print()
|
||||
except ImportError:
|
||||
pass # boto3 or bedrock_adapter not available
|
||||
print()
|
||||
|
||||
# Main menu
|
||||
|
||||
@@ -419,6 +419,27 @@ DEFAULT_CONFIG = {
|
||||
"protect_last_n": 20, # minimum recent messages to keep uncompressed
|
||||
|
||||
},
|
||||
|
||||
# AWS Bedrock provider configuration.
|
||||
# Only used when model.provider is "bedrock".
|
||||
"bedrock": {
|
||||
"region": "", # AWS region for Bedrock API calls (empty = AWS_REGION env var → us-east-1)
|
||||
"discovery": {
|
||||
"enabled": True, # Auto-discover models via ListFoundationModels
|
||||
"provider_filter": [], # Only show models from these providers (e.g. ["anthropic", "amazon"])
|
||||
"refresh_interval": 3600, # Cache discovery results for this many seconds
|
||||
},
|
||||
"guardrail": {
|
||||
# Amazon Bedrock Guardrails — content filtering and safety policies.
|
||||
# Create a guardrail in the Bedrock console, then set the ID and version here.
|
||||
# See: https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html
|
||||
"guardrail_identifier": "", # e.g. "abc123def456"
|
||||
"guardrail_version": "", # e.g. "1" or "DRAFT"
|
||||
"stream_processing_mode": "async", # "sync" or "async"
|
||||
"trace": "disabled", # "enabled", "disabled", or "enabled_full"
|
||||
},
|
||||
},
|
||||
|
||||
"smart_model_routing": {
|
||||
"enabled": False,
|
||||
"max_simple_chars": 160,
|
||||
@@ -974,6 +995,22 @@ OPTIONAL_ENV_VARS = {
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"AWS_REGION": {
|
||||
"description": "AWS region for Bedrock API calls (e.g. us-east-1, eu-central-1)",
|
||||
"prompt": "AWS Region",
|
||||
"url": "https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html",
|
||||
"password": False,
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
"AWS_PROFILE": {
|
||||
"description": "AWS named profile for Bedrock authentication (from ~/.aws/credentials)",
|
||||
"prompt": "AWS Profile",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "provider",
|
||||
"advanced": True,
|
||||
},
|
||||
|
||||
# ── Tool API keys ──
|
||||
"EXA_API_KEY": {
|
||||
|
||||
@@ -860,6 +860,31 @@ def run_doctor(args):
|
||||
except Exception as _e:
|
||||
print(f"\r {color('⚠', Colors.YELLOW)} {_label} {color(f'({_e})', Colors.DIM)} ")
|
||||
|
||||
# -- AWS Bedrock --
|
||||
# Bedrock uses the AWS SDK credential chain, not API keys.
|
||||
try:
|
||||
from agent.bedrock_adapter import has_aws_credentials, resolve_aws_auth_env_var, resolve_bedrock_region
|
||||
if has_aws_credentials():
|
||||
_auth_var = resolve_aws_auth_env_var()
|
||||
_region = resolve_bedrock_region()
|
||||
_label = "AWS Bedrock".ljust(20)
|
||||
print(f" Checking AWS Bedrock...", end="", flush=True)
|
||||
try:
|
||||
import boto3
|
||||
_br_client = boto3.client("bedrock", region_name=_region)
|
||||
_br_resp = _br_client.list_foundation_models()
|
||||
_model_count = len(_br_resp.get("modelSummaries", []))
|
||||
print(f"\r {color('✓', Colors.GREEN)} {_label} {color(f'({_auth_var}, {_region}, {_model_count} models)', Colors.DIM)} ")
|
||||
except ImportError:
|
||||
print(f"\r {color('⚠', Colors.YELLOW)} {_label} {color('(boto3 not installed — pip install hermes-agent[bedrock])', Colors.DIM)} ")
|
||||
issues.append("Install boto3 for Bedrock: pip install hermes-agent[bedrock]")
|
||||
except Exception as _e:
|
||||
_err_name = type(_e).__name__
|
||||
print(f"\r {color('⚠', Colors.YELLOW)} {_label} {color(f'({_err_name}: {_e})', Colors.DIM)} ")
|
||||
issues.append(f"AWS Bedrock: {_err_name} — check IAM permissions for bedrock:ListFoundationModels")
|
||||
except ImportError:
|
||||
pass # bedrock_adapter not available — skip silently
|
||||
|
||||
# =========================================================================
|
||||
# Check: Submodules
|
||||
# =========================================================================
|
||||
|
||||
@@ -1139,6 +1139,8 @@ def select_provider_and_model(args=None):
|
||||
_model_flow_anthropic(config, current_model)
|
||||
elif selected_provider == "kimi-coding":
|
||||
_model_flow_kimi(config, current_model)
|
||||
elif selected_provider == "bedrock":
|
||||
_model_flow_bedrock(config, current_model)
|
||||
elif selected_provider in ("gemini", "deepseek", "xai", "zai", "kimi-coding-cn", "minimax", "minimax-cn", "kilocode", "opencode-zen", "opencode-go", "ai-gateway", "alibaba", "huggingface", "xiaomi", "arcee"):
|
||||
_model_flow_api_key_provider(config, selected_provider, current_model)
|
||||
|
||||
@@ -2425,6 +2427,252 @@ def _model_flow_kimi(config, current_model=""):
|
||||
print("No change.")
|
||||
|
||||
|
||||
def _model_flow_bedrock_api_key(config, region, current_model=""):
|
||||
"""Bedrock API Key mode — uses the OpenAI-compatible bedrock-mantle endpoint.
|
||||
|
||||
For developers who don't have an AWS account but received a Bedrock API Key
|
||||
from their AWS admin. Works like any OpenAI-compatible endpoint.
|
||||
"""
|
||||
from hermes_cli.auth import _prompt_model_selection, _save_model_choice, deactivate_provider
|
||||
from hermes_cli.config import load_config, save_config, get_env_value, save_env_value
|
||||
from hermes_cli.models import _PROVIDER_MODELS
|
||||
|
||||
mantle_base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
|
||||
|
||||
# Prompt for API key
|
||||
existing_key = get_env_value("AWS_BEARER_TOKEN_BEDROCK") or ""
|
||||
if existing_key:
|
||||
print(f" Bedrock API Key: {existing_key[:12]}... ✓")
|
||||
else:
|
||||
print(f" Endpoint: {mantle_base_url}")
|
||||
print()
|
||||
try:
|
||||
import getpass
|
||||
api_key = getpass.getpass(" Bedrock API Key: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
if not api_key:
|
||||
print(" Cancelled.")
|
||||
return
|
||||
save_env_value("AWS_BEARER_TOKEN_BEDROCK", api_key)
|
||||
existing_key = api_key
|
||||
print(" ✓ API key saved.")
|
||||
print()
|
||||
|
||||
# Model selection — use static list (mantle doesn't need boto3 for discovery)
|
||||
model_list = _PROVIDER_MODELS.get("bedrock", [])
|
||||
print(f" Showing {len(model_list)} curated models")
|
||||
|
||||
if model_list:
|
||||
selected = _prompt_model_selection(model_list, current_model=current_model)
|
||||
else:
|
||||
try:
|
||||
selected = input(" Model ID: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
selected = None
|
||||
|
||||
if selected:
|
||||
_save_model_choice(selected)
|
||||
|
||||
# Save as custom provider pointing to bedrock-mantle
|
||||
cfg = load_config()
|
||||
model = cfg.get("model")
|
||||
if not isinstance(model, dict):
|
||||
model = {"default": model} if model else {}
|
||||
cfg["model"] = model
|
||||
model["provider"] = "custom"
|
||||
model["base_url"] = mantle_base_url
|
||||
model.pop("api_mode", None) # chat_completions is the default
|
||||
|
||||
# Also save region in bedrock config for reference
|
||||
bedrock_cfg = cfg.get("bedrock", {})
|
||||
if not isinstance(bedrock_cfg, dict):
|
||||
bedrock_cfg = {}
|
||||
bedrock_cfg["region"] = region
|
||||
cfg["bedrock"] = bedrock_cfg
|
||||
|
||||
# Save the API key env var name so hermes knows where to find it
|
||||
save_env_value("OPENAI_API_KEY", existing_key)
|
||||
save_env_value("OPENAI_BASE_URL", mantle_base_url)
|
||||
|
||||
save_config(cfg)
|
||||
deactivate_provider()
|
||||
|
||||
print(f" Default model set to: {selected} (via Bedrock API Key, {region})")
|
||||
print(f" Endpoint: {mantle_base_url}")
|
||||
else:
|
||||
print(" No change.")
|
||||
|
||||
|
||||
def _model_flow_bedrock(config, current_model=""):
|
||||
"""AWS Bedrock provider: verify credentials, pick region, discover models.
|
||||
|
||||
Uses the native Converse API via boto3 — not the OpenAI-compatible endpoint.
|
||||
Auth is handled by the AWS SDK default credential chain (env vars, profile,
|
||||
instance role), so no API key prompt is needed.
|
||||
"""
|
||||
from hermes_cli.auth import _prompt_model_selection, _save_model_choice, deactivate_provider
|
||||
from hermes_cli.config import load_config, save_config
|
||||
from hermes_cli.models import _PROVIDER_MODELS
|
||||
|
||||
# 1. Check for AWS credentials
|
||||
try:
|
||||
from agent.bedrock_adapter import (
|
||||
has_aws_credentials,
|
||||
resolve_aws_auth_env_var,
|
||||
resolve_bedrock_region,
|
||||
discover_bedrock_models,
|
||||
)
|
||||
except ImportError:
|
||||
print(" ✗ boto3 is not installed. Install it with:")
|
||||
print(" pip install boto3")
|
||||
print()
|
||||
return
|
||||
|
||||
if not has_aws_credentials():
|
||||
print(" ⚠ No AWS credentials detected via environment variables.")
|
||||
print(" Bedrock will use boto3's default credential chain (IMDS, SSO, etc.)")
|
||||
print()
|
||||
|
||||
auth_var = resolve_aws_auth_env_var()
|
||||
if auth_var:
|
||||
print(f" AWS credentials: {auth_var} ✓")
|
||||
else:
|
||||
print(" AWS credentials: boto3 default chain (instance role / SSO)")
|
||||
print()
|
||||
|
||||
# 2. Region selection
|
||||
current_region = resolve_bedrock_region()
|
||||
try:
|
||||
region_input = input(f" AWS Region [{current_region}]: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
region = region_input or current_region
|
||||
|
||||
# 2b. Authentication mode
|
||||
print(" Choose authentication method:")
|
||||
print()
|
||||
print(" 1. IAM credential chain (recommended)")
|
||||
print(" Works with EC2 instance roles, SSO, env vars, aws configure")
|
||||
print(" 2. Bedrock API Key")
|
||||
print(" Enter your Bedrock API Key directly — also supports")
|
||||
print(" team scenarios where an admin distributes keys")
|
||||
print()
|
||||
try:
|
||||
auth_choice = input(" Choice [1]: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return
|
||||
|
||||
if auth_choice == "2":
|
||||
_model_flow_bedrock_api_key(config, region, current_model)
|
||||
return
|
||||
|
||||
# 3. Model discovery — try live API first, fall back to static list
|
||||
print(f" Discovering models in {region}...")
|
||||
live_models = discover_bedrock_models(region)
|
||||
|
||||
if live_models:
|
||||
_EXCLUDE_PREFIXES = (
|
||||
"stability.", "cohere.embed", "twelvelabs.", "us.stability.",
|
||||
"us.cohere.embed", "us.twelvelabs.", "global.cohere.embed",
|
||||
"global.twelvelabs.",
|
||||
)
|
||||
_EXCLUDE_SUBSTRINGS = ("safeguard", "voxtral", "palmyra-vision")
|
||||
filtered = []
|
||||
for m in live_models:
|
||||
mid = m["id"]
|
||||
if any(mid.startswith(p) for p in _EXCLUDE_PREFIXES):
|
||||
continue
|
||||
if any(s in mid.lower() for s in _EXCLUDE_SUBSTRINGS):
|
||||
continue
|
||||
filtered.append(m)
|
||||
|
||||
# Deduplicate: prefer inference profiles (us.*, global.*) over bare
|
||||
# foundation model IDs.
|
||||
profile_base_ids = set()
|
||||
for m in filtered:
|
||||
mid = m["id"]
|
||||
if mid.startswith(("us.", "global.")):
|
||||
base = mid.split(".", 1)[1] if "." in mid[3:] else mid
|
||||
profile_base_ids.add(base)
|
||||
|
||||
deduped = []
|
||||
for m in filtered:
|
||||
mid = m["id"]
|
||||
if not mid.startswith(("us.", "global.")) and mid in profile_base_ids:
|
||||
continue
|
||||
deduped.append(m)
|
||||
|
||||
_RECOMMENDED = [
|
||||
"us.anthropic.claude-sonnet-4-6",
|
||||
"us.anthropic.claude-opus-4-6",
|
||||
"us.anthropic.claude-haiku-4-5",
|
||||
"us.amazon.nova-pro",
|
||||
"us.amazon.nova-lite",
|
||||
"us.amazon.nova-micro",
|
||||
"deepseek.v3",
|
||||
"us.meta.llama4-maverick",
|
||||
"us.meta.llama4-scout",
|
||||
]
|
||||
|
||||
def _sort_key(m):
|
||||
mid = m["id"]
|
||||
for i, rec in enumerate(_RECOMMENDED):
|
||||
if mid.startswith(rec):
|
||||
return (0, i, mid)
|
||||
if mid.startswith("global."):
|
||||
return (1, 0, mid)
|
||||
return (2, 0, mid)
|
||||
|
||||
deduped.sort(key=_sort_key)
|
||||
model_list = [m["id"] for m in deduped]
|
||||
print(f" Found {len(model_list)} text model(s) (filtered from {len(live_models)} total)")
|
||||
else:
|
||||
model_list = _PROVIDER_MODELS.get("bedrock", [])
|
||||
if model_list:
|
||||
print(f" Using {len(model_list)} curated models (live discovery unavailable)")
|
||||
else:
|
||||
print(" No models found. Check IAM permissions for bedrock:ListFoundationModels.")
|
||||
return
|
||||
|
||||
# 4. Model selection
|
||||
if model_list:
|
||||
selected = _prompt_model_selection(model_list, current_model=current_model)
|
||||
else:
|
||||
try:
|
||||
selected = input(" Model ID: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
selected = None
|
||||
|
||||
if selected:
|
||||
_save_model_choice(selected)
|
||||
|
||||
cfg = load_config()
|
||||
model = cfg.get("model")
|
||||
if not isinstance(model, dict):
|
||||
model = {"default": model} if model else {}
|
||||
cfg["model"] = model
|
||||
model["provider"] = "bedrock"
|
||||
model["base_url"] = f"https://bedrock-runtime.{region}.amazonaws.com"
|
||||
model.pop("api_mode", None) # bedrock_converse is auto-detected
|
||||
|
||||
bedrock_cfg = cfg.get("bedrock", {})
|
||||
if not isinstance(bedrock_cfg, dict):
|
||||
bedrock_cfg = {}
|
||||
bedrock_cfg["region"] = region
|
||||
cfg["bedrock"] = bedrock_cfg
|
||||
|
||||
save_config(cfg)
|
||||
deactivate_provider()
|
||||
|
||||
print(f" Default model set to: {selected} (via AWS Bedrock, {region})")
|
||||
else:
|
||||
print(" No change.")
|
||||
|
||||
|
||||
def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
||||
"""Generic flow for API-key providers (z.ai, MiniMax, OpenCode, etc.)."""
|
||||
from hermes_cli.auth import (
|
||||
|
||||
@@ -303,6 +303,22 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||
"XiaomiMiMo/MiMo-V2-Flash",
|
||||
"moonshotai/Kimi-K2-Thinking",
|
||||
],
|
||||
# AWS Bedrock — static fallback list used when dynamic discovery is
|
||||
# unavailable (no boto3, no credentials, or API error). The agent
|
||||
# prefers live discovery via ListFoundationModels + ListInferenceProfiles.
|
||||
# Use inference profile IDs (us.*) since most models require them.
|
||||
"bedrock": [
|
||||
"us.anthropic.claude-sonnet-4-6",
|
||||
"us.anthropic.claude-opus-4-6-v1",
|
||||
"us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"us.amazon.nova-pro-v1:0",
|
||||
"us.amazon.nova-lite-v1:0",
|
||||
"us.amazon.nova-micro-v1:0",
|
||||
"deepseek.v3.2",
|
||||
"us.meta.llama4-maverick-17b-instruct-v1:0",
|
||||
"us.meta.llama4-scout-17b-instruct-v1:0",
|
||||
],
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -536,6 +552,7 @@ CANONICAL_PROVIDERS: list[ProviderEntry] = [
|
||||
ProviderEntry("opencode-zen", "OpenCode Zen", "OpenCode Zen (35+ curated models, pay-as-you-go)"),
|
||||
ProviderEntry("opencode-go", "OpenCode Go", "OpenCode Go (open models, $10/month subscription)"),
|
||||
ProviderEntry("ai-gateway", "Vercel AI Gateway", "Vercel AI Gateway (200+ models, pay-per-use)"),
|
||||
ProviderEntry("bedrock", "AWS Bedrock", "AWS Bedrock (Claude, Nova, Llama, DeepSeek — IAM or API key)"),
|
||||
]
|
||||
|
||||
# Derived dicts — used throughout the codebase
|
||||
@@ -587,6 +604,10 @@ _PROVIDER_ALIASES = {
|
||||
"huggingface-hub": "huggingface",
|
||||
"mimo": "xiaomi",
|
||||
"xiaomi-mimo": "xiaomi",
|
||||
"aws": "bedrock",
|
||||
"aws-bedrock": "bedrock",
|
||||
"amazon-bedrock": "bedrock",
|
||||
"amazon": "bedrock",
|
||||
"grok": "xai",
|
||||
"x-ai": "xai",
|
||||
"x.ai": "xai",
|
||||
@@ -1957,6 +1978,42 @@ def validate_requested_model(
|
||||
|
||||
# api_models is None — couldn't reach API. Accept and persist,
|
||||
# but warn so typos don't silently break things.
|
||||
|
||||
# Bedrock: use our own discovery instead of HTTP /models endpoint.
|
||||
# Bedrock's bedrock-runtime URL doesn't support /models — it uses the
|
||||
# AWS SDK control plane (ListFoundationModels + ListInferenceProfiles).
|
||||
if normalized == "bedrock":
|
||||
try:
|
||||
from agent.bedrock_adapter import discover_bedrock_models, resolve_bedrock_region
|
||||
region = resolve_bedrock_region()
|
||||
discovered = discover_bedrock_models(region)
|
||||
discovered_ids = {m["id"] for m in discovered}
|
||||
if requested in discovered_ids:
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
# Not in discovered list — still accept (user may have custom
|
||||
# inference profiles or cross-account access), but warn.
|
||||
suggestions = get_close_matches(requested, list(discovered_ids), n=3, cutoff=0.4)
|
||||
suggestion_text = ""
|
||||
if suggestions:
|
||||
suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions)
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": False,
|
||||
"message": (
|
||||
f"Note: `{requested}` was not found in Bedrock model discovery for {region}. "
|
||||
f"It may still work with custom inference profiles or cross-account access."
|
||||
f"{suggestion_text}"
|
||||
),
|
||||
}
|
||||
except Exception:
|
||||
pass # Fall through to generic warning
|
||||
|
||||
provider_label = _PROVIDER_LABELS.get(normalized, normalized)
|
||||
return {
|
||||
"accepted": True,
|
||||
|
||||
@@ -236,6 +236,12 @@ ALIASES: Dict[str, str] = {
|
||||
"mimo": "xiaomi",
|
||||
"xiaomi-mimo": "xiaomi",
|
||||
|
||||
# bedrock
|
||||
"aws": "bedrock",
|
||||
"aws-bedrock": "bedrock",
|
||||
"amazon-bedrock": "bedrock",
|
||||
"amazon": "bedrock",
|
||||
|
||||
# arcee
|
||||
"arcee-ai": "arcee",
|
||||
"arceeai": "arcee",
|
||||
@@ -262,6 +268,7 @@ _LABEL_OVERRIDES: Dict[str, str] = {
|
||||
"copilot-acp": "GitHub Copilot ACP",
|
||||
"xiaomi": "Xiaomi MiMo",
|
||||
"local": "Local endpoint",
|
||||
"bedrock": "AWS Bedrock",
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +278,7 @@ TRANSPORT_TO_API_MODE: Dict[str, str] = {
|
||||
"openai_chat": "chat_completions",
|
||||
"anthropic_messages": "anthropic_messages",
|
||||
"codex_responses": "codex_responses",
|
||||
"bedrock_converse": "bedrock_converse",
|
||||
}
|
||||
|
||||
|
||||
@@ -388,6 +396,10 @@ def determine_api_mode(provider: str, base_url: str = "") -> str:
|
||||
if pdef is not None:
|
||||
return TRANSPORT_TO_API_MODE.get(pdef.transport, "chat_completions")
|
||||
|
||||
# Direct provider checks for providers not in HERMES_OVERLAYS
|
||||
if provider == "bedrock":
|
||||
return "bedrock_converse"
|
||||
|
||||
# URL-based heuristics for custom / unknown providers
|
||||
if base_url:
|
||||
url_lower = base_url.rstrip("/").lower()
|
||||
@@ -395,6 +407,8 @@ def determine_api_mode(provider: str, base_url: str = "") -> str:
|
||||
return "anthropic_messages"
|
||||
if "api.openai.com" in url_lower:
|
||||
return "codex_responses"
|
||||
if "bedrock-runtime" in url_lower and "amazonaws.com" in url_lower:
|
||||
return "bedrock_converse"
|
||||
|
||||
return "chat_completions"
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def _copilot_runtime_api_mode(model_cfg: Dict[str, Any], api_key: str) -> str:
|
||||
return "chat_completions"
|
||||
|
||||
|
||||
_VALID_API_MODES = {"chat_completions", "codex_responses", "anthropic_messages"}
|
||||
_VALID_API_MODES = {"chat_completions", "codex_responses", "anthropic_messages", "bedrock_converse"}
|
||||
|
||||
|
||||
def _parse_api_mode(raw: Any) -> Optional[str]:
|
||||
@@ -836,6 +836,77 @@ def resolve_runtime_provider(
|
||||
"requested_provider": requested_provider,
|
||||
}
|
||||
|
||||
# AWS Bedrock (native Converse API via boto3)
|
||||
if provider == "bedrock":
|
||||
from agent.bedrock_adapter import (
|
||||
has_aws_credentials,
|
||||
resolve_aws_auth_env_var,
|
||||
resolve_bedrock_region,
|
||||
is_anthropic_bedrock_model,
|
||||
)
|
||||
# When the user explicitly selected bedrock (not auto-detected),
|
||||
# trust boto3's credential chain — it handles IMDS, ECS task roles,
|
||||
# Lambda execution roles, SSO, and other implicit sources that our
|
||||
# env-var check can't detect.
|
||||
is_explicit = requested_provider in ("bedrock", "aws", "aws-bedrock", "amazon-bedrock", "amazon")
|
||||
if not is_explicit and not has_aws_credentials():
|
||||
raise AuthError(
|
||||
"No AWS credentials found for Bedrock. Configure one of:\n"
|
||||
" - AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY\n"
|
||||
" - AWS_PROFILE (for SSO / named profiles)\n"
|
||||
" - IAM instance role (EC2, ECS, Lambda)\n"
|
||||
"Or run 'aws configure' to set up credentials.",
|
||||
code="no_aws_credentials",
|
||||
)
|
||||
# Read bedrock-specific config from config.yaml
|
||||
from hermes_cli.config import load_config as _load_bedrock_config
|
||||
_bedrock_cfg = _load_bedrock_config().get("bedrock", {})
|
||||
# Region priority: config.yaml bedrock.region → env var → us-east-1
|
||||
region = (_bedrock_cfg.get("region") or "").strip() or resolve_bedrock_region()
|
||||
auth_source = resolve_aws_auth_env_var() or "aws-sdk-default-chain"
|
||||
# Build guardrail config if configured
|
||||
_gr = _bedrock_cfg.get("guardrail", {})
|
||||
guardrail_config = None
|
||||
if _gr.get("guardrail_identifier") and _gr.get("guardrail_version"):
|
||||
guardrail_config = {
|
||||
"guardrailIdentifier": _gr["guardrail_identifier"],
|
||||
"guardrailVersion": _gr["guardrail_version"],
|
||||
}
|
||||
if _gr.get("stream_processing_mode"):
|
||||
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.
|
||||
_current_model = str(model_cfg.get("default") or "").strip()
|
||||
if is_anthropic_bedrock_model(_current_model):
|
||||
# Claude on Bedrock → AnthropicBedrock SDK → anthropic_messages path
|
||||
runtime = {
|
||||
"provider": "bedrock",
|
||||
"api_mode": "anthropic_messages",
|
||||
"base_url": f"https://bedrock-runtime.{region}.amazonaws.com",
|
||||
"api_key": "aws-sdk",
|
||||
"source": auth_source,
|
||||
"region": region,
|
||||
"bedrock_anthropic": True, # Signal to use AnthropicBedrock client
|
||||
"requested_provider": requested_provider,
|
||||
}
|
||||
else:
|
||||
# Non-Claude (Nova, DeepSeek, Llama, etc.) → Converse API
|
||||
runtime = {
|
||||
"provider": "bedrock",
|
||||
"api_mode": "bedrock_converse",
|
||||
"base_url": f"https://bedrock-runtime.{region}.amazonaws.com",
|
||||
"api_key": "aws-sdk",
|
||||
"source": auth_source,
|
||||
"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)
|
||||
pconfig = PROVIDER_REGISTRY.get(provider)
|
||||
if pconfig and pconfig.auth_type == "api_key":
|
||||
|
||||
Reference in New Issue
Block a user