UNPKG

@aid-on/llm-throttle

Version:

高精度なLLMレート制限ライブラリ - Precise dual rate limiting for LLM APIs (RPM + TPM)

1 lines 87.5 kB
{"version":3,"sources":["../node_modules/@aid-on/fuzztok/src/index.ts","../src/errors.ts","../src/token-bucket.ts","../src/utils/async-lock.ts","../src/utils/clock.ts","../src/utils/validation.ts","../src/storage/in-memory.ts","../src/utils/fuzztok-integration.ts","../src/index.ts"],"sourcesContent":["/**\n * Fuzzy Token Estimator\n * \n * 独自アルゴリズムによる高速・軽量なトークン推定ライブラリ\n * モデル設定は外部から注入する設計\n */\n\nimport { TextPayload, BaseTokenConfig } from './types/index.js';\n\n/**\n * 文字種別の判定ユーティリティ\n */\nexport class CharacterClassifier {\n /**\n * CJK(中国語・日本語・韓国語)文字およびマルチバイト文字の判定\n */\n static isCJKCharacter(char: string): boolean {\n const code = char.charCodeAt(0);\n return (\n // CJK統合漢字拡張A-G、互換漢字など\n (code >= 0x2e80 && code <= 0x2eff) || // CJK部首補助\n (code >= 0x3000 && code <= 0x303f) || // CJK記号と句読点\n (code >= 0x3040 && code <= 0x309f) || // ひらがな\n (code >= 0x30a0 && code <= 0x30ff) || // カタカナ\n (code >= 0x3100 && code <= 0x312f) || // 注音符号\n (code >= 0x3130 && code <= 0x318f) || // ハングル互換字母\n (code >= 0x3190 && code <= 0x319f) || // 漢文用記号\n (code >= 0x31a0 && code <= 0x31bf) || // 注音字母拡張\n (code >= 0x31c0 && code <= 0x31ef) || // CJKストローク\n (code >= 0x31f0 && code <= 0x31ff) || // カタカナ拡張\n (code >= 0x3200 && code <= 0x32ff) || // 囲みCJK文字・月\n (code >= 0x3300 && code <= 0x33ff) || // CJK互換\n (code >= 0x3400 && code <= 0x4dbf) || // CJK統合漢字拡張A\n (code >= 0x4e00 && code <= 0x9fff) || // CJK統合漢字\n (code >= 0xa000 && code <= 0xa48f) || // イ文字\n (code >= 0xa490 && code <= 0xa4cf) || // イ文字部首\n (code >= 0xac00 && code <= 0xd7af) || // ハングル音節文字\n (code >= 0xf900 && code <= 0xfaff) || // CJK互換漢字\n (code >= 0xfe30 && code <= 0xfe4f) || // CJK互換形\n (code >= 0xff00 && code <= 0xffef) || // 半角・全角形\n (code >= 0x20000 && code <= 0x2a6df) || // CJK統合漢字拡張B\n (code >= 0x2a700 && code <= 0x2b73f) || // CJK統合漢字拡張C\n (code >= 0x2b740 && code <= 0x2b81f) || // CJK統合漢字拡張D\n (code >= 0x2b820 && code <= 0x2ceaf) || // CJK統合漢字拡張E\n (code >= 0x2ceb0 && code <= 0x2ebef) || // CJK統合漢字拡張F\n (code >= 0x30000 && code <= 0x3134f) // CJK統合漢字拡張G\n );\n }\n\n /**\n * より詳細な文字種別の判定\n */\n static getCharacterType(char: string): 'cjk' | 'latin' | 'digit' | 'symbol' | 'whitespace' {\n if (this.isCJKCharacter(char)) return 'cjk';\n if (/[a-zA-Z\\u00c0-\\u024f\\u1e00-\\u1eff]/.test(char)) return 'latin'; // 拡張ラテン文字も含む\n if (/[0-9\\u0660-\\u0669\\u06f0-\\u06f9]/.test(char)) return 'digit'; // アラビア数字等も含む\n if (/\\s/.test(char)) return 'whitespace';\n return 'symbol';\n }\n\n /**\n * テキスト全体の言語構成を分析\n */\n static analyzeTextComposition(text: string): {\n cjk: number;\n latin: number;\n digits: number;\n symbols: number;\n whitespace: number;\n total: number;\n cjkRatio: number;\n } {\n const composition = {\n cjk: 0,\n latin: 0,\n digits: 0,\n symbols: 0,\n whitespace: 0,\n total: text.length\n };\n\n for (const char of text) {\n const type = this.getCharacterType(char);\n if (type === 'digit') {\n composition.digits++;\n } else if (type === 'symbol') {\n composition.symbols++;\n } else {\n composition[type]++;\n }\n }\n\n return {\n ...composition,\n cjkRatio: composition.total > 0 ? composition.cjk / composition.total : 0\n };\n }\n}\n\n/**\n * ファジー推定用の拡張設定\n */\nexport interface FuzzyModelConfig extends BaseTokenConfig {\n cjkTokensPerChar: number; // CJK文字1文字あたりのトークン数\n mixedTextMultiplier: number; // 混在テキストの補正係数\n numberTokensPerChar?: number; // 数字のトークン化率\n symbolTokensPerChar?: number; // 記号のトークン化率\n whitespaceHandling?: 'ignore' | 'count' | 'compress'; // 空白文字の扱い\n}\n\n/**\n * モデル設定プロバイダーのインターフェース\n * 外部ライブラリがこのインターフェースを実装して設定を提供\n */\nexport interface ModelConfigProvider {\n /**\n * モデル名から設定を取得\n */\n getConfig(modelName: string): FuzzyModelConfig | undefined;\n \n /**\n * サポートされているモデル名の一覧を取得\n */\n getSupportedModels(): string[];\n \n /**\n * デフォルトのモデル名を取得(オプション)\n */\n getDefaultModel?(): string | undefined;\n}\n\n/**\n * シンプルなモデル設定プロバイダーの実装\n * ユーザーが設定を直接渡す場合に使用\n */\nexport class SimpleModelConfigProvider implements ModelConfigProvider {\n private configs: Map<string, FuzzyModelConfig>;\n private defaultModel?: string;\n\n constructor(configs: Record<string, FuzzyModelConfig>, defaultModel?: string) {\n this.configs = new Map(Object.entries(configs));\n this.defaultModel = defaultModel;\n }\n\n getConfig(modelName: string): FuzzyModelConfig | undefined {\n return this.configs.get(modelName);\n }\n\n getSupportedModels(): string[] {\n return Array.from(this.configs.keys());\n }\n\n getDefaultModel(): string | undefined {\n return this.defaultModel;\n }\n}\n\n/**\n * トークン推定結果の詳細情報\n */\nexport interface EstimationResult {\n tokens: number;\n breakdown: {\n cjk: number;\n latin: number;\n digits: number;\n symbols: number;\n overhead: number;\n };\n textAnalysis: {\n totalChars: number;\n cjkRatio: number;\n adjustmentFactor: number;\n };\n confidence: 'high' | 'medium' | 'low';\n modelUsed: string;\n}\n\n/**\n * デフォルトのフォールバック設定\n * モデル設定が見つからない場合に使用\n */\nconst DEFAULT_FALLBACK_CONFIG: FuzzyModelConfig = {\n charsPerToken: 4,\n overhead: 10,\n cjkTokensPerChar: 1.5,\n mixedTextMultiplier: 1.05,\n numberTokensPerChar: 3.5,\n symbolTokensPerChar: 2.5,\n whitespaceHandling: 'compress'\n};\n\n/**\n * メインのファジートークン推定器\n */\nexport class FuzzyTokenEstimator {\n private modelProvider: ModelConfigProvider;\n private fallbackConfig: FuzzyModelConfig;\n private defaultModel?: string;\n\n constructor(\n modelProvider: ModelConfigProvider,\n options?: {\n fallbackConfig?: FuzzyModelConfig;\n defaultModel?: string;\n }\n ) {\n this.modelProvider = modelProvider;\n this.fallbackConfig = options?.fallbackConfig || DEFAULT_FALLBACK_CONFIG;\n this.defaultModel = options?.defaultModel || modelProvider.getDefaultModel?.();\n }\n\n /**\n * 現在のモデルプロバイダーを取得\n */\n getModelProvider(): ModelConfigProvider {\n return this.modelProvider;\n }\n\n /**\n * モデルプロバイダーを変更\n */\n setModelProvider(provider: ModelConfigProvider): void {\n this.modelProvider = provider;\n }\n\n /**\n * 指定されたモデルの設定を取得(フォールバック付き)\n */\n private getModelConfig(modelName?: string): { config: FuzzyModelConfig; model: string } {\n const model = modelName || this.defaultModel || 'unknown';\n const config = this.modelProvider.getConfig(model) || this.fallbackConfig;\n return { config, model };\n }\n\n /**\n * 詳細な推定結果を返すメインメソッド\n */\n estimateDetailed(text: string, modelName?: string): EstimationResult {\n const { config, model } = this.getModelConfig(modelName);\n\n if (!text) {\n return {\n tokens: config.overhead,\n breakdown: {\n cjk: 0,\n latin: 0,\n digits: 0,\n symbols: 0,\n overhead: config.overhead\n },\n textAnalysis: {\n totalChars: 0,\n cjkRatio: 0,\n adjustmentFactor: 1\n },\n confidence: 'high',\n modelUsed: model\n };\n }\n\n // テキスト構成を分析\n const composition = CharacterClassifier.analyzeTextComposition(text);\n \n // 各文字種別ごとにトークンを計算\n const breakdown = {\n cjk: 0,\n latin: 0,\n digits: 0,\n symbols: 0,\n overhead: config.overhead\n };\n\n // 連続する同種文字をグループ化して計算\n let currentType: ReturnType<typeof CharacterClassifier.getCharacterType> | null = null;\n let currentGroup = '';\n\n const processGroup = () => {\n if (!currentGroup || !currentType) return;\n\n switch (currentType) {\n case 'cjk':\n breakdown.cjk += currentGroup.length * config.cjkTokensPerChar;\n break;\n case 'latin':\n breakdown.latin += Math.ceil(currentGroup.length / config.charsPerToken);\n break;\n case 'digit':\n breakdown.digits += Math.ceil(currentGroup.length / (config.numberTokensPerChar || 3.5));\n break;\n case 'symbol':\n breakdown.symbols += Math.ceil(currentGroup.length / (config.symbolTokensPerChar || 2.5));\n break;\n case 'whitespace':\n if (config.whitespaceHandling === 'count') {\n breakdown.symbols += currentGroup.length * 0.3;\n }\n break;\n }\n currentGroup = '';\n };\n\n // 文字をグループ化して処理\n for (const char of text) {\n const type = CharacterClassifier.getCharacterType(char);\n \n if (type !== currentType) {\n processGroup();\n currentType = type;\n }\n currentGroup += char;\n }\n processGroup();\n\n // 基本トークン数を計算\n let baseTokens = Object.values(breakdown).reduce((sum, val) => sum + val, 0);\n\n // 混在テキストの補正を適用\n baseTokens *= config.mixedTextMultiplier;\n\n // CJK文字の比率に基づく調整\n const adjustmentFactor = this.calculateAdjustmentFactor(composition.cjkRatio);\n const finalTokens = Math.ceil(baseTokens * adjustmentFactor);\n\n // 信頼度を計算\n const confidence = this.calculateConfidence(composition);\n\n return {\n tokens: finalTokens,\n breakdown,\n textAnalysis: {\n totalChars: text.length,\n cjkRatio: composition.cjkRatio,\n adjustmentFactor\n },\n confidence,\n modelUsed: model\n };\n }\n\n /**\n * シンプルなトークン数のみを返すメソッド\n */\n estimate(text: string, modelName?: string): number {\n return this.estimateDetailed(text, modelName).tokens;\n }\n\n /**\n * TextPayload形式での推定\n */\n estimatePayload(payload: TextPayload): number {\n const promptTokens = typeof payload.prompt === 'string' \n ? this.estimate(payload.prompt, payload.model)\n : this.getModelConfig(payload.model).config.overhead;\n\n const maxTokens = payload.maxTokens && payload.maxTokens > 0 \n ? payload.maxTokens \n : 500;\n\n // 安全マージン(10%)を追加\n return Math.ceil((promptTokens + maxTokens) * 1.1);\n }\n\n /**\n * 日本語比率に基づく調整係数を計算\n */\n private calculateAdjustmentFactor(japaneseRatio: number): number {\n if (japaneseRatio > 0.8) {\n return 0.6;\n } else if (japaneseRatio === 0) {\n return 1.0;\n } else if (japaneseRatio < 0.2) {\n return 0.7;\n } else {\n return 0.7 - ((japaneseRatio - 0.2) / 0.6) * 0.1;\n }\n }\n\n /**\n * 推定の信頼度を計算\n */\n private calculateConfidence(composition: ReturnType<typeof CharacterClassifier.analyzeTextComposition>): 'high' | 'medium' | 'low' {\n if (composition.total < 10) return 'low';\n if (composition.cjkRatio > 0.9 || composition.cjkRatio < 0.1) return 'high';\n if (composition.symbols / composition.total > 0.3) return 'low';\n return 'medium';\n }\n\n /**\n * バッチ推定\n */\n estimateBatch(texts: string[], modelName?: string): EstimationResult[] {\n return texts.map(text => this.estimateDetailed(text, modelName));\n }\n\n /**\n * ストリーミングテキストの推定\n */\n async *estimateStream(\n textStream: AsyncIterable<string>,\n modelName?: string\n ): AsyncGenerator<{ chunk: string; tokens: number; total: number }> {\n let total = 0;\n \n for await (const chunk of textStream) {\n const tokens = this.estimate(chunk, modelName);\n total += tokens;\n yield { chunk, tokens, total };\n }\n }\n\n /**\n * 利用可能なモデルの一覧を取得\n */\n getSupportedModels(): string[] {\n return this.modelProvider.getSupportedModels();\n }\n}\n\n/**\n * ファクトリー関数 - ModelConfigProviderを使用\n */\nexport function createFuzzyEstimator(\n modelProvider: ModelConfigProvider,\n options?: {\n fallbackConfig?: FuzzyModelConfig;\n defaultModel?: string;\n }\n): FuzzyTokenEstimator {\n return new FuzzyTokenEstimator(modelProvider, options);\n}\n\n/**\n * 簡易ファクトリー関数 - 設定オブジェクトを直接渡す\n */\nexport function createSimpleFuzzyEstimator(\n modelConfigs: Record<string, FuzzyModelConfig>,\n defaultModel?: string\n): FuzzyTokenEstimator {\n const provider = new SimpleModelConfigProvider(modelConfigs, defaultModel);\n return new FuzzyTokenEstimator(provider, { defaultModel });\n}\n\n/**\n * トークン数からコストを計算するユーティリティ\n * コスト情報も外部から注入可能\n */\nexport interface CostProvider {\n getCost(model: string): { input: number; output: number } | undefined;\n}\n\nexport class TokenCostCalculator {\n constructor(private costProvider: CostProvider) {}\n\n calculate(model: string, inputTokens: number, outputTokens: number): {\n inputCost: number;\n outputCost: number;\n totalCost: number;\n formattedTotal: string;\n available: boolean;\n } {\n const pricing = this.costProvider.getCost(model);\n \n if (!pricing) {\n return {\n inputCost: 0,\n outputCost: 0,\n totalCost: 0,\n formattedTotal: 'N/A',\n available: false\n };\n }\n \n const inputCost = (inputTokens / 1000) * pricing.input;\n const outputCost = (outputTokens / 1000) * pricing.output;\n const totalCost = inputCost + outputCost;\n\n return {\n inputCost,\n outputCost,\n totalCost,\n formattedTotal: `$${totalCost.toFixed(4)}`,\n available: true\n };\n }\n}\n\n/**\n * デバッグ用のビジュアライザー\n */\nexport class TokenEstimationVisualizer {\n static visualize(text: string, result: EstimationResult): string {\n const bar = (value: number, max: number, width: number = 20) => {\n const filled = Math.round((value / max) * width);\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n };\n\n const maxTokens = Math.max(...Object.values(result.breakdown));\n \n return `\n=== Token Estimation Visualization ===\nModel: ${result.modelUsed}\nText: \"${text.slice(0, 50)}${text.length > 50 ? '...' : ''}\"\nTotal Tokens: ${result.tokens}\nConfidence: ${result.confidence}\n\nBreakdown:\nCJK [${bar(result.breakdown.cjk, maxTokens)}] ${result.breakdown.cjk.toFixed(1)}\nLatin [${bar(result.breakdown.latin, maxTokens)}] ${result.breakdown.latin.toFixed(1)}\nDigits [${bar(result.breakdown.digits, maxTokens)}] ${result.breakdown.digits.toFixed(1)}\nSymbols [${bar(result.breakdown.symbols, maxTokens)}] ${result.breakdown.symbols.toFixed(1)}\nOverhead [${bar(result.breakdown.overhead, maxTokens)}] ${result.breakdown.overhead}\n\nText Analysis:\n- Total Characters: ${result.textAnalysis.totalChars}\n- CJK Ratio: ${(result.textAnalysis.cjkRatio * 100).toFixed(1)}%\n- Adjustment Factor: ${result.textAnalysis.adjustmentFactor.toFixed(2)}\n`;\n }\n}\n","export class RateLimitError extends Error {\n constructor(\n message: string,\n public readonly reason: 'rpm_limit' | 'tpm_limit',\n public readonly availableIn: number\n ) {\n super(message);\n this.name = 'RateLimitError';\n }\n}\n\nexport class InvalidConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidConfigError';\n }\n}","import { TokenBucketConfig, TokenBucketState } from './types/index.js';\nimport { InvalidConfigError } from './errors.js';\nimport { ThrottleStorage } from './storage/index.js';\n\nexport class TokenBucket {\n private _capacity: number;\n private _available: number;\n private _refillRate: number;\n private _lastRefill: number;\n private _clock: () => number;\n private _storage?: ThrottleStorage<unknown>;\n private _storageKey?: string;\n private _initialized: boolean = false;\n\n constructor(config: TokenBucketConfig, storage?: ThrottleStorage<unknown>) {\n this.validateConfig(config);\n \n this._capacity = config.capacity;\n this._available = config.initialTokens ?? config.capacity;\n this._refillRate = config.refillRate;\n this._clock = config.clock ?? (() => Date.now());\n this._lastRefill = this._clock();\n this._storage = storage;\n this._storageKey = config.storageKey;\n }\n\n private validateConfig(config: TokenBucketConfig): void {\n if (config.capacity <= 0) {\n throw new InvalidConfigError('Capacity must be greater than 0');\n }\n if (config.refillRate <= 0) {\n throw new InvalidConfigError('Refill rate must be greater than 0');\n }\n if (config.initialTokens !== undefined && config.initialTokens < 0) {\n throw new InvalidConfigError('Initial tokens cannot be negative');\n }\n if (config.initialTokens !== undefined && config.initialTokens > config.capacity) {\n throw new InvalidConfigError('Initial tokens cannot exceed capacity');\n }\n }\n\n get capacity(): number {\n return this._capacity;\n }\n\n get available(): number {\n this.refill();\n return this._available;\n }\n\n get refillRate(): number {\n return this._refillRate;\n }\n\n private refill(): void {\n const now = this._clock();\n const timePassed = (now - this._lastRefill) / 1000; // seconds\n \n if (timePassed <= 0) return;\n \n const tokensToAdd = timePassed * this._refillRate;\n this._available = Math.min(\n this._capacity,\n this._available + tokensToAdd\n );\n this._lastRefill = now;\n }\n\n hasTokens(count: number): boolean {\n if (count < 0) return false;\n this.refill();\n return this._available >= count;\n }\n\n consume(count: number): boolean {\n if (count < 0) {\n throw new Error('Cannot consume negative tokens');\n }\n \n this.refill();\n if (this._available >= count) {\n this._available -= count;\n this.persistState();\n return true;\n }\n return false;\n }\n\n refund(count: number): void {\n if (count < 0) {\n throw new Error('Cannot refund negative tokens');\n }\n \n this._available = Math.min(this._capacity, this._available + count);\n this.persistState();\n }\n\n timeUntilNextToken(): number {\n this.refill();\n if (this._available >= 1) return 0;\n return Math.ceil((1 - this._available) / this._refillRate * 1000); // ms\n }\n\n timeUntilTokens(count: number): number {\n if (count <= 0) return 0;\n \n this.refill();\n if (this._available >= count) return 0;\n \n const needed = count - this._available;\n return Math.ceil(needed / this._refillRate * 1000); // ms\n }\n\n reset(): void {\n this._available = this._capacity;\n this._lastRefill = this._clock();\n this.persistState();\n }\n\n /**\n * Get current internal state for snapshots\n */\n getState(): TokenBucketState {\n this.refill();\n return {\n available: this._available,\n capacity: this._capacity,\n lastRefill: this._lastRefill\n };\n }\n\n /**\n * Restore state from snapshot\n */\n restoreState(state: TokenBucketState): void {\n if (state.available < 0 || state.available > state.capacity) {\n throw new Error('Invalid state: available tokens out of range');\n }\n if (state.capacity !== this._capacity) {\n throw new Error('Invalid state: capacity mismatch');\n }\n \n this._available = state.available;\n this._lastRefill = state.lastRefill;\n }\n\n /**\n * Validate internal consistency\n */\n validateConsistency(): boolean {\n this.refill();\n return this._available >= 0 && \n this._available <= this._capacity && \n this._capacity > 0 && \n this._refillRate > 0;\n }\n\n /**\n * Initialize from storage if available\n */\n async initializeFromStorage(): Promise<void> {\n if (!this._storage || !this._storageKey || this._initialized) {\n return;\n }\n\n try {\n const storedState = await this._storage.loadTokenBucketState(this._storageKey);\n if (storedState && storedState.capacity === this._capacity) {\n // Only restore if capacity matches (configuration hasn't changed)\n this._available = storedState.available;\n this._lastRefill = storedState.lastRefill;\n }\n } catch (error) {\n // Ignore storage errors during initialization\n } finally {\n this._initialized = true;\n }\n }\n\n /**\n * Persist current state to storage\n */\n private persistState(): void {\n if (!this._storage || !this._storageKey) {\n return;\n }\n\n // Fire and forget - don't block on storage operations\n const state = this.getState();\n this._storage.saveTokenBucketState(this._storageKey, state).catch(() => {\n // Ignore storage errors\n });\n }\n}","/**\n * Simple async lock implementation for protecting critical sections\n */\n\ninterface QueueItem {\n resolve: () => void;\n reject: (error: Error) => void;\n}\n\nexport class AsyncLock {\n private locked = false;\n private queue: QueueItem[] = [];\n\n /**\n * Acquire the lock\n */\n async acquire(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (!this.locked) {\n this.locked = true;\n resolve();\n } else {\n this.queue.push({ resolve, reject });\n }\n });\n }\n\n /**\n * Release the lock\n */\n release(): void {\n if (!this.locked) {\n throw new Error('Cannot release a lock that is not acquired');\n }\n\n if (this.queue.length > 0) {\n const next = this.queue.shift()!;\n next.resolve();\n } else {\n this.locked = false;\n }\n }\n\n /**\n * Execute a function with the lock acquired\n */\n async withLock<T>(fn: () => Promise<T>): Promise<T> {\n await this.acquire();\n try {\n return await fn();\n } finally {\n this.release();\n }\n }\n\n /**\n * Check if the lock is currently held\n */\n isLocked(): boolean {\n return this.locked;\n }\n\n /**\n * Get the number of pending operations waiting for the lock\n */\n getQueueLength(): number {\n return this.queue.length;\n }\n\n /**\n * Clear all pending operations (useful for cleanup)\n */\n clear(): void {\n const error = new Error('AsyncLock cleared');\n this.queue.forEach(item => item.reject(error));\n this.queue = [];\n this.locked = false;\n }\n}","/**\n * Clock utilities for high-precision timing\n */\n\nlet hasNodeHrtime: boolean;\nlet hasPerformanceNow: boolean;\n\n// Check availability at module load time\ntry {\n hasNodeHrtime = typeof process !== 'undefined' && \n typeof process.hrtime !== 'undefined' && \n typeof process.hrtime.bigint === 'function';\n} catch {\n hasNodeHrtime = false;\n}\n\ntry {\n hasPerformanceNow = typeof performance !== 'undefined' && \n typeof performance.now === 'function';\n} catch {\n hasPerformanceNow = false;\n}\n\n/**\n * Creates a monotonic clock function based on the environment\n */\nexport function createMonotonicClock(): () => number {\n if (hasNodeHrtime) {\n // Node.js: Use high-resolution time\n const startTime = process.hrtime.bigint();\n return () => {\n const current = process.hrtime.bigint();\n return Number(current - startTime) / 1000000; // Convert nanoseconds to milliseconds\n };\n } else if (hasPerformanceNow) {\n // Browser: Use performance.now()\n const startTime = performance.now();\n return () => performance.now() - startTime;\n } else {\n // Fallback: Use Date.now() (not monotonic but better than nothing)\n const startTime = Date.now();\n return () => Date.now() - startTime;\n }\n}\n\n/**\n * Creates a standard clock function (Date.now)\n */\nexport function createStandardClock(): () => number {\n return () => Date.now();\n}\n\n/**\n * Auto-detects and creates the best available clock\n */\nexport function createOptimalClock(preferMonotonic: boolean = true): () => number {\n if (preferMonotonic && (hasNodeHrtime || hasPerformanceNow)) {\n return createMonotonicClock();\n }\n return createStandardClock();\n}\n\n/**\n * Gets environment information for debugging\n */\nexport function getClockInfo(): {\n hasNodeHrtime: boolean;\n hasPerformanceNow: boolean;\n recommendedClock: 'monotonic' | 'standard';\n} {\n return {\n hasNodeHrtime,\n hasPerformanceNow,\n recommendedClock: (hasNodeHrtime || hasPerformanceNow) ? 'monotonic' : 'standard'\n };\n}","/**\n * Configuration validation utilities\n */\n\nimport { DualRateLimitConfig, ValidationRule, Logger } from '../types/index.js';\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n warnings: string[];\n}\n\n/**\n * Default validation rules for DualRateLimitConfig\n */\nexport const defaultValidationRules: ValidationRule<DualRateLimitConfig>[] = [\n {\n name: 'rpm_positive',\n validate: (config) => config.rpm > 0 || 'RPM must be greater than 0',\n level: 'error'\n },\n {\n name: 'tpm_positive', \n validate: (config) => config.tpm > 0 || 'TPM must be greater than 0',\n level: 'error'\n },\n {\n name: 'burst_rpm_valid',\n validate: (config) => {\n if (config.burstRPM !== undefined && config.burstRPM < config.rpm) {\n return 'Burst RPM cannot be less than RPM';\n }\n return true;\n },\n level: 'error'\n },\n {\n name: 'burst_tpm_valid',\n validate: (config) => {\n if (config.burstTPM !== undefined && config.burstTPM < config.tpm) {\n return 'Burst TPM cannot be less than TPM';\n }\n return true;\n },\n level: 'error'\n },\n {\n name: 'burst_rpm_limit',\n validate: (config) => {\n if (config.burstRPM !== undefined && config.burstRPM > config.rpm * 10) {\n return 'Burst RPM should not exceed 10x the base RPM for optimal performance';\n }\n return true;\n },\n level: 'warn'\n },\n {\n name: 'burst_tpm_limit',\n validate: (config) => {\n if (config.burstTPM !== undefined && config.burstTPM > config.tpm * 10) {\n return 'Burst TPM should not exceed 10x the base TPM for optimal performance';\n }\n return true;\n },\n level: 'warn'\n },\n {\n name: 'rpm_high_warning',\n validate: (config) => {\n if (config.rpm > 10000) {\n return 'RPM above 10,000 may impact performance and API stability';\n }\n return true;\n },\n level: 'warn'\n },\n {\n name: 'tpm_high_warning', \n validate: (config) => {\n if (config.tpm > 1000000) {\n return 'TPM above 1,000,000 may impact performance and memory usage';\n }\n return true;\n },\n level: 'warn'\n },\n {\n name: 'history_retention_valid',\n validate: (config) => {\n if (config.historyRetentionMs !== undefined && config.historyRetentionMs <= 0) {\n return 'History retention must be positive';\n }\n return true;\n },\n level: 'error'\n },\n {\n name: 'max_history_valid',\n validate: (config) => {\n if (config.maxHistoryRecords !== undefined && config.maxHistoryRecords <= 0) {\n return 'Max history records must be positive';\n }\n return true;\n },\n level: 'error'\n },\n {\n name: 'efficiency_window_valid',\n validate: (config) => {\n if (config.efficiencyWindowSize !== undefined && config.efficiencyWindowSize <= 0) {\n return 'Efficiency window size must be positive';\n }\n return true;\n },\n level: 'error'\n }\n];\n\n/**\n * Validates configuration against rules\n */\nexport function validateConfig(\n config: DualRateLimitConfig,\n customRules: ValidationRule<DualRateLimitConfig>[] = [],\n logger?: Logger\n): ValidationResult {\n const result: ValidationResult = {\n valid: true,\n errors: [],\n warnings: []\n };\n\n // Combine default and custom rules\n const allRules = [...defaultValidationRules, ...customRules];\n\n for (const rule of allRules) {\n try {\n const validationResult = rule.validate(config);\n \n if (validationResult !== true) {\n const message = typeof validationResult === 'string' \n ? validationResult \n : `Validation failed for rule: ${rule.name}`;\n \n if (rule.level === 'error') {\n result.errors.push(message);\n result.valid = false;\n } else {\n result.warnings.push(message);\n }\n }\n } catch (error) {\n const message = `Validation rule '${rule.name}' threw an error: ${error instanceof Error ? error.message : String(error)}`;\n result.errors.push(message);\n result.valid = false;\n }\n }\n\n // Log warnings if logger is provided\n if (logger && result.warnings.length > 0) {\n result.warnings.forEach(warning => logger.warn(`Config validation warning: ${warning}`));\n }\n\n return result;\n}\n\n/**\n * Validates and normalizes configuration, throwing on errors\n */\nexport function validateAndNormalizeConfig(\n config: DualRateLimitConfig,\n customRules: ValidationRule<DualRateLimitConfig>[] = [],\n logger?: Logger\n): DualRateLimitConfig {\n // Basic type check\n if (!config || typeof config !== 'object') {\n throw new Error('Config must be an object');\n }\n\n const result = validateConfig(config, customRules, logger);\n \n if (!result.valid) {\n throw new Error(`Configuration validation failed: ${result.errors.join(', ')}`);\n }\n\n // Return normalized config with defaults\n return {\n ...config,\n adjustmentFailureStrategy: config.adjustmentFailureStrategy || 'warn',\n maxHistoryRecords: config.maxHistoryRecords || 10000,\n historyRetentionMs: config.historyRetentionMs || 60000,\n efficiencyWindowSize: config.efficiencyWindowSize || 50,\n logger: config.logger || console\n };\n}","import { ThrottleStorage } from './types.js';\nimport { ConsumptionRecord, TokenBucketState } from '../types/index.js';\n\n/**\n * In-memory storage implementation (default)\n */\nexport class InMemoryStorage<TMetadata = Record<string, unknown>> implements ThrottleStorage<TMetadata> {\n private tokenBucketStates: Map<string, TokenBucketState> = new Map();\n private consumptionHistory: ConsumptionRecord<TMetadata>[] = [];\n private compensationDebt: number = 0;\n\n async saveTokenBucketState(key: string, state: TokenBucketState): Promise<void> {\n this.tokenBucketStates.set(key, { ...state });\n }\n\n async loadTokenBucketState(key: string): Promise<TokenBucketState | null> {\n const state = this.tokenBucketStates.get(key);\n return state ? { ...state } : null;\n }\n\n async saveConsumptionHistory(records: ConsumptionRecord<TMetadata>[]): Promise<void> {\n this.consumptionHistory = [...records];\n }\n\n async loadConsumptionHistory(limit?: number): Promise<ConsumptionRecord<TMetadata>[]> {\n if (limit && limit > 0) {\n return this.consumptionHistory.slice(-limit);\n }\n return [...this.consumptionHistory];\n }\n\n async addConsumptionRecord(record: ConsumptionRecord<TMetadata>): Promise<void> {\n this.consumptionHistory.push({ ...record });\n }\n\n async cleanupConsumptionHistory(olderThan: number): Promise<number> {\n const originalLength = this.consumptionHistory.length;\n this.consumptionHistory = this.consumptionHistory.filter(\n record => record.timestamp > olderThan\n );\n return originalLength - this.consumptionHistory.length;\n }\n\n async saveCompensationDebt(debt: number): Promise<void> {\n this.compensationDebt = debt;\n }\n\n async loadCompensationDebt(): Promise<number> {\n return this.compensationDebt;\n }\n\n async clear(): Promise<void> {\n this.tokenBucketStates.clear();\n this.consumptionHistory = [];\n this.compensationDebt = 0;\n }\n\n async isAvailable(): Promise<boolean> {\n return true;\n }\n}","/**\n * Integration utility for @aid-on/fuzztok token estimation\n */\n\ninterface FuzztokModule {\n encode: (text: string) => number[];\n decode: (tokens: number[]) => string;\n countTokens: (text: string) => number;\n}\n\nlet fuzztokModule: FuzztokModule | null = null;\nlet fuzztokAvailable = false;\n\n/**\n * Attempts to load the fuzztok module\n */\nasync function loadFuzztok(): Promise<boolean> {\n if (fuzztokModule !== null) {\n return fuzztokAvailable;\n }\n\n try {\n // Dynamic import to handle optional dependency\n // @ts-expect-error - Optional dependency may not be available\n fuzztokModule = await import('@aid-on/fuzztok');\n fuzztokAvailable = true;\n return true;\n } catch (error) {\n // Module not available or failed to load\n fuzztokAvailable = false;\n return false;\n }\n}\n\n/**\n * Estimates token count for given text using fuzztok\n */\nexport async function estimateTokens(text: string): Promise<number> {\n const loaded = await loadFuzztok();\n \n if (!loaded || !fuzztokModule) {\n // Fallback estimation: roughly 4 characters per token for English text\n return Math.ceil(text.length / 4);\n }\n\n try {\n return fuzztokModule.countTokens(text);\n } catch (error) {\n // Fallback on error\n return Math.ceil(text.length / 4);\n }\n}\n\n/**\n * Checks if fuzztok is available\n */\nexport async function isFuzztokAvailable(): Promise<boolean> {\n return await loadFuzztok();\n}\n\n/**\n * Simple synchronous token estimation (fallback method)\n */\nexport function simpleFallbackEstimate(text: string): number {\n if (!text) return 0;\n \n // More sophisticated fallback estimation\n // Account for different types of content\n const words = text.split(/\\s+/).length;\n const chars = text.length;\n \n // Average of word-based and character-based estimation\n const wordBasedEstimate = Math.ceil(words * 1.3); // ~1.3 tokens per word\n const charBasedEstimate = Math.ceil(chars / 4); // ~4 chars per token\n \n return Math.max(1, Math.round((wordBasedEstimate + charBasedEstimate) / 2));\n}\n\n/**\n * Estimates tokens with automatic fallback\n */\nexport async function robustEstimateTokens(text: string): Promise<number> {\n if (!text) return 0;\n \n try {\n return await estimateTokens(text);\n } catch (error) {\n return simpleFallbackEstimate(text);\n }\n}","import { \n ConsumptionRecord, \n RateLimitCheckResult,\n RateLimitMetrics,\n Logger,\n AdjustmentFailureStrategy,\n StateSnapshot,\n MemoryMetrics\n} from './types/index.js';\nimport { RateLimitError } from './errors.js';\nimport { TokenBucket } from './token-bucket.js';\nimport { AsyncLock } from './utils/async-lock.js';\nimport { createOptimalClock } from './utils/clock.js';\nimport { validateAndNormalizeConfig } from './utils/validation.js';\nimport { ThrottleStorage, InMemoryStorage } from './storage/index.js';\n\n/**\n * Enhanced configuration for LLMThrottle with clear storage options\n */\nexport interface LLMThrottleConfig<TMetadata = Record<string, unknown>> {\n /** Requests per minute limit */\n rpm: number;\n /** Tokens per minute limit */\n tpm: number;\n /** Optional burst capacity for RPM (defaults to rpm) */\n burstRPM?: number;\n /** Optional burst capacity for TPM (defaults to tpm) */\n burstTPM?: number;\n /** Optional custom clock function for testing */\n clock?: () => number;\n /** Optional custom logger (defaults to console) */\n logger?: Logger;\n /** Strategy when adjustConsumption fails to consume additional tokens */\n adjustmentFailureStrategy?: AdjustmentFailureStrategy;\n /** Maximum number of consumption records to keep in memory */\n maxHistoryRecords?: number;\n /** Maximum history retention time in milliseconds (defaults to 60000) */\n historyRetentionMs?: number;\n /** Enable monotonic clock (auto-detected by default) */\n monotonicClock?: boolean;\n /** Custom validation rules */\n validationRules?: Array<{name: string; validate: (value: unknown) => boolean | string; level: 'error' | 'warn'}>;\n /** Number of records to use for efficiency calculation (defaults to 50) */\n efficiencyWindowSize?: number;\n /** Storage implementation for persistence */\n storage?: ThrottleStorage<TMetadata>;\n}\n\n/**\n * LLM Throttle - Rate limiter with dual constraints (RPM + TPM) and optional persistence\n */\nexport class LLMThrottle<TMetadata = Record<string, unknown>> {\n private rpmBucket: TokenBucket;\n private tpmBucket: TokenBucket;\n private consumptionHistory: ConsumptionRecord<TMetadata>[] = [];\n private clock: () => number;\n private logger: Logger;\n // private _config: LLMThrottleConfig; // Kept for future use\n private lock = new AsyncLock();\n private compensationDebt = 0;\n private historyRetentionMs: number;\n private maxHistoryRecords: number;\n private efficiencyWindowSize: number;\n private adjustmentFailureStrategy: AdjustmentFailureStrategy;\n private storage: ThrottleStorage<TMetadata>;\n private storageEnabled: boolean;\n private initialized: boolean = false;\n\n /**\n * Create a new LLMThrottle instance\n * @param config Configuration including optional storage implementation\n */\n constructor(config: LLMThrottleConfig<TMetadata>) {\n // Convert to DualRateLimitConfig for validation\n const legacyConfig = {\n ...config,\n storage: config.storage ? { \n enabled: true, \n implementation: config.storage \n } : undefined\n };\n\n // Validate and normalize config\n const tempLogger = config.logger || console;\n const validatedConfig = validateAndNormalizeConfig(legacyConfig, config.validationRules, tempLogger);\n this.logger = validatedConfig.logger || console;\n this.historyRetentionMs = validatedConfig.historyRetentionMs || 60000;\n this.maxHistoryRecords = validatedConfig.maxHistoryRecords || 10000;\n this.efficiencyWindowSize = validatedConfig.efficiencyWindowSize || 50;\n this.adjustmentFailureStrategy = validatedConfig.adjustmentFailureStrategy || 'warn';\n \n // Set up storage - cleaner interface\n this.storageEnabled = !!config.storage;\n this.storage = config.storage || new InMemoryStorage<TMetadata>();\n \n // Set up clock (monotonic by default unless explicitly disabled)\n if (validatedConfig.clock) {\n this.clock = validatedConfig.clock;\n } else {\n this.clock = createOptimalClock(validatedConfig.monotonicClock !== false);\n }\n \n this.rpmBucket = new TokenBucket({\n capacity: validatedConfig.burstRPM || validatedConfig.rpm,\n refillRate: validatedConfig.rpm / 60, // per second\n initialTokens: validatedConfig.burstRPM || validatedConfig.rpm,\n clock: this.clock,\n storageKey: 'rpm'\n }, this.storageEnabled ? this.storage : undefined);\n\n this.tpmBucket = new TokenBucket({\n capacity: validatedConfig.burstTPM || validatedConfig.tpm,\n refillRate: validatedConfig.tpm / 60,\n initialTokens: validatedConfig.burstTPM || validatedConfig.tpm,\n clock: this.clock,\n storageKey: 'tpm'\n }, this.storageEnabled ? this.storage : undefined);\n }\n\n /**\n * Initialize from storage if available\n * Call this after creating the instance to restore persisted state\n */\n async initialize(): Promise<void> {\n if (this.initialized || !this.storageEnabled) {\n return;\n }\n\n try {\n // Initialize token buckets from storage\n await Promise.all([\n this.rpmBucket.initializeFromStorage(),\n this.tpmBucket.initializeFromStorage()\n ]);\n\n // Load compensation debt\n const storedDebt = await this.storage.loadCompensationDebt();\n if (storedDebt >= 0) {\n this.compensationDebt = storedDebt;\n }\n\n // Load consumption history\n const history = await this.storage.loadConsumptionHistory(this.maxHistoryRecords);\n if (history.length > 0) {\n this.consumptionHistory = history;\n this.cleanupHistory(); // Ensure loaded history is within retention limits\n }\n\n this.logger.info('Throttle state initialized from storage');\n } catch (error) {\n this.logger.warn(`Failed to initialize from storage: ${error instanceof Error ? error.message : String(error)}`);\n } finally {\n this.initialized = true;\n }\n }\n\n canProcess(estimatedTokens: number): RateLimitCheckResult {\n if (estimatedTokens < 0) {\n throw new Error('Estimated tokens cannot be negative');\n }\n\n if (!this.rpmBucket.hasTokens(1)) {\n return {\n allowed: false,\n reason: 'rpm_limit',\n availableIn: this.rpmBucket.timeUntilNextToken(),\n availableTokens: {\n rpm: this.rpmBucket.available,\n tpm: this.tpmBucket.available\n }\n };\n }\n\n if (!this.tpmBucket.hasTokens(estimatedTokens)) {\n return {\n allowed: false,\n reason: 'tpm_limit',\n availableIn: this.tpmBucket.timeUntilTokens(estimatedTokens),\n availableTokens: {\n rpm: this.rpmBucket.available,\n tpm: this.tpmBucket.available\n }\n };\n }\n\n return { \n allowed: true,\n availableTokens: {\n rpm: this.rpmBucket.available,\n tpm: this.tpmBucket.available\n }\n };\n }\n\n // Synchronous version (backward compatibility)\n consume(requestId: string, estimatedTokens: number, metadata?: TMetadata): boolean {\n if (!requestId || requestId.trim() === '') {\n throw new Error('Request ID cannot be empty');\n }\n \n // For sync version, apply compensation but don't use async lock\n const totalTokensNeeded = estimatedTokens + this.compensationDebt;\n \n const check = this.canProcess(totalTokensNeeded);\n if (!check.allowed) {\n return false;\n }\n\n this.rpmBucket.consume(1);\n this.tpmBucket.consume(totalTokensNeeded);\n \n // Reset compensation debt after successful consumption\n const appliedCompensation = this.compensationDebt;\n this.compensationDebt = 0;\n\n const record: ConsumptionRecord<TMetadata> = {\n timestamp: this.clock(),\n tokens: estimatedTokens,\n requestId,\n metadata,\n estimatedTokens,\n compensationDebt: appliedCompensation\n };\n \n this.consumptionHistory.push(record);\n \n // Persist to storage if enabled\n if (this.storageEnabled) {\n this.storage.addConsumptionRecord(record).catch(() => {\n // Ignore storage errors\n });\n }\n\n this.cleanupHistory();\n return true;\n }\n\n // Async version for concurrent scenarios\n async consumeAsync(requestId: string, estimatedTokens: number, metadata?: TMetadata): Promise<boolean> {\n if (!requestId || requestId.trim() === '') {\n throw new Error('Request ID cannot be empty');\n }\n \n return await this.lock.withLock(async () => {\n // Apply any pending compensation\n const totalTokensNeeded = estimatedTokens + this.compensationDebt;\n \n const check = this.canProcess(totalTokensNeeded);\n if (!check.allowed) {\n return false;\n }\n\n this.rpmBucket.consume(1);\n this.tpmBucket.consume(totalTokensNeeded);\n \n // Reset compensation debt after successful consumption\n const appliedCompensation = this.compensationDebt;\n this.compensationDebt = 0;\n\n const record: ConsumptionRecord<TMetadata> = {\n timestamp: this.clock(),\n tokens: estimatedTokens,\n requestId,\n metadata,\n estimatedTokens,\n compensationDebt: appliedCompensation\n };\n \n this.consumptionHistory.push(record);\n \n // Persist to storage if enabled\n if (this.storageEnabled) {\n this.storage.addConsumptionRecord(record).catch(() => {\n // Ignore storage errors\n });\n }\n\n this.cleanupHistory();\n return true;\n });\n }\n\n // Synchronous version (backward compatibility)\n consumeOrThrow(requestId: string, estimatedTokens: number, metadata?: TMetadata): void {\n const consumed = this.consume(requestId, estimatedTokens, metadata);\n if (!consumed) {\n const check = this.canProcess(estimatedTokens + this.compensationDebt);\n throw new RateLimitError(\n `Rate limit exceeded: ${check.reason}`,\n check.reason!,\n check.availableIn!\n );\n }\n }\n\n async consumeOrThrowAsync(requestId: string, estimatedTokens: number, metadata?: TMetadata): Promise<void> {\n const consumed = await this.consumeAsync(requestId, estimatedTokens, metadata);\n if (!consumed) {\n const check = this.canProcess(estimatedTokens + this.compensationDebt);\n throw new RateLimitError(\n `Rate limit exceeded: ${check.reason}`,\n check.reason!,\n check.availableIn!\n );\n }\n }\n\n // Synchronous version (backward compatibility)\n adjustConsumption(requestId: string, actualTokens: number): void {\n if (actualTokens < 0) {\n throw new Error('Actual tokens cannot be negative');\n }\n\n const record = this.consumptionHistory.find(\n item => item.requestId === requestId\n );\n\n if (!record) {\n throw new Error(`Request ID '${requestId}' not found in consumption history`);\n }\n\n const difference = actualTokens - record.tokens;\n \n if (difference > 0) {\n // Need to consume additional tokens\n const consumed = this.tpmBucket.consume(difference);\n if (!consumed) {\n this.handleAdjustmentFailureSync(requestId, difference);\n }\n } else if (difference < 0) {\n // Refund excess tokens\n this.tpmBucket.refund(-difference);\n }\n \n record.tokens = actualTokens;\n record.actualTokens = actualTokens;\n }\n\n async adjustConsumptionAsync(requestId: string, actualTokens: number): Promise<void> {\n if (actualTokens < 0) {\n throw new Error('Actual tokens cannot be negative');\n }\n\n return await this.lock.withLock(async () => {\n const record = this.consumptionHistory.find(\n item => item.requestId === requestId\n );\n\n if (!record) {\n throw new Error(`Request ID '${requestId}' not found in consumption history`);\n }\n\n const difference = actualTokens - record.tokens;\n \n if (difference > 0) {\n // Need to consume additional tokens\n const consumed = this.tpmBucket.consume(difference);\n if (!consumed) {\n await this.handleAdjustmentFailure(requestId, difference);\n }\n } else if (difference < 0) {\n // Refund excess tokens\n this.tpmBucket.refund(-difference);\n }\n \n record.tokens = actualTokens;\n record.actualTokens = actualTokens;\n });\n }\n\n private handleAdjustmentFailureSync(requestId: string, additionalTokens: number): void {\n const message = `Failed to consume additional ${additionalTokens} tokens for request ${requestId}`;\n \n switch (this.adjustmentFailureStrategy) {\n case 'strict':\n throw new RateLimitError(\n message,\n 'tpm_limit',\n this.tpmBucket.timeUntilTokens(additionalTokens)\n );\n \n case 'warn':\n this.logger.warn(message);\n break;\n \n case 'compensate':\n this.compensationDebt += additionalTokens;\n this.logger.info(`Adding ${additionalTokens} tokens to compensation debt. Total debt: ${this.compensationDebt}`);\n this.persistCompensationDebt();\n break;\n }\n }\n\n private async handleAdjustmentFailure(requestId: string, additionalTokens: number): Promise<void> {\n const message = `Failed to consume additional ${additionalTokens} tokens for request ${requestId}`;\n \n switch (this.adjustmentFailureStrategy) {\n case 'strict':\n throw new RateLimitError(\n message,\n 'tpm_limit',\n this.tpmBucket.timeUntilTokens(additionalTokens)\n );\n \n case 'warn':\n this.logger.warn(message);\n break;\n \n case 'compensate':\n this.compensationDebt += additionalTokens;\n this.logger.info(`Adding ${additionalTokens} tokens to compensation debt. Total debt: ${this.compensationDebt}`);\n this.persistCompensationDebt();\n break;\n }\n }\n\n getMetrics(): RateLimitMetrics {\n this.cleanupHistory();\n \n const rpmUsed = this.rpmBucket.capacity - this.rpmBucket.available;\n const tpmUsed = this.tpmBucket.capacity - this.tpmBucket.available;\n\n const historyStats = this.getHistoryStatistics();\n const memoryStats = this.getMemoryMetrics();\n\n return {\n rpm: {\n used: rpmUsed,\n available: this.rpmBucket.available,\n limit: this.rpmBucket.capacity,\n percentage: (rpmUsed / this.rpmBucket.capacity) * 100\n },\n tpm: {\n used: tpmUsed,\n available: this.tpmBucket.available,\n limit: this.tpmBucket.capacity,\n percentage: (tpmUsed / this.tpmBucket.capacity) * 100\n },\n efficiency: this.calculateEfficiency(),\n consumptionHistory: historyStats,\n memory: memoryStats,\n compensation: {\n totalDebt: this.compensationDebt,\n pendingCompensation: this.compensationDebt\n }\n };\n }\n\n getConsumptionHistory(): ConsumptionRecord<TMetadata>[] {\n this.cleanupHistory();\n return [...this.consumptionHistory];\n }\n\n // Synchronous version (backward compatibility)\n reset(): void {\n this.rpmBucket.reset();\n this.tpmBucket.reset();\n this.consumptionHistory = [];\n this.compensationDebt = 0;\n \n if (this.storageEnabled) {\n this.storage.clear().catch(() => {\n // Ignore storage errors\n });\n }\n }\n\n async resetAsync(): Promise<void> {\n return await this.lock.withLock(async () => {\n this.rpmBucket.reset();\n this.tpmBucket.reset();\n this.consumptionHistory = [];\n this.compensationDebt = 0;\n \n if (this.storageEnabled) {\n await this.storage.clear();\n }\n });\n }\n\n setHistoryRetention(ms: number): void {\n if (ms <= 0) {\n th