feat: 缓存命中率有来源了(assistant/chunk 的 usage 事件)+ DSH 启动自动拉起浮窗
挖到真源:事件流里 assistant/chunk 带 chunk.type='usage'
{inputTokens,outputTokens,cacheReadTokens,reasoningTokens}
从此算出真实指标(不再依赖 tokenMeter 的估算):
缓存命中率 = cacheRead/(input+cacheRead) 实测 0.9976(与 App 状态栏 99% 一致)
总量 = output + input + cacheRead 实测 88.99M
tok/s = outputTokens/该步耗时 实测 37.3
上下文占用 = 最后一步的 input+cacheRead / 窗口(不是历史累加) 实测 56.3%
插件与镜像桥两侧都接上了;镜像桥改为跟随插件认定的当前会话。
另:autoOpen(默认开)——DSH 启动后 1.5s 若浮窗没在跑就自动拉起(用户选定)。
This commit is contained in:
+46
-9
@@ -155,7 +155,7 @@ const SAMPLE_TYPES = new Set(['user/message', 'assistant/chunk', 'assistant/mess
|
||||
export function apply(ctx, config) {
|
||||
// 浮窗设置:DSH 设置页写进 profile 条目 config 的值 + 默认值
|
||||
const floatCfg = Object.assign(
|
||||
{ topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque', tool: true, button: true },
|
||||
{ topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque', tool: true, button: true, autoOpen: true },
|
||||
(config && typeof config === 'object') ? config : {},
|
||||
)
|
||||
|
||||
@@ -237,7 +237,7 @@ export function apply(ctx, config) {
|
||||
|
||||
function counter(id) {
|
||||
let c = counts.get(id)
|
||||
if (!c) { c = { turns: 0, steps: 0, outTokens: 0, decodeMs: 0, stepStart: 0, lastTps: null }; counts.set(id, c) }
|
||||
if (!c) { c = { turns: 0, steps: 0, outTokens: 0, inTokens: 0, cacheRead: 0, decodeMs: 0, stepStart: 0, lastTps: null }; counts.set(id, c) }
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -275,11 +275,25 @@ export function apply(ctx, config) {
|
||||
}
|
||||
else if (type === 'step/start') c.stepStart = Date.now()
|
||||
else if (type === 'step/end') c.steps++
|
||||
else if (type === 'assistant/chunk') {
|
||||
// 真实用量在这里(实测):data.chunk = {type:'usage', usage:{inputTokens,outputTokens,cacheReadTokens,reasoningTokens}}
|
||||
const ch = d2(event)?.chunk
|
||||
if (ch?.type === 'usage' && ch.usage) {
|
||||
const u = ch.usage
|
||||
c.inTokens += u.inputTokens || 0
|
||||
c.cacheRead += u.cacheReadTokens || 0
|
||||
c.outTokens += u.outputTokens || 0
|
||||
c.lastPrompt = (u.inputTokens || 0) + (u.cacheReadTokens || 0) // 当前上下文占用(最后一步输入)
|
||||
const ms = c.stepStart ? Date.now() - c.stepStart : 0
|
||||
if (u.outputTokens && ms > 0) { c.decodeMs += ms; c.lastTps = u.outputTokens / (ms / 1000) }
|
||||
}
|
||||
}
|
||||
else if (type === 'assistant/message') {
|
||||
const usage = event?.usage || event?.message?.usage || {}
|
||||
// 兜底:有些版本把 usage 挂在 message 上
|
||||
const usage = d2(event)?.usage || d2(event)?.message?.usage || {}
|
||||
const out = usage.outputTokens ?? usage.output_tokens ?? usage.completionTokens
|
||||
const ms = c.stepStart ? Date.now() - c.stepStart : 0
|
||||
if (out && ms > 0) { c.outTokens += out; c.decodeMs += ms; c.lastTps = out / (ms / 1000) }
|
||||
if (out && ms > 0 && !c.outTokens) { c.outTokens += out; c.decodeMs += ms; c.lastTps = out / (ms / 1000) }
|
||||
}
|
||||
const row = toRow(event)
|
||||
lastRow = row ? { type, row } : { type, row: null }
|
||||
@@ -297,13 +311,16 @@ export function apply(ctx, config) {
|
||||
let meterKeysLogged = false
|
||||
function metricsFor(session) {
|
||||
const id = session?.id || lastSessionId || 'unknown'
|
||||
const c = counts.get(id) || { turns: 0, steps: 0, outTokens: 0, decodeMs: 0, lastTps: null }
|
||||
const c = counts.get(id) || { turns: 0, steps: 0, outTokens: 0, inTokens: 0, cacheRead: 0, decodeMs: 0, lastTps: null }
|
||||
const promptTokens = (c.inTokens || 0) + (c.cacheRead || 0)
|
||||
const out = {
|
||||
turns: c.turns || null,
|
||||
steps: c.steps || null,
|
||||
tps: c.lastTps,
|
||||
tokens: null,
|
||||
cacheHit: null,
|
||||
// 总量 = 输出 + (未缓存输入 + 命中缓存输入)
|
||||
tokens: (c.outTokens || 0) + promptTokens || null,
|
||||
// 命中率 = 命中缓存输入 / 全部输入(与 App 状态栏口径一致)
|
||||
cacheHit: promptTokens > 0 ? (c.cacheRead || 0) / promptTokens : null,
|
||||
contextUsed: null,
|
||||
}
|
||||
// 有 session-projection 就用它(更权威),否则用事件计数兜底
|
||||
@@ -322,7 +339,8 @@ export function apply(ctx, config) {
|
||||
if (m) {
|
||||
// 第一次把真实字段名记进状态文件,便于在用户机器上核对映射
|
||||
if (!meterKeysLogged) { meterKeysLogged = true; log('tokenMeter keys: ' + Object.keys(m).join(',')) }
|
||||
out.tokens = m.totalTokens ?? m.surfaceTokens ?? null
|
||||
// 事件流里已经拿到真实用量时不要被 tokenMeter 的估算覆盖
|
||||
if (!out.tokens) out.tokens = m.totalTokens ?? m.surfaceTokens ?? null
|
||||
const u = m.usage || m.providerUsage || m.usageBuckets
|
||||
if (u) {
|
||||
const read = u.cacheRead ?? u.cache_read ?? u.cacheReadTokens ?? 0
|
||||
@@ -331,10 +349,16 @@ export function apply(ctx, config) {
|
||||
if (denom > 0) out.cacheHit = read / denom
|
||||
}
|
||||
let capacity = contextWindows.get(id) || modelContext() || 1000000 // 兜底:DSH 常用 1M 上下文
|
||||
const used = m.surfaceTokens ?? m.totalTokens
|
||||
// 上下文占比:优先用"最后一步输入"(事件流里拿得到,就是当前 surface),其次 tokenMeter
|
||||
const used = c.lastPrompt || m.surfaceTokens || m.totalTokens
|
||||
if (capacity && used) out.contextUsed = Math.min(1, used / capacity)
|
||||
}
|
||||
} catch (e) { if (!meterKeysLogged) { meterKeysLogged = true; log('tokenMeter measure failed: ' + String(e?.message || e)) } }
|
||||
// tokenMeter 拿不到时,用事件流里的"最后一步输入"兜底算上下文占比
|
||||
if (out.contextUsed == null && c.lastPrompt) {
|
||||
const cap = contextWindows.get(id) || modelContext() || 1000000
|
||||
if (cap) out.contextUsed = Math.min(1, c.lastPrompt / cap)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -785,5 +809,18 @@ border:1px solid rgba(120,140,180,.35);background:rgba(22,26,34,.92);color:#e6ea
|
||||
} catch (e) { log('settings registration failed:', String(e?.message || e)) }
|
||||
writeStatus()
|
||||
|
||||
// ---------------------------------------------------------------- DSH 启动时自动拉起浮窗
|
||||
// 用户选的就是这条:DSH 每次重启都会把浮窗(进程树后代)带走,所以启动后自动补一个。
|
||||
// 用 setTimeout 延后,让宿主先起完;回调里不碰 ctx(那时候它可能已经失效)。
|
||||
if (floatCfg.autoOpen !== false) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (shellAlive()) { log('autoOpen: float already running'); return }
|
||||
log('autoOpen: spawning float window')
|
||||
spawnShell().then((r) => log('autoOpen -> ' + short(r, 120))).catch((e) => log('autoOpen failed: ' + String(e?.message || e)))
|
||||
} catch (e) { log('autoOpen error: ' + String(e?.message || e)) }
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
log('ready; endpoints:', `${ROUTE}/events`, `${ROUTE}/state`, `${ROUTE}/health`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user