diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index 43d3150ccdb..2c7d9c5bf5c 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -178,6 +178,14 @@ def build_models_payload( user_models.update(m.lower() for m in (row.get("models") or [])) if user_models: 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. + if row.get("is_user_defined"): + continue slug = row.get("slug", "") if not _is_aggregator(slug): continue diff --git a/tests/hermes_cli/test_inventory.py b/tests/hermes_cli/test_inventory.py index e81288f9ab1..e4490a901d1 100644 --- a/tests/hermes_cli/test_inventory.py +++ b/tests/hermes_cli/test_inventory.py @@ -606,3 +606,35 @@ def test_aggregator_dedup_multiple_user_providers(): assert or_row["models"] == ["model-z"] assert or_row["total_models"] == 1 + +def test_aggregator_dedup_does_not_empty_user_defined_custom_provider(): + """A named custom provider has slug ``custom:``, which makes it + *both* ``is_user_defined=True`` *and* ``is_aggregator()==True`` + (is_aggregator reports True for every ``custom:*`` slug). The dedup + must skip user-defined rows: their models populate ``user_models``, so + filtering them against that set would strip the row's entire catalog and + hide the provider from the picker. Regression for the #45954 dedup + emptying ``custom:*`` providers (e.g. a local llama.cpp endpoint or an + Anthropic-compatible proxy).""" + rows = [ + _user_provider_row("custom:my-proxy", ["my-model-a", "my-model-b"]), + _aggregator_row("openrouter", ["my-model-a", "other/model"]), + ] + ctx = _empty_ctx() + with _list_auth_returning(rows): + payload = build_models_payload(ctx) + + proxy_row = next( + r for r in payload["providers"] if r["slug"] == "custom:my-proxy" + ) + or_row = next(r for r in payload["providers"] if r["slug"] == "openrouter") + + # The user's own custom provider keeps all of its models. + assert proxy_row["models"] == ["my-model-a", "my-model-b"] + assert proxy_row["total_models"] == 2 + + # A genuine aggregator is still deduped against the user's models. + assert "my-model-a" not in or_row["models"] + assert "other/model" in or_row["models"] + assert or_row["total_models"] == 1 +