From a23f18cc3e27c84977e9b409be652b3ce5dc64db Mon Sep 17 00:00:00 2001 From: Ruda Porto Filgueiras Date: Mon, 27 Apr 2026 18:52:16 +0200 Subject: [PATCH] fix(bedrock): add live model discovery and region resolution for non-US regions provider_model_ids("bedrock") fell through to a static _PROVIDER_MODELS table containing only hardcoded us.* model IDs. Users configured for non-US AWS regions (eu-central-1, ap-northeast-1, etc.) saw wrong or no models in /model and autocomplete. Root causes fixed: 1. models.py: provider_model_ids() now calls discover_bedrock_models() keyed by the resolved region before falling back to the static table. A new bedrock_model_ids_or_none() helper in bedrock_adapter.py consolidates the discover -> extract IDs -> fallback pattern used by all three call sites. 2. providers.py: registers bedrock in HERMES_OVERLAYS with transport=bedrock_converse and auth_type=aws_sdk so get_provider("bedrock") and resolve_provider_full("bedrock") work. 3. model_switch.py: list_authenticated_providers() sections 2 and 3 detect AWS credentials via has_aws_credentials() for aws_sdk overlays and use live discovery for the model list. 4. bedrock_adapter.py: resolve_bedrock_region() reads the configured region from botocore.session before falling back to us-east-1, covering users who set their region in ~/.aws/config via a named profile rather than env vars. 5. tui_gateway/server.py: passes provider= to get_model_context_length() so context window lookups work correctly for the Bedrock provider. --- agent/bedrock_adapter.py | 44 +++++++++++++++++++++++++++++++++++--- hermes_cli/model_switch.py | 31 ++++++++++++++++++++++++++- hermes_cli/models.py | 12 +++++++++++ hermes_cli/providers.py | 4 ++++ tui_gateway/server.py | 1 + 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/agent/bedrock_adapter.py b/agent/bedrock_adapter.py index 48674a5628..c1dc6bb979 100644 --- a/agent/bedrock_adapter.py +++ b/agent/bedrock_adapter.py @@ -291,14 +291,52 @@ def has_aws_credentials(env: Optional[Dict[str, str]] = None) -> bool: def resolve_bedrock_region(env: Optional[Dict[str, str]] = None) -> str: """Resolve the AWS region for Bedrock API calls. - Priority: AWS_REGION → AWS_DEFAULT_REGION → us-east-1 (fallback). + Priority: + 1. AWS_REGION env var + 2. AWS_DEFAULT_REGION env var + 3. boto3/botocore configured region (from ~/.aws/config or SSO profile) + 4. us-east-1 (hard fallback) + + The boto3 fallback is critical for EU/AP users who configure their region + in ~/.aws/config via a named profile rather than env vars — without it, + live model discovery would always return us.* profile IDs regardless of + the user's actual region. """ env = env if env is not None else os.environ - return ( + explicit = ( env.get("AWS_REGION", "").strip() or env.get("AWS_DEFAULT_REGION", "").strip() - or "us-east-1" ) + if explicit: + return explicit + try: + import botocore.session + region = botocore.session.get_session().get_config_variable("region") + if region: + return region + except Exception: + pass + return "us-east-1" + + +def bedrock_model_ids_or_none() -> Optional[List[str]]: + """Live-discover Bedrock model IDs for the active region. + + Returns a list of model ID strings if discovery succeeds and yields + at least one model, or ``None`` on failure / empty result. Callers + should fall back to the static curated list when ``None`` is returned. + + This helper consolidates the discover → extract-ids → fallback + pattern that was previously duplicated across ``provider_model_ids``, + ``list_authenticated_providers`` section 2, and section 3. + """ + try: + discovered = discover_bedrock_models(resolve_bedrock_region()) + if discovered: + return [m["id"] for m in discovered] + except Exception: + pass + return None # --------------------------------------------------------------------------- diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index aed6542804..2cdb661844 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1180,6 +1180,15 @@ def list_authenticated_providers( if hermes_slug in {"copilot", "copilot-acp"}: model_ids = provider_model_ids(hermes_slug) + # For aws_sdk providers (bedrock), use live discovery so the list + # reflects the active region (eu.*, ap.*) not the static us.* list. + elif overlay.auth_type == "aws_sdk": + try: + from agent.bedrock_adapter import bedrock_model_ids_or_none + _ids = bedrock_model_ids_or_none() + model_ids = _ids if _ids is not None else (curated.get(hermes_slug, []) or curated.get(pid, [])) + except Exception: + model_ids = curated.get(hermes_slug, []) or curated.get(pid, []) else: # Use curated list — look up by Hermes slug, fall back to overlay key model_ids = curated.get(hermes_slug, []) or curated.get(pid, []) @@ -1242,10 +1251,30 @@ def list_authenticated_providers( except Exception: pass + # Special case: aws_sdk auth (bedrock) — no API key env vars, + # credentials come from the boto3 credential chain (env vars, + # ~/.aws/credentials, instance roles, etc.) + if not _cp_has_creds and _cp_config and getattr(_cp_config, "auth_type", "") == "aws_sdk": + try: + from agent.bedrock_adapter import has_aws_credentials + _cp_has_creds = has_aws_credentials() + except Exception: + pass + if not _cp_has_creds: continue - _cp_model_ids = curated.get(_cp.slug, []) + # For bedrock, use live discovery so the list reflects the active + # region (eu.*, us.*, ap.*) instead of the hardcoded us.* static list. + if _cp_config and getattr(_cp_config, "auth_type", "") == "aws_sdk": + try: + from agent.bedrock_adapter import bedrock_model_ids_or_none + _ids = bedrock_model_ids_or_none() + _cp_model_ids = _ids if _ids is not None else curated.get(_cp.slug, []) + except Exception: + _cp_model_ids = curated.get(_cp.slug, []) + else: + _cp_model_ids = curated.get(_cp.slug, []) _cp_total = len(_cp_model_ids) _cp_top = _cp_model_ids[:max_models] diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 204c688cb2..320c8f97f4 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -1988,6 +1988,18 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) live = fetch_api_models(api_key, base_url) if live: return live + # Bedrock uses live discovery keyed by the resolved AWS region so that + # EU/AP users see eu.*/ap.* model IDs instead of the static us.* list. + # Note: early return intentionally skips _MODELS_DEV_PREFERRED merge + # below — bedrock is not expected to appear in that table. + if normalized == "bedrock": + try: + from agent.bedrock_adapter import bedrock_model_ids_or_none + ids = bedrock_model_ids_or_none() + if ids is not None: + return ids + except Exception: + pass curated_static = list(_PROVIDER_MODELS.get(normalized, [])) if normalized in _MODELS_DEV_PREFERRED: return _merge_with_models_dev(normalized, curated_static) diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 32c6e3fe8c..5620250e0f 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -183,6 +183,10 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = { transport="openai_chat", # default; overridden by api_mode in config base_url_env_var="AZURE_FOUNDRY_BASE_URL", ), + "bedrock": HermesOverlay( + transport="bedrock_converse", + auth_type="aws_sdk", + ), } diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 812ef9386d..710888822b 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2407,6 +2407,7 @@ def _(rid, params: dict) -> dict: getattr(agent, "model", "") or _resolve_model(), base_url=getattr(agent, "base_url", "") or "", api_key=getattr(agent, "api_key", "") or "", + provider=getattr(agent, "provider", "") or "", ) ctx = preprocess_context_references( prompt,