@loopstack/quota
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 and LlmGenerateObjectTool 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
|
v
Check processing-time quota --> exceeded? --> throw Error
|
v
Look up tool-specific calculator in QuotaCalculatorRegistry
|
v
Check tool-specific quota --> exceeded? --> throw Error
|
v
Execute tool
|
v
Report processing-time usage (wall-clock ms)
|
v
Report tool-specific usage (e.g. LLM cost in microcents)
|
v
Return 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 { ToolExecutionContext, ToolResult } from '@loopstack/common';
import { QuotaCalculatorRegistry, ToolQuotaCalculator } from '@loopstack/quota';
class ApiCallQuotaCalculator implements ToolQuotaCalculator {
quotaType = 'api-calls';
calculateQuotaUsage(_context: ToolExecutionContext, _result: ToolResult) {
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, ToolResult, @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-module — Claude integration whose tools are metered by the built-in
AiGenerateTextQuotaCalculator
About
Author: Jakob Klippel
License: MIT