mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-24 10:52:21 +00:00
fix(picker): keep flat-namespace reseller first-party models in desktop picker
OpenCode Go (and OpenCode Zen) showed only a subset of the models they serve in the desktop/CLI model picker — e.g. opencode-go rendered 13 of 19, silently dropping minimax-m3/m2.7/m2.5, glm-5/5.1, deepseek-v4-flash. Root cause: the picker dedup in build_models_payload strips any model from an aggregator row that overlaps a user-defined provider's catalog (so a local proxy isn't shadowed by OpenRouter). It gated on is_aggregator(), which is True for opencode-go/zen because their flat /v1/models returns bare IDs the model-switch resolver searches. But those are flat-namespace RESELLERS, not routing aggregators — every model they list is first-party, so deduping them against a user proxy that happens to serve a same-named model guts their own catalog. Fix: add is_routing_aggregator() (True only for true routers like OpenRouter and custom:* proxies; False for opencode-go/zen) and gate the picker dedup on it. is_aggregator() is unchanged so model-switch flat catalog resolution keeps working. Both desktop entry points (model.options JSON-RPC and /api/model/options REST) and hermes model share build_models_payload, so all surfaces get the full list. Fixes #47077
This commit is contained in:
parent
ef6492b648
commit
a6ce9b2fbb
4 changed files with 107 additions and 8 deletions
|
|
@ -639,6 +639,46 @@ def test_aggregator_dedup_does_not_empty_user_defined_custom_provider():
|
|||
assert or_row["total_models"] == 1
|
||||
|
||||
|
||||
def test_flat_namespace_reseller_keeps_first_party_models_overlapping_user_proxy():
|
||||
"""opencode-go / opencode-zen are flagged ``is_aggregator=True`` (their
|
||||
flat ``/v1/models`` returns bare IDs the model-switch resolver searches),
|
||||
but they are NOT routing aggregators — every model they list is a
|
||||
first-party model under the user's subscription. When a user also runs a
|
||||
custom proxy that happens to serve a same-named model, the picker dedup
|
||||
must NOT strip the reseller's own catalog. Regression for #47077, where
|
||||
opencode-go showed only 13 of 19 models because minimax-m3/m2.7/m2.5,
|
||||
glm-5/5.1, and deepseek-v4-flash were deduped against an overlapping
|
||||
custom provider.
|
||||
"""
|
||||
rows = [
|
||||
_user_provider_row("custom:my-proxy", [
|
||||
"minimax-m3", "minimax-m2.7", "glm-5", "deepseek-v4-flash",
|
||||
]),
|
||||
_aggregator_row("opencode-go", [
|
||||
"kimi-k2.6", "minimax-m3", "minimax-m2.7", "glm-5",
|
||||
"deepseek-v4-flash", "qwen3.7-max",
|
||||
]),
|
||||
_aggregator_row("openrouter", ["minimax-m3", "anthropic/claude-sonnet-4.6"]),
|
||||
]
|
||||
ctx = _empty_ctx()
|
||||
with _list_auth_returning(rows):
|
||||
payload = build_models_payload(ctx)
|
||||
|
||||
go_row = next(r for r in payload["providers"] if r["slug"] == "opencode-go")
|
||||
or_row = next(r for r in payload["providers"] if r["slug"] == "openrouter")
|
||||
|
||||
# The reseller keeps ALL of its first-party models — nothing stripped.
|
||||
assert go_row["models"] == [
|
||||
"kimi-k2.6", "minimax-m3", "minimax-m2.7", "glm-5",
|
||||
"deepseek-v4-flash", "qwen3.7-max",
|
||||
]
|
||||
assert go_row["total_models"] == 6
|
||||
|
||||
# A TRUE routing aggregator is still deduped against the user's models.
|
||||
assert "minimax-m3" not in or_row["models"]
|
||||
assert "anthropic/claude-sonnet-4.6" in or_row["models"]
|
||||
|
||||
|
||||
def test_two_custom_providers_with_overlap_both_survive():
|
||||
"""Two user-defined custom endpoints that happen to expose an
|
||||
overlapping model must each keep their full catalog. Neither is the
|
||||
|
|
|
|||
|
|
@ -129,6 +129,23 @@ def test_is_aggregator_leaves_unknown_provider_non_aggregator():
|
|||
assert providers_mod.is_aggregator("not-a-provider") is False
|
||||
|
||||
|
||||
def test_is_routing_aggregator_excludes_flat_namespace_resellers():
|
||||
"""opencode-go / opencode-zen stay ``is_aggregator=True`` (model-switch
|
||||
relies on it to search their flat bare-name catalog), but they are NOT
|
||||
routing aggregators — their models are first-party, so the picker dedup
|
||||
must not strip them. (#47077)"""
|
||||
# Still aggregators for model-switch flat-catalog resolution.
|
||||
assert providers_mod.is_aggregator("opencode-go") is True
|
||||
assert providers_mod.is_aggregator("opencode-zen") is True
|
||||
# But NOT routing aggregators for picker-dedup purposes.
|
||||
assert providers_mod.is_routing_aggregator("opencode-go") is False
|
||||
assert providers_mod.is_routing_aggregator("opencode-zen") is False
|
||||
# True routers and custom proxies remain routing aggregators.
|
||||
assert providers_mod.is_routing_aggregator("openrouter") is True
|
||||
assert providers_mod.is_routing_aggregator("custom:litellm") is True
|
||||
assert providers_mod.is_routing_aggregator("not-a-provider") is False
|
||||
|
||||
|
||||
def test_switch_model_accepts_explicit_named_custom_provider(monkeypatch):
|
||||
"""Shared /model switch pipeline should accept --provider for custom_providers."""
|
||||
monkeypatch.setattr(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue