Quota Module
Quota tracking module for the Loopstack automation framework.
Opt-in, Redis-backed quota tracking and enforcement for tool calls. Ships with calculators for AI token cost (USD-based, per-model pricing) and processing time, and lets you register custom calculators for any tool.
When to Use
- You run workflows that call LLM tools and need to track or cap per-user spend
- You want to enforce processing-time limits across all tool calls
- You need a pluggable quota system where different tools report different cost metrics
- You are building a multi-tenant platform and need per-user usage counters backed by Redis
If you only need simple rate limiting (requests per minute), a standard NestJS throttler may be simpler. This module is designed for cumulative usage tracking with heterogeneous cost models.
Installation
npm install @loopstack/quotaRegister the module with explicit options:
import { QuotaModule } from '@loopstack/quota';
@Module({ imports: [ QuotaModule.forRoot({ enabled: true, redisHost: 'localhost', redisPort: 6379, redisPassword: process.env.REDIS_PASSWORD, }), ],})export class AppModule {}Or read configuration from environment variables:
@Module({ imports: [QuotaModule.forRootAsync()],})export class AppModule {}forRootAsync() reads QUOTA_ENABLED, QUOTA_REDIS_HOST (fallback REDIS_HOST), QUOTA_REDIS_PORT (fallback REDIS_PORT), and QUOTA_REDIS_PASSWORD (fallback REDIS_PASSWORD).
When enabled is false (the default), the Redis connection is skipped and the interceptor becomes a no-op — safe to leave wired up in development.
Quick Start
Once the module is registered, the QuotaInterceptor is automatically discovered via @UseToolInterceptor() and runs on every tool call. No additional wiring is needed for the built-in calculators.
The module auto-registers AiGenerateTextQuotaCalculator for LlmGenerateTextTool, LlmGenerateObjectTool and WebFetchTool (@loopstack/web, metered when it summarizes with a prompt) on startup.
To read or manipulate quota counters directly, inject QuotaClientService:
import { Injectable } from '@nestjs/common';import { QuotaClientService } from '@loopstack/quota';
@Injectable()export class BillingService { constructor(private readonly quotaClient: QuotaClientService) {}
async getUserUsage(userId: string) { const llmCost = await this.quotaClient.checkQuota(userId, 'llm-cost'); const processingTime = await this.quotaClient.checkQuota(userId, 'processing-time-ms'); return { llmCost, processingTime }; }}How It Works
The QuotaInterceptor wraps every tool call with a pre-check / post-report cycle:
Tool call starts | vCheck processing-time quota --> exceeded? --> throw Error | vLook up tool-specific calculator in QuotaCalculatorRegistry | vCheck tool-specific quota --> exceeded? --> throw Error | vExecute tool | vReport processing-time usage (wall-clock ms) | vReport tool-specific usage (e.g. LLM cost in microcents) | vReturn resultQuota types
Two quota types are tracked out of the box:
| Quota Type | Calculator | Unit | Description |
|---|---|---|---|
llm-cost |
AiGenerateTextQuotaCalculator |
Microcents (1 USD = 100,000,000) | Computes cost from input/output/cache tokens using per-model pricing |
processing-time-ms |
ProcessingTimeQuotaCalculator |
Milliseconds | Wall-clock duration of each tool call |
Model pricing
AiGenerateTextQuotaCalculator uses getModelPricing(provider, model) to look up per-token rates. The pricing table includes Claude (Opus 4, Sonnet 4, Haiku 4, 3.5 Sonnet, 3.5 Haiku) and OpenAI (GPT-4o, GPT-4.1, o3, o4-mini, and variants). Lookup uses longest-prefix matching with provider-level fallbacks.
Redis key structure
Counters are stored as Redis keys with the pattern:
user:{userId}:quota:{quotaType}:used — cumulative usage (INCRBY)user:{userId}:quota:{quotaType}:limit — configured limit (-1 = unlimited)If no limit key exists in Redis, the quota is blocked by default. Set the limit key to -1 for unlimited access.
Fail-open behavior
If Redis is unreachable, both checkQuota and report fail open — the tool call proceeds and a warning is logged.
Registering a custom calculator
Inject QuotaCalculatorRegistry and register from any module’s onModuleInit:
import { Module, OnModuleInit } from '@nestjs/common';import { ToolEnvelope, ToolExecutionContext } from '@loopstack/common';import { QuotaCalculatorRegistry, ToolQuotaCalculator } from '@loopstack/quota';
class ApiCallQuotaCalculator implements ToolQuotaCalculator { quotaType = 'api-calls';
calculateQuotaUsage(_context: ToolExecutionContext, _result: ToolEnvelope) { return { quotaType: this.quotaType, actualAmount: 1 }; }}
@Module({})export class MyModule implements OnModuleInit { constructor(private readonly registry: QuotaCalculatorRegistry) {}
onModuleInit() { this.registry.register('MyApiTool', new ApiCallQuotaCalculator()); }}The registry key is the tool’s class name (not the @Tool({ name }) value).
Configuration
QuotaModule.forRoot(options)
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
false |
Enable quota tracking and Redis connection |
redisHost |
string |
'localhost' |
Redis host |
redisPort |
number |
6379 |
Redis port |
redisPassword |
string |
— | Redis password |
Environment variables (forRootAsync)
| Variable | Fallback | Description |
|---|---|---|
QUOTA_ENABLED |
— | Set to 'true' to enable |
QUOTA_REDIS_HOST |
REDIS_HOST |
Redis host |
QUOTA_REDIS_PORT |
REDIS_PORT |
Redis port |
QUOTA_REDIS_PASSWORD |
REDIS_PASSWORD |
Redis password |
Public API
- Module:
QuotaModule—forRoot(options),forRootAsync() - Services:
QuotaClientService—checkQuota(userId, quotaType),report(userId, quotaType, amount) - Services:
QuotaCalculatorRegistry—register(toolClassName, calculator),get(toolClassName),has(toolClassName) - Interceptor:
QuotaInterceptor— auto-discovered via@UseToolInterceptor({ priority: 50 }) - Calculators:
AiGenerateTextQuotaCalculator(quota type:llm-cost),ProcessingTimeQuotaCalculator(quota type:processing-time-ms) - Interfaces:
ToolQuotaCalculator,QuotaClientServiceInterface,QuotaCheckResult,QuotaUsage - Tokens:
QUOTA_CLIENT_SERVICE,QUOTA_REDIS - Utilities:
getModelPricing(provider, model),ModelPricing
Dependencies
| Package | Role |
|---|---|
@loopstack/common |
ToolInterceptor, ToolExecutionContext, ToolEnvelope, @UseToolInterceptor |
@nestjs/common |
NestJS dependency injection |
ioredis |
Redis client for quota counters |
Related
- LLM Providers — how LLM tools report token usage metadata consumed by the quota calculator
- @loopstack/claude — Claude integration whose tools are metered by the built-in
AiGenerateTextQuotaCalculator
About
Author: Jakob Klippel
License: MIT