From eb70ab894b6b30706a2198d8722abce93c76be45 Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:11:56 -0600 Subject: [PATCH] fix(inventory): avoid fresh Nous tier checks in picker payloads --- hermes_cli/inventory.py | 18 +++++-- hermes_cli/model_switch.py | 6 ++- tests/hermes_cli/test_inventory.py | 86 ++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index 89e3bd70277..48fc4e928d1 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -116,6 +116,7 @@ def build_models_payload( canonical_order: bool = False, pricing: bool = False, capabilities: bool = False, + force_fresh_nous_tier: bool = False, max_models: int = 50, ) -> dict: """Build the ``{providers, model, provider}`` shape every consumer @@ -139,6 +140,10 @@ def build_models_payload( ``{model: {fast, reasoning}}`` so pickers can gate the model-options controls (fast toggle / reasoning) to what each model actually supports, instead of offering knobs the backend would reject. + - ``force_fresh_nous_tier``: bypass the short Nous free-tier cache when + selecting Portal-recommended Nous models and applying tier gating. Keep + this false for UI picker opens; explicit auth/model flows can opt in + when they need freshly-purchased credits to show up immediately. """ from hermes_cli.model_switch import list_authenticated_providers @@ -148,6 +153,7 @@ def build_models_payload( current_model=ctx.current_model, user_providers=ctx.user_providers, custom_providers=ctx.custom_providers, + force_fresh_nous_tier=force_fresh_nous_tier, max_models=max_models, ) @@ -158,7 +164,7 @@ def build_models_payload( if canonical_order: rows = _reorder_canonical(rows) if pricing: - _apply_pricing(rows) + _apply_pricing(rows, force_fresh_nous_tier=force_fresh_nous_tier) if capabilities: _apply_capabilities(rows) @@ -293,7 +299,11 @@ def _reorder_canonical(rows: list[dict]) -> list[dict]: return canon + extras -def _apply_pricing(rows: list[dict]) -> None: +def _apply_pricing( + rows: list[dict], + *, + force_fresh_nous_tier: bool = False, +) -> None: """Enrich each provider row with per-model pricing + Nous tier gating. Mutates ``rows`` in-place. For every row whose provider supports live @@ -359,7 +369,9 @@ def _apply_pricing(rows: list[dict]) -> None: if slug == "nous": try: if nous_free_tier is None: - nous_free_tier = check_nous_free_tier(force_fresh=True) + nous_free_tier = check_nous_free_tier( + force_fresh=force_fresh_nous_tier + ) row["free_tier"] = bool(nous_free_tier) if nous_free_tier: _selectable, unavailable = partition_nous_models_by_tier( diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index c8c1078343b..1a28b904281 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1178,6 +1178,7 @@ def list_authenticated_providers( current_base_url: str = "", user_providers: dict = None, custom_providers: list | None = None, + force_fresh_nous_tier: bool = False, max_models: int = 8, current_model: str = "", ) -> List[dict]: @@ -1197,6 +1198,9 @@ def list_authenticated_providers( - source: str — "built-in", "models.dev", "user-config" Only includes providers that have API keys set or are user-defined endpoints. + ``force_fresh_nous_tier`` bypasses the short Nous tier cache for explicit + account-sensitive flows. UI picker opens should leave it false so they do + not block on fresh Portal/account checks every time. """ import os from agent.models_dev import ( @@ -1539,7 +1543,7 @@ def list_authenticated_providers( _portal = _st.get("portal_base_url", "") or "" except Exception: _portal = "" - if _nous_free(force_fresh=True): + if _nous_free(force_fresh=force_fresh_nous_tier): model_ids, _ = _union_free(model_ids, _pricing, _portal) else: model_ids, _ = _union_paid(model_ids, _pricing, _portal) diff --git a/tests/hermes_cli/test_inventory.py b/tests/hermes_cli/test_inventory.py index 1b24ba6bdd6..9450d46af50 100644 --- a/tests/hermes_cli/test_inventory.py +++ b/tests/hermes_cli/test_inventory.py @@ -141,6 +141,18 @@ def _list_auth_returning(rows: list[dict]): ) +def _nous_row(model: str = "openai/gpt-5.5") -> dict: + return { + "slug": "nous", + "name": "Nous", + "models": [model], + "total_models": 1, + "is_current": True, + "is_user_defined": False, + "source": "built-in", + } + + def test_build_models_payload_returns_expected_shape(): rows = [ {"slug": "openrouter", "name": "OpenRouter", "models": ["m1"], @@ -173,6 +185,80 @@ def test_build_models_payload_does_not_call_provider_model_ids(): mock_pm.assert_not_called() +def test_build_models_payload_uses_cached_nous_tier_by_default(): + """Picker payloads should not force fresh Nous account checks. + + Desktop/status picker opens are request/response UI paths. They can hit + the short free-tier cache; explicit model/auth flows can still opt into a + fresh account check when needed. + """ + ctx = _empty_ctx(provider="nous", model="openai/gpt-5.5") + rows = [_nous_row()] + with patch( + "hermes_cli.model_switch.list_authenticated_providers", + return_value=rows, + ) as mock_list: + build_models_payload(ctx) + + mock_list.assert_called_once() + assert mock_list.call_args.kwargs["force_fresh_nous_tier"] is False + + +def test_build_models_payload_can_force_fresh_nous_tier(): + ctx = _empty_ctx(provider="nous", model="openai/gpt-5.5") + rows = [_nous_row()] + with patch( + "hermes_cli.model_switch.list_authenticated_providers", + return_value=rows, + ) as mock_list: + build_models_payload(ctx, force_fresh_nous_tier=True) + + mock_list.assert_called_once() + assert mock_list.call_args.kwargs["force_fresh_nous_tier"] is True + + +def test_pricing_uses_cached_nous_tier_by_default(): + rows = [_nous_row()] + ctx = _empty_ctx(provider="nous", model="openai/gpt-5.5") + with ( + _list_auth_returning(rows), + patch( + "hermes_cli.models.get_pricing_for_provider", + return_value={ + "openai/gpt-5.5": { + "prompt": "0.000001", + "completion": "0.000002", + }, + }, + ), + patch("hermes_cli.models.check_nous_free_tier", return_value=False) as mock_free, + ): + build_models_payload(ctx, pricing=True) + + mock_free.assert_called_once_with(force_fresh=False) + + +def test_pricing_can_force_fresh_nous_tier(): + rows = [_nous_row()] + ctx = _empty_ctx(provider="nous", model="openai/gpt-5.5") + with ( + _list_auth_returning(rows), + patch( + "hermes_cli.models.get_pricing_for_provider", + return_value={ + "openai/gpt-5.5": { + "prompt": "0.000001", + "completion": "0.000002", + }, + }, + ), + patch("hermes_cli.models.check_nous_free_tier", return_value=False) as mock_free, + ): + build_models_payload(ctx, pricing=True, force_fresh_nous_tier=True) + + mock_free.assert_called_once_with(force_fresh=True) + + def test_include_unconfigured_appends_canonical_skeletons(): """include_unconfigured=True adds CANONICAL_PROVIDERS rows that list_authenticated_providers didn't emit. Skeleton rows have empty