196 lines
6.9 KiB
JavaScript
196 lines
6.9 KiB
JavaScript
// Token 使用统计模块 — 记录每次 AI 请求的 token 用量、速度,并计算消耗预估
|
|
|
|
import { persistSet } from './persistence';
|
|
|
|
const STORAGE_KEY = 'author-token-stats';
|
|
|
|
/**
|
|
* 读取所有记录
|
|
*/
|
|
export function getTokenRecords() {
|
|
if (typeof window === 'undefined') return [];
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return [];
|
|
const data = JSON.parse(raw);
|
|
return Array.isArray(data.records) ? data.records : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 添加一条 token 使用记录
|
|
* @param {{
|
|
* promptTokens: number,
|
|
* completionTokens: number,
|
|
* totalTokens: number,
|
|
* durationMs: number,
|
|
* source: 'chat' | 'inline',
|
|
* provider?: string,
|
|
* model?: string
|
|
* }} record
|
|
*/
|
|
export function addTokenRecord(record) {
|
|
if (typeof window === 'undefined') return;
|
|
const records = getTokenRecords();
|
|
records.push({
|
|
timestamp: Date.now(),
|
|
promptTokens: record.promptTokens || 0,
|
|
completionTokens: record.completionTokens || 0,
|
|
totalTokens: record.totalTokens || 0,
|
|
durationMs: record.durationMs || 0,
|
|
source: record.source || 'chat',
|
|
provider: record.provider || 'unknown',
|
|
model: record.model || 'unknown',
|
|
});
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ records }));
|
|
persistSet(STORAGE_KEY, { records }).catch(() => { });
|
|
}
|
|
|
|
/**
|
|
* 清空所有统计数据
|
|
*/
|
|
export function clearTokenStats() {
|
|
if (typeof window === 'undefined') return;
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ records: [] }));
|
|
persistSet(STORAGE_KEY, { records: [] }).catch(() => { });
|
|
}
|
|
|
|
/**
|
|
* 计算汇总统计
|
|
* @returns {{
|
|
* totalRequests: number,
|
|
* totalTokens: number,
|
|
* totalPromptTokens: number,
|
|
* totalCompletionTokens: number,
|
|
* avgSpeed: number,
|
|
* recentSpeeds: {speed: number, tokens: number, timestamp: number}[],
|
|
* projections: {
|
|
* perDay: {tokens: number, requests: number},
|
|
* perWeek: {tokens: number, requests: number},
|
|
* perMonth: {tokens: number, requests: number},
|
|
* perQuarter: {tokens: number, requests: number},
|
|
* perYear: {tokens: number, requests: number}
|
|
* },
|
|
* firstRecordTime: number | null,
|
|
* trackedDays: number
|
|
* }}
|
|
*/
|
|
export function getTokenStats() {
|
|
const records = getTokenRecords();
|
|
|
|
if (records.length === 0) {
|
|
return {
|
|
totalRequests: 0,
|
|
totalTokens: 0,
|
|
totalPromptTokens: 0,
|
|
totalCompletionTokens: 0,
|
|
avgSpeed: 0,
|
|
rates: { tps: 0, tpm: 0, tph: 0, tpd: 0, rpm: 0, rph: 0, rpd: 0 },
|
|
recentSpeeds: [],
|
|
projections: {
|
|
perDay: { tokens: 0, requests: 0 },
|
|
perWeek: { tokens: 0, requests: 0 },
|
|
perMonth: { tokens: 0, requests: 0 },
|
|
perQuarter: { tokens: 0, requests: 0 },
|
|
perYear: { tokens: 0, requests: 0 },
|
|
},
|
|
modelBreakdown: [],
|
|
firstRecordTime: null,
|
|
trackedDays: 0,
|
|
};
|
|
}
|
|
|
|
const totalRequests = records.length;
|
|
const totalTokens = records.reduce((s, r) => s + r.totalTokens, 0);
|
|
const totalPromptTokens = records.reduce((s, r) => s + r.promptTokens, 0);
|
|
const totalCompletionTokens = records.reduce((s, r) => s + r.completionTokens, 0);
|
|
|
|
// 平均速度(tokens/s):用 completionTokens / durationMs
|
|
const validSpeedRecords = records.filter(r => r.durationMs > 0 && r.completionTokens > 0);
|
|
const avgSpeed = validSpeedRecords.length > 0
|
|
? validSpeedRecords.reduce((s, r) => s + (r.completionTokens / r.durationMs * 1000), 0) / validSpeedRecords.length
|
|
: 0;
|
|
|
|
// 最近 10 次请求速度
|
|
const recentSpeeds = validSpeedRecords.slice(-10).map(r => ({
|
|
speed: r.completionTokens / r.durationMs * 1000,
|
|
tokens: r.completionTokens,
|
|
timestamp: r.timestamp,
|
|
}));
|
|
|
|
// 时间跨度计算
|
|
const firstRecordTime = records[0].timestamp;
|
|
const now = Date.now();
|
|
const trackedMs = now - firstRecordTime;
|
|
const trackedMinutes = Math.max(1, trackedMs / (60 * 1000));
|
|
const trackedHours = Math.max(1 / 60, trackedMs / (60 * 60 * 1000));
|
|
const trackedDays = Math.max(1 / 24, trackedMs / (24 * 60 * 60 * 1000));
|
|
|
|
// 消耗速率(基于实际统计时间)
|
|
const rates = {
|
|
tps: avgSpeed, // tokens per second(生成速度,已有)
|
|
tpm: totalTokens / trackedMinutes,
|
|
tph: totalTokens / trackedHours,
|
|
tpd: totalTokens / trackedDays,
|
|
rpm: totalRequests / trackedMinutes,
|
|
rph: totalRequests / trackedHours,
|
|
rpd: totalRequests / trackedDays,
|
|
};
|
|
|
|
// 消耗预估:基于日均值
|
|
const dailyTokens = totalTokens / Math.max(1, trackedDays);
|
|
const dailyRequests = totalRequests / Math.max(1, trackedDays);
|
|
|
|
const projections = {
|
|
perDay: { tokens: Math.round(dailyTokens), requests: Math.round(dailyRequests * 10) / 10 },
|
|
perWeek: { tokens: Math.round(dailyTokens * 7), requests: Math.round(dailyRequests * 7 * 10) / 10 },
|
|
perMonth: { tokens: Math.round(dailyTokens * 30), requests: Math.round(dailyRequests * 30 * 10) / 10 },
|
|
perQuarter: { tokens: Math.round(dailyTokens * 90), requests: Math.round(dailyRequests * 90 * 10) / 10 },
|
|
perYear: { tokens: Math.round(dailyTokens * 365), requests: Math.round(dailyRequests * 365 * 10) / 10 },
|
|
};
|
|
|
|
// 按 provider + model 分组统计
|
|
const modelMap = {};
|
|
for (const r of records) {
|
|
const key = `${r.provider || 'unknown'}||${r.model || 'unknown'}`;
|
|
if (!modelMap[key]) {
|
|
modelMap[key] = { provider: r.provider || 'unknown', model: r.model || 'unknown', tokens: 0, promptTokens: 0, completionTokens: 0, requests: 0, totalDurationMs: 0, validSpeedCount: 0, speedSum: 0 };
|
|
}
|
|
const m = modelMap[key];
|
|
m.tokens += r.totalTokens;
|
|
m.promptTokens += r.promptTokens;
|
|
m.completionTokens += r.completionTokens;
|
|
m.requests += 1;
|
|
if (r.durationMs > 0 && r.completionTokens > 0) {
|
|
m.speedSum += r.completionTokens / r.durationMs * 1000;
|
|
m.validSpeedCount += 1;
|
|
}
|
|
}
|
|
const modelBreakdown = Object.values(modelMap).map(m => ({
|
|
provider: m.provider,
|
|
model: m.model,
|
|
tokens: m.tokens,
|
|
promptTokens: m.promptTokens,
|
|
completionTokens: m.completionTokens,
|
|
requests: m.requests,
|
|
avgSpeed: m.validSpeedCount > 0 ? m.speedSum / m.validSpeedCount : 0,
|
|
tokenPercent: totalTokens > 0 ? Math.round(m.tokens / totalTokens * 1000) / 10 : 0,
|
|
})).sort((a, b) => b.tokens - a.tokens);
|
|
|
|
return {
|
|
totalRequests,
|
|
totalTokens,
|
|
totalPromptTokens,
|
|
totalCompletionTokens,
|
|
avgSpeed,
|
|
rates,
|
|
recentSpeeds,
|
|
projections,
|
|
modelBreakdown,
|
|
firstRecordTime,
|
|
trackedDays: trackedDays < 1 ? '< 1' : Math.round(trackedDays * 10) / 10,
|
|
};
|
|
}
|