mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-04 02:21:47 +00:00
fix(model_switch): dedup /model picker rows when custom provider endpoint matches a built-in (#16970) (#17511)
When a user authenticates a built-in provider via env var (e.g. DASHSCOPE_API_KEY triggers the built-in 'alibaba' row) AND defines a custom_providers entry pointing at the same endpoint, the picker previously emitted two rows for one endpoint. The built-in row already carries the canonical slug, curated model list, and correct auth wiring, so the shadow custom entry is redundant. Adds a _builtin_endpoints set populated as sections 1/2/2b emit rows. Each entry is the provider's effective base URL (env override via base_url_env_var wins over the static inference_base_url, so DASHSCOPE_BASE_URL-overridden endpoints dedup correctly). Section 4 skips any grouped custom entry whose base_url matches. Intentionally does NOT repurpose model_catalog.enabled as a 'hide built-ins' flag. That config controls the remote curated-manifest fetch (documented on the model-catalog reference page) and overloading it would silently change behavior for users who disable it for network/privacy reasons. Three new tests: - shadow dedup fires when endpoint matches static inference_base_url - dedup does NOT hide custom entries on genuinely distinct endpoints - dedup honors the base_url_env_var override path
This commit is contained in:
parent
fa3338c171
commit
e120cd5941
2 changed files with 179 additions and 0 deletions
|
|
@ -1018,6 +1018,37 @@ def list_authenticated_providers(
|
|||
results: List[dict] = []
|
||||
seen_slugs: set = set() # lowercase-normalized to catch case variants (#9545)
|
||||
seen_mdev_ids: set = set() # prevent duplicate entries for aliases (e.g. kimi-coding + kimi-coding-cn)
|
||||
# Effective base URLs of every built-in row we emit (normalized lower+rstrip).
|
||||
# Section 4 uses this to hide ``custom_providers`` entries that point at the
|
||||
# same endpoint as a built-in (e.g. a user-defined "my-dashscope" on
|
||||
# https://coding-intl.dashscope.aliyuncs.com/v1 collides with the built-in
|
||||
# alibaba-coding-plan row when DASHSCOPE_API_KEY is present). Fixes #16970.
|
||||
_builtin_endpoints: set = set()
|
||||
|
||||
def _norm_url(url: str) -> str:
|
||||
return str(url or "").strip().rstrip("/").lower()
|
||||
|
||||
def _record_builtin_endpoint(slug: str) -> None:
|
||||
"""Record the effective base URL for a built-in provider row.
|
||||
|
||||
Prefers the live env-override (e.g. DASHSCOPE_BASE_URL) over the
|
||||
static inference_base_url so the dedup matches what a user typing
|
||||
that URL into custom_providers would actually hit."""
|
||||
try:
|
||||
from hermes_cli.auth import PROVIDER_REGISTRY as _reg
|
||||
except Exception:
|
||||
return
|
||||
pcfg = _reg.get(slug)
|
||||
if not pcfg:
|
||||
return
|
||||
url = ""
|
||||
if getattr(pcfg, "base_url_env_var", ""):
|
||||
url = os.environ.get(pcfg.base_url_env_var, "") or ""
|
||||
if not url:
|
||||
url = getattr(pcfg, "inference_base_url", "") or ""
|
||||
normed = _norm_url(url)
|
||||
if normed:
|
||||
_builtin_endpoints.add(normed)
|
||||
|
||||
data = fetch_models_dev()
|
||||
|
||||
|
|
@ -1124,6 +1155,7 @@ def list_authenticated_providers(
|
|||
})
|
||||
seen_slugs.add(slug.lower())
|
||||
seen_mdev_ids.add(mdev_id)
|
||||
_record_builtin_endpoint(slug)
|
||||
|
||||
# --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---
|
||||
from hermes_cli.providers import HERMES_OVERLAYS
|
||||
|
|
@ -1238,6 +1270,7 @@ def list_authenticated_providers(
|
|||
})
|
||||
seen_slugs.add(pid.lower())
|
||||
seen_slugs.add(hermes_slug.lower())
|
||||
_record_builtin_endpoint(hermes_slug)
|
||||
|
||||
# --- 2b. Cross-check canonical provider list ---
|
||||
# Catches providers that are in CANONICAL_PROVIDERS but weren't found
|
||||
|
|
@ -1317,6 +1350,7 @@ def list_authenticated_providers(
|
|||
"source": "canonical",
|
||||
})
|
||||
seen_slugs.add(_cp.slug.lower())
|
||||
_record_builtin_endpoint(_cp.slug)
|
||||
|
||||
# --- 3. User-defined endpoints from config ---
|
||||
# Track (name, base_url) of what section 3 emits so section 4 can skip
|
||||
|
|
@ -1526,6 +1560,15 @@ def list_authenticated_providers(
|
|||
)
|
||||
if _pair_key[0] and _pair_key[1] and _pair_key in _section3_emitted_pairs:
|
||||
continue
|
||||
# Skip if a built-in row (sections 1/2/2b) already represents this
|
||||
# endpoint. Fixes #16970: a user-defined "my-dashscope" pointing at
|
||||
# https://coding-intl.dashscope.aliyuncs.com/v1 duplicates the
|
||||
# built-in alibaba-coding-plan row whenever DASHSCOPE_API_KEY is
|
||||
# set. The built-in row carries the curated model list, correct
|
||||
# auth wiring, and canonical slug — keep it and hide the shadow.
|
||||
_grp_url_norm = _pair_key[1]
|
||||
if _grp_url_norm and _grp_url_norm in _builtin_endpoints:
|
||||
continue
|
||||
results.append({
|
||||
"slug": slug,
|
||||
"name": grp["name"],
|
||||
|
|
|
|||
|
|
@ -453,6 +453,142 @@ def test_list_authenticated_providers_no_duplicate_labels_across_schemas(monkeyp
|
|||
)
|
||||
|
||||
|
||||
def test_list_authenticated_providers_hides_custom_shadowing_builtin_endpoint(monkeypatch):
|
||||
"""#16970: a custom_providers entry whose ``base_url`` matches a built-in
|
||||
provider's endpoint should be hidden. The built-in row already represents
|
||||
that endpoint with its canonical slug, curated model list, and auth wiring.
|
||||
|
||||
Repro: user sets ``DASHSCOPE_API_KEY`` (triggers the built-in ``alibaba``
|
||||
row pointing at the static ``inference_base_url``) AND defines a
|
||||
``my-alibaba`` custom provider pointing at the same URL. Before the fix,
|
||||
the picker showed both rows for one endpoint.
|
||||
"""
|
||||
monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-test")
|
||||
monkeypatch.setattr(
|
||||
"agent.models_dev.fetch_models_dev",
|
||||
lambda: {
|
||||
"alibaba": {
|
||||
"name": "Alibaba Cloud (DashScope)",
|
||||
"env": ["DASHSCOPE_API_KEY"],
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {})
|
||||
|
||||
custom_providers = [
|
||||
{
|
||||
"name": "my-alibaba",
|
||||
# Matches PROVIDER_REGISTRY['alibaba'].inference_base_url exactly.
|
||||
"base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "sk-sp-test",
|
||||
"model": "qwen3.6-plus",
|
||||
"models": {"qwen3.6-plus": {"context_length": 500000}},
|
||||
}
|
||||
]
|
||||
|
||||
providers = list_authenticated_providers(
|
||||
current_provider="my-alibaba",
|
||||
user_providers={},
|
||||
custom_providers=custom_providers,
|
||||
max_models=50,
|
||||
)
|
||||
|
||||
slugs = [p["slug"] for p in providers]
|
||||
# Built-in alibaba row should be present.
|
||||
assert "alibaba" in slugs, (
|
||||
f"Expected built-in alibaba row, got slugs: {slugs}"
|
||||
)
|
||||
# Custom shadow row should be hidden — its base_url matches the built-in's.
|
||||
assert not any("my-alibaba" in s for s in slugs), (
|
||||
f"Custom my-alibaba should have been dedup'd against the built-in "
|
||||
f"alibaba endpoint, got slugs: {slugs}"
|
||||
)
|
||||
|
||||
|
||||
def test_list_authenticated_providers_keeps_custom_with_distinct_endpoint(monkeypatch):
|
||||
"""Dedup must only apply when the endpoint matches a built-in. A custom
|
||||
provider on a genuinely distinct endpoint stays visible even if a
|
||||
built-in is also authenticated."""
|
||||
monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-test")
|
||||
monkeypatch.setattr(
|
||||
"agent.models_dev.fetch_models_dev",
|
||||
lambda: {
|
||||
"alibaba": {
|
||||
"name": "Alibaba Cloud (DashScope)",
|
||||
"env": ["DASHSCOPE_API_KEY"],
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {})
|
||||
|
||||
custom_providers = [
|
||||
{
|
||||
"name": "my-private-relay",
|
||||
"base_url": "https://relay.example.internal/v1",
|
||||
"api_key": "sk-relay-test",
|
||||
"model": "qwen3.6-plus",
|
||||
"models": {"qwen3.6-plus": {}},
|
||||
}
|
||||
]
|
||||
|
||||
providers = list_authenticated_providers(
|
||||
current_provider="my-private-relay",
|
||||
user_providers={},
|
||||
custom_providers=custom_providers,
|
||||
max_models=50,
|
||||
)
|
||||
|
||||
slugs = [p["slug"] for p in providers]
|
||||
assert any("my-private-relay" in s for s in slugs), (
|
||||
f"Custom provider on distinct endpoint must stay visible, got: {slugs}"
|
||||
)
|
||||
|
||||
|
||||
def test_list_authenticated_providers_dedup_honors_base_url_env_override(monkeypatch):
|
||||
"""The dedup must track the EFFECTIVE endpoint — if DASHSCOPE_BASE_URL
|
||||
overrides the static inference_base_url, a custom provider pointing at
|
||||
the overridden URL (not the static one) should still be recognized as
|
||||
a duplicate."""
|
||||
monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-test")
|
||||
monkeypatch.setenv(
|
||||
"DASHSCOPE_BASE_URL",
|
||||
"https://custom-dashscope.example.com/v1",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"agent.models_dev.fetch_models_dev",
|
||||
lambda: {
|
||||
"alibaba": {
|
||||
"name": "Alibaba Cloud (DashScope)",
|
||||
"env": ["DASHSCOPE_API_KEY"],
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {})
|
||||
|
||||
custom_providers = [
|
||||
{
|
||||
"name": "my-dashscope-override",
|
||||
# Same URL as DASHSCOPE_BASE_URL env override above.
|
||||
"base_url": "https://custom-dashscope.example.com/v1",
|
||||
"api_key": "sk-test",
|
||||
"model": "qwen3.6-plus",
|
||||
}
|
||||
]
|
||||
|
||||
providers = list_authenticated_providers(
|
||||
current_provider="alibaba",
|
||||
user_providers={},
|
||||
custom_providers=custom_providers,
|
||||
max_models=50,
|
||||
)
|
||||
|
||||
slugs = [p["slug"] for p in providers]
|
||||
assert not any("my-dashscope-override" in s for s in slugs), (
|
||||
f"Custom entry matching env-overridden built-in endpoint should be "
|
||||
f"dedup'd, got: {slugs}"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for _get_named_custom_provider with providers: dict
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue