Agent/utils/metrics-client.ts
2026-08-06 22:17:39 +08:00

274 lines
7.9 KiB
TypeScript

// @ts-nocheck
import { zhongtaiMetricsUrl, CONFIG, loadModelSource, loadAnthropicConfig, loadOllamaConfig, loadOmlxConfig, loadSparkleConfig } from './config.js';
const els = {
model: document.getElementById('badge-model'),
cache: document.getElementById('badge-cache'),
tokens: document.getElementById('badge-tokens'),
mem: document.getElementById('badge-mem'),
cpu: document.getElementById('badge-cpu'),
};
let _timer = null;
let _lastTokRate = 0;
let _lastTokAt = 0;
let _cacheRead = 0;
let _cacheWrite = 0;
let _cacheInput = 0;
let _cacheAt = 0;
let _cacheLast = null;
export function reportTokens(tokens, durationMs) {
if (!tokens || !durationMs || durationMs < 100) return;
_lastTokRate = Math.round(tokens / (durationMs / 1000));
_lastTokAt = Date.now();
}
let _streamStartAt = 0;
let _streamChars = 0;
let _streamTokens = 0;
let _lastStreamUpdate = 0;
export function reportStreamDelta(charCount) {
const now = Date.now();
if (_streamStartAt === 0) {
_streamStartAt = now;
_streamChars = 0;
_streamTokens = 0;
}
_streamChars += charCount;
_streamTokens = Math.ceil(_streamChars / 3.5);
const dt = (now - _streamStartAt) / 1000;
if (dt > 0.3) {
_lastTokRate = Math.round(_streamTokens / dt);
_lastTokAt = now;
_lastStreamUpdate = now;
}
}
export function reportStreamEnd(finalTokens) {
if (finalTokens && finalTokens > 0) {
const dt = (Date.now() - _streamStartAt) / 1000;
if (dt > 0.1) {
_lastTokRate = Math.round(finalTokens / dt);
_lastTokAt = Date.now();
}
}
_streamStartAt = 0;
_streamChars = 0;
_streamTokens = 0;
}
export function reportPromptCache(usage) {
const parsed = parseCacheUsage(usage);
if (!parsed) return;
_cacheRead += parsed.read;
_cacheWrite += parsed.write;
_cacheInput += parsed.input;
_cacheAt = Date.now();
_cacheLast = parsed;
renderCacheBadge();
}
export function resetPromptCache() {
_cacheRead = 0;
_cacheWrite = 0;
_cacheInput = 0;
_cacheAt = 0;
_cacheLast = null;
renderCacheBadge();
}
function parseCacheUsage(usage) {
if (!usage || typeof usage !== 'object') return null;
const aRead = Number(usage.cache_read_input_tokens) || 0;
const aWrite = Number(usage.cache_creation_input_tokens) || 0;
const aInput = Number(usage.input_tokens) || 0;
const hasAnthropicCacheKeys = (
'cache_read_input_tokens' in usage
|| 'cache_creation_input_tokens' in usage
);
const oPrompt = Number(usage.prompt_tokens) || 0;
const oCached = Number(
usage.prompt_tokens_details?.cached_tokens
?? usage.cached_tokens
) || 0;
if (hasAnthropicCacheKeys) {
const read = Math.max(aRead, oCached);
const write = aWrite;
const input = aInput > 0
? aInput
: Math.max(0, oPrompt - oCached);
if (read + write + input <= 0) return null;
return { read, write, input };
}
if (oPrompt > 0 || oCached > 0) {
return {
read: oCached,
write: 0,
input: Math.max(0, (oPrompt || oCached) - oCached),
};
}
if (aInput > 0) {
return { read: 0, write: 0, input: aInput };
}
return null;
}
function renderCacheBadge() {
if (!els.cache) return;
const total = _cacheRead + _cacheWrite + _cacheInput;
if (total <= 0) {
els.cache.textContent = '缓存命中 -';
els.cache.className = 'badge';
els.cache.title = 'Prompt 缓存命中率(尚无数据)';
return;
}
const pct = Math.round((_cacheRead / total) * 100);
els.cache.textContent = `缓存命中 ${pct}%`;
els.cache.className = pct > 0 ? 'badge active' : 'badge';
const last = _cacheLast
? ` · 上次 读${_cacheLast.read}/写${_cacheLast.write}/新${_cacheLast.input}`
: '';
els.cache.title = `本会话累计命中率 ${pct}%`
+ ` · 读 ${_cacheRead} · 写 ${_cacheWrite} · 未缓存 ${_cacheInput}`
+ last;
}
async function fetchOnce() {
try {
const r = await fetch(zhongtaiMetricsUrl());
if (!r.ok) return;
const m = await r.json();
render(m);
} catch (e) {
}
}
function render(m) {
const source = loadModelSource();
if (source === 'anthropic') {
const cfg = loadAnthropicConfig();
const tierModel = ({
fast: cfg.fastModel,
standard: cfg.standardModel,
pro: cfg.proModel,
})[cfg.currentTier] || cfg.fastModel;
const tierLabel = ({ fast: '快速', standard: '平衡', pro: '增强' })[cfg.currentTier] || '';
const proto = cfg.protocol === 'openai' ? 'OpenAI' : 'Anthropic';
els.model.textContent = `云端 ${shortName(tierModel)}`;
els.model.className = 'badge active';
els.model.title = `云端 · ${tierLabel}档 · ${proto} · ${cfg.baseUrl}`;
} else if (source === 'omlx') {
const cfg = loadOmlxConfig();
els.model.textContent = `oMLX ${shortName(cfg.model)}`;
els.model.className = 'badge active';
els.model.title = `本地 oMLX · ${cfg.baseUrl}`;
} else if (source === 'sparkle') {
const cfg = loadSparkleConfig();
els.model.textContent = `Sparkle ${shortName(cfg.model)}`;
els.model.className = 'badge active';
els.model.title = `本地 Sparkle · ${cfg.baseUrl}`;
} else {
const model = m.ollama;
if (model?.running && model.models?.length > 0) {
const m0 = model.models[0];
const name = shortName(m0.name);
els.model.textContent = `本地 ${name}`;
els.model.className = 'badge active';
} else {
els.model.textContent = '本地 未加载';
els.model.className = 'badge';
}
}
renderCacheBadge();
if (_lastTokAt && Date.now() - _lastTokAt < 5000) {
els.tokens.textContent = `速率 ${_lastTokRate} tok/s`;
els.tokens.className = 'badge active';
} else {
els.tokens.textContent = `速率 - tok/s`;
els.tokens.className = 'badge';
_lastTokRate = 0;
}
if (m.memory) {
const used = m.memory.used;
const total = m.memory.total;
const pct = m.memory.usagePercent.toFixed(0);
const model = m.ollama;
if (source === 'ollama' && model?.processMem && model.running) {
els.mem.textContent = `模型 ${gb(model.processMem)}G · 系统 ${pct}%`;
els.mem.title = `模型占用 ${gb(model.processMem)}GB · 系统内存压力 ${pct}%(活动监视器口径,总 ${gb(total)}GB)`;
} else if (source === 'omlx') {
els.mem.textContent = `内存/显存 ${gb(used)}/${gb(total)}GB · ${pct}%`;
els.mem.title = `oMLX 模型占用看 admin dashboard · 系统内存压力 ${pct}%`;
} else {
els.mem.textContent = `内存 ${gb(used)}/${gb(total)}GB · ${pct}%`;
els.mem.title = `系统内存使用 ${pct}%(总 ${gb(total)}GB)`;
}
els.mem.className = pct > 90 ? 'badge danger' : pct > 75 ? 'badge warn' : 'badge';
}
if (m.cpu) {
const cpu = m.cpu;
const cpuPct = cpu.usagePercent.toFixed(0);
els.cpu.textContent = `CPU ${cpuPct}% · ${cpu.count}`;
els.cpu.className = cpu.usagePercent > 80 ? 'badge danger' : cpu.usagePercent > 50 ? 'badge warn' : 'badge';
}
}
function gb(bytes) {
return (bytes / 1024 / 1024 / 1024).toFixed(1);
}
export function shortName(name) {
if (!name) return '?';
return String(name)
.replace(':latest', '')
.replace(/-A\d+B(?=-|$)/i, '');
}
export function start() {
if (_timer) return;
fetchOnce();
_timer = setInterval(fetchOnce, CONFIG.METRICS_POLL_MS);
}
export function stop() {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
}