Free→Paid Model Transition Alert
When a model that was previously seen as free (explicit $0 in pricing) starts incurring cost, the plugin injects a one-shot warning into the conversation so the operator is aware of the change.
Detection flow (live)
Section titled “Detection flow (live)”post_api_requestfires withcost == 0.0ANDis_free_model(model, provider)returnsTrue(explicit input=0 AND output=0; a paid model’s zero-token $0 call no longer qualifies — issue #89) →record_free_model(model, provider)inserts the pair intoknown_free_models(INSERT OR IGNORE).- A later
post_api_requestfires withcost > 0.0. The plugin checksis_known_free_model(model, provider)— ifTrue, it queues a pending alert in_pending_free_paid_alerts: dict[str, tuple[str, float]](module-level in__init__.py), mappingsession_id → (model, cost). - The next
pre_llm_callfor that session pops the pending alert and injects a one-shot warning into the conversation context. The alert fires exactly once per detection event (pop-on-consume).
pre_llm_call now handles two independent injection paths: budget alerts (one per
window per scope, anti-spam via budget_alerts table) and free→paid alerts (one
per detection event, state held in _pending_free_paid_alerts).
Dashboard persistence (free_paid_transitions, schema v6)
Section titled “Dashboard persistence (free_paid_transitions, schema v6)”The in-memory _pending_free_paid_alerts dict is ephemeral — consumed by
pre_llm_call and lost on plugin reload. To give the dashboard a historical
view of “which models flipped”, every detection in post_api_request also
calls db.record_free_paid_transition(model, provider, session_id, cost)
which INSERT OR IGNOREs into the v6 free_paid_transitions table.
PRIMARY KEY (model, provider) — only the FIRST flip is recorded; later
paid calls on the same pair are no-ops. The dashboard endpoint
GET /tier-transitions?window_hours=72 reads from this table (read-only,
PRAGMA query_only=ON). The widget (TierTransitionsWidget) is rendered
inside TelemetryPage, NOT via registerSlot: no shell slot in the
catalogue known at the time fit a “tier change” surface, and registering an
unknown slot name is a silent no-op. (The catalogue has since grown —
header-banner may now fit. See
Dashboard Plugin Surface § Slot catalogue.) The widget hides itself when no
transitions fall in the window, so the page is unchanged during the happy
path.
known_free_models table schema
Section titled “known_free_models table schema”model TEXT — model identifier (as received from post_api_request)provider TEXT — provider identifier, or '' for wildcard rowsfirst_seen_at TEXT — ISO-8601 UTC timestamp of first $0 observationPRIMARY KEY (model, provider)Every (model, provider) pair that is ever seen at an explicit $0 cost is
recorded here. A provider='' wildcard row is used for the backfill path (see
below).
is_known_free_model lookup semantics
Section titled “is_known_free_model lookup semantics”is_known_free_model(model, provider) returns True if either of these rows
exists in known_free_models:
- An exact
(model, provider)row (live-recorded pair) - A wildcard
(model, '')row (backfill-seeded pair)
The wildcard covers sessions from before the model was first seen live, and handles cases where the same model is served by multiple providers and all are expected to be free.
Backfill mechanism (pre-v5 upgrade path)
Section titled “Backfill mechanism (pre-v5 upgrade path)”On every plugin load, register() calls get_known_free_models() (from
pricing.py) to retrieve all models with explicit input=0.0 AND output=0.0
in the pricing table, then calls backfill_known_free_models(models) (from
db.py) which inserts (model, provider='') wildcard rows for each — INSERT OR
IGNORE, so it’s safe to call on every load. Users who upgrade from pre-v5 get
immediate coverage without needing to replay historical sessions.
is_explicitly_priced distinction
Section titled “is_explicitly_priced distinction”is_explicitly_priced(model, provider) -> bool in pricing.py returns True
only when the model has an explicit pricing entry in the pricing table (even if
that entry is $0). This distinguishes a genuine free model from an unknown model
that fell through to the $0.00 unknown-model fallback. Without this guard, any
unrecognized model would be recorded as “free” and trigger spurious alerts when
the pricing table is later populated.
Id-change transitions (is_free_tier_transition, issue #32)
Section titled “Id-change transitions (is_free_tier_transition, issue #32)”A known_free_models row is keyed on the model id seen at $0. But some providers
move a model from free to paid by changing the id — dropping a :free suffix
(or renaming the promo to its paid base) — so the first paid call arrives under a
different id than the recorded :free row, and a plain is_known_free_model
lookup misses.
is_free_tier_transition(model, provider) in db.py bridges that gap. For an
incoming paid model it returns True if a stored <id>:free row matches via
either:
- Bare rename —
model + ":free"is a known-free row (nvidia/nemotron-3-ultra←nvidia/nemotron-3-ultra:free). - Suffixed paid id — a stored
<base>:freewhose<base>is a prefix ofmodelat a token boundary (-,:,/,_), sonvidia/nemotron-3-ultra-550b-a55b←nvidia/nemotron-3-ultra:freebut an unrelated…-ultrablenddoes not false-positive.
post_api_request checks is_known_free_model(...) OR is_free_tier_transition(...)
on the cost > 0.0 branch. The concrete case: the nvidia/nemotron-3-ultra:free
promo ends 2026-06-18 and bills as nvidia/nemotron-3-ultra; the paid price is
seeded in _DEFAULT_PRICING so cost > 0 once billing starts, and the reverse
lookup connects the paid id back to the recorded :free row to fire the alert.
No user-side config is required (changed by issue #32): while the promo is
live, the :free id resolves to $0 via the :free suffix rule and is recorded
as known-free automatically — so the :free row exists for the reverse lookup to
match, with no _subscription entry needed. This holds for whichever free form
the gateway sends: the short nvidia/nemotron-3-ultra:free (matched on the paid
side by bare rename) and the OpenRouter long form
nvidia/nemotron-3-ultra-550b-a55b:free (matched by bare rename of its own
suffix-dropped paid id …-550b-a55b). An explicit _subscription entry is still
honored and overrides the rule, but is no longer necessary.
hermes-telemetry is an independent community plugin for Hermes Agent. It is not affiliated with or endorsed by Nous Research.