mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(model-switch): keep same-endpoint custom providers with different names as separate picker rows
This commit is contained in:
parent
7ed18dae90
commit
1c3a48965b
2 changed files with 97 additions and 17 deletions
|
|
@ -2453,11 +2453,17 @@ def list_authenticated_providers(
|
|||
if custom_providers and isinstance(custom_providers, list):
|
||||
from collections import OrderedDict
|
||||
|
||||
# Key by endpoint + credential identity + wire protocol instead of
|
||||
# slug: names frequently differ per model ("Ollama — X") while the
|
||||
# endpoint stays the same. Keep same-host providers with distinct
|
||||
# env-backed credentials or API protocols separate so picker selection
|
||||
# cannot route through the wrong credential/mode pair.
|
||||
# Key by endpoint + credential identity + wire protocol + display
|
||||
# prefix instead of slug: names frequently differ per model
|
||||
# ("Ollama — X") while the endpoint stays the same. Keep same-host
|
||||
# providers with distinct env-backed credentials or API protocols
|
||||
# separate so picker selection cannot route through the wrong
|
||||
# credential/mode pair. The display prefix (text before " — " /
|
||||
# " - ") is included so intentionally distinct providers sharing an
|
||||
# endpoint (e.g. a proxy fronting cerebras, groq and perplexity at
|
||||
# a single base_url) each get their own picker row instead of
|
||||
# collapsing into one. Per-model suffix entries that share the same
|
||||
# prefix ("Ollama — A", "Ollama — B") still group together.
|
||||
groups: "OrderedDict[tuple, dict]" = OrderedDict()
|
||||
for entry in custom_providers:
|
||||
if not isinstance(entry, dict):
|
||||
|
|
@ -2504,19 +2510,19 @@ def list_authenticated_providers(
|
|||
entry_extra_headers = _extra_headers_from_config(entry)
|
||||
headers_identity = tuple(sorted(entry_extra_headers.items()))
|
||||
|
||||
group_key = (api_url, credential_identity, api_mode, headers_identity)
|
||||
# Display-name prefix (text before " — " / " - "), used both
|
||||
# as a grouping dimension and to derive the row's display name.
|
||||
_display_prefix = raw_name
|
||||
for sep in ("—", " - "):
|
||||
if sep in _display_prefix:
|
||||
_display_prefix = _display_prefix.split(sep)[0].strip()
|
||||
break
|
||||
|
||||
group_key = (api_url, credential_identity, api_mode, headers_identity, _display_prefix.lower())
|
||||
if group_key not in groups:
|
||||
# Strip per-model suffix so "Ollama — GLM 5.1" becomes
|
||||
# "Ollama" for the grouped row. Em dash is the convention
|
||||
# Hermes's own writer uses; a hyphen variant is accepted
|
||||
# for hand-edited configs.
|
||||
display_name = raw_name
|
||||
for sep in ("—", " - "):
|
||||
if sep in display_name:
|
||||
display_name = display_name.split(sep)[0].strip()
|
||||
break
|
||||
if not display_name:
|
||||
display_name = raw_name
|
||||
# Reuse the prefix computed above as the row display name;
|
||||
# fall back to the raw name if stripping left it empty.
|
||||
display_name = _display_prefix or raw_name
|
||||
slug = custom_provider_slug(display_name)
|
||||
groups[group_key] = {
|
||||
"slug": slug,
|
||||
|
|
|
|||
|
|
@ -1557,3 +1557,77 @@ def test_save_discovered_models_preserves_list_of_dicts_form(monkeypatch):
|
|||
assert save_calls == [], (
|
||||
"List-of-dicts models must not be replaced with a flat list"
|
||||
)
|
||||
|
||||
|
||||
def test_shared_url_different_display_names_are_separate_rows(monkeypatch):
|
||||
"""Multiple custom_providers entries sharing base_url + api_key + api_mode
|
||||
but with *different* display-name prefixes (e.g. a proxy fronting
|
||||
cerebras, groq and perplexity at one URL) must each get their own picker
|
||||
row, not collapse into one."""
|
||||
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
|
||||
monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {})
|
||||
# Stub live discovery so the test is deterministic regardless of network.
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.fetch_api_models",
|
||||
lambda api_key, base_url, **kwargs: [],
|
||||
)
|
||||
|
||||
providers = list_authenticated_providers(
|
||||
current_provider="openrouter",
|
||||
current_base_url="https://openrouter.ai/api/v1",
|
||||
user_providers={},
|
||||
custom_providers=[
|
||||
{"name": "Cerebras", "base_url": "https://proxy.example.com/v1",
|
||||
"api_key": "proxy-key", "model": "llama-4-scout"},
|
||||
{"name": "Groq", "base_url": "https://proxy.example.com/v1",
|
||||
"api_key": "proxy-key", "model": "llama-4-scout"},
|
||||
{"name": "Perplexity", "base_url": "https://proxy.example.com/v1",
|
||||
"api_key": "proxy-key", "model": "sonar-pro"},
|
||||
],
|
||||
max_models=50,
|
||||
)
|
||||
|
||||
custom = [p for p in providers if p.get("is_user_defined")]
|
||||
names = sorted(p["name"] for p in custom)
|
||||
assert names == ["Cerebras", "Groq", "Perplexity"], (
|
||||
f"expected three separate rows, got {names}"
|
||||
)
|
||||
# Each row carries only its own model (no cross-contamination).
|
||||
by_name = {p["name"]: p["models"] for p in custom}
|
||||
assert by_name["Cerebras"] == ["llama-4-scout"]
|
||||
assert by_name["Groq"] == ["llama-4-scout"]
|
||||
assert by_name["Perplexity"] == ["sonar-pro"]
|
||||
|
||||
|
||||
def test_shared_url_per_model_suffix_still_collapses(monkeypatch):
|
||||
"""Per-model suffix entries sharing the same display-name prefix (e.g.
|
||||
"Ollama — A", "Ollama — B") must still collapse into one row even with
|
||||
the display-prefix grouping dimension."""
|
||||
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
|
||||
monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {})
|
||||
# Stub live discovery so a locally-running Ollama cannot override the
|
||||
# static configured models and make the assertion flaky.
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.fetch_api_models",
|
||||
lambda api_key, base_url, **kwargs: [],
|
||||
)
|
||||
|
||||
providers = list_authenticated_providers(
|
||||
current_provider="openrouter",
|
||||
current_base_url="https://openrouter.ai/api/v1",
|
||||
user_providers={},
|
||||
custom_providers=[
|
||||
{"name": "Ollama \u2014 GLM 5.1", "base_url": "http://localhost:11434/v1",
|
||||
"api_key": "ollama", "model": "glm-5.1"},
|
||||
{"name": "Ollama \u2014 Qwen3-coder", "base_url": "http://localhost:11434/v1",
|
||||
"api_key": "ollama", "model": "qwen3-coder"},
|
||||
],
|
||||
max_models=50,
|
||||
)
|
||||
|
||||
custom = [p for p in providers if p.get("is_user_defined")]
|
||||
assert len(custom) == 1, (
|
||||
f"expected one collapsed row, got {[p['name'] for p in custom]}"
|
||||
)
|
||||
assert custom[0]["name"] == "Ollama"
|
||||
assert set(custom[0]["models"]) == {"glm-5.1", "qwen3-coder"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue