From d7f1514d968f84a8fd762cf054c489fb2cbfc7c9 Mon Sep 17 00:00:00 2001 From: Abdul Munim Date: Sat, 23 May 2026 09:56:23 +0200 Subject: [PATCH] fix(models): surface bedrock provider in WebUI model picker (#2720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bedrock was silently dropped from the picker because: 1. 'bedrock' absent from _PROVIDER_DISPLAY — group header fell back to title-cased id; more critically the group fell to the else branch 2. 'bedrock' absent from _PROVIDER_MODELS — else branch has no auto-detected models, so the group was never appended 3. Fallback env-var detection (hermes_cli unavailable) never checked AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY Fix: - Add 'bedrock': 'AWS Bedrock' to _PROVIDER_DISPLAY - Add static fallback model list to _PROVIDER_MODELS['bedrock'] with global Anthropic Claude 4.x cross-region inference profile IDs; live discovery via hermes_cli.models.provider_model_ids('bedrock') is used first (existing _read_live_provider_model_ids machinery) - Detect bedrock in env fallback path when both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are present Tests: tests/test_issue2720_bedrock_model_picker.py (5 new tests) --- api/config.py | 17 ++++ tests/test_issue2720_bedrock_model_picker.py | 96 ++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 tests/test_issue2720_bedrock_model_picker.py diff --git a/api/config.py b/api/config.py index 54c2bce5..db4afe67 100644 --- a/api/config.py +++ b/api/config.py @@ -713,6 +713,7 @@ _PROVIDER_DISPLAY = { "x-ai": "xAI", "nvidia": "NVIDIA NIM", "xiaomi": "Xiaomi", + "bedrock": "AWS Bedrock", } # Provider alias → canonical slug. Users configure providers using the @@ -1213,6 +1214,16 @@ _PROVIDER_MODELS = { "xai-oauth": [ {"id": "grok-4.20", "label": "Grok 4.20"}, ], + # AWS Bedrock — static fallback list; live model list is fetched via + # hermes_cli.models.provider_model_ids("bedrock") when available (#2720). + "bedrock": [ + {"id": "global.anthropic.claude-opus-4-7", "label": "Global Anthropic Claude Opus 4.7"}, + {"id": "global.anthropic.claude-opus-4-6-v1", "label": "Global Anthropic Claude Opus 4.6"}, + {"id": "global.anthropic.claude-sonnet-4-6", "label": "Global Anthropic Claude Sonnet 4.6"}, + {"id": "global.anthropic.claude-opus-4-5-20251101-v1:0", "label": "GLOBAL Anthropic Claude Opus 4.5"}, + {"id": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", "label": "Global Claude Sonnet 4.5"}, + {"id": "global.anthropic.claude-haiku-4-5-20251001-v1:0", "label": "Global Anthropic Claude Haiku 4.5"}, + ], } @@ -3007,6 +3018,8 @@ def get_available_models() -> dict: "MINIMAX_CN_API_KEY", "XAI_API_KEY", "MISTRAL_API_KEY", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", ): val = os.getenv(k) if val: @@ -3046,6 +3059,10 @@ def get_available_models() -> dict: detected_providers.add("opencode-zen") if all_env.get("OPENCODE_GO_API_KEY"): detected_providers.add("opencode-go") + # AWS Bedrock uses IAM credentials rather than a single API key. + # Detect when both access key and secret are available (#2720). + if all_env.get("AWS_ACCESS_KEY_ID") and all_env.get("AWS_SECRET_ACCESS_KEY"): + detected_providers.add("bedrock") # LM Studio: detect via LM_API_KEY + LM_BASE_URL in ~/.hermes/.env if all_env.get("LM_API_KEY") and all_env.get("LM_BASE_URL"): detected_providers.add("lmstudio") diff --git a/tests/test_issue2720_bedrock_model_picker.py b/tests/test_issue2720_bedrock_model_picker.py new file mode 100644 index 00000000..dc68ab65 --- /dev/null +++ b/tests/test_issue2720_bedrock_model_picker.py @@ -0,0 +1,96 @@ +"""Regression coverage for #2720: Bedrock models must appear in the WebUI model picker.""" + +from __future__ import annotations + +import builtins + +import api.config as config + + +def _force_env_fallback(monkeypatch): + """Force get_available_models() down its explicit env-var fallback path.""" + real_import = builtins.__import__ + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ("hermes_cli.models", "hermes_cli.auth"): + raise ImportError(name) + return real_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + +def _run_available_models_with_cfg(monkeypatch, tmp_path, cfg): + old_cfg = dict(config.cfg) + old_mtime = config._cfg_mtime + monkeypatch.setattr(config, "_models_cache_path", tmp_path / "models_cache.json") + monkeypatch.setattr(config, "_get_config_path", lambda: tmp_path / "missing-config.yaml") + monkeypatch.setattr("api.profiles.get_active_hermes_home", lambda: tmp_path, raising=False) + config.cfg.clear() + config.cfg.update(cfg) + config._cfg_mtime = 0.0 + config.invalidate_models_cache() + try: + return config.get_available_models() + finally: + config.cfg.clear() + config.cfg.update(old_cfg) + config._cfg_mtime = old_mtime + config.invalidate_models_cache() + + +def test_bedrock_in_provider_display(): + """_PROVIDER_DISPLAY must have a human-readable label for 'bedrock'.""" + assert "bedrock" in config._PROVIDER_DISPLAY, ( + "_PROVIDER_DISPLAY is missing 'bedrock' — the group header in the model picker " + "will fall back to 'Bedrock' (title-cased id) instead of 'AWS Bedrock'" + ) + assert config._PROVIDER_DISPLAY["bedrock"] == "AWS Bedrock" + + +def test_bedrock_in_provider_models(): + """_PROVIDER_MODELS must have a static fallback list for 'bedrock'.""" + assert "bedrock" in config._PROVIDER_MODELS, ( + "_PROVIDER_MODELS is missing 'bedrock' — the group builder falls to the " + "else/auto-detected branch where an empty model list silently drops the group" + ) + assert len(config._PROVIDER_MODELS["bedrock"]) > 0, ( + "_PROVIDER_MODELS['bedrock'] must have at least one static fallback model" + ) + + +def test_bedrock_static_models_have_required_fields(): + """Every static bedrock model entry must have both 'id' and 'label'.""" + for model in config._PROVIDER_MODELS["bedrock"]: + assert "id" in model and model["id"], f"Missing id in bedrock model entry: {model}" + assert "label" in model and model["label"], f"Missing label in bedrock model entry: {model}" + + +def test_bedrock_aws_credentials_detected_in_env_fallback(monkeypatch, tmp_path): + """AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY must trigger bedrock group (no hermes_cli).""" + _force_env_fallback(monkeypatch) + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + + result = _run_available_models_with_cfg(monkeypatch, tmp_path, {"model": {}}) + groups = {group["provider_id"]: group for group in result["groups"]} + + assert "bedrock" in groups, ( + "bedrock group missing from model picker even with AWS_ACCESS_KEY_ID and " + "AWS_SECRET_ACCESS_KEY set — env-var fallback path does not detect bedrock (#2720)" + ) + assert groups["bedrock"]["provider"] == "AWS Bedrock" + assert len(groups["bedrock"]["models"]) > 0 + + +def test_bedrock_missing_secret_key_not_detected(monkeypatch, tmp_path): + """Only AWS_ACCESS_KEY_ID (without the secret) must NOT trigger bedrock group.""" + _force_env_fallback(monkeypatch) + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE") + monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False) + + result = _run_available_models_with_cfg(monkeypatch, tmp_path, {"model": {}}) + groups = {group["provider_id"]: group for group in result["groups"]} + + assert "bedrock" not in groups, ( + "bedrock must not appear when only AWS_ACCESS_KEY_ID is set without the secret" + )