Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,10 @@ await client.identify(
Or call `await client.prewarm({"id": "your-company-id"}, ["credit-type-id"])`
directly. Both are no-ops in server mode, and neither raises.

Pre-warming resolves the company the way the server does: it looks the keys up
first, whatever they are named, and only when nothing matches does it read a
value carrying Schematic's `comp_` prefix as the company id.

### When a check cannot gate

A check that cannot gate, because the API is unreachable, Redis is down, or the
Expand Down
8 changes: 4 additions & 4 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Credit lease conformance suite

`SPEC.md` and `vectors/*.json` are copied verbatim from schematic-node
(`conformance/` on `main`), the reference implementation for client-mode credit
leases. Do not edit them here: fix or extend them in schematic-node and copy the
result back, or the SDKs stop pinning the same behavior.
`SPEC.md` and `vectors/*.json` are copied verbatim from schematic-node's
`conformance/`, the reference implementation for client-mode credit leases. Do
not edit them here: fix or extend them in schematic-node and copy the result
back, or the SDKs stop pinning the same behavior.

`tests/conformance/test_vectors.py` is this repo's runner. The runner is the
only language-specific piece; every SDK reimplements it and must pass the same
Expand Down
43 changes: 27 additions & 16 deletions conformance/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Credit-metered features are gated client-side without a wire call per check. The
1. **Leases** a tranche of credits from the server per `(company_id, credit_type_id)`. The server
pre-debits the company balance by the granted amount; the SDK tracks a local view of how much
of the tranche remains un-reserved (`local_remaining_credits`).
2. **Reserves** `usage x consumption_rate` credits from the lease at `check()` time, atomically
2. **Reserves** `ceil(usage) x consumption_rate` credits from the lease at `check()` time, atomically
(check-and-debit). A successful, engine-approved check returns a *reservation handle*.
3. **Settles** the reservation at `track()` time with the actual usage: the actually-consumed
credits stay debited, the unspent slice is refunded to the lease, and a Track event bills the
Expand Down Expand Up @@ -147,7 +147,7 @@ Leases and reservations both expire:
| `company_id`, `credit_type_id` | Slot key. |
| `event_subtype` | Event the settle will bill as. |
| `quantity_reserved` | Caller-declared usage (event units). |
| `credits_reserved` | `quantity_reserved x consumption_rate`. |
| `credits_reserved` | `ceil(quantity_reserved) x consumption_rate` (whole event units, so the hold matches what the settle bills). |
| `consumption_rate` | Rate at reservation time. |
| `expires_at` | Reservation TTL deadline (sweep target). |
| `eval_ctx` | Company/user keys used at check time; threaded onto the Track event. |
Expand Down Expand Up @@ -277,13 +277,20 @@ Rules:
Sizing to the shortfall matters: a single check needing more than `remaining + lease_size`
would otherwise fail its post-extend retry forever regardless of server balance.
`expires_at = now + lease_duration_ms`.
- A caller that joins an extend already in flight must be sized too. If its own
`additional_amount` exceeds the one the in-flight extend asked for, it waits that flight out
and then issues **exactly one** further extend, re-sized against the slot the flight just
moved; if the flight's ask already covers it, it issues nothing. A joiner that silently
inherits a tranche-sized ask fails its post-extend retry with credits sitting on the server.
The follow-up never chains — a company whose balance cannot reach the request would otherwise
spin.
- A caller that joins an extend already in flight must be sized too. It joins that flight only
when the flight's `additional_amount` covers its own; if the flight asked for less, it waits
that flight out, re-reads the slot the flight just moved, and sizes itself against the balance
left behind. After **two** such joins it issues **exactly one** extend of its own instead of
joining again. A joiner that silently inherits a tranche-sized ask fails its post-extend retry
with credits sitting on the server, whether that ask came from the flight it first found or
from a smaller follow-up another caller registered while it waited. The follow-up never chains:
a company whose balance cannot reach the request would otherwise spin.
- A joiner's wait is capped at the caller's **per-check timeout** when one is given. The flight
runs on the timeout of whichever call started it, which for a steady-state refresh is the
client default, so a check with a budget of its own must not sit behind it. On expiry the
joiner stops waiting and resolves to "no lease", which sends the check down its
[failure path](#failure-handling) by mode; the flight itself continues for the callers still on
it, and whatever it installs is there for the next check to read.
- On response, reconcile via the store's `extend` with the server's **total** and new expiry,
**pinned** to the extended lease's id.
- Failures resolve to "no lease" without throwing (often fire-and-forget).
Expand Down Expand Up @@ -325,7 +332,9 @@ Then:
- Credit entitlement missing `credit_id`, a positive `consumption_rate`, or a resolvable
`event_subtype` (caller's explicit subtype wins over the entitlement's) → fall back.
- Probe error → fall back (it is a resolution step, not the gate).
6. `credit_cost = usage x consumption_rate`.
6. `credit_cost = ceil(usage) x consumption_rate`. A fraction of an event is not something the
server bills, so the hold rounds up rather than moving the local ledger by less than the Track
event will.
7. **Acquire** a lease for `(company, credit_id)`. Failure → [failure handling](#failure-handling)
with reason `lease_acquire_failed`.
8. **Reserve** `credit_cost` via `try_reserve`. On refusal, opportunistically
Expand Down Expand Up @@ -372,7 +381,8 @@ configured `on_acquire_failure` mode (default **fail-closed**):

`track_with_reservation(reservation, actual_quantity)` settles a reservation:

1. `credits = actual_quantity x reservation.consumption_rate`.
1. `credits = ceil(actual_quantity) x reservation.consumption_rate`, rounded up the same way the
hold is, so the debit moves the lease by exactly what the Track event bills.
2. `consume(reservation.id, credits)`:
- **Settled locally** (claim succeeded): the clamped consumed slice stays debited; the unspent
slice is refunded to the lease (pinned).
Expand All @@ -382,11 +392,12 @@ configured `on_acquire_failure` mode (default **fail-closed**):
over. This is why `reservation_ttl_ms` should exceed the longest expected gap between
`check()` and `track_with_reservation()`.
3. Either way, emit the Track event built from the **caller-held handle** (not the store):
`event = event_subtype`, `quantity = actual_quantity` (the *unclamped* actual — the server is
the source of truth for real consumption; only local bookkeeping clamps to the reserved
amount), `lease_id = reservation.lease_id` (routes the server-side consumption through the
lease's sub-ledger instead of double-debiting the pre-debited grant), plus the reservation's
`eval_ctx` company/user and any caller traits.
`event = event_subtype`, `quantity = ceil(actual_quantity)` (the *unclamped* actual, rounded
up: the server is the source of truth for real consumption and only local bookkeeping clamps
to the reserved amount, but the event's quantity has to be a whole number or the server
rejects it while processing and the usage is never billed), `lease_id = reservation.lease_id`
(routes the server-side consumption through the lease's sub-ledger instead of double-debiting
the pre-debited grant), plus the reservation's `eval_ctx` company/user and any caller traits.
4. The Track carries a deterministic idempotency key derived from the reservation id
(`"lease-reservation:" + reservation.id` in Node); the server dedupes by it for 24h, so a
recovery emit racing the normal emit, or an accidental double settle, collapses to one billed
Expand Down
82 changes: 82 additions & 0 deletions conformance/vectors/fractional-usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"category": "fractional_usage",
"vectors": [
{
"name": "fractional_usage_rounds_the_hold_and_the_settle_up",
"description": "Usage below one whole event unit: the hold is ceil(usage) x rate, the engine gates on that same cost, and a settle at the same fractional actual debits ceil(actual) x rate and bills ceil(actual) units, so the local ledger moves by exactly what the Track event bills. The reservation still records the caller-declared quantity.",
"given": {
"config": {
"lease_duration_ms": 300000,
"reservation_ttl_ms": 60000,
"lease_size": 1000,
"low_water_mark": 0.25
},
"leases": [
{
"lease_id": "lse_1",
"company_id": "co_1",
"credit_type_id": "ct_1",
"granted_amount": 1000,
"expires_at_ms": 300000
}
]
},
"operations": [
{
"op": "check",
"flag_key": "inference",
"company": { "id": "co_1", "credit_balances": { "ct_1": 5000 } },
"usage": 0.5,
"event_subtype": "inference_tokens",
"save_reservation_as": "r1",
"engine": [
{
"value": true,
"reason": "probe",
"entitlement": {
"value_type": "credit",
"credit_id": "ct_1",
"consumption_rate": 10,
"event_subtype": "inference_tokens"
}
},
{ "value": true, "reason": "ok" }
],
"expect": {
"allowed": true,
"has_reservation": true,
"reservation": {
"lease_id": "lse_1",
"quantity_reserved": 0.5,
"credits_reserved": 10,
"consumption_rate": 10
},
"engine_calls": [{ "credit_balance": 5000 }, { "credit_balance": 1000, "credit_cost": 10 }]
}
},
{
"op": "get_lease",
"company_id": "co_1",
"credit_type_id": "ct_1",
"expect": { "exists": true, "local_remaining_credits": 990 }
},
{
"op": "track",
"handle": "r1",
"actual_quantity": 0.5,
"expect": {
"settled_locally": true,
"track": { "event": "inference_tokens", "quantity": 1, "lease_id": "lse_1" }
}
},
{
"op": "get_lease",
"company_id": "co_1",
"credit_type_id": "ct_1",
"expect": { "exists": true, "local_remaining_credits": 990 }
},
{ "op": "reserved_credits", "company_id": "co_1", "credit_type_id": "ct_1", "expect": { "total": 0 } }
]
}
]
}
Loading
Loading