mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(bedrock): region-scoped model picker + geo-aware recommendations (#28156)
Bug 2 of #28156: the picker offered us./global. inference profiles to EU-region endpoints (unroutable — AWS rejects them regardless of credentials) and _RECOMMENDED hardcoded us.anthropic.* ids, so non-US pickers pinned profiles their endpoint can't invoke. - bedrock_model_routable_from_region(): geo-prefixed profiles are only offered in their own geography (full AWS prefix set incl. apac./jp./ ca./sa./me./af.); bare ids and global.* pass everywhere; unknown region shapes hide nothing. - Recommendations match geo-agnostically on the base model id, so an EU picker pins eu.anthropic.claude-sonnet-4-6; in-region geo profiles sort above global.* for the same model (addresses the global.*-first ordering complaint from the issue thread). - Dedup generalized from (us., global.) to all profile prefixes.
This commit is contained in:
parent
b6f749af94
commit
3b15afafa2
2 changed files with 167 additions and 20 deletions
|
|
@ -27,6 +27,54 @@ import subprocess
|
|||
from hermes_cli.config import clear_model_endpoint_credentials
|
||||
|
||||
|
||||
# AWS cross-region inference profile prefixes. Any geo-prefixed profile only
|
||||
# routes from endpoints in its own geography, so the Bedrock picker must not
|
||||
# offer (e.g.) us.* profiles to an eu-central-2 endpoint — selecting one
|
||||
# produces a config AWS rejects regardless of credentials (#28156).
|
||||
# global.* routes from everywhere. Full set per the AWS cross-region
|
||||
# inference docs.
|
||||
BEDROCK_GEO_PREFIXES = (
|
||||
"us.", "eu.", "ap.", "apac.", "jp.", "ca.", "sa.", "me.", "af.",
|
||||
)
|
||||
|
||||
|
||||
def bedrock_region_geo_prefix(region_name: str) -> str:
|
||||
"""Map an AWS region name to its inference-profile geo prefix ('' = unknown)."""
|
||||
r = (region_name or "").lower()
|
||||
for geo, region_prefixes in (
|
||||
("us.", ("us-", "us_gov")),
|
||||
("eu.", ("eu-",)),
|
||||
("ap.", ("ap-",)),
|
||||
("ca.", ("ca-",)),
|
||||
("sa.", ("sa-",)),
|
||||
("me.", ("me-",)),
|
||||
("af.", ("af-",)),
|
||||
):
|
||||
if r.startswith(region_prefixes):
|
||||
return geo
|
||||
return ""
|
||||
|
||||
|
||||
def bedrock_model_routable_from_region(model_id: str, region_name: str) -> bool:
|
||||
"""True when *model_id* can be invoked from *region_name*'s endpoint.
|
||||
|
||||
Bare foundation-model ids and ``global.*`` profiles route from anywhere.
|
||||
Geo-prefixed inference profiles (``us.*``, ``eu.*``, ...) only route from
|
||||
endpoints in their own geography. Unknown region shapes hide nothing.
|
||||
"""
|
||||
mid = (model_id or "").lower()
|
||||
matched_geo = next((p for p in BEDROCK_GEO_PREFIXES if mid.startswith(p)), None)
|
||||
if matched_geo is None or mid.startswith("global."):
|
||||
return True
|
||||
geo = bedrock_region_geo_prefix(region_name)
|
||||
if not geo:
|
||||
return True
|
||||
if geo == "ap.":
|
||||
# Asia-Pacific regions can carry ap./apac./jp. profile spellings.
|
||||
return matched_geo in ("ap.", "apac.", "jp.")
|
||||
return matched_geo == geo
|
||||
|
||||
|
||||
def _prune_replaced_custom_model_config_credentials(
|
||||
base_url: str,
|
||||
*,
|
||||
|
|
@ -2265,6 +2313,8 @@ def _model_flow_bedrock(config, current_model=""):
|
|||
"global.twelvelabs.",
|
||||
)
|
||||
_EXCLUDE_SUBSTRINGS = ("safeguard", "voxtral", "palmyra-vision")
|
||||
|
||||
|
||||
filtered = []
|
||||
for m in live_models:
|
||||
mid = m["id"]
|
||||
|
|
@ -2272,44 +2322,59 @@ def _model_flow_bedrock(config, current_model=""):
|
|||
continue
|
||||
if any(s in mid.lower() for s in _EXCLUDE_SUBSTRINGS):
|
||||
continue
|
||||
if not bedrock_model_routable_from_region(mid, region):
|
||||
continue
|
||||
filtered.append(m)
|
||||
|
||||
# Deduplicate: prefer inference profiles (us.*, global.*) over bare
|
||||
# foundation model IDs.
|
||||
# Deduplicate: prefer inference profiles (geo-prefixed or global.*)
|
||||
# over bare foundation model IDs.
|
||||
_PROFILE_PREFIXES = BEDROCK_GEO_PREFIXES + ("global.",)
|
||||
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)
|
||||
_pp = next((p for p in _PROFILE_PREFIXES if mid.startswith(p)), None)
|
||||
if _pp:
|
||||
profile_base_ids.add(mid[len(_pp):])
|
||||
|
||||
deduped = []
|
||||
for m in filtered:
|
||||
mid = m["id"]
|
||||
if not mid.startswith(("us.", "global.")) and mid in profile_base_ids:
|
||||
if (
|
||||
not mid.startswith(_PROFILE_PREFIXES)
|
||||
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",
|
||||
# Recommended models, matched geo-agnostically so an EU (eu.*) or
|
||||
# APAC (apac.*) picker pins its own region's profile of the same
|
||||
# model rather than a us.* one it can't route to (#28156).
|
||||
_RECOMMENDED_BASES = [
|
||||
"anthropic.claude-sonnet-4-6",
|
||||
"anthropic.claude-opus-4-6",
|
||||
"anthropic.claude-haiku-4-5",
|
||||
"amazon.nova-pro",
|
||||
"amazon.nova-lite",
|
||||
"amazon.nova-micro",
|
||||
"deepseek.v3",
|
||||
"us.meta.llama4-maverick",
|
||||
"us.meta.llama4-scout",
|
||||
"meta.llama4-maverick",
|
||||
"meta.llama4-scout",
|
||||
]
|
||||
|
||||
def _base_id(mid: str) -> str:
|
||||
_pp = next((p for p in _PROFILE_PREFIXES if mid.startswith(p)), None)
|
||||
return mid[len(_pp):] if _pp else mid
|
||||
|
||||
def _sort_key(m):
|
||||
mid = m["id"]
|
||||
for i, rec in enumerate(_RECOMMENDED):
|
||||
if mid.startswith(rec):
|
||||
return (0, i, mid)
|
||||
base = _base_id(mid)
|
||||
for i, rec in enumerate(_RECOMMENDED_BASES):
|
||||
if base.startswith(rec):
|
||||
# In-region geo profile beats global.* for the same model
|
||||
return (0, i, 0 if not mid.startswith("global.") else 1, mid)
|
||||
if mid.startswith("global."):
|
||||
return (1, 0, mid)
|
||||
return (2, 0, mid)
|
||||
return (1, 0, 0, mid)
|
||||
return (2, 0, 0, mid)
|
||||
|
||||
deduped.sort(key=_sort_key)
|
||||
model_list = [m["id"] for m in deduped]
|
||||
|
|
|
|||
82
tests/hermes_cli/test_bedrock_region_scoped_picker.py
Normal file
82
tests/hermes_cli/test_bedrock_region_scoped_picker.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""Regression tests for #28156 — Bedrock picker must be region-scoped.
|
||||
|
||||
Geo-prefixed cross-region inference profiles (us.*, eu.*, apac.*, ...) only
|
||||
route from endpoints in their own geography. Offering us.* profiles to an
|
||||
eu-central-2 picker produces configs AWS rejects regardless of credentials.
|
||||
"""
|
||||
|
||||
from hermes_cli.model_setup_flows import (
|
||||
BEDROCK_GEO_PREFIXES,
|
||||
bedrock_model_routable_from_region,
|
||||
bedrock_region_geo_prefix,
|
||||
)
|
||||
|
||||
|
||||
class TestRegionGeoPrefix:
|
||||
def test_known_geographies(self):
|
||||
assert bedrock_region_geo_prefix("us-east-1") == "us."
|
||||
assert bedrock_region_geo_prefix("eu-central-2") == "eu."
|
||||
assert bedrock_region_geo_prefix("ap-southeast-1") == "ap."
|
||||
assert bedrock_region_geo_prefix("ca-central-1") == "ca."
|
||||
assert bedrock_region_geo_prefix("sa-east-1") == "sa."
|
||||
assert bedrock_region_geo_prefix("me-south-1") == "me."
|
||||
assert bedrock_region_geo_prefix("af-south-1") == "af."
|
||||
|
||||
def test_unknown_region_is_empty(self):
|
||||
assert bedrock_region_geo_prefix("") == ""
|
||||
assert bedrock_region_geo_prefix("moon-base-1") == ""
|
||||
|
||||
|
||||
class TestRoutableFromRegion:
|
||||
def test_us_profile_not_offered_in_eu(self):
|
||||
assert not bedrock_model_routable_from_region(
|
||||
"us.anthropic.claude-sonnet-4-6", "eu-central-2"
|
||||
)
|
||||
|
||||
def test_eu_profile_offered_in_eu(self):
|
||||
assert bedrock_model_routable_from_region(
|
||||
"eu.anthropic.claude-sonnet-4-6", "eu-central-2"
|
||||
)
|
||||
|
||||
def test_global_profile_offered_everywhere(self):
|
||||
for region in ("eu-central-2", "us-east-1", "ap-southeast-1"):
|
||||
assert bedrock_model_routable_from_region(
|
||||
"global.anthropic.claude-sonnet-4-6", region
|
||||
)
|
||||
|
||||
def test_bare_foundation_id_offered_everywhere(self):
|
||||
assert bedrock_model_routable_from_region(
|
||||
"anthropic.claude-3-sonnet-20240229-v1:0", "eu-central-2"
|
||||
)
|
||||
|
||||
def test_apac_spellings_route_in_ap_regions(self):
|
||||
for prefix in ("ap.", "apac.", "jp."):
|
||||
assert bedrock_model_routable_from_region(
|
||||
f"{prefix}anthropic.claude-sonnet-4-6", "ap-northeast-1"
|
||||
)
|
||||
|
||||
def test_eu_profile_not_offered_in_us(self):
|
||||
assert not bedrock_model_routable_from_region(
|
||||
"eu.anthropic.claude-sonnet-4-6", "us-east-1"
|
||||
)
|
||||
|
||||
def test_unknown_region_hides_nothing(self):
|
||||
assert bedrock_model_routable_from_region(
|
||||
"us.anthropic.claude-sonnet-4-6", ""
|
||||
)
|
||||
|
||||
|
||||
class TestGeoPrefixContract:
|
||||
def test_every_geo_prefix_maps_to_a_routable_region_or_is_alias(self):
|
||||
"""Invariant: each geo prefix is either reachable from some region's
|
||||
geo mapping or an ap-family alias — no dead entries."""
|
||||
mapped = {
|
||||
bedrock_region_geo_prefix(r)
|
||||
for r in (
|
||||
"us-east-1", "eu-central-2", "ap-southeast-1",
|
||||
"ca-central-1", "sa-east-1", "me-south-1", "af-south-1",
|
||||
)
|
||||
}
|
||||
ap_aliases = {"apac.", "jp."}
|
||||
for prefix in BEDROCK_GEO_PREFIXES:
|
||||
assert prefix in mapped or prefix in ap_aliases
|
||||
Loading…
Add table
Add a link
Reference in a new issue