mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 19:50:15 +00:00
fix(models): surface bedrock provider in WebUI model picker (#2720)
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)
This commit is contained in:
committed by
nesquena-hermes
parent
01e4159818
commit
d7f1514d96
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user