diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index fd4e8f2..35c49a8 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -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 + } }, }; } diff --git a/src/plugins/api-potluck/api-routes.js b/src/plugins/api-potluck/api-routes.js index 2c8d9dd..9253e67 100644 --- a/src/plugins/api-potluck/api-routes.js +++ b/src/plugins/api-potluck/api-routes.js @@ -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, diff --git a/src/plugins/api-potluck/index.js b/src/plugins/api-potluck/index.js index e56dd58..fec5bbe 100644 --- a/src/plugins/api-potluck/index.js +++ b/src/plugins/api-potluck/index.js @@ -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 }); } diff --git a/src/plugins/api-potluck/key-manager.js b/src/plugins/api-potluck/key-manager.js index e5df5a6..397d6a8 100644 --- a/src/plugins/api-potluck/key-manager.js +++ b/src/plugins/api-potluck/key-manager.js @@ -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) { diff --git a/src/plugins/model-usage-stats/stats-manager.js b/src/plugins/model-usage-stats/stats-manager.js index 2d50fb0..d128a8f 100644 --- a/src/plugins/model-usage-stats/stats-manager.js +++ b/src/plugins/model-usage-stats/stats-manager.js @@ -254,7 +254,7 @@ function normalizeUsageCandidate(candidate) { return { promptTokens, completionTokens, - totalTokens: totalTokens || promptTokens + completionTokens, + totalTokens: totalTokens || (promptTokens + completionTokens), cachedTokens }; } diff --git a/static/model-usage-stats.html b/static/model-usage-stats.html index 6a9b33e..3a72c70 100644 --- a/static/model-usage-stats.html +++ b/static/model-usage-stats.html @@ -8,7 +8,7 @@ @@ -57,6 +57,7 @@
总请求数
0
累计成功落库的模型调用次数
Prompt Tokens
0
输入 token 的累计值
+
Cached Tokens
0
缓存命中的累计值
Completion Tokens
0
输出 token 的累计值
总 Tokens
0
等待数据
@@ -91,7 +92,7 @@
- +
ProviderModel请求数PromptCompletionTotal最近使用
ProviderModel请求数PromptCachedCompletionTotal最近使用
@@ -115,10 +116,10 @@ async function request(url,options={}){const res=await fetch(url,{...options,headers:{...(options.headers||{}),...headers()}}),text=await res.text();let payload=null;try{payload=text?JSON.parse(text):null}catch{throw new Error(`接口返回了非 JSON 内容: ${text||'(empty)'}`)}if(!res.ok){if(payload?.error?.code==='PLUGIN_DISABLED')throw new Error('插件未启用:请先在插件管理中启用 model-usage-stats。');throw new Error(payload?.error?.message||payload?.message||`请求失败 (${res.status})`)}return payload} function flatten(data){const list=[];for(const [provider,pd] of Object.entries(data.providers||{})){for(const [model,md] of Object.entries(pd.models||{}))list.push({provider,model,...md})}return list} function bars(id,items,getValue,getLabel){const box=el(id);box.innerHTML='';if(!items.length){box.innerHTML='
暂无数据
等待统计写入
';return}const max=Math.max(...items.map(getValue),1);items.forEach(item=>{const v=Number(getValue(item)||0),w=Math.max(v/max*100,2),node=document.createElement('div');node.className='bar';node.innerHTML=`
${getLabel(item)}
${fmtToken(v)}
`;box.appendChild(node)})} - function renderSummary(data){const s=data.summary||{};el('totalRequests').textContent=fmt(s.requestCount);el('promptTokens').textContent=fmtToken(s.promptTokens);el('completionTokens').textContent=fmtToken(s.completionTokens);el('totalTokens').textContent=fmtToken(s.totalTokens);el('updatedAt').textContent=data.updatedAt?`更新于 ${new Date(data.updatedAt).toLocaleString('zh-CN')}`:'尚未写入'} - function renderProviders(data){const providers=Object.entries(data.providers||{}).map(([name,p])=>({name,data:p,count:Object.keys(p.models||{}).length})).sort((a,b)=>(b.data.summary?.totalTokens||0)-(a.data.summary?.totalTokens||0));const box=el('providerCards');box.innerHTML='';if(!providers.length){box.innerHTML='
暂无 Provider 统计
当前还没有可展示的累计数据。
';return}providers.forEach(p=>{const s=p.data.summary||{};const node=document.createElement('article');node.className='provider';node.innerHTML=`
${p.name}
包含 ${fmt(p.count)} 个模型
${fmt(s.requestCount)} 次调用
Total Tokens
${fmtToken(s.totalTokens)}
Prompt Tokens
${fmtToken(s.promptTokens)}
Completion Tokens
${fmtToken(s.completionTokens)}
最近使用
${rel(s.lastUsedAt)}
`;box.appendChild(node)})} + function renderSummary(data){const s=data.summary||{};el('totalRequests').textContent=fmt(s.requestCount);el('promptTokens').textContent=fmtToken(s.promptTokens);el('cachedTokens').textContent=fmtToken(s.cachedTokens);el('completionTokens').textContent=fmtToken(s.completionTokens);el('totalTokens').textContent=fmtToken(s.totalTokens);el('updatedAt').textContent=data.updatedAt?`更新于 ${new Date(data.updatedAt).toLocaleString('zh-CN')}`:'尚未写入'} + function renderProviders(data){const providers=Object.entries(data.providers||{}).map(([name,p])=>({name,data:p,count:Object.keys(p.models||{}).length})).sort((a,b)=>(b.data.summary?.totalTokens||0)-(a.data.summary?.totalTokens||0));const box=el('providerCards');box.innerHTML='';if(!providers.length){box.innerHTML='
暂无 Provider 统计
当前还没有可展示的累计数据。
';return}providers.forEach(p=>{const s=p.data.summary||{};const node=document.createElement('article');node.className='provider';node.innerHTML=`
${p.name}
包含 ${fmt(p.count)} 个模型
${fmt(s.requestCount)} 次调用
Total Tokens
${fmtToken(s.totalTokens)}
Prompt
${fmtToken(s.promptTokens)}
Cached
${fmtToken(s.cachedTokens)}
Completion
${fmtToken(s.completionTokens)}
最近使用
${rel(s.lastUsedAt)}
`;box.appendChild(node)})} function filteredRows(){const keyword=el('searchInput').value.trim().toLowerCase();const [field,dir]=el('sortSelect').value.split('-');const list=keyword?rows.filter(r=>r.provider.toLowerCase().includes(keyword)||r.model.toLowerCase().includes(keyword)):rows.slice();list.sort((a,b)=>{if(field==='provider'||field==='model'){const l=String(a[field]||''),r=String(b[field]||'');return dir==='desc'?r.localeCompare(l):l.localeCompare(r)}const l=Number(a[field]||0),r=Number(b[field]||0);return dir==='desc'?r-l:l-r});return list} - function renderTable(){const tbody=el('tableBody'),list=filteredRows();tbody.innerHTML='';if(!list.length){tbody.innerHTML='没有匹配的数据';return}list.forEach(r=>{const tr=document.createElement('tr');tr.innerHTML=`${r.provider}${r.model}${fmt(r.requestCount)}${fmtToken(r.promptTokens)}${fmtToken(r.completionTokens)}${fmtToken(r.totalTokens)}${rel(r.lastUsedAt)}`;tbody.appendChild(tr)})} + function renderTable(){const tbody=el('tableBody'),list=filteredRows();tbody.innerHTML='';if(!list.length){tbody.innerHTML='没有匹配的数据';return}list.forEach(r=>{const tr=document.createElement('tr');tr.innerHTML=`${r.provider}${r.model}${fmt(r.requestCount)}${fmtToken(r.promptTokens)}${fmtToken(r.cachedTokens)}${fmtToken(r.completionTokens)}${fmtToken(r.totalTokens)}${rel(r.lastUsedAt)}`;tbody.appendChild(tr)})} 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')}} diff --git a/static/potluck-user.html b/static/potluck-user.html index b531f63..6888528 100644 --- a/static/potluck-user.html +++ b/static/potluck-user.html @@ -414,10 +414,18 @@
今日 Tokens
0
+
+
今日缓存 Tokens
+
0
+
累计 Tokens
0
+
+
累计缓存 Tokens
+
0
+
@@ -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) { diff --git a/static/potluck.html b/static/potluck.html index f0673d5..d4e0dd3 100644 --- a/static/potluck.html +++ b/static/potluck.html @@ -616,10 +616,18 @@
今日总 Tokens
0
+
+
今日缓存 Tokens
+
0
+
累计 Tokens
0
+
+
累计缓存 Tokens
+
0
+
@@ -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 @@
今日/限额
${key.todayUsage}/${key.dailyLimit}
-
${formatTokenCompact(key.todayTotalTokens || 0)} Tokens
+
${formatTokenCompact(key.todayTotalTokens || 0)} Tokens ${key.todayCachedTokens ? `(含 ${formatTokenCompact(key.todayCachedTokens)} 缓存)` : ''}
累计
${key.totalUsage}
-
${formatTokenCompact(key.totalTokens || 0)} Tokens
+
${formatTokenCompact(key.totalTokens || 0)} Tokens ${key.totalCachedTokens ? `(含 ${formatTokenCompact(key.totalCachedTokens)} 缓存)` : ''}
最后调用