NousResearch/hermes-agent

Model picker gaps and Windows notification fix

Cluster 47077 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #50809 on 22 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #50809. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #50808. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address missing models in the opencode-go picker, fixes to ensure reseller/first-party models appear correctly in the desktop picker, and a Windows-specific fix to set AppUserModelID so notifications work properly. The members span two adjacent but distinct themes: model catalog completeness and a desktop platform bug.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #50808 fix(desktop): set AppUserModelID on Windows so notifications fire

    merged to main

    files: apps/desktop/electron/main.cjs

    Show the diff
    • apps/desktop/electron/main.cjs
      @@ -620,6 +620,16 @@ function previewFileMetadata(filePath, mimeType) {
       }
       
       app.setName(APP_NAME)
      +// Windows toast notifications silently no-op unless an AppUserModelID is set:
      +// `new Notification().show()` returns without error and nothing appears. The
      +// AUMID must match the installed Start Menu shortcut's AUMID, which
      +// electron-builder derives from the build `appId` (com.nousresearch.hermes) —
      +// keep this string in sync with package.json `build.appId`. macOS/Linux don't
      +// need this, so gate it on Windows. (Fixes: desktop approval/turn notifications
      +// never firing on Windows.)
      +if (IS_WINDOWS) {
      +  app.setAppUserModelId('com.nousresearch.hermes')
      +}
       // Seed the native About panel with the live Hermes version. This is refreshed
       // on every open via the explicit "About" menu handler (refreshAboutPanel), so
       // an in-place `hermes update` mid-session is reflected without an app restart;
  • #50809 fix(picker): keep flat-namespace reseller (opencode-go/zen) first-party models in desktop picker

    merged to main

    files: hermes_cli/inventory.py, hermes_cli/providers.py, tests/hermes_cli/test_inventory.py, tests/hermes_cli/test_model_switch_custom_providers.py

    Show the diff
    • hermes_cli/inventory.py
      @@ -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]
    • hermes_cli/providers.py
      @@ -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.
       
    • tests/hermes_cli/test_inventory.py
      @@ -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
    • tests/hermes_cli/test_model_switch_custom_providers.py
      @@ -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(

1 PR closed without merging

From the close comments: other 1

  • #47104 fix(opencode-go): add missing models to curated catalog (minimax-m3, deepseek-v4-flash, qwen3.7-plus, kimi-k2.7-code, hy3-preview) · other
Closed issues · 1
  • Closed#47077[Bug]: /model picker for opencode-go missing MiniMax, GLM, deepseek-v4-flash models (shows 13 of 19)
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#50808, #50809
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
22 Jun 2026

For your agent

The same facts, typed.