Skip to content

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.

  1. post_api_request fires with cost == 0.0 AND is_free_model(model, provider) returns True (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 into known_free_models (INSERT OR IGNORE).
  2. A later post_api_request fires with cost > 0.0. The plugin checks is_known_free_model(model, provider) — if True, it queues a pending alert in _pending_free_paid_alerts: dict[str, tuple[str, float]] (module-level in __init__.py), mapping session_id → (model, cost).
  3. The next pre_llm_call for 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.

model TEXT — model identifier (as received from post_api_request)
provider TEXT — provider identifier, or '' for wildcard rows
first_seen_at TEXT — ISO-8601 UTC timestamp of first $0 observation
PRIMARY 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(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.

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(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:

  1. Bare rename — model + ":free" is a known-free row (nvidia/nemotron-3-ultra ← nvidia/nemotron-3-ultra:free).
  2. Suffixed paid id — a stored <base>:free whose <base> is a prefix of model at a token boundary (-, :, /, _), so nvidia/nemotron-3-ultra-550b-a55b ← nvidia/nemotron-3-ultra:free but an unrelated …-ultrablend does 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.