mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
添加claude账号维度计算token费用
This commit is contained in:
@@ -282,6 +282,104 @@ class RedisClient {
|
||||
]);
|
||||
}
|
||||
|
||||
// 📊 记录账户级别的使用统计
|
||||
async incrementAccountUsage(accountId, totalTokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
|
||||
const now = new Date();
|
||||
const today = getDateStringInTimezone(now);
|
||||
const tzDate = getDateInTimezone(now);
|
||||
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
|
||||
const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`;
|
||||
|
||||
// 账户级别统计的键
|
||||
const accountKey = `account_usage:${accountId}`;
|
||||
const accountDaily = `account_usage:daily:${accountId}:${today}`;
|
||||
const accountMonthly = `account_usage:monthly:${accountId}:${currentMonth}`;
|
||||
const accountHourly = `account_usage:hourly:${accountId}:${currentHour}`;
|
||||
|
||||
// 账户按模型统计的键
|
||||
const accountModelDaily = `account_usage:model:daily:${accountId}:${model}:${today}`;
|
||||
const accountModelMonthly = `account_usage:model:monthly:${accountId}:${model}:${currentMonth}`;
|
||||
const accountModelHourly = `account_usage:model:hourly:${accountId}:${model}:${currentHour}`;
|
||||
|
||||
// 处理token分配
|
||||
const finalInputTokens = inputTokens || 0;
|
||||
const finalOutputTokens = outputTokens || 0;
|
||||
const finalCacheCreateTokens = cacheCreateTokens || 0;
|
||||
const finalCacheReadTokens = cacheReadTokens || 0;
|
||||
const actualTotalTokens = finalInputTokens + finalOutputTokens + finalCacheCreateTokens + finalCacheReadTokens;
|
||||
const coreTokens = finalInputTokens + finalOutputTokens;
|
||||
|
||||
await Promise.all([
|
||||
// 账户总体统计
|
||||
this.client.hincrby(accountKey, 'totalTokens', coreTokens),
|
||||
this.client.hincrby(accountKey, 'totalInputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountKey, 'totalOutputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountKey, 'totalCacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountKey, 'totalCacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountKey, 'totalAllTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountKey, 'totalRequests', 1),
|
||||
|
||||
// 账户每日统计
|
||||
this.client.hincrby(accountDaily, 'tokens', coreTokens),
|
||||
this.client.hincrby(accountDaily, 'inputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountDaily, 'outputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountDaily, 'cacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountDaily, 'cacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountDaily, 'allTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountDaily, 'requests', 1),
|
||||
|
||||
// 账户每月统计
|
||||
this.client.hincrby(accountMonthly, 'tokens', coreTokens),
|
||||
this.client.hincrby(accountMonthly, 'inputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountMonthly, 'outputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountMonthly, 'cacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountMonthly, 'allTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountMonthly, 'requests', 1),
|
||||
|
||||
// 账户每小时统计
|
||||
this.client.hincrby(accountHourly, 'tokens', coreTokens),
|
||||
this.client.hincrby(accountHourly, 'inputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountHourly, 'outputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountHourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountHourly, 'cacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountHourly, 'allTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountHourly, 'requests', 1),
|
||||
|
||||
// 账户按模型统计 - 每日
|
||||
this.client.hincrby(accountModelDaily, 'inputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountModelDaily, 'outputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountModelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountModelDaily, 'cacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountModelDaily, 'allTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountModelDaily, 'requests', 1),
|
||||
|
||||
// 账户按模型统计 - 每月
|
||||
this.client.hincrby(accountModelMonthly, 'inputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountModelMonthly, 'outputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountModelMonthly, 'cacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountModelMonthly, 'allTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountModelMonthly, 'requests', 1),
|
||||
|
||||
// 账户按模型统计 - 每小时
|
||||
this.client.hincrby(accountModelHourly, 'inputTokens', finalInputTokens),
|
||||
this.client.hincrby(accountModelHourly, 'outputTokens', finalOutputTokens),
|
||||
this.client.hincrby(accountModelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
||||
this.client.hincrby(accountModelHourly, 'cacheReadTokens', finalCacheReadTokens),
|
||||
this.client.hincrby(accountModelHourly, 'allTokens', actualTotalTokens),
|
||||
this.client.hincrby(accountModelHourly, 'requests', 1),
|
||||
|
||||
// 设置过期时间
|
||||
this.client.expire(accountDaily, 86400 * 32), // 32天过期
|
||||
this.client.expire(accountMonthly, 86400 * 365), // 1年过期
|
||||
this.client.expire(accountHourly, 86400 * 7), // 7天过期
|
||||
this.client.expire(accountModelDaily, 86400 * 32), // 32天过期
|
||||
this.client.expire(accountModelMonthly, 86400 * 365), // 1年过期
|
||||
this.client.expire(accountModelHourly, 86400 * 7) // 7天过期
|
||||
]);
|
||||
}
|
||||
|
||||
async getUsageStats(keyId) {
|
||||
const totalKey = `usage:${keyId}`;
|
||||
const today = getDateStringInTimezone();
|
||||
@@ -369,6 +467,110 @@ class RedisClient {
|
||||
};
|
||||
}
|
||||
|
||||
// 📊 获取账户使用统计
|
||||
async getAccountUsageStats(accountId) {
|
||||
const accountKey = `account_usage:${accountId}`;
|
||||
const today = getDateStringInTimezone();
|
||||
const accountDailyKey = `account_usage:daily:${accountId}:${today}`;
|
||||
const tzDate = getDateInTimezone();
|
||||
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
|
||||
const accountMonthlyKey = `account_usage:monthly:${accountId}:${currentMonth}`;
|
||||
|
||||
const [total, daily, monthly] = await Promise.all([
|
||||
this.client.hgetall(accountKey),
|
||||
this.client.hgetall(accountDailyKey),
|
||||
this.client.hgetall(accountMonthlyKey)
|
||||
]);
|
||||
|
||||
// 获取账户创建时间来计算平均值
|
||||
const accountData = await this.client.hgetall(`claude_account:${accountId}`);
|
||||
const createdAt = accountData.createdAt ? new Date(accountData.createdAt) : new Date();
|
||||
const now = new Date();
|
||||
const daysSinceCreated = Math.max(1, Math.ceil((now - createdAt) / (1000 * 60 * 60 * 24)));
|
||||
|
||||
const totalTokens = parseInt(total.totalTokens) || 0;
|
||||
const totalRequests = parseInt(total.totalRequests) || 0;
|
||||
|
||||
// 计算平均RPM和TPM
|
||||
const totalMinutes = Math.max(1, daysSinceCreated * 24 * 60);
|
||||
const avgRPM = totalRequests / totalMinutes;
|
||||
const avgTPM = totalTokens / totalMinutes;
|
||||
|
||||
// 处理账户统计数据
|
||||
const handleAccountData = (data) => {
|
||||
const tokens = parseInt(data.totalTokens) || parseInt(data.tokens) || 0;
|
||||
const inputTokens = parseInt(data.totalInputTokens) || parseInt(data.inputTokens) || 0;
|
||||
const outputTokens = parseInt(data.totalOutputTokens) || parseInt(data.outputTokens) || 0;
|
||||
const requests = parseInt(data.totalRequests) || parseInt(data.requests) || 0;
|
||||
const cacheCreateTokens = parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0;
|
||||
const cacheReadTokens = parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0;
|
||||
const allTokens = parseInt(data.totalAllTokens) || parseInt(data.allTokens) || 0;
|
||||
|
||||
const actualAllTokens = allTokens || (inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens);
|
||||
|
||||
return {
|
||||
tokens: tokens,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheCreateTokens: cacheCreateTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
allTokens: actualAllTokens,
|
||||
requests: requests
|
||||
};
|
||||
};
|
||||
|
||||
const totalData = handleAccountData(total);
|
||||
const dailyData = handleAccountData(daily);
|
||||
const monthlyData = handleAccountData(monthly);
|
||||
|
||||
return {
|
||||
accountId: accountId,
|
||||
total: totalData,
|
||||
daily: dailyData,
|
||||
monthly: monthlyData,
|
||||
averages: {
|
||||
rpm: Math.round(avgRPM * 100) / 100,
|
||||
tpm: Math.round(avgTPM * 100) / 100,
|
||||
dailyRequests: Math.round((totalRequests / daysSinceCreated) * 100) / 100,
|
||||
dailyTokens: Math.round((totalTokens / daysSinceCreated) * 100) / 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 📈 获取所有账户的使用统计
|
||||
async getAllAccountsUsageStats() {
|
||||
try {
|
||||
// 获取所有Claude账户
|
||||
const accountKeys = await this.client.keys('claude_account:*');
|
||||
const accountStats = [];
|
||||
|
||||
for (const accountKey of accountKeys) {
|
||||
const accountId = accountKey.replace('claude_account:', '');
|
||||
const accountData = await this.client.hgetall(accountKey);
|
||||
|
||||
if (accountData.name) {
|
||||
const stats = await this.getAccountUsageStats(accountId);
|
||||
accountStats.push({
|
||||
id: accountId,
|
||||
name: accountData.name,
|
||||
email: accountData.email || '',
|
||||
status: accountData.status || 'unknown',
|
||||
isActive: accountData.isActive === 'true',
|
||||
...stats
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按当日token使用量排序
|
||||
accountStats.sort((a, b) => (b.daily.allTokens || 0) - (a.daily.allTokens || 0));
|
||||
|
||||
return accountStats;
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get all accounts usage stats:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 🧹 清空所有API Key的使用统计数据
|
||||
async resetAllUsageStats() {
|
||||
const client = this.getClientSafe();
|
||||
|
||||
@@ -791,6 +791,73 @@ router.post('/gemini-accounts/:accountId/refresh', authenticateAdmin, async (req
|
||||
}
|
||||
});
|
||||
|
||||
// 📊 账户使用统计
|
||||
|
||||
// 获取所有账户的使用统计
|
||||
router.get('/accounts/usage-stats', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const accountsStats = await redis.getAllAccountsUsageStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: accountsStats,
|
||||
summary: {
|
||||
totalAccounts: accountsStats.length,
|
||||
activeToday: accountsStats.filter(account => account.daily.requests > 0).length,
|
||||
totalDailyTokens: accountsStats.reduce((sum, account) => sum + (account.daily.allTokens || 0), 0),
|
||||
totalDailyRequests: accountsStats.reduce((sum, account) => sum + (account.daily.requests || 0), 0)
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get accounts usage stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get accounts usage stats',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个账户的使用统计
|
||||
router.get('/accounts/:accountId/usage-stats', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
const accountStats = await redis.getAccountUsageStats(accountId);
|
||||
|
||||
// 获取账户基本信息
|
||||
const accountData = await claudeAccountService.getAccount(accountId);
|
||||
if (!accountData) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Account not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...accountStats,
|
||||
accountInfo: {
|
||||
name: accountData.name,
|
||||
email: accountData.email,
|
||||
status: accountData.status,
|
||||
isActive: accountData.isActive,
|
||||
createdAt: accountData.createdAt
|
||||
}
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get account usage stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get account usage stats',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 📊 系统统计
|
||||
|
||||
// 获取系统概览
|
||||
|
||||
@@ -68,8 +68,9 @@ async function handleMessagesRequest(req, res) {
|
||||
const cacheReadTokens = usageData.cache_read_input_tokens || 0;
|
||||
const model = usageData.model || 'unknown';
|
||||
|
||||
// 记录真实的token使用量(包含模型信息和所有4种token)
|
||||
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model).catch(error => {
|
||||
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
||||
const accountId = usageData.accountId;
|
||||
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model, accountId).catch(error => {
|
||||
logger.error('❌ Failed to record stream usage:', error);
|
||||
});
|
||||
|
||||
@@ -135,8 +136,9 @@ async function handleMessagesRequest(req, res) {
|
||||
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0;
|
||||
const model = jsonData.model || req.body.model || 'unknown';
|
||||
|
||||
// 记录真实的token使用量(包含模型信息和所有4种token)
|
||||
await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
|
||||
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
||||
const accountId = response.accountId;
|
||||
await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model, accountId);
|
||||
|
||||
// 更新时间窗口内的token计数
|
||||
if (req.rateLimitInfo) {
|
||||
|
||||
@@ -234,18 +234,27 @@ class ApiKeyService {
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 记录使用情况(支持缓存token)
|
||||
async recordUsage(keyId, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
|
||||
// 📊 记录使用情况(支持缓存token和账户级别统计)
|
||||
async recordUsage(keyId, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown', accountId = null) {
|
||||
try {
|
||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||
|
||||
// 记录API Key级别的使用统计
|
||||
await redis.incrementTokenUsage(keyId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
|
||||
|
||||
// 更新最后使用时间(性能优化:只在实际使用时更新)
|
||||
// 获取API Key数据以确定关联的账户
|
||||
const keyData = await redis.getApiKey(keyId);
|
||||
if (keyData && Object.keys(keyData).length > 0) {
|
||||
// 更新最后使用时间
|
||||
keyData.lastUsedAt = new Date().toISOString();
|
||||
// 使用记录时不需要重新建立哈希映射
|
||||
await redis.setApiKey(keyId, keyData);
|
||||
|
||||
// 记录账户级别的使用统计
|
||||
const claudeAccountId = accountId || keyData.claudeAccountId;
|
||||
if (claudeAccountId) {
|
||||
await redis.incrementAccountUsage(claudeAccountId, totalTokens, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
|
||||
logger.database(`📊 Recorded account usage: ${claudeAccountId} - ${totalTokens} tokens`);
|
||||
}
|
||||
}
|
||||
|
||||
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`];
|
||||
@@ -274,6 +283,16 @@ class ApiKeyService {
|
||||
return await redis.getUsageStats(keyId);
|
||||
}
|
||||
|
||||
// 📊 获取账户使用统计
|
||||
async getAccountUsageStats(accountId) {
|
||||
return await redis.getAccountUsageStats(accountId);
|
||||
}
|
||||
|
||||
// 📈 获取所有账户使用统计
|
||||
async getAllAccountsUsageStats() {
|
||||
return await redis.getAllAccountsUsageStats();
|
||||
}
|
||||
|
||||
|
||||
// 🧹 清理过期的API Keys
|
||||
async cleanupExpiredKeys() {
|
||||
|
||||
@@ -181,6 +181,8 @@ class ClaudeRelayService {
|
||||
|
||||
logger.info(`✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens, Output: ~${Math.round(outputTokens)} tokens`);
|
||||
|
||||
// 在响应中添加accountId,以便调用方记录账户级别统计
|
||||
response.accountId = accountId;
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error(`❌ Claude relay request failed for key: ${apiKeyData.name || apiKeyData.id}:`, error.message);
|
||||
@@ -619,7 +621,10 @@ class ClaudeRelayService {
|
||||
const proxyAgent = await this._getProxyAgent(accountId);
|
||||
|
||||
// 发送流式请求并捕获usage数据
|
||||
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer, options);
|
||||
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, (usageData) => {
|
||||
// 在usageCallback中添加accountId
|
||||
usageCallback({ ...usageData, accountId });
|
||||
}, accountId, sessionHash, streamTransformer, options);
|
||||
} catch (error) {
|
||||
logger.error('❌ Claude stream relay with usage capture failed:', error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user