feat(api-potluck): 添加缓存 tokens 统计支持
- 在 Claude 转换器中增加缓存 tokens 字段提取 - 扩展 API 路由返回缓存 tokens 数据 - 在 key-manager 中新增缓存 tokens 统计逻辑 - 更新前端页面展示缓存 tokens 统计信息 - 统一所有相关模块的缓存 tokens 处理逻辑
This commit is contained in:
parent
51c0af2f15
commit
1a56e422c4
8 changed files with 73 additions and 22 deletions
|
|
@ -315,6 +315,10 @@ export class ClaudeConverter extends BaseConverter {
|
|||
prompt_tokens: claudeResponse.usage?.input_tokens || 0,
|
||||
completion_tokens: claudeResponse.usage?.output_tokens || 0,
|
||||
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0),
|
||||
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -423,13 +423,15 @@ export async function handlePotluckUserApiRoutes(method, path, req, res) {
|
|||
resetDate: keyData.lastResetDate,
|
||||
promptTokens: keyData.todayPromptTokens || 0,
|
||||
completionTokens: keyData.todayCompletionTokens || 0,
|
||||
totalTokens: keyData.todayTotalTokens || 0
|
||||
totalTokens: keyData.todayTotalTokens || 0,
|
||||
cachedTokens: keyData.todayCachedTokens || 0
|
||||
},
|
||||
total: keyData.totalUsage,
|
||||
tokens: {
|
||||
prompt: keyData.totalPromptTokens || 0,
|
||||
completion: keyData.totalCompletionTokens || 0,
|
||||
total: keyData.totalTokens || 0
|
||||
total: keyData.totalTokens || 0,
|
||||
cached: keyData.totalCachedTokens || 0
|
||||
},
|
||||
lastUsedAt: keyData.lastUsedAt,
|
||||
createdAt: keyData.createdAt,
|
||||
|
|
|
|||
|
|
@ -65,12 +65,20 @@ function normalizeUsageCandidate(candidate) {
|
|||
candidate.total_tokens ??
|
||||
usage?.total_tokens ??
|
||||
usage?.totalTokenCount
|
||||
) || promptTokens + completionTokens;
|
||||
);
|
||||
|
||||
const cachedTokens = toNumber(
|
||||
candidate.cached_tokens ??
|
||||
usage?.cached_tokens ??
|
||||
usage?.cache_read_input_tokens ??
|
||||
usage?.cachedContentTokenCount
|
||||
);
|
||||
|
||||
return {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens
|
||||
totalTokens: totalTokens || (promptTokens + completionTokens),
|
||||
cachedTokens
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +87,8 @@ function mergeUsage(baseUsage, nextUsage) {
|
|||
return {
|
||||
promptTokens: Math.max(baseUsage.promptTokens, nextUsage.promptTokens),
|
||||
completionTokens: Math.max(baseUsage.completionTokens, nextUsage.completionTokens),
|
||||
totalTokens: Math.max(baseUsage.totalTokens, nextUsage.totalTokens)
|
||||
totalTokens: Math.max(baseUsage.totalTokens, nextUsage.totalTokens),
|
||||
cachedTokens: Math.max(baseUsage.cachedTokens || 0, nextUsage.cachedTokens || 0)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +96,8 @@ function extractUsage(...candidates) {
|
|||
return candidates.reduce((usage, candidate) => mergeUsage(usage, normalizeUsageCandidate(candidate)), {
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0
|
||||
totalTokens: 0,
|
||||
cachedTokens: 0
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ function createUsageBucket() {
|
|||
requestCount: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0
|
||||
totalTokens: 0,
|
||||
cachedTokens: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +76,8 @@ function normalizeUsageBucket(bucket) {
|
|||
requestCount: toNumber(bucket?.requestCount),
|
||||
promptTokens: toNumber(bucket?.promptTokens),
|
||||
completionTokens: toNumber(bucket?.completionTokens),
|
||||
totalTokens: toNumber(bucket?.totalTokens)
|
||||
totalTokens: toNumber(bucket?.totalTokens),
|
||||
cachedTokens: toNumber(bucket?.cachedTokens)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -105,9 +107,11 @@ function normalizeKeyData(keyData = {}) {
|
|||
todayPromptTokens: toNumber(keyData.todayPromptTokens),
|
||||
todayCompletionTokens: toNumber(keyData.todayCompletionTokens),
|
||||
todayTotalTokens: toNumber(keyData.todayTotalTokens),
|
||||
todayCachedTokens: toNumber(keyData.todayCachedTokens),
|
||||
totalPromptTokens: toNumber(keyData.totalPromptTokens),
|
||||
totalCompletionTokens: toNumber(keyData.totalCompletionTokens),
|
||||
totalTokens: toNumber(keyData.totalTokens),
|
||||
totalCachedTokens: toNumber(keyData.totalCachedTokens),
|
||||
usageHistory: {}
|
||||
};
|
||||
|
||||
|
|
@ -131,6 +135,7 @@ function addUsage(target, usage = {}) {
|
|||
target.promptTokens += toNumber(usage.promptTokens);
|
||||
target.completionTokens += toNumber(usage.completionTokens);
|
||||
target.totalTokens += toNumber(usage.totalTokens);
|
||||
target.cachedTokens += toNumber(usage.cachedTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,7 +177,7 @@ function syncWriteToFile() {
|
|||
try {
|
||||
const dir = path.dirname(KEYS_STORE_FILE);
|
||||
if (!existsSync(dir)) {
|
||||
require('fs').mkdirSync(dir, { recursive: true });
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8');
|
||||
} catch (error) {
|
||||
|
|
@ -248,6 +253,7 @@ function checkAndResetDailyCount(keyData) {
|
|||
keyData.todayPromptTokens = 0;
|
||||
keyData.todayCompletionTokens = 0;
|
||||
keyData.todayTotalTokens = 0;
|
||||
keyData.todayCachedTokens = 0;
|
||||
keyData.lastResetDate = today;
|
||||
}
|
||||
return keyData;
|
||||
|
|
@ -255,7 +261,6 @@ function checkAndResetDailyCount(keyData) {
|
|||
|
||||
/**
|
||||
* 创建新的 API Key
|
||||
|
||||
* @param {string} name - Key 名称
|
||||
* @param {number} [dailyLimit] - 每日限额,不传则使用配置的默认值
|
||||
*/
|
||||
|
|
@ -278,9 +283,11 @@ export async function createKey(name = '', dailyLimit = null) {
|
|||
todayPromptTokens: 0,
|
||||
todayCompletionTokens: 0,
|
||||
todayTotalTokens: 0,
|
||||
todayCachedTokens: 0,
|
||||
totalPromptTokens: 0,
|
||||
totalCompletionTokens: 0,
|
||||
totalTokens: 0,
|
||||
totalCachedTokens: 0,
|
||||
lastResetDate: today,
|
||||
lastUsedAt: null,
|
||||
enabled: true
|
||||
|
|
@ -354,6 +361,7 @@ export async function resetKeyUsage(keyId) {
|
|||
keyStore.keys[keyId].todayPromptTokens = 0;
|
||||
keyStore.keys[keyId].todayCompletionTokens = 0;
|
||||
keyStore.keys[keyId].todayTotalTokens = 0;
|
||||
keyStore.keys[keyId].todayCachedTokens = 0;
|
||||
keyStore.keys[keyId].lastResetDate = getTodayDateString();
|
||||
if (!keyStore.keys[keyId].usageHistory) keyStore.keys[keyId].usageHistory = {};
|
||||
keyStore.keys[keyId].usageHistory[getTodayDateString()] = normalizeUsageHistoryDay();
|
||||
|
|
@ -448,7 +456,7 @@ export async function validateKey(apiKey) {
|
|||
* @param {string} apiKey - API Key
|
||||
* @param {string} provider - 使用的提供商
|
||||
* @param {string} model - 使用的模型
|
||||
* @param {{promptTokens?: number, completionTokens?: number, totalTokens?: number}} usage - token 用量
|
||||
* @param {{promptTokens?: number, completionTokens?: number, totalTokens?: number, cachedTokens?: number}} usage - token 用量
|
||||
*/
|
||||
export async function incrementUsage(apiKey, provider = 'unknown', model = 'unknown', usage = {}) {
|
||||
ensureLoaded();
|
||||
|
|
@ -469,9 +477,11 @@ export async function incrementUsage(apiKey, provider = 'unknown', model = 'unkn
|
|||
keyData.todayPromptTokens += toNumber(usage.promptTokens);
|
||||
keyData.todayCompletionTokens += toNumber(usage.completionTokens);
|
||||
keyData.todayTotalTokens += toNumber(usage.totalTokens);
|
||||
keyData.todayCachedTokens += toNumber(usage.cachedTokens);
|
||||
keyData.totalPromptTokens += toNumber(usage.promptTokens);
|
||||
keyData.totalCompletionTokens += toNumber(usage.completionTokens);
|
||||
keyData.totalTokens += toNumber(usage.totalTokens);
|
||||
keyData.totalCachedTokens += toNumber(usage.cachedTokens);
|
||||
keyData.lastUsedAt = new Date().toISOString();
|
||||
|
||||
// 记录个人按天统计 (每个 Key 独立)
|
||||
|
|
@ -514,8 +524,8 @@ export async function getStats() {
|
|||
ensureLoaded();
|
||||
const keys = Object.values(keyStore.keys);
|
||||
let enabledKeys = 0, todayTotalUsage = 0, totalUsage = 0;
|
||||
let todayPromptTokens = 0, todayCompletionTokens = 0, todayTotalTokens = 0;
|
||||
let totalPromptTokens = 0, totalCompletionTokens = 0, totalTokens = 0;
|
||||
let todayPromptTokens = 0, todayCompletionTokens = 0, todayTotalTokens = 0, todayCachedTokens = 0;
|
||||
let totalPromptTokens = 0, totalCompletionTokens = 0, totalTokens = 0, totalCachedTokens = 0;
|
||||
const aggregatedHistory = {};
|
||||
|
||||
for (const key of keys) {
|
||||
|
|
@ -526,9 +536,11 @@ export async function getStats() {
|
|||
todayPromptTokens += key.todayPromptTokens || 0;
|
||||
todayCompletionTokens += key.todayCompletionTokens || 0;
|
||||
todayTotalTokens += key.todayTotalTokens || 0;
|
||||
todayCachedTokens += key.todayCachedTokens || 0;
|
||||
totalPromptTokens += key.totalPromptTokens || 0;
|
||||
totalCompletionTokens += key.totalCompletionTokens || 0;
|
||||
totalTokens += key.totalTokens || 0;
|
||||
totalCachedTokens += key.totalCachedTokens || 0;
|
||||
|
||||
// 汇总每个 Key 的历史数据
|
||||
if (key.usageHistory) {
|
||||
|
|
@ -566,9 +578,11 @@ export async function getStats() {
|
|||
todayPromptTokens,
|
||||
todayCompletionTokens,
|
||||
todayTotalTokens,
|
||||
todayCachedTokens,
|
||||
totalPromptTokens,
|
||||
totalCompletionTokens,
|
||||
totalTokens,
|
||||
totalCachedTokens,
|
||||
usageHistory: aggregatedHistory
|
||||
};
|
||||
}
|
||||
|
|
@ -576,7 +590,7 @@ export async function getStats() {
|
|||
|
||||
/**
|
||||
* 批量更新所有 Key 的每日限额
|
||||
* @param {number} newLimit - 新的每日限额
|
||||
* @param {number} newLimit - 新s的每日限额
|
||||
* @returns {Promise<{total: number, updated: number}>}
|
||||
*/
|
||||
export async function applyDailyLimitToAllKeys(newLimit) {
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ function normalizeUsageCandidate(candidate) {
|
|||
return {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens: totalTokens || promptTokens + completionTokens,
|
||||
totalTokens: totalTokens || (promptTokens + completionTokens),
|
||||
cachedTokens
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -414,10 +414,18 @@
|
|||
<div class="label">今日 Tokens</div>
|
||||
<div class="value" id="statTodayTokens">0</div>
|
||||
</div>
|
||||
<div class="stat-card cyan">
|
||||
<div class="label">今日缓存 Tokens</div>
|
||||
<div class="value" id="statTodayCachedTokens">0</div>
|
||||
</div>
|
||||
<div class="stat-card purple">
|
||||
<div class="label">累计 Tokens</div>
|
||||
<div class="value" id="statTotalTokens">0</div>
|
||||
</div>
|
||||
<div class="stat-card cyan">
|
||||
<div class="label">累计缓存 Tokens</div>
|
||||
<div class="value" id="statTotalCachedTokens">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的使用分布统计区域 -->
|
||||
|
|
@ -596,7 +604,9 @@
|
|||
|
||||
// Token 用量
|
||||
document.getElementById('statTodayTokens').textContent = formatTokenCompact(data.usage?.totalTokens || 0);
|
||||
document.getElementById('statTodayCachedTokens').textContent = formatTokenCompact(data.usage?.cachedTokens || 0);
|
||||
document.getElementById('statTotalTokens').textContent = formatTokenCompact(data.tokens?.total || 0);
|
||||
document.getElementById('statTotalCachedTokens').textContent = formatTokenCompact(data.tokens?.cached || 0);
|
||||
|
||||
// 最后使用时间
|
||||
if (data.lastUsedAt) {
|
||||
|
|
|
|||
|
|
@ -616,10 +616,18 @@
|
|||
<div class="label">今日总 Tokens</div>
|
||||
<div class="value" id="todayTokens">0</div>
|
||||
</div>
|
||||
<div class="stat-card cyan">
|
||||
<div class="label">今日缓存 Tokens</div>
|
||||
<div class="value" id="todayCachedTokens">0</div>
|
||||
</div>
|
||||
<div class="stat-card pink">
|
||||
<div class="label">累计 Tokens</div>
|
||||
<div class="value" id="totalTokens">0</div>
|
||||
</div>
|
||||
<div class="stat-card cyan">
|
||||
<div class="label">累计缓存 Tokens</div>
|
||||
<div class="value" id="totalCachedTokens">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用分布统计区域 -->
|
||||
|
|
@ -835,7 +843,9 @@
|
|||
document.getElementById('todayUsage').textContent = stats.todayTotalUsage;
|
||||
document.getElementById('totalUsage').textContent = stats.totalUsage;
|
||||
document.getElementById('todayTokens').textContent = formatTokenCompact(stats.todayTotalTokens);
|
||||
document.getElementById('todayCachedTokens').textContent = formatTokenCompact(stats.todayCachedTokens || 0);
|
||||
document.getElementById('totalTokens').textContent = formatTokenCompact(stats.totalTokens);
|
||||
document.getElementById('totalCachedTokens').textContent = formatTokenCompact(stats.totalCachedTokens || 0);
|
||||
|
||||
// 渲染使用历史分布
|
||||
renderUsageHistory(stats.usageHistory);
|
||||
|
|
@ -1043,13 +1053,13 @@
|
|||
<div class="key-stat">
|
||||
<div class="label">今日/限额</div>
|
||||
<div class="value ${valueClass}">${key.todayUsage}/${key.dailyLimit}</div>
|
||||
<div class="value muted">${formatTokenCompact(key.todayTotalTokens || 0)} Tokens</div>
|
||||
<div class="value muted">${formatTokenCompact(key.todayTotalTokens || 0)} Tokens ${key.todayCachedTokens ? `(含 ${formatTokenCompact(key.todayCachedTokens)} 缓存)` : ''}</div>
|
||||
<div class="progress-bar"><div class="fill ${progressClass}" style="width:${Math.min(usagePercent, 100)}%"></div></div>
|
||||
</div>
|
||||
<div class="key-stat">
|
||||
<div class="label">累计</div>
|
||||
<div class="value">${key.totalUsage}</div>
|
||||
<div class="value muted">${formatTokenCompact(key.totalTokens || 0)} Tokens</div>
|
||||
<div class="value muted">${formatTokenCompact(key.totalTokens || 0)} Tokens ${key.totalCachedTokens ? `(含 ${formatTokenCompact(key.totalCachedTokens)} 缓存)` : ''}</div>
|
||||
</div>
|
||||
<div class="key-stat">
|
||||
<div class="label">最后调用</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue