fix(model-picker): no "Free" tag on subscription-billed rows for window plans

Rows the gateway bills to a subscription the account holds (billing_mode:
subscription) keep their free usage level, so a full allowance never dims
them, but the pickers no longer label them "Free": that subscription pays.
The ids ride on plan_usage.subscription_model_ids for Desktop/TUI; CLI and
messaging pickers use usage_tag_levels().
This commit is contained in:
rob-maron authored and Teknium committed 2026-10-27 12:44:44 -07:00
1 parent 9e8973110f
commit 384e4bde56
11 files changed
+166 -7

No files matched your search

+26
View File
@@ -106,4 +106,30 @@ describe('keepWarmNousRow', () => {
expect(row?.usage_levels).toEqual(warm.providers?.[0].usage_levels)
expect(row?.unavailable_models).toEqual([])
})
it("keeps the warm subscription-billed ids (no Free tag) on the pending row's fresh plan_usage", () => {
const gauges = [{ label: 'day', left_fraction: 0.5, style: 'bar' }]
const fresh = [{ label: 'day', left_fraction: 0.2, style: 'bar' }]
const warmSubscribed: ModelOptionsResult = {
providers: [
{
...warm.providers![0],
plan_usage: { gauges, kind: 'usage_window', subscription_model_ids: ['example/model-sub'] },
usage_levels: { ...warm.providers![0].usage_levels, 'example/model-sub': 'free' }
}
]
}
const pendingSubscribed: ModelOptionsResult = {
providers: [{ ...pending.providers![0], plan_usage: { gauges: fresh, kind: 'usage_window' } }]
}
const row = (keepWarmNousRow(warmSubscribed, pendingSubscribed) as ModelOptionsResult).providers?.[0]
expect(row?.usage_levels?.['example/model-sub']).toBe('free')
expect(row?.plan_usage?.subscription_model_ids).toEqual(['example/model-sub'])
// The gauges are the pending row's own (fresh), not the warm row's.
expect(row?.plan_usage?.gauges).toEqual(fresh)
})
})
+8
View File
@@ -51,6 +51,14 @@ export function keepWarmNousRow(oldData: unknown, newData: unknown): unknown {
kept[field] = warm[field]
}
// The kept `usage_levels` still say `free` for subscription-billed rows; the ids
// that keep the "Free" tag off them ride on `plan_usage` (whose gauges stay fresh), so carry them too.
const subscribed = warm.plan_usage?.subscription_model_ids
if (row.plan_usage && subscribed?.length) {
kept.plan_usage = { ...row.plan_usage, subscription_model_ids: subscribed }
}
return { ...row, ...kept } as ModelOptionProvider
})
@@ -108,6 +108,7 @@ export interface PlanUsage {
plan?: string | null
gauges?: PlanUsageGauge[]
top_tier_model_ids?: string[]
subscription_model_ids?: string[]
featured_model_ids?: string[]
paused_reason?: string | null
credits_pay?: boolean | null
@@ -27101,6 +27101,13 @@
"title": "Top Tier Model Ids",
"type": "array"
},
"subscription_model_ids": {
"items": {
"type": "string"
},
"title": "Subscription Model Ids",
"type": "array"
},
"featured_model_ids": {
"items": {
"type": "string"
+7 -1
View File
@@ -31,10 +31,16 @@ export function usageLevel(provider: PlanProvider, model: string): UsageLevel |
}
/** The tag a picker row shows: only Large (it has its own monthly limit) and Free (never runs out). Small /
* Medium change only how fast usage drains, so they carry no tag. */
* Medium change only how fast usage drains, so they carry no tag. A row billed to a subscription the account
* holds (`plan_usage.subscription_model_ids`) keeps its `free` level, so it is never dimmed, but carries no
* tag: "Free" would read as free of charge when that subscription pays. */
export function usageTag(provider: PlanProvider, model: string): 'free' | 'top_tier' | null {
const level = usageLevel(provider, model)
if (level && (provider?.plan_usage?.subscription_model_ids ?? []).includes(model)) {
return null
}
return level === 'free' || level === 'top_tier' ? level : null
}
+3 -2
View File
@@ -80,8 +80,9 @@ class _ModelPickerRows:
usage = usage.with_pricing(pricing)
self.usage = usage
if self.window_mode:
from hermes_cli.model_picker_usage import usage_levels
self.levels = usage_levels(all_models, pricing)
from hermes_cli.model_picker_usage import usage_tag_levels
# Tags only: dimming reads ``usage.blocks`` (free_ids), so subscription-billed rows still never dim.
self.levels = usage_tag_levels(all_models, pricing)
self.has_pricing = False
self.has_cache = False
self.any_on_sale = False
+29 -2
View File
@@ -88,6 +88,10 @@ class PickerUsage:
# Window only: model ids that draw on nothing (the gateway's ``free`` word or a free catalog row). A full
# allowance never blocks them. Empty until applied.
free_ids: frozenset[str] = frozenset()
# Window only: model ids the gateway bills to a subscription the account holds (``billing_mode:
# subscription``). They stay in ``free_ids`` (they draw on nothing, so nothing dims them); this set only
# keeps the word "Free" off them, since that subscription pays. Empty until applied.
subscription_ids: frozenset[str] = frozenset()
# Window only: past a full allowance, does the turn bill top-up credits (pay as you go)? True: "Keep
# going on credits" is on and there is a top-up balance. False: it stops. None: unknown (older portal).
credits_pay: Optional[bool] = None
@@ -106,8 +110,9 @@ class PickerUsage:
levels = usage_levels(list(pricing), pricing) # same rule as the tags and agent.plan_limits.is_top_tier
free = frozenset(m for m, level in levels.items() if level == "free")
marked = frozenset(m for m, level in levels.items() if level == "top_tier")
subscribed = frozenset(m for m in pricing if is_subscription_billed_row(pricing.get(m)))
from dataclasses import replace
return replace(self, top_tier_ids=marked, free_ids=free)
return replace(self, top_tier_ids=marked, free_ids=free, subscription_ids=subscribed)
def exhausted_gauge(self, model_id: str) -> Optional[UsageGauge]:
"""Window only: the spent allowance that stops this model (an all-scope one, or the top-tier one for a
@@ -187,6 +192,9 @@ class PickerUsage:
],
# The gateway's marks (what the desktop's used-up notes read).
"top_tier_model_ids": sorted(self.top_tier_ids),
# Subscription-billed: still ``free`` in ``usage_levels`` (never dimmed), but
# pickers show no "Free" tag on them (``plan-usage.ts`` ``usageTag``).
"subscription_model_ids": sorted(self.subscription_ids),
"featured_model_ids": list(self.featured_model_ids),
"paused_reason": self.paused_reason,
"credits_pay": self.credits_pay,
@@ -260,6 +268,21 @@ def usage_levels(model_ids: list[str], pricing: Optional[dict[str, dict[str, Any
return out
def is_subscription_billed_row(pricing_row: Any) -> bool:
"""The gateway bills this catalog row to a subscription the account holds (``billing_mode: subscription``)."""
from hermes_cli.models import _is_subscription_billed
return _is_subscription_billed(pricing_row)
def usage_tag_levels(model_ids: list[str], pricing: Optional[dict[str, dict[str, Any]]]) -> dict[str, str]:
"""The levels a picker row shows a tag for: :func:`usage_levels` without the subscription-billed rows.
Those still draw nothing from the window (``usage_levels`` keeps them ``free``, which is what keeps a full
allowance from dimming them), but "Free" would read as free of charge when that subscription pays. The
gateway gives them no plan label either. Display only."""
levels = usage_levels(model_ids, pricing)
return {m: lv for m, lv in levels.items() if not is_subscription_billed_row((pricing or {}).get(m))}
def is_free_model(pricing_row: Optional[dict[str, Any]]) -> bool:
"""True when the catalog lists the model at $0 in and $0 out (``:free`` / natively free). A row
with no pricing is NOT free — unknown price is not a promise."""
@@ -448,7 +471,11 @@ def window_row_labels(provider_row: Any, model_ids: list) -> list[str]:
ids = [str(m) for m in model_ids]
plan = provider_row.get("plan_usage") if isinstance(provider_row, dict) else None
levels = provider_row.get("usage_levels") if isinstance(plan, dict) and plan.get("kind") == "usage_window" else None
tags = [usage_level_label(levels.get(m)) for m in ids] if isinstance(levels, dict) else []
# Subscription-billed rows keep their ``free`` level but carry no tag.
subscribed = (set(plan.get("subscription_model_ids") or ())
if isinstance(plan, dict) and isinstance(levels, dict) else set())
tags = ([usage_level_label(None if m in subscribed else levels.get(m)) for m in ids]
if isinstance(levels, dict) else [])
if not any(tags):
return ids
width = min(max(len(m) for m in ids), 48)
+3 -2
View File
@@ -1421,7 +1421,7 @@ def _attach_window_model_notes(providers: list[dict]) -> None:
nous = next((p for p in providers if str(p.get("slug", "")).lower() == "nous"), None)
if nous is None:
return
from hermes_cli.model_picker_usage import nous_picker_usage, usage_level_label, usage_levels
from hermes_cli.model_picker_usage import nous_picker_usage, usage_level_label, usage_tag_levels
fresh = nous_picker_usage()
usage = fresh or nous_picker_usage(any_age=True)
if usage is None:
@@ -1429,7 +1429,8 @@ def _attach_window_model_notes(providers: list[dict]) -> None:
from agent.plan_limits import cached_pricing
pricing = cached_pricing() # this profile's catalog only (cache-only, no credential refresh)
models = [str(m) for m in nous.get("models", [])]
labels = {m: label for m, lv in usage_levels(models, pricing).items() if (label := usage_level_label(lv))}
# Subscription-billed rows carry no tag (usage_tag_levels).
labels = {m: label for m, lv in usage_tag_levels(models, pricing).items() if (label := usage_level_label(lv))}
if labels:
nous["model_labels"] = labels
if fresh is None:
@@ -15,6 +15,7 @@ from hermes_cli.model_picker_usage import (
plan_gauge_lines,
render_bar,
usage_levels,
usage_tag_levels,
)
@@ -567,3 +568,68 @@ def test_window_plan_feeds_the_existing_usage_chip(monkeypatch):
other: dict = {"slug": "nous"}
inventory._attach_nous_plan_usage(other, warm=False)
assert other == {"slug": "nous"}
# ── subscription-billed rows ────────────────────────────────
# Picker pricing shape. The gateway marks a subscription-billed row ``billing_mode: subscription`` and gives it no
# ``usage_level``; its listed price never applies.
_SUBSCRIPTION_BILLED = {
"example/model-sub": {"prompt": "0.000005", "completion": "0.00004", "billing_mode": "subscription"},
"gift/m:free": {"prompt": "0", "completion": "0"},
"example/model-b": {"prompt": "0.000005", "completion": "0.000025", "usage_level": "top_tier", "top_tier": True},
}
def test_subscription_billed_rows_draw_nothing_but_carry_no_free_tag():
# Still ``free`` to the window: nothing dims it, and the payload level the desktop/TUI dim from stays.
assert usage_levels(list(_SUBSCRIPTION_BILLED), _SUBSCRIPTION_BILLED)["example/model-sub"] == "free"
# The tag set drops it; real $0 rows and Large rows keep theirs.
assert usage_tag_levels(list(_SUBSCRIPTION_BILLED), _SUBSCRIPTION_BILLED) == {
"gift/m:free": "free", "example/model-b": "top_tier"}
usage = _window_usage(day_used_bp=10_000, top_tier_used_bp=10_000).with_pricing(_SUBSCRIPTION_BILLED)
assert not usage.blocks("example/model-sub")
assert not usage.blocks("gift/m:free")
assert usage.blocks("example/model-b")
assert usage.to_payload()["subscription_model_ids"] == ["example/model-sub"]
# No catalog: nothing marked.
assert _window_usage().to_payload()["subscription_model_ids"] == []
def test_window_picker_rows_leave_subscription_billed_rows_untagged():
rows = _ModelPickerRows(list(_SUBSCRIPTION_BILLED), _SUBSCRIPTION_BILLED, current_model="", sale_chrome=True,
usage=_window_usage(day_used_bp=10_000))
assert rows.label("example/model-sub").split("example/model-sub", 1)[1].strip() == ""
assert rows.label("gift/m:free").split("gift/m:free", 1)[1].strip() == "Free"
assert rows.label("example/model-b").split("example/model-b", 1)[1].strip() == "Large"
assert rows.has_top_tier
# Dimming is unchanged: the full day stops the paid Large row only.
assert not rows.usage.blocks("example/model-sub") and rows.usage.blocks("example/model-b")
def test_the_in_chat_cli_picker_leaves_subscription_billed_rows_untagged(monkeypatch):
from hermes_cli import model_picker_usage as mpu
monkeypatch.setattr(mpu, "nous_picker_usage", lambda **kw: None)
row = {"slug": "nous",
"plan_usage": {"kind": "usage_window", "subscription_model_ids": ["example/model-sub"]},
"usage_levels": {"example/model-sub": "free", "gift/m:free": "free"}}
rows, _line = mpu.annotate_window_plan_rows(row, ["example/model-sub", "gift/m:free"])
assert [r.split() for r in rows] == [["example/model-sub"], ["gift/m:free", "Free"]]
# An older payload without the field: unchanged (both tagged Free).
old = {**row, "plan_usage": {"kind": "usage_window"}}
assert [r.split() for r in mpu.annotate_window_plan_rows(old, ["example/model-sub"])[0]] == \
[["example/model-sub", "Free"]]
def test_messaging_pickers_leave_subscription_billed_rows_untagged(monkeypatch):
from hermes_cli import model_picker_usage as mpu
from hermes_cli import model_switch_providers as msp
from agent import plan_limits as pl
monkeypatch.setattr(mpu, "nous_picker_usage", lambda **kw: _window_usage())
monkeypatch.setattr(pl, "cached_pricing", lambda base_url="": dict(_SUBSCRIPTION_BILLED))
providers = [{"slug": "nous", "api_url": "https://x/v1", "models": list(_SUBSCRIPTION_BILLED)}]
msp._attach_window_model_notes(providers)
assert providers[0]["model_labels"] == {"gift/m:free": "Free", "example/model-b": "Large"}
assert mpu.picker_row_suffix(providers[0], "example/model-sub") == ""
+2
View File
@@ -293,6 +293,8 @@ class PlanUsage(Result):
gauges: list[PlanUsageGauge] = Field(default_factory=list)
# Ids the gateway labels ``top_tier`` (Large).
top_tier_model_ids: list[str] = Field(default_factory=list)
# Ids billed to a subscription the account holds: ``free`` in ``usage_levels`` (never dimmed), no "Free" tag.
subscription_model_ids: list[str] = Field(default_factory=list)
featured_model_ids: list[str] = Field(default_factory=list)
paused_reason: str | None = None
# Past a full allowance, top-up credits pay (pay as you go): Keep going on credits on + a balance.
+14
View File
@@ -98,6 +98,20 @@ describe('blockingGauge / planBlocks (the one rule, agent/plan_limits.py twin)',
expect(usageTag(dayFull, 'a/low')).toBeNull()
})
it("subscription-billed rows: never dimmed, no Free tag", () => {
const subscribed: Provider = {
...dayFull,
plan_usage: { ...dayFull.plan_usage!, subscription_model_ids: ['a/zero'] }
}
expect(planBlocks(subscribed, 'a/zero')).toBe(false)
expect(usageTag(subscribed, 'a/zero')).toBeNull()
expect(usageTag(subscribed, 'a/free:free')).toBe('free')
expect(usageTag(subscribed, 'a/large')).toBe('top_tier')
// An older backend without the field: unchanged.
expect(usageTag(dayFull, 'a/zero')).toBe('free')
})
it('formats the reset compactly', () => {
expect(formatResetsIn(NOW + 30_000, NOW)).toBe('soon')
expect(formatResetsIn(NOW + 12 * 60_000, NOW)).toBe('12m')