mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +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
|
|
@ -173,11 +173,11 @@ def build_models_payload(
|
|||
# aggregator rows honest: they only show models the user can't get
|
||||
# from a more-specific provider. (#45954)
|
||||
try:
|
||||
from hermes_cli.providers import is_aggregator as _is_aggregator
|
||||
from hermes_cli.providers import is_routing_aggregator as _is_routing_aggregator
|
||||
except Exception:
|
||||
_is_aggregator = None # type: ignore[assignment]
|
||||
_is_routing_aggregator = None # type: ignore[assignment]
|
||||
|
||||
if _is_aggregator is not None:
|
||||
if _is_routing_aggregator is not None:
|
||||
user_models: set[str] = set()
|
||||
for row in rows:
|
||||
if row.get("is_user_defined"):
|
||||
|
|
@ -186,14 +186,21 @@ def build_models_payload(
|
|||
for row in rows:
|
||||
# A user's own configured provider is never an "aggregator
|
||||
# duplicate" of itself: user_models is built from these very
|
||||
# rows, and is_aggregator() reports True for every custom:*
|
||||
# slug. Without this guard the dedup strips a user-defined
|
||||
# custom provider's entire model list (all of it lives in
|
||||
# user_models), emptying its picker row.
|
||||
# rows, and is_routing_aggregator() reports True for every
|
||||
# custom:* slug. Without this guard the dedup strips a
|
||||
# user-defined custom provider's entire model list (all of it
|
||||
# lives in user_models), emptying its picker row.
|
||||
if row.get("is_user_defined"):
|
||||
continue
|
||||
slug = row.get("slug", "")
|
||||
if not _is_aggregator(slug):
|
||||
# Only strip overlaps from TRUE routing aggregators (OpenRouter,
|
||||
# custom:* proxies). Flat-namespace resellers (opencode-go /
|
||||
# opencode-zen) serve every listed model as a first-party model,
|
||||
# so their rows must keep models that a user's proxy happens to
|
||||
# share a name with — otherwise a subscription provider's own
|
||||
# catalog (minimax-m3, glm-5, deepseek-v4-flash, ...) is silently
|
||||
# gutted in the picker. (#47077)
|
||||
if not _is_routing_aggregator(slug):
|
||||
continue
|
||||
original = row.get("models") or []
|
||||
filtered = [m for m in original if m.lower() not in user_models]
|
||||
|
|
|
|||
|
|
@ -489,6 +489,41 @@ def is_aggregator(provider: str) -> bool:
|
|||
return pdef.is_aggregator if pdef else False
|
||||
|
||||
|
||||
# Flat-namespace resellers (e.g. opencode-go, opencode-zen) are flagged
|
||||
# ``is_aggregator=True`` because their live ``/v1/models`` returns bare model
|
||||
# IDs ("deepseek-v4-flash") rather than ``vendor/model`` routing slugs — the
|
||||
# model-switch resolver relies on that flag to search their flat catalog
|
||||
# (see model_switch.py step d). But they are NOT routing aggregators: every
|
||||
# model they list is a first-party model served under their own subscription,
|
||||
# not a passthrough route to another provider's endpoint. The picker dedup
|
||||
# (build_models_payload) must treat them differently from true routers like
|
||||
# OpenRouter — a reseller's first-party "minimax-m3" must never be stripped
|
||||
# just because a user's custom proxy also happens to serve a same-named model.
|
||||
_FLAT_NAMESPACE_RESELLERS: frozenset[str] = frozenset({
|
||||
# Use normalized provider IDs: normalize_provider("opencode-zen") -> "opencode".
|
||||
"opencode-go",
|
||||
"opencode",
|
||||
})
|
||||
|
||||
|
||||
def is_routing_aggregator(provider: str) -> bool:
|
||||
"""Return True only for TRUE routing aggregators (e.g. OpenRouter, named
|
||||
``custom:*`` proxies) — those that route bare/vendor-slugged model names
|
||||
to *other* providers' endpoints.
|
||||
|
||||
Distinct from :func:`is_aggregator`, which also reports True for
|
||||
flat-namespace resellers (opencode-go/zen) whose catalog is entirely
|
||||
first-party. Use this gate when the question is "would selecting this
|
||||
model silently re-route the call away from the user's intended provider?"
|
||||
— i.e. the picker dedup. Resellers answer no: their listed models are
|
||||
their own, so their rows must not be deduped against user proxies.
|
||||
"""
|
||||
provider_norm = normalize_provider(provider or "")
|
||||
if provider_norm in _FLAT_NAMESPACE_RESELLERS:
|
||||
return False
|
||||
return is_aggregator(provider_norm)
|
||||
|
||||
|
||||
def determine_api_mode(provider: str, base_url: str = "") -> str:
|
||||
"""Determine the API mode (wire protocol) for a provider/endpoint.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue