mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: APIKey列表费用及Token显示不准确的问题,目前显示总数
feat: 增加APIKey过期设置,以及到期续期的能力
This commit is contained in:
@@ -324,11 +324,13 @@ class RedisClient {
|
||||
const allTokens = parseInt(data.totalAllTokens) || parseInt(data.allTokens) || 0;
|
||||
|
||||
const totalFromSeparate = inputTokens + outputTokens;
|
||||
// 计算实际的总tokens(包含所有类型)
|
||||
const actualAllTokens = allTokens || (inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens);
|
||||
|
||||
if (totalFromSeparate === 0 && tokens > 0) {
|
||||
// 旧数据:没有输入输出分离
|
||||
return {
|
||||
tokens,
|
||||
tokens: tokens, // 保持兼容性,但统一使用allTokens
|
||||
inputTokens: Math.round(tokens * 0.3), // 假设30%为输入
|
||||
outputTokens: Math.round(tokens * 0.7), // 假设70%为输出
|
||||
cacheCreateTokens: 0, // 旧数据没有缓存token
|
||||
@@ -337,14 +339,14 @@ class RedisClient {
|
||||
requests
|
||||
};
|
||||
} else {
|
||||
// 新数据或无数据
|
||||
// 新数据或无数据 - 统一使用allTokens作为tokens的值
|
||||
return {
|
||||
tokens,
|
||||
tokens: actualAllTokens, // 统一使用allTokens作为总数
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheCreateTokens,
|
||||
cacheReadTokens,
|
||||
allTokens: allTokens || (inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens), // 计算或使用存储的值
|
||||
allTokens: actualAllTokens,
|
||||
requests
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,77 @@ const router = express.Router();
|
||||
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const apiKeys = await apiKeyService.getAllApiKeys();
|
||||
|
||||
// 为每个API Key计算准确的费用
|
||||
for (const apiKey of apiKeys) {
|
||||
if (apiKey.usage && apiKey.usage.total) {
|
||||
const client = redis.getClientSafe();
|
||||
|
||||
// 使用与展开模型统计相同的数据源
|
||||
// 获取所有时间的模型统计数据
|
||||
const monthlyKeys = await client.keys(`usage:${apiKey.id}:model:monthly:*:*`);
|
||||
const modelStatsMap = new Map();
|
||||
|
||||
// 汇总所有月份的数据
|
||||
for (const key of monthlyKeys) {
|
||||
const match = key.match(/usage:.+:model:monthly:(.+):\d{4}-\d{2}$/);
|
||||
if (!match) continue;
|
||||
|
||||
const model = match[1];
|
||||
const data = await client.hgetall(key);
|
||||
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
if (!modelStatsMap.has(model)) {
|
||||
modelStatsMap.set(model, {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreateTokens: 0,
|
||||
cacheReadTokens: 0
|
||||
});
|
||||
}
|
||||
|
||||
const stats = modelStatsMap.get(model);
|
||||
stats.inputTokens += parseInt(data.inputTokens) || 0;
|
||||
stats.outputTokens += parseInt(data.outputTokens) || 0;
|
||||
stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
|
||||
stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
let totalCost = 0;
|
||||
|
||||
// 计算每个模型的费用
|
||||
for (const [model, stats] of modelStatsMap) {
|
||||
const usage = {
|
||||
input_tokens: stats.inputTokens,
|
||||
output_tokens: stats.outputTokens,
|
||||
cache_creation_input_tokens: stats.cacheCreateTokens,
|
||||
cache_read_input_tokens: stats.cacheReadTokens
|
||||
};
|
||||
|
||||
const costResult = CostCalculator.calculateCost(usage, model);
|
||||
totalCost += costResult.costs.total;
|
||||
}
|
||||
|
||||
// 如果没有详细的模型数据,使用总量数据和默认模型计算
|
||||
if (modelStatsMap.size === 0) {
|
||||
const usage = {
|
||||
input_tokens: apiKey.usage.total.inputTokens || 0,
|
||||
output_tokens: apiKey.usage.total.outputTokens || 0,
|
||||
cache_creation_input_tokens: apiKey.usage.total.cacheCreateTokens || 0,
|
||||
cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0
|
||||
};
|
||||
|
||||
const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022');
|
||||
totalCost = costResult.costs.total;
|
||||
}
|
||||
|
||||
// 添加格式化的费用到响应数据
|
||||
apiKey.usage.total.cost = totalCost;
|
||||
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: apiKeys });
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get API keys:', error);
|
||||
@@ -112,7 +183,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyId } = req.params;
|
||||
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels } = req.body;
|
||||
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, expiresAt } = req.body;
|
||||
|
||||
// 只允许更新指定字段
|
||||
const updates = {};
|
||||
@@ -178,6 +249,21 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.restrictedModels = restrictedModels;
|
||||
}
|
||||
|
||||
// 处理过期时间字段
|
||||
if (expiresAt !== undefined) {
|
||||
if (expiresAt === null) {
|
||||
// null 表示永不过期
|
||||
updates.expiresAt = null;
|
||||
} else {
|
||||
// 验证日期格式
|
||||
const expireDate = new Date(expiresAt);
|
||||
if (isNaN(expireDate.getTime())) {
|
||||
return res.status(400).json({ error: 'Invalid expiration date format' });
|
||||
}
|
||||
updates.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
await apiKeyService.updateApiKey(keyId, updates);
|
||||
|
||||
logger.success(`📝 Admin updated API key: ${keyId}`);
|
||||
@@ -582,8 +668,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
||||
redis.getSystemAverages()
|
||||
]);
|
||||
|
||||
// 计算使用统计(包含cache tokens)
|
||||
const totalTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.tokens || 0), 0);
|
||||
// 计算使用统计(统一使用allTokens)
|
||||
const totalTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.allTokens || 0), 0);
|
||||
const totalRequestsUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.requests || 0), 0);
|
||||
const totalInputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.inputTokens || 0), 0);
|
||||
const totalOutputTokensUsed = apiKeys.reduce((sum, key) => sum + (key.usage?.total?.outputTokens || 0), 0);
|
||||
|
||||
@@ -283,14 +283,17 @@ class ApiKeyService {
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const key of apiKeys) {
|
||||
if (key.expiresAt && new Date(key.expiresAt) < now) {
|
||||
await redis.deleteApiKey(key.id);
|
||||
// 检查是否已过期且仍处于激活状态
|
||||
if (key.expiresAt && new Date(key.expiresAt) < now && key.isActive === 'true') {
|
||||
// 将过期的 API Key 标记为禁用状态,而不是直接删除
|
||||
await this.updateApiKey(key.id, { isActive: false });
|
||||
logger.info(`🔒 API Key ${key.id} (${key.name}) has expired and been disabled`);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
logger.success(`🧹 Cleaned up ${cleanedCount} expired API keys`);
|
||||
logger.success(`🧹 Disabled ${cleanedCount} expired API keys`);
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
|
||||
Reference in New Issue
Block a user