fix(credits): suppress depleted banner on stealth-preview models
Stealth-preview SKUs (e.g. stealth/ox-alpha) are free-tier but carry no :free suffix, so is_free_tier_model() returned False for them. On gateway sessions (which never run the model picker's pricing fetch), the free-model suppression of the credits.depleted banner never engaged, and any response carrying paid_access:false triggered a false "Credit access paused" notice. Add stealth/ prefix detection to is_free_tier_model() as a zero-network signal, same design as the existing :free suffix check. Fail-open to False (banner still shows) if the prefix changes — recoverable noise, never a masked depletion on a paid model. Closes #91843
This commit is contained in:
@@ -226,12 +226,15 @@ def is_free_tier_model(model: str, base_url: str = "") -> bool:
|
||||
1. The ``:free`` suffix — the canonical Nous free SKU marker (e.g.
|
||||
``nvidia/nemotron-3-ultra:free``). Free by construction on the API side
|
||||
(spend is forced to 0 for ``:free`` ids).
|
||||
2. A peek into the in-process pricing cache in ``hermes_cli.models``
|
||||
2. The ``stealth/`` prefix — Nous stealth-preview SKUs (e.g.
|
||||
``stealth/ox-alpha``) are free-tier but carry no ``:free`` suffix. Spend
|
||||
is forced to zero server-side, so these are also free by construction.
|
||||
3. A peek into the in-process pricing cache in ``hermes_cli.models``
|
||||
(populated when the model picker fetched ``/v1/models`` pricing for
|
||||
*base_url*). PEEK ONLY — a cache miss never triggers a fetch. This is
|
||||
CLI/TUI-session best-effort: gateway sessions never run the picker's
|
||||
pricing fetch, so suppression there rests entirely on the ``:free``
|
||||
suffix (which all Nous free SKUs carry).
|
||||
suffix and ``stealth/`` prefix.
|
||||
|
||||
Fail-open to False (the depleted notice still shows) on any error: wrongly
|
||||
showing the warning is recoverable noise; wrongly hiding it on a paid model
|
||||
@@ -241,6 +244,15 @@ def is_free_tier_model(model: str, base_url: str = "") -> bool:
|
||||
return False
|
||||
if model.endswith(":free"):
|
||||
return True
|
||||
# Stealth-preview SKUs (e.g. stealth/ox-alpha) are free-tier but carry no
|
||||
# ``:free`` suffix. Spend is forced to zero server-side, so a ``paid_access:
|
||||
# false`` header on these models is a false positive for the depleted banner.
|
||||
# The ``stealth/`` prefix is the Nous naming convention for these SKUs and
|
||||
# is checked here as a zero-network signal, same design as the ``:free``
|
||||
# suffix above. Fail-open to False (the banner still shows) if the prefix
|
||||
# ever changes — recoverable noise, never a masked depletion on a paid model.
|
||||
if model.startswith("stealth/"):
|
||||
return True
|
||||
if not base_url:
|
||||
return False
|
||||
try:
|
||||
|
||||
@@ -351,6 +351,47 @@ class TestIsFreeTierModel:
|
||||
monkeypatch.setattr(models_mod, "_pricing_cache", _Exploding())
|
||||
assert is_free_tier_model("some/model", "https://inference-api.nousresearch.com") is False
|
||||
|
||||
def test_stealth_prefix_detected_as_free(self):
|
||||
"""Stealth-preview SKUs (stealth/...) are free-tier but carry no
|
||||
:free suffix. Suppression must engage so the depleted banner doesn't
|
||||
fire on a false paid_access:false from the server's stealth pool."""
|
||||
from agent.credits_tracker import is_free_tier_model
|
||||
|
||||
# No base_url needed — stealth/ is a zero-network signal, same as :free.
|
||||
assert is_free_tier_model("stealth/ox-alpha", "") is True
|
||||
assert is_free_tier_model("stealth/ox-alpha", "https://inference-api.nousresearch.com/v1") is True
|
||||
# Non-stealth model without :free suffix → not free (without pricing cache).
|
||||
assert is_free_tier_model("some/paid-model", "") is False
|
||||
|
||||
def test_depleted_suppressed_for_stealth_model(self):
|
||||
"""End-to-end: paid_access:false on a stealth/ model must NOT fire
|
||||
the depleted banner (the exact scenario from issue #91843)."""
|
||||
from agent.credits_tracker import (
|
||||
CreditsState, evaluate_credits_notices, is_free_tier_model,
|
||||
)
|
||||
|
||||
state = CreditsState(
|
||||
version=1,
|
||||
remaining_micros=0,
|
||||
remaining_usd="0.00",
|
||||
subscription_micros=0,
|
||||
subscription_usd="0.00",
|
||||
purchased_micros=0,
|
||||
purchased_usd="0.00",
|
||||
paid_access=False,
|
||||
captured_at=1.0,
|
||||
from_header=True,
|
||||
)
|
||||
model = "stealth/ox-alpha"
|
||||
base_url = "https://inference-api.nousresearch.com/v1"
|
||||
model_is_free = is_free_tier_model(model, base_url)
|
||||
assert model_is_free is True
|
||||
|
||||
latch = fresh_latch()
|
||||
to_show, to_clear = evaluate_credits_notices(state, latch, model_is_free=model_is_free)
|
||||
assert all(n.key != "credits.depleted" for n in to_show)
|
||||
assert "credits.depleted" not in latch["active"]
|
||||
|
||||
|
||||
# ── Scenario 6: denominator none (uf is None) ────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user