Usage Metering
Reserve and refund usage before LLM calls
Usage metering lets you control whether a request is allowed to run before it reaches the
provider. Use reserveUsage to reserve quota or budget before dispatch, and refundUsage to
release that reservation when a call fails after reservation succeeded.
reserveUsage and refundUsage receive { coalesced, signal }.
coalescedistruewhen the caller is sharing an existing in-flight cached request.signalis the caller-suppliedAbortSignal, when one was provided.- A successful reservation is refunded if the request is aborted before the wrapped operation starts.
- If
reserveUsagefails, no refund is performed because no reservation was created.
const llm = new VernLLM({
client: openai,
model: 'gpt-4o',
});
await llm.call({
userContent: 'Summarize this document',
reserveUsage: async ({ signal }) => {
await billing.reserve({ signal });
},
refundUsage: async ({ signal }) => {
await billing.refund({ signal });
},
});How it works
Before the request
reserveUsage runs before any provider request is dispatched. If it fails, the call stops
immediately.
After a successful reservation
The LLM request proceeds normally. Retries do not trigger reserveUsage again because it wraps
the logical call.
After a failed call
refundUsage runs when a reservation succeeded but the call ultimately fails. Errors from
refundUsage are caught and logged, then swallowed without changing the original call error or
rejection. Monitor refund failures separately if failed refunds require follow-up.
Reservation failures
If reserveUsage throws, the call fails with LLMError('quota_exceeded') and no request is sent
to the provider. The original error is preserved on err.cause. A failed reservation is never
refunded because no usage was reserved.
With caching
Usage metering also works with cachedCall. Provide usage hooks at the cache level, not inside the
nested call options. CachedCallParams's call field doesn't type-permit reserveUsage/
refundUsage at all, so this is a compile error for a normally-typed caller. A caller that bypasses
the type system and sets them inside call anyway hits a runtime LLMError('validation') instead
of having them silently stripped: reserving usage twice for the same logical request, or silently
skipping reservation, are both worth failing loudly over.
On a cache miss, the usage flow runs around the underlying call. A cache hit skips the LLM call entirely, so no new reservation or usage record is created.
const result = await llm.cachedCall({
cacheKey: `cv:${cvId}`,
ttl: 3600,
call: { systemPrompt, userContent },
reserveUsage: ({ coalesced, signal }) => quota.reserve(userId, { coalesced, signal }),
refundUsage: ({ coalesced, signal }) => quota.refund(userId, { coalesced, signal }),
});Concurrent cached calls
When multiple callers request the same cache key at the same time, only the first caller runs the underlying call. Other callers share the same in-flight result.
Each caller still gets its own reserveUsage and refundUsage lifecycle, its own signal, and
its own cancellation behavior. The coalesced flag lets your application decide how shared
requests should be metered.
Although the underlying call executes only once per in-flight window, reserveUsage and
refundUsage are still invoked once per caller. If the call fails, every caller is refunded
independently. Aborting one coalesced caller does not cancel the shared in-flight call for other
callers.
Relationship with usage tracking
Usage metering controls whether a request can start.
Usage tracking reports provider usage after a provider response, through onUsage on success or
onUsageFailure if VernLLM's own post-processing then fails.
See Usage Tracking for token reporting details.
A typical billing flow uses both:
const llm = new VernLLM({
client: openai,
model: 'gpt-4o',
onUsage: (usage) => {
billing.record(usage);
},
});
await llm.call({
userContent: 'Write a summary',
reserveUsage: async ({ signal }) => {
await billing.reserve({ signal });
},
refundUsage: async ({ signal }) => {
await billing.refund({ signal });
},
});