mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat: add cache token pricing support in usage calculation
- Add cache price field to ModelPrice interface - Support both cached_tokens and cache_tokens fields for compatibility - Separate prompt, cache, and completion pricing in cost calculation - Deduct cached tokens from input tokens before prompt pricing - Refactor getApiStats/getModelStats to reuse calculateCost function - Update i18n labels for model pricing
This commit is contained in:
@@ -511,8 +511,9 @@
|
|||||||
"model_price_model_label": "Model",
|
"model_price_model_label": "Model",
|
||||||
"model_price_select_placeholder": "Choose a model",
|
"model_price_select_placeholder": "Choose a model",
|
||||||
"model_price_select_hint": "Models come from usage details",
|
"model_price_select_hint": "Models come from usage details",
|
||||||
"model_price_prompt": "Prompt price ($/1M tokens)",
|
"model_price_prompt": "Prompt price",
|
||||||
"model_price_completion": "Completion price ($/1M tokens)",
|
"model_price_completion": "Completion price",
|
||||||
|
"model_price_cache": "Cache price",
|
||||||
"model_price_save": "Save Price",
|
"model_price_save": "Save Price",
|
||||||
"model_price_empty": "No model prices set",
|
"model_price_empty": "No model prices set",
|
||||||
"model_price_model": "Model",
|
"model_price_model": "Model",
|
||||||
|
|||||||
@@ -521,8 +521,9 @@
|
|||||||
"model_price_model_label": "选择模型",
|
"model_price_model_label": "选择模型",
|
||||||
"model_price_select_placeholder": "选择模型",
|
"model_price_select_placeholder": "选择模型",
|
||||||
"model_price_select_hint": "模型列表来自使用统计明细",
|
"model_price_select_hint": "模型列表来自使用统计明细",
|
||||||
"model_price_prompt": "提示价格 ($/1M tokens)",
|
"model_price_prompt": "提示价格",
|
||||||
"model_price_completion": "补全价格 ($/1M tokens)",
|
"model_price_completion": "补全价格",
|
||||||
|
"model_price_cache": "缓存价格",
|
||||||
"model_price_save": "保存价格",
|
"model_price_save": "保存价格",
|
||||||
"model_price_empty": "暂未设置任何模型价格",
|
"model_price_empty": "暂未设置任何模型价格",
|
||||||
"model_price_model": "模型",
|
"model_price_model": "模型",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface RateStats {
|
|||||||
export interface ModelPrice {
|
export interface ModelPrice {
|
||||||
prompt: number;
|
prompt: number;
|
||||||
completion: number;
|
completion: number;
|
||||||
|
cache: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsageDetail {
|
export interface UsageDetail {
|
||||||
@@ -42,6 +43,7 @@ export interface UsageDetail {
|
|||||||
output_tokens: number;
|
output_tokens: number;
|
||||||
reasoning_tokens: number;
|
reasoning_tokens: number;
|
||||||
cached_tokens: number;
|
cached_tokens: number;
|
||||||
|
cache_tokens?: number;
|
||||||
total_tokens: number;
|
total_tokens: number;
|
||||||
};
|
};
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
@@ -214,11 +216,15 @@ export function extractTotalTokens(detail: any): number {
|
|||||||
if (typeof tokens.total_tokens === 'number') {
|
if (typeof tokens.total_tokens === 'number') {
|
||||||
return tokens.total_tokens;
|
return tokens.total_tokens;
|
||||||
}
|
}
|
||||||
const tokenKeys = ['input_tokens', 'output_tokens', 'reasoning_tokens', 'cached_tokens'];
|
const inputTokens = typeof tokens.input_tokens === 'number' ? tokens.input_tokens : 0;
|
||||||
return tokenKeys.reduce((sum, key) => {
|
const outputTokens = typeof tokens.output_tokens === 'number' ? tokens.output_tokens : 0;
|
||||||
const value = tokens[key];
|
const reasoningTokens = typeof tokens.reasoning_tokens === 'number' ? tokens.reasoning_tokens : 0;
|
||||||
return sum + (typeof value === 'number' ? value : 0);
|
const cachedTokens = Math.max(
|
||||||
}, 0);
|
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 => {
|
details.forEach(detail => {
|
||||||
const tokens = detail?.tokens || {};
|
const tokens = detail?.tokens || {};
|
||||||
if (typeof tokens.cached_tokens === 'number') {
|
cachedTokens += Math.max(
|
||||||
cachedTokens += tokens.cached_tokens;
|
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') {
|
if (typeof tokens.reasoning_tokens === 'number') {
|
||||||
reasoningTokens += tokens.reasoning_tokens;
|
reasoningTokens += tokens.reasoning_tokens;
|
||||||
}
|
}
|
||||||
@@ -311,11 +318,23 @@ export function calculateCost(detail: any, modelPrices: Record<string, ModelPric
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const tokens = detail?.tokens || {};
|
const tokens = detail?.tokens || {};
|
||||||
const promptTokens = Number(tokens.input_tokens) || 0;
|
const rawInputTokens = Number(tokens.input_tokens);
|
||||||
const completionTokens = Number(tokens.output_tokens) || 0;
|
const rawCompletionTokens = Number(tokens.output_tokens);
|
||||||
|
const rawCachedTokensPrimary = Number(tokens.cached_tokens);
|
||||||
|
const rawCachedTokensAlternate = Number(tokens.cache_tokens);
|
||||||
|
|
||||||
|
const inputTokens = Number.isFinite(rawInputTokens) ? Math.max(rawInputTokens, 0) : 0;
|
||||||
|
const completionTokens = Number.isFinite(rawCompletionTokens) ? Math.max(rawCompletionTokens, 0) : 0;
|
||||||
|
const cachedTokens = Math.max(
|
||||||
|
Number.isFinite(rawCachedTokensPrimary) ? Math.max(rawCachedTokensPrimary, 0) : 0,
|
||||||
|
Number.isFinite(rawCachedTokensAlternate) ? Math.max(rawCachedTokensAlternate, 0) : 0
|
||||||
|
);
|
||||||
|
const promptTokens = Math.max(inputTokens - cachedTokens, 0);
|
||||||
|
|
||||||
const promptCost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0);
|
const promptCost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0);
|
||||||
|
const cachedCost = (cachedTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.cache) || 0);
|
||||||
const completionCost = (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
|
const completionCost = (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
|
||||||
const total = promptCost + completionCost;
|
const total = promptCost + cachedCost + completionCost;
|
||||||
return Number.isFinite(total) && total > 0 ? total : 0;
|
return Number.isFinite(total) && total > 0 ? total : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,14 +368,27 @@ export function loadModelPrices(): Record<string, ModelPrice> {
|
|||||||
const normalized: Record<string, ModelPrice> = {};
|
const normalized: Record<string, ModelPrice> = {};
|
||||||
Object.entries(parsed).forEach(([model, price]: [string, any]) => {
|
Object.entries(parsed).forEach(([model, price]: [string, any]) => {
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
const prompt = Number(price?.prompt);
|
const promptRaw = Number(price?.prompt);
|
||||||
const completion = Number(price?.completion);
|
const completionRaw = Number(price?.completion);
|
||||||
if (!Number.isFinite(prompt) && !Number.isFinite(completion)) {
|
const cacheRaw = Number(price?.cache);
|
||||||
|
|
||||||
|
if (!Number.isFinite(promptRaw) && !Number.isFinite(completionRaw) && !Number.isFinite(cacheRaw)) {
|
||||||
return;
|
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] = {
|
normalized[model] = {
|
||||||
prompt: Number.isFinite(prompt) && prompt >= 0 ? prompt : 0,
|
prompt,
|
||||||
completion: Number.isFinite(completion) && completion >= 0 ? completion : 0
|
completion,
|
||||||
|
cache
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return normalized;
|
return normalized;
|
||||||
@@ -404,14 +436,7 @@ export function getApiStats(usageData: any, modelPrices: Record<string, ModelPri
|
|||||||
if (price) {
|
if (price) {
|
||||||
const details = Array.isArray(modelData.details) ? modelData.details : [];
|
const details = Array.isArray(modelData.details) ? modelData.details : [];
|
||||||
details.forEach((detail: any) => {
|
details.forEach((detail: any) => {
|
||||||
const tokens = detail?.tokens || {};
|
totalCost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -454,14 +479,7 @@ export function getModelStats(usageData: any, modelPrices: Record<string, ModelP
|
|||||||
if (price) {
|
if (price) {
|
||||||
const details = Array.isArray(modelData.details) ? modelData.details : [];
|
const details = Array.isArray(modelData.details) ? modelData.details : [];
|
||||||
details.forEach((detail: any) => {
|
details.forEach((detail: any) => {
|
||||||
const tokens = detail?.tokens || {};
|
existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
modelMap.set(modelName, existing);
|
modelMap.set(modelName, existing);
|
||||||
|
|||||||
Reference in New Issue
Block a user