Skip to Content

@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/quota

Register 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 result

Quota types

Two quota types are tracked out of the box:

Quota TypeCalculatorUnitDescription
llm-costAiGenerateTextQuotaCalculatorMicrocents (1 USD = 100,000,000)Computes cost from input/output/cache tokens using per-model pricing
processing-time-msProcessingTimeQuotaCalculatorMillisecondsWall-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)

OptionTypeDefaultDescription
enabledbooleanfalseEnable quota tracking and Redis connection
redisHoststring'localhost'Redis host
redisPortnumber6379Redis port
redisPasswordstringRedis password

Environment variables (forRootAsync)

VariableFallbackDescription
QUOTA_ENABLEDSet to 'true' to enable
QUOTA_REDIS_HOSTREDIS_HOSTRedis host
QUOTA_REDIS_PORTREDIS_PORTRedis port
QUOTA_REDIS_PASSWORDREDIS_PASSWORDRedis password

Public API

  • Module: QuotaModuleforRoot(options), forRootAsync()
  • Services: QuotaClientServicecheckQuota(userId, quotaType), report(userId, quotaType, amount)
  • Services: QuotaCalculatorRegistryregister(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

PackageRole
@loopstack/commonToolInterceptor, ToolExecutionContext, ToolResult, @UseToolInterceptor
@nestjs/commonNestJS dependency injection
ioredisRedis client for quota counters
  • 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

Last updated on