One policy row per scope, keyed by scope_type and scope_id, with model_id nullable. Adding a limit at a new level, or a model-specific override, is an insert rather than a migration (FR5, FR6, FR8).
Every policy row carries a version and an effective_from. The version is what the limiter reports as a metric, which is how policy propagation delay becomes measurable rather than assumed.
reservation and usage_event are separate entities sharing request_id. The reservation is short-lived operational state; the usage event is the immutable accounting fact. Conflating them would put a 400-day retention requirement on hot state.
What is not here
No prompt text, no completion text, no message content of any kind. The model has no column that could hold one.
decision_audit stores the reason code and the limiting scope, not the request. It is what answers 'why was I throttled at 14:03' without retaining anything sensitive.
Provider credentials live in Vault and are referenced by path, never stored in this schema.
Retention
usage_event online in ClickHouse for 400 days, then Iceberg on MinIO for 7 years for audit.
decision_audit online for 90 days — long enough for a billing dispute, short enough to keep the table cheap.
reservation rows exist only in Valkey and are never written to PostgreSQL; the entity is shown here to make the request_id join explicit.