fix(tools): reuse subscription features for toolset listing

This commit is contained in:
szzhoujiarui 2026-08-01 09:56:51 +00:00 committed by kshitij
parent a1b3ce6bfa
commit 16bd5d23b4
4 changed files with 118 additions and 5 deletions

View File

@ -26,6 +26,7 @@ from hermes_cli.config import (
from hermes_cli.colors import Colors, color
from hermes_cli.nous_subscription import (
MANAGED_FEATURE_COVERAGE_CATEGORY,
NousSubscriptionFeatures,
apply_nous_managed_defaults,
get_nous_subscription_features,
)
@ -2618,6 +2619,7 @@ def _toolset_has_keys(
config: dict = None,
*,
force_fresh: bool = False,
features: Optional[NousSubscriptionFeatures] = None,
) -> bool:
"""Check if a toolset's required API keys are configured."""
if config is None:
@ -2633,7 +2635,10 @@ def _toolset_has_keys(
return False
if ts_key in {"web", "image_gen", "video_gen", "tts", "stt", "browser"}:
features = get_nous_subscription_features(config, force_fresh=force_fresh)
if features is None:
features = get_nous_subscription_features(
config, force_fresh=force_fresh
)
feature = features.features.get(ts_key)
if feature and (feature.available or feature.managed_by_nous):
return True
@ -2641,7 +2646,12 @@ def _toolset_has_keys(
# Check TOOL_CATEGORIES first (provider-aware)
cat = TOOL_CATEGORIES.get(ts_key)
if cat:
for provider in _visible_providers(cat, config, force_fresh=force_fresh):
for provider in _visible_providers(
cat,
config,
force_fresh=force_fresh,
features=features,
):
env_vars = provider.get("env_vars", [])
if not env_vars:
return True # No-key provider (e.g. Local Browser, Edge TTS)
@ -3076,6 +3086,7 @@ def _visible_providers(
config: dict,
*,
force_fresh: bool = False,
features: Optional[NousSubscriptionFeatures] = None,
) -> list[dict]:
"""Return provider entries visible for the current auth/config state.
@ -3085,7 +3096,8 @@ def _visible_providers(
login + entitlement check (see ``_configure_provider``); the row only
*activates* the gateway once paid access is confirmed.
"""
features = get_nous_subscription_features(config, force_fresh=force_fresh)
if features is None:
features = get_nous_subscription_features(config, force_fresh=force_fresh)
acct = features.account_info
# Pool-only users (entitled to managed tools via the free tool pool but with
# no paid access) get image gen but NOT video gen — the pool doesn't fund

View File

@ -58,6 +58,7 @@ async def get_toolsets(profile: Optional[str] = None):
_get_platform_tools,
_toolset_configuration_platform,
_toolset_has_keys,
get_nous_subscription_features,
gui_toolset_label,
)
from hermes_cli.platforms import platform_label
@ -77,6 +78,7 @@ async def get_toolsets(profile: Optional[str] = None):
)
for platform in target_platforms
}
features = get_nous_subscription_features(config)
result = []
for name, label, desc in toolset_rows:
try:
@ -104,7 +106,7 @@ async def get_toolsets(profile: Optional[str] = None):
),
"enabled": is_enabled,
"available": is_enabled,
"configured": _toolset_has_keys(name, config),
"configured": _toolset_has_keys(name, config, features=features),
"tools": tools,
})
return result

View File

@ -6,7 +6,8 @@ from unittest.mock import patch
import pytest
from hermes_cli.nous_account import NousPortalAccountInfo
from hermes_cli.nous_account import NousPortalAccountInfo, NousToolAccessInfo
from hermes_cli.nous_subscription import NousSubscriptionFeatures
from hermes_cli.tools_config import (
_DEFAULT_OFF_TOOLSETS,
_RECENTLY_SHIPPED_TOOLSETS,
@ -544,6 +545,74 @@ def _fake_features(*, logged_in: bool, paid: bool = True):
return SimpleNamespace(nous_auth_present=logged_in, account_info=account)
def test_visible_providers_reuses_logged_out_feature_snapshot(monkeypatch):
import hermes_cli.tools_config as tools_config
account = NousPortalAccountInfo(
logged_in=False,
source="none",
fresh=False,
paid_service_access=None,
)
features = NousSubscriptionFeatures(
subscribed=False,
nous_auth_present=False,
provider_is_nous=False,
features={},
account_info=account,
)
monkeypatch.setattr(
tools_config,
"get_nous_subscription_features",
lambda *args, **kwargs: pytest.fail("feature snapshot was resolved again"),
)
providers = _visible_providers(
TOOL_CATEGORIES["image_gen"], {}, features=features
)
assert any(
provider.get("managed_nous_feature") == "image_gen"
for provider in providers
)
def test_visible_providers_reuses_pool_video_feature_snapshot(monkeypatch):
import hermes_cli.tools_config as tools_config
account = NousPortalAccountInfo(
logged_in=True,
source="jwt",
fresh=False,
paid_service_access=False,
tool_access=NousToolAccessInfo(
enabled=True,
coverage={"fal-video": False},
),
)
features = NousSubscriptionFeatures(
subscribed=True,
nous_auth_present=True,
provider_is_nous=False,
features={},
account_info=account,
)
monkeypatch.setattr(
tools_config,
"get_nous_subscription_features",
lambda *args, **kwargs: pytest.fail("feature snapshot was resolved again"),
)
providers = _visible_providers(
TOOL_CATEGORIES["video_gen"], {}, features=features
)
assert not any(
provider.get("managed_nous_feature") == "video_gen"
for provider in providers
)
# ── Windows console-flash guard for post-setup subprocess spawns ──────────────

View File

@ -1948,6 +1948,36 @@ class TestNewEndpoints:
config["platform_toolsets"]["discord"]
)
def test_toolsets_resolve_subscription_features_once(self, monkeypatch):
import hermes_cli.tools_config as tools_config
from hermes_cli.nous_subscription import NousSubscriptionFeatures
calls = 0
features = NousSubscriptionFeatures(
subscribed=False,
nous_auth_present=False,
provider_is_nous=False,
features={},
account_info=None,
)
def resolve_features(config, *, force_fresh=False):
nonlocal calls
calls += 1
return features
monkeypatch.setattr(
tools_config,
"get_nous_subscription_features",
resolve_features,
)
resp = self.client.get("/api/tools/toolsets")
assert resp.status_code == 200
assert resp.json()
assert calls == 1
def test_get_toolset_config_returns_provider_matrix(self):
"""GET .../config returns provider rows with structured env_vars."""