| const redis = require('../models/redis') |
| const apiKeyService = require('./apiKeyService') |
| const CostCalculator = require('../utils/costCalculator') |
| const logger = require('../utils/logger') |
|
|
| class CostInitService { |
| |
| |
| |
| |
| async initializeAllCosts() { |
| try { |
| logger.info('💰 Starting cost initialization for all API Keys...') |
|
|
| const apiKeys = await apiKeyService.getAllApiKeys() |
| const client = redis.getClientSafe() |
|
|
| let processedCount = 0 |
| let errorCount = 0 |
|
|
| for (const apiKey of apiKeys) { |
| try { |
| await this.initializeApiKeyCosts(apiKey.id, client) |
| processedCount++ |
|
|
| if (processedCount % 10 === 0) { |
| logger.info(`💰 Processed ${processedCount} API Keys...`) |
| } |
| } catch (error) { |
| errorCount++ |
| logger.error(`❌ Failed to initialize costs for API Key ${apiKey.id}:`, error) |
| } |
| } |
|
|
| logger.success( |
| `💰 Cost initialization completed! Processed: ${processedCount}, Errors: ${errorCount}` |
| ) |
| return { processed: processedCount, errors: errorCount } |
| } catch (error) { |
| logger.error('❌ Failed to initialize costs:', error) |
| throw error |
| } |
| } |
|
|
| |
| |
| |
| async initializeApiKeyCosts(apiKeyId, client) { |
| |
| const modelKeys = await client.keys(`usage:${apiKeyId}:model:*:*:*`) |
|
|
| |
| const dailyCosts = new Map() |
| const monthlyCosts = new Map() |
| const hourlyCosts = new Map() |
|
|
| for (const key of modelKeys) { |
| |
| const match = key.match( |
| /usage:(.+):model:(daily|monthly|hourly):(.+):(\d{4}-\d{2}(?:-\d{2})?(?::\d{2})?)$/ |
| ) |
| if (!match) { |
| continue |
| } |
|
|
| const [, , period, model, dateStr] = match |
|
|
| |
| const data = await client.hgetall(key) |
| if (!data || Object.keys(data).length === 0) { |
| continue |
| } |
|
|
| |
| const usage = { |
| input_tokens: parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0, |
| output_tokens: parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0, |
| cache_creation_input_tokens: |
| parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0, |
| cache_read_input_tokens: |
| parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0 |
| } |
|
|
| const costResult = CostCalculator.calculateCost(usage, model) |
| const cost = costResult.costs.total |
|
|
| |
| if (period === 'daily') { |
| const currentCost = dailyCosts.get(dateStr) || 0 |
| dailyCosts.set(dateStr, currentCost + cost) |
| } else if (period === 'monthly') { |
| const currentCost = monthlyCosts.get(dateStr) || 0 |
| monthlyCosts.set(dateStr, currentCost + cost) |
| } else if (period === 'hourly') { |
| const currentCost = hourlyCosts.get(dateStr) || 0 |
| hourlyCosts.set(dateStr, currentCost + cost) |
| } |
| } |
|
|
| |
| const promises = [] |
|
|
| |
| for (const [date, cost] of dailyCosts) { |
| const key = `usage:cost:daily:${apiKeyId}:${date}` |
| promises.push( |
| client.set(key, cost.toString()), |
| client.expire(key, 86400 * 30) |
| ) |
| } |
|
|
| |
| for (const [month, cost] of monthlyCosts) { |
| const key = `usage:cost:monthly:${apiKeyId}:${month}` |
| promises.push( |
| client.set(key, cost.toString()), |
| client.expire(key, 86400 * 90) |
| ) |
| } |
|
|
| |
| for (const [hour, cost] of hourlyCosts) { |
| const key = `usage:cost:hourly:${apiKeyId}:${hour}` |
| promises.push( |
| client.set(key, cost.toString()), |
| client.expire(key, 86400 * 7) |
| ) |
| } |
|
|
| |
| let totalCost = 0 |
| for (const cost of dailyCosts.values()) { |
| totalCost += cost |
| } |
|
|
| |
| if (totalCost > 0) { |
| const totalKey = `usage:cost:total:${apiKeyId}` |
| promises.push(client.set(totalKey, totalCost.toString())) |
| } |
|
|
| await Promise.all(promises) |
|
|
| logger.debug( |
| `💰 Initialized costs for API Key ${apiKeyId}: Daily entries: ${dailyCosts.size}, Total cost: $${totalCost.toFixed(2)}` |
| ) |
| } |
|
|
| |
| |
| |
| async needsInitialization() { |
| try { |
| const client = redis.getClientSafe() |
|
|
| |
| const costKeys = await client.keys('usage:cost:*') |
|
|
| |
| if (costKeys.length === 0) { |
| logger.info('💰 No cost data found, initialization needed') |
| return true |
| } |
|
|
| |
| const sampleKeys = await client.keys('usage:*:model:daily:*:*') |
| if (sampleKeys.length > 10) { |
| |
| const sampleSize = Math.min(10, sampleKeys.length) |
| for (let i = 0; i < sampleSize; i++) { |
| const usageKey = sampleKeys[Math.floor(Math.random() * sampleKeys.length)] |
| const match = usageKey.match(/usage:(.+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/) |
| if (match) { |
| const [, keyId, , date] = match |
| const costKey = `usage:cost:daily:${keyId}:${date}` |
| const hasCost = await client.exists(costKey) |
| if (!hasCost) { |
| logger.info( |
| `💰 Found usage without cost data for key ${keyId} on ${date}, initialization needed` |
| ) |
| return true |
| } |
| } |
| } |
| } |
|
|
| logger.info('💰 Cost data appears to be up to date') |
| return false |
| } catch (error) { |
| logger.error('❌ Failed to check initialization status:', error) |
| return false |
| } |
| } |
| } |
|
|
| module.exports = new CostInitService() |
|
|