diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 71b70ad..32a4cff 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -511,8 +511,9 @@ "model_price_model_label": "Model", "model_price_select_placeholder": "Choose a model", "model_price_select_hint": "Models come from usage details", - "model_price_prompt": "Prompt price ($/1M tokens)", - "model_price_completion": "Completion price ($/1M tokens)", + "model_price_prompt": "Prompt price", + "model_price_completion": "Completion price", + "model_price_cache": "Cache price", "model_price_save": "Save Price", "model_price_empty": "No model prices set", "model_price_model": "Model", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 8039789..fe1e9d6 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -521,8 +521,9 @@ "model_price_model_label": "选择模型", "model_price_select_placeholder": "选择模型", "model_price_select_hint": "模型列表来自使用统计明细", - "model_price_prompt": "提示价格 ($/1M tokens)", - "model_price_completion": "补全价格 ($/1M tokens)", + "model_price_prompt": "提示价格", + "model_price_completion": "补全价格", + "model_price_cache": "缓存价格", "model_price_save": "保存价格", "model_price_empty": "暂未设置任何模型价格", "model_price_model": "模型", diff --git a/src/utils/usage.ts b/src/utils/usage.ts index 2b08643..b38f7c1 100644 --- a/src/utils/usage.ts +++ b/src/utils/usage.ts @@ -31,6 +31,7 @@ export interface RateStats { export interface ModelPrice { prompt: number; completion: number; + cache: number; } export interface UsageDetail { @@ -42,6 +43,7 @@ export interface UsageDetail { output_tokens: number; reasoning_tokens: number; cached_tokens: number; + cache_tokens?: number; total_tokens: number; }; failed: boolean; @@ -214,11 +216,15 @@ export function extractTotalTokens(detail: any): number { if (typeof tokens.total_tokens === 'number') { return tokens.total_tokens; } - const tokenKeys = ['input_tokens', 'output_tokens', 'reasoning_tokens', 'cached_tokens']; - return tokenKeys.reduce((sum, key) => { - const value = tokens[key]; - return sum + (typeof value === 'number' ? value : 0); - }, 0); + const inputTokens = typeof tokens.input_tokens === 'number' ? tokens.input_tokens : 0; + const outputTokens = typeof tokens.output_tokens === 'number' ? tokens.output_tokens : 0; + const reasoningTokens = typeof tokens.reasoning_tokens === 'number' ? tokens.reasoning_tokens : 0; + const cachedTokens = Math.max( + typeof tokens.cached_tokens === 'number' ? Math.max(tokens.cached_tokens, 0) : 0, + typeof tokens.cache_tokens === 'number' ? Math.max(tokens.cache_tokens, 0) : 0 + ); + + return inputTokens + outputTokens + reasoningTokens + cachedTokens; } /** @@ -235,9 +241,10 @@ export function calculateTokenBreakdown(usageData: any): TokenBreakdown { details.forEach(detail => { const tokens = detail?.tokens || {}; - if (typeof tokens.cached_tokens === 'number') { - cachedTokens += tokens.cached_tokens; - } + cachedTokens += Math.max( + typeof tokens.cached_tokens === 'number' ? Math.max(tokens.cached_tokens, 0) : 0, + typeof tokens.cache_tokens === 'number' ? Math.max(tokens.cache_tokens, 0) : 0 + ); if (typeof tokens.reasoning_tokens === 'number') { reasoningTokens += tokens.reasoning_tokens; } @@ -311,11 +318,23 @@ export function calculateCost(detail: any, modelPrices: Record 0 ? total : 0; } @@ -349,14 +368,27 @@ export function loadModelPrices(): Record { const normalized: Record = {}; Object.entries(parsed).forEach(([model, price]: [string, any]) => { if (!model) return; - const prompt = Number(price?.prompt); - const completion = Number(price?.completion); - if (!Number.isFinite(prompt) && !Number.isFinite(completion)) { + const promptRaw = Number(price?.prompt); + const completionRaw = Number(price?.completion); + const cacheRaw = Number(price?.cache); + + if (!Number.isFinite(promptRaw) && !Number.isFinite(completionRaw) && !Number.isFinite(cacheRaw)) { return; } + + const prompt = Number.isFinite(promptRaw) && promptRaw >= 0 ? promptRaw : 0; + const completion = Number.isFinite(completionRaw) && completionRaw >= 0 ? completionRaw : 0; + const cache = + Number.isFinite(cacheRaw) && cacheRaw >= 0 + ? cacheRaw + : Number.isFinite(promptRaw) && promptRaw >= 0 + ? promptRaw + : prompt; + normalized[model] = { - prompt: Number.isFinite(prompt) && prompt >= 0 ? prompt : 0, - completion: Number.isFinite(completion) && completion >= 0 ? completion : 0 + prompt, + completion, + cache }; }); return normalized; @@ -404,14 +436,7 @@ export function getApiStats(usageData: any, modelPrices: Record { - const tokens = detail?.tokens || {}; - const promptTokens = Number(tokens.input_tokens) || 0; - const completionTokens = Number(tokens.output_tokens) || 0; - const cost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0) + - (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0); - if (Number.isFinite(cost) && cost > 0) { - totalCost += cost; - } + totalCost += calculateCost({ ...detail, __modelName: modelName }, modelPrices); }); } }); @@ -454,14 +479,7 @@ export function getModelStats(usageData: any, modelPrices: Record { - const tokens = detail?.tokens || {}; - const promptTokens = Number(tokens.input_tokens) || 0; - const completionTokens = Number(tokens.output_tokens) || 0; - const cost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0) + - (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0); - if (Number.isFinite(cost) && cost > 0) { - existing.cost += cost; - } + existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices); }); } modelMap.set(modelName, existing);