# NousResearch/hermes-agent · Model picker gaps and Windows notification fix

## Open now

- 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.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 2 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #50809. Verify it addresses your case.
- **Recommended:** read PR #50809 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-50808`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member 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.

## Closed / duplicate issues

- #47077: \[Bug\]: /model picker for opencode-go missing MiniMax, GLM, deepseek-v4-flash models (shows 13 of 19)

## Prior & competing PRs

- **#50808: fix(desktop): set AppUserModelID on Windows so notifications fire**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/electron/main.cjs`
  - fix hunks:
  - `apps/desktop/electron/main.cjs`

    ```diff
    @@ -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 · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/inventory.py`, `hermes_cli/providers.py`, `tests/hermes_cli/test_inventory.py`, `tests/hermes_cli/test_model_switch_custom_providers.py`
  - fix hunks:
  - `hermes_cli/inventory.py`

    ```diff
    @@ -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"):
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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(
    ```
- **#47104: fix(opencode-go): add missing models to curated catalog (minimax-m3, deepseek-v4-flash, qwen3.7-plus, kimi-k2.7-code, hy3-preview)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`

## Fix status

- Merged to main in PR #50809 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-22`.
- Subsystem: `hermes_cli`
