diff --git a/VERSION b/VERSION index 14239ef..ea55a03 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.13.6 +2.13.7 diff --git a/src/plugins/api-potluck/api-routes.js b/src/plugins/api-potluck/api-routes.js index 9253e67..1c987ae 100644 --- a/src/plugins/api-potluck/api-routes.js +++ b/src/plugins/api-potluck/api-routes.js @@ -10,6 +10,7 @@ import { deleteKey, updateKeyLimit, resetKeyUsage, + resetKeyTokenStats, toggleKey, updateKeyName, regenerateKey, @@ -17,7 +18,8 @@ import { validateKey, KEY_PREFIX, applyDailyLimitToAllKeys, - getAllKeyIds + getAllKeyIds, + resetAllTokenStats } from './key-manager.js'; import logger from '../../utils/logger.js'; @@ -131,6 +133,18 @@ export async function handlePotluckApiRoutes(method, path, req, res) { return true; } + // POST /api/potluck/stats/reset-tokens - 重置全部 Key 的 Token 统计 + if (method === 'POST' && path === '/api/potluck/stats/reset-tokens') { + const result = await resetAllTokenStats(); + const stats = await getStats(); + sendJson(res, 200, { + success: true, + message: `已重置 ${result.updated}/${result.total} 个 Key 的 Token 统计`, + data: stats + }); + return true; + } + // GET /api/potluck/keys - 获取所有 Key 列表 if (method === 'GET' && path === '/api/potluck/keys') { const keys = await listKeys(); @@ -243,6 +257,21 @@ export async function handlePotluckApiRoutes(method, path, req, res) { return true; } + // POST /api/potluck/keys/:keyId/reset-tokens - 重置 Token 统计 + if (method === 'POST' && subPath === '/reset-tokens') { + const keyData = await resetKeyTokenStats(keyId); + if (!keyData) { + sendJson(res, 404, { success: false, error: { message: '未找到 Key' } }); + return true; + } + sendJson(res, 200, { + success: true, + message: 'Token 统计重置成功', + data: keyData + }); + return true; + } + // POST /api/potluck/keys/:keyId/toggle - 切换启用/禁用状态 if (method === 'POST' && subPath === '/toggle') { const keyData = await toggleKey(keyId); diff --git a/src/plugins/api-potluck/index.js b/src/plugins/api-potluck/index.js index fec5bbe..636cb5f 100644 --- a/src/plugins/api-potluck/index.js +++ b/src/plugins/api-potluck/index.js @@ -15,11 +15,13 @@ import { deleteKey, updateKeyLimit, resetKeyUsage, + resetKeyTokenStats, toggleKey, updateKeyName, validateKey, incrementUsage, getStats, + resetAllTokenStats, KEY_PREFIX, setConfigGetter } from './key-manager.js'; @@ -47,6 +49,13 @@ function normalizeUsageCandidate(candidate) { } const usage = candidate.usage || candidate.message?.usage || candidate.usageMetadata || candidate.response?.usage || null; + const reasoningTokens = toNumber( + candidate.completion_tokens_details?.reasoning_tokens ?? + candidate.output_tokens_details?.reasoning_tokens ?? + usage?.completion_tokens_details?.reasoning_tokens ?? + usage?.output_tokens_details?.reasoning_tokens ?? + usage?.thoughtsTokenCount + ); const promptTokens = toNumber( candidate.prompt_tokens ?? usage?.prompt_tokens ?? @@ -60,7 +69,7 @@ function normalizeUsageCandidate(candidate) { usage?.output_tokens ?? usage?.candidatesTokenCount ?? usage?.outputTokenCount - ); + ) + reasoningTokens; const totalTokens = toNumber( candidate.total_tokens ?? usage?.total_tokens ?? @@ -286,11 +295,13 @@ const apiPotluckPlugin = { deleteKey, updateKeyLimit, resetKeyUsage, + resetKeyTokenStats, toggleKey, updateKeyName, validateKey, incrementUsage, getStats, + resetAllTokenStats, KEY_PREFIX, extractPotluckKey, isPotluckRequest @@ -307,11 +318,13 @@ export { deleteKey, updateKeyLimit, resetKeyUsage, + resetKeyTokenStats, toggleKey, updateKeyName, validateKey, incrementUsage, getStats, + resetAllTokenStats, KEY_PREFIX, extractPotluckKey, isPotluckRequest diff --git a/src/plugins/api-potluck/key-manager.js b/src/plugins/api-potluck/key-manager.js index 397d6a8..f4d4203 100644 --- a/src/plugins/api-potluck/key-manager.js +++ b/src/plugins/api-potluck/key-manager.js @@ -138,6 +138,31 @@ function addUsage(target, usage = {}) { target.cachedTokens += toNumber(usage.cachedTokens); } +function resetUsageBucketTokens(bucket) { + if (!bucket || typeof bucket !== 'object') return; + bucket.promptTokens = 0; + bucket.completionTokens = 0; + bucket.totalTokens = 0; + bucket.cachedTokens = 0; +} + +function resetUsageHistoryTokens(usageHistory) { + if (!usageHistory || typeof usageHistory !== 'object') return; + + for (const day of Object.values(usageHistory)) { + if (!day || typeof day !== 'object') continue; + resetUsageBucketTokens(day.summary); + + for (const usage of Object.values(day.providers || {})) { + resetUsageBucketTokens(usage); + } + + for (const usage of Object.values(day.models || {})) { + resetUsageBucketTokens(usage); + } + } +} + /** * 初始化:从文件加载数据到内存 */ @@ -369,6 +394,59 @@ export async function resetKeyUsage(keyId) { return keyStore.keys[keyId]; } +/** + * 重置单个 Key 的 Token 统计(保留调用次数) + */ +export async function resetKeyTokenStats(keyId) { + ensureLoaded(); + const keyData = keyStore.keys[keyId]; + if (!keyData) return null; + + keyData.todayPromptTokens = 0; + keyData.todayCompletionTokens = 0; + keyData.todayTotalTokens = 0; + keyData.todayCachedTokens = 0; + keyData.totalPromptTokens = 0; + keyData.totalCompletionTokens = 0; + keyData.totalTokens = 0; + keyData.totalCachedTokens = 0; + resetUsageHistoryTokens(keyData.usageHistory); + + markDirty(); + await persistIfDirty(); + logger.info(`[API Potluck] Reset token stats for key: ${keyId.substring(0, 12)}...`); + return keyData; +} + +/** + * 重置所有 Key 的 Token 统计(保留调用次数) + */ +export async function resetAllTokenStats() { + ensureLoaded(); + let updated = 0; + + for (const keyData of Object.values(keyStore.keys)) { + keyData.todayPromptTokens = 0; + keyData.todayCompletionTokens = 0; + keyData.todayTotalTokens = 0; + keyData.todayCachedTokens = 0; + keyData.totalPromptTokens = 0; + keyData.totalCompletionTokens = 0; + keyData.totalTokens = 0; + keyData.totalCachedTokens = 0; + resetUsageHistoryTokens(keyData.usageHistory); + updated++; + } + + if (updated > 0) { + markDirty(); + await persistIfDirty(); + } + + logger.info(`[API Potluck] Reset token stats for all keys: ${updated}`); + return { total: Object.keys(keyStore.keys).length, updated }; +} + /** * 切换 Key 的启用/禁用状态 */ diff --git a/src/plugins/model-usage-stats/api-routes.js b/src/plugins/model-usage-stats/api-routes.js index 28c64a3..6070288 100644 --- a/src/plugins/model-usage-stats/api-routes.js +++ b/src/plugins/model-usage-stats/api-routes.js @@ -1,7 +1,7 @@ import logger from '../../utils/logger.js'; import { checkAuth } from '../../ui-modules/auth.js'; import { isAuthorized } from '../../utils/common.js'; -import { getStats, resetStats } from './stats-manager.js'; +import { getStats, resetStats, resetTokenStats } from './stats-manager.js'; function sendJson(res, statusCode, data) { res.writeHead(statusCode, { 'Content-Type': 'application/json' }); @@ -59,6 +59,16 @@ export async function handleModelUsageStatsRoutes(method, path, req, res, config }); return true; } + + if ((method === 'POST' || method === 'DELETE') && path === '/api/model-usage-stats/reset-tokens') { + const stats = await resetTokenStats(); + sendJson(res, 200, { + success: true, + message: '模型 Token 统计已重置', + data: stats + }); + return true; + } } catch (error) { logger.error('[Model Usage Stats] Route error:', error.message); sendJson(res, 500, { diff --git a/src/plugins/model-usage-stats/index.js b/src/plugins/model-usage-stats/index.js index 70b5d9b..8a8e2ac 100644 --- a/src/plugins/model-usage-stats/index.js +++ b/src/plugins/model-usage-stats/index.js @@ -6,6 +6,7 @@ import { recordStreamChunkUsage, recordUnaryUsage, resetStats, + resetTokenStats, setConfigGetter } from './stats-manager.js'; @@ -81,12 +82,14 @@ const modelUsageStatsPlugin = { exports: { getStats, - resetStats + resetStats, + resetTokenStats } }; export default modelUsageStatsPlugin; export { getStats, - resetStats + resetStats, + resetTokenStats }; diff --git a/src/plugins/model-usage-stats/stats-manager.js b/src/plugins/model-usage-stats/stats-manager.js index d128a8f..eba4c61 100644 --- a/src/plugins/model-usage-stats/stats-manager.js +++ b/src/plugins/model-usage-stats/stats-manager.js @@ -220,6 +220,13 @@ function normalizeUsageCandidate(candidate) { } const usage = candidate.usage || candidate.message?.usage || candidate.usageMetadata || candidate.response?.usage || null; + const reasoningTokens = toNumber( + candidate.completion_tokens_details?.reasoning_tokens ?? + candidate.output_tokens_details?.reasoning_tokens ?? + usage?.completion_tokens_details?.reasoning_tokens ?? + usage?.output_tokens_details?.reasoning_tokens ?? + usage?.thoughtsTokenCount + ); const promptTokens = toNumber( candidate.prompt_tokens ?? usage?.prompt_tokens ?? @@ -233,7 +240,7 @@ function normalizeUsageCandidate(candidate) { usage?.output_tokens ?? usage?.candidatesTokenCount ?? usage?.outputTokenCount - ); + ) + reasoningTokens; const totalTokens = toNumber( candidate.total_tokens ?? usage?.total_tokens ?? @@ -324,6 +331,14 @@ function applyUsage(target, usage, timestamp) { target.lastUsedAt = timestamp; } +function resetUsageBlockTokens(block) { + if (!block || typeof block !== 'object') return; + block.promptTokens = 0; + block.completionTokens = 0; + block.totalTokens = 0; + block.cachedTokens = 0; +} + export function setConfigGetter(getter) { configGetter = getter; } @@ -399,3 +414,23 @@ export async function resetStats() { logger.warn('[Model Usage Stats] Stats store reset'); return getStats(); } + +export async function resetTokenStats() { + ensureLoaded(); + + resetUsageBlockTokens(statsStore.summary); + + for (const providerStore of Object.values(statsStore.providers || {})) { + resetUsageBlockTokens(providerStore.summary); + + for (const modelStore of Object.values(providerStore.models || {})) { + resetUsageBlockTokens(modelStore); + } + } + + pendingRequests.clear(); + markDirty(); + await persistIfDirty(); + logger.warn('[Model Usage Stats] Token stats reset'); + return getStats(); +} diff --git a/static/model-usage-stats.html b/static/model-usage-stats.html index 3a72c70..d28b662 100644 --- a/static/model-usage-stats.html +++ b/static/model-usage-stats.html @@ -70,6 +70,7 @@

Provider 视图

+
@@ -123,7 +124,8 @@ function render(data){rows=flatten(data);el('emptyState').style.display=rows.length?'none':'block';renderSummary(data);const providers=Object.entries(data.providers||{}).map(([name,p])=>({name,totalTokens:Number(p.summary?.totalTokens||0)})).sort((a,b)=>b.totalTokens-a.totalTokens).slice(0,8);const topModels=rows.slice().sort((a,b)=>Number(b.totalTokens||0)-Number(a.totalTokens||0)).slice(0,8);bars('providerBars',providers,i=>i.totalTokens,i=>i.name);bars('topModelBars',topModels,i=>Number(i.totalTokens||0),i=>`${i.provider} / ${i.model}`);renderProviders(data);renderTable()} async function loadData(){try{saveCredential();status('正在加载统计数据...');const payload=await request(API_BASE);render(payload.data||payload);badge('已连接',true);status(`已加载 ${fmt(rows.length)} 条模型统计。`,'success')}catch(error){console.error(error);badge('连接失败',false);status(error.message,'error')}} async function resetData(){if(!confirm('确认重置全部模型统计吗?此操作会清空已落库的累计数据。'))return;try{status('正在重置统计数据...');const payload=await request(`${API_BASE}/reset`,{method:'POST'});render(payload.data||payload);status('统计数据已重置。','success')}catch(error){console.error(error);status(error.message,'error')}} - el('connectBtn').addEventListener('click',loadData);el('refreshBtn').addEventListener('click',loadData);el('resetBtn').addEventListener('click',resetData);el('clearBtn').addEventListener('click',clearCredential);el('searchInput').addEventListener('input',renderTable);el('sortSelect').addEventListener('change',renderTable);el('credentialValue').addEventListener('keydown',e=>{if(e.key==='Enter')loadData()}); + async function resetTokenData(){if(!confirm('确认重置模型 Token 统计吗?这会清空 Prompt / Completion / Cached / Total Tokens,但保留请求次数与最近使用时间。'))return;try{status('正在重置 Token 统计...');const payload=await request(`${API_BASE}/reset-tokens`,{method:'POST'});render(payload.data||payload);status('模型 Token 统计已重置。','success')}catch(error){console.error(error);status(error.message,'error')}} + el('connectBtn').addEventListener('click',loadData);el('refreshBtn').addEventListener('click',loadData);el('resetBtn').addEventListener('click',resetData);el('resetTokensBtn').addEventListener('click',resetTokenData);el('clearBtn').addEventListener('click',clearCredential);el('searchInput').addEventListener('input',renderTable);el('sortSelect').addEventListener('change',renderTable);el('credentialValue').addEventListener('keydown',e=>{if(e.key==='Enter')loadData()}); restoreCredential();if(el('credentialValue').value.trim())loadData(); diff --git a/static/potluck.html b/static/potluck.html index d4e0dd3..b8acb50 100644 --- a/static/potluck.html +++ b/static/potluck.html @@ -678,6 +678,9 @@
+ @@ -1072,6 +1075,7 @@
+ @@ -1138,6 +1142,20 @@ else { showToast(result?.error?.message || '操作失败', 'error'); } } + async function resetTokenStats(keyId) { + if (!confirm('确定要重置该 Key 的 Token 统计吗?这会清空该 Key 的今日/累计 Token 与历史 Token 统计,但保留调用次数。')) return; + const result = await apiRequest(`${API_BASE}/keys/${encodeURIComponent(keyId)}/reset-tokens`, { method: 'POST' }); + if (result && result.success) { showToast('Token 统计已重置', 'success'); loadData(); } + else { showToast(result?.error?.message || '操作失败', 'error'); } + } + + async function resetAllTokenStats() { + if (!confirm('确定要重置全部 Key 的 Token 统计吗?这会清空所有 Key 的今日/累计 Token 与历史 Token 统计,但保留调用次数。')) return; + const result = await apiRequest(`${API_BASE}/stats/reset-tokens`, { method: 'POST' }); + if (result && result.success) { showToast('全部 Token 统计已重置', 'success'); loadData(); } + else { showToast(result?.error?.message || '操作失败', 'error'); } + } + function openEditLimit(keyId, currentLimit) { document.getElementById('editKeyId').value = keyId; document.getElementById('newLimit').value = currentLimit;