From 08f40eb114d613aee4c9d7cc9f1ffca9c1d8592e Mon Sep 17 00:00:00 2001 From: dbarr5 Date: Fri, 14 Aug 2026 15:17:21 -0400 Subject: [PATCH] feat(models): show the UVT cap on locked rows, not just usable ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A locked row answered "what unlocks it" and not "what you get for unlocking it" — so the one number that argues for upgrading was missing from the only row where the upgrade decision happens. The cause is that `monthly_uvt_cap` answers for the CALLER's tier. On a locked row that is null by construction: the caller's tier does not include the model, so it has no cap on it. The renderer then fell back to showing only the gate. Server-side companion adds `unlock_uvt_cap` — the ceiling the model carries on `tier_min`. This reads it and renders both, joined by a separator: before opus 1M needs pro after opus 1M needs pro · cap 60k Available rows are unchanged and still show the caller's own cap. The field is optional on CatalogItem so an older server that does not send it degrades to exactly the previous output rather than rendering "cap undefined". tsc clean; model_picker suite 14 pass. --- src/commands/models.ts | 14 +++++++++----- src/types.ts | 7 +++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/commands/models.ts b/src/commands/models.ts index 0a19195..f86c6b2 100644 --- a/src/commands/models.ts +++ b/src/commands/models.ts @@ -107,12 +107,16 @@ function renderGroup(title: string, items: CatalogItem[], activeDefault: string) padVisible(m.available ? m.label : theme.dim(m.label), labelW), m.context_window != null ? theme.dim(compactNumber(m.context_window)) : "", ]; - // The one thing a locked row has to answer is "what unlocks it". + // A locked row has to answer two things: what unlocks it, and what you get + // for unlocking it. The second used to be missing — `monthly_uvt_cap` + // answers for the CALLER's tier, so on a locked row it is null by + // construction, and the number that argues for upgrading was absent from + // the only row that needed it. `unlock_uvt_cap` is the cap at `tier_min`. const gate = !m.available && m.tier_min ? theme.yellow(`needs ${m.tier_min}`) : ""; - const cap = m.available && m.monthly_uvt_cap != null - ? theme.dim(`cap ${compactNumber(m.monthly_uvt_cap)}`) - : ""; - return ` ${marker} ${padVisible(id, idW)} ${meta.join(" ")} ${gate || cap}`.trimEnd(); + const capValue = m.available ? m.monthly_uvt_cap : m.unlock_uvt_cap; + const cap = capValue != null ? theme.dim(`cap ${compactNumber(capValue)}`) : ""; + const trailing = [gate, cap].filter(Boolean).join(theme.dim(" · ")); + return ` ${marker} ${padVisible(id, idW)} ${meta.join(" ")} ${trailing}`.trimEnd(); }); return `\n ${theme.iceBlue(title)}\n${rows.join("\n")}\n`; diff --git a/src/types.ts b/src/types.ts index e27bec3..c914281 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,6 +44,13 @@ export interface CatalogItem { available: boolean; /** Hard monthly UVT ceiling on the caller's tier, or null if uncapped. */ monthly_uvt_cap: number | null; + /** + * The ceiling this model would carry on `tier_min` — the number that argues + * for upgrading. On a locked row `monthly_uvt_cap` is null by construction + * (it answers for the caller's tier, which does not include the model), so + * this is the only cap a locked row can show. Optional: older servers omit it. + */ + unlock_uvt_cap?: number | null; is_default: boolean; }