Cosmo Docs
Server

Metering Module

Token usage tracking and quota enforcement

The Metering module tracks LLM token consumption and enforces per-user daily quotas. Source: packages/server/src/modules/metering/.

REST Endpoints

All endpoints are prefixed with /api/v1/metering and require ClerkAuthGuard.

MethodPathDescription
GET/metering/orgOrg-level usage for billing period
GET/metering/user?period=day|week|monthUser's usage for the given calendar period (default day)
GET/metering/quotaUser's current quota status

Metering Service (services/metering.service.ts)

recordLLMUsage(params)

Called by ChatService after every LLM response. Parameters:

FieldTypeDescription
orgIdstringOrganisation
userIdstringUser who made the request
modelstringModel ID (e.g. claude-sonnet-4-20250514)
inputTokensnumberPrompt tokens
outputTokensnumberCompletion tokens
metadataJSON?Extra context (session ID, workspace, etc.)

Cost is calculated based on model pricing from @cosmohq/core.

checkQuota(orgId, userId?)

Returns the current quota status. Called by QuotaGuard before every chat request.

Response:

{
  allowed: boolean;
  usedTokens: number;
  limitTokens: number;
  percentage: number;  // 0–100
  plan: Plan;
  resetAt?: string;  // ISO instant of the next UTC midnight — absent only on the unlimited (-1) branch
}

Orgs whose plan has tokensPerUserPerDay === -1 always return allowed: true (unlimited quota). Note: the ENTERPRISE plan currently has a fixed daily limit of 10M tokens, same as PREMIUM.

resetAt is computed once, here, and threaded through everywhere the reset time is needed (QuotaGuard's exception payload, both usage views) — no caller should recompute "next UTC midnight" independently.

getOrgUsageSummary(orgId)

Aggregates token usage for the org across the current billing period:

{
  totalInputTokens: number;
  totalOutputTokens: number;
  totalTokens: number;
  totalCostUsd: number;
  byModel: { model: string; tokens: number; cost: number }[];
  byUser: { userId: string; tokens: number; cost: number }[];
}

getUserUsageSummary(orgId, userId, period?)

Usage totals for the requesting user over the given calendar period — 'day' (default), 'week', or 'month':

{
  period: { start: string; end: string; granularity: 'day' | 'week' | 'month' };
  totalInputTokens: number;
  totalOutputTokens: number;
  totalTokens: number;
  totalCostUsd: number;
  count: number;
}

'day' uses the same UTC midnight-to-midnight window as checkQuota (the only period with an enforced limit). 'week' is the UTC calendar week (Monday–Monday) and 'month' the UTC calendar month, both computed independently of the org's Subscription/billing period — these are informational rollups only, since plans.config.ts defines no weekly or monthly limit.

Quota Limits by Plan

PlanDaily Token Limit (per user)
NO_PLAN0 (blocked)
FREE1,000,000
STANDARD2,000,000
PREMIUM10,000,000
ENTERPRISE10,000,000

Quotas reset at midnight UTC each day (records are scoped by date in the query).

Usage Record Schema

FieldTypeDescription
idstringPrimary key
orgIdstringOrganisation
userIdstringUser
typeUsageType enumUsage category (LLM_CHAT, LLM_AGENT, etc.)
inputTokensintInput tokens
outputTokensintOutput tokens
modelstring?Model ID (nullable)
costUsdDecimalCost in USD
metadataJSON?Extra context
createdAtDateTimeUsage timestamp (used for date filtering)

QuotaGuard

packages/server/src/common/guards/quota.guard.ts is applied to POST /api/v1/chat:

  1. Calls MeteringService.checkQuota(orgId, userId).
  2. If allowed: false, throws ForbiddenException → HTTP 403 with a structured body: { statusCode: 403, code: 'QUOTA_EXCEEDED', message, usage, resetAt }. GlobalExceptionFilter preserves an allowlisted subset of a structured HttpException body — code, usage, resetAt, inviteId (not every field, and not just the flattened message) — so clients can branch on code: 'QUOTA_EXCEEDED' instead of string-matching the message.
  3. If allowed: true, the request proceeds to the chat handler.

The status stays 403 (not 429) — code is the intended discriminator, and status codes elsewhere in the desktop client (e.g. 401 for a revoked session) are narrowly-scoped special cases, not a general pattern to extend.

Proactive quota warning

recordLLMUsage also computes a threshold signal after writing each usage record — 'approaching_limit' at ≥80% of the daily limit, 'at_limit' at 100%. ChatService awaits this call (rather than fire-and-forget) so the signal can ride the chat stream's terminal done event as quotaWarning, letting a client warn the user before their next request gets blocked, without polling. Consumed by the desktop chat panel to show a one-time-per-crossing toast.