@goodandready/dsh-cost-meter 0.8.27 → 0.8.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analytics.js +56 -1
- package/lib/client.js +154 -18
- package/lib/commands.js +4 -0
- package/lib/export.js +12 -1
- package/lib/index.js +7 -0
- package/lib/projection.js +12 -0
- package/lib/routes.js +16 -1
- package/package.json +1 -1
package/lib/analytics.js
CHANGED
|
@@ -90,4 +90,59 @@ export function calculateBurnRateAndProjection(turns, totalCostUsd, options = {}
|
|
|
90
90
|
projectedCostUsd,
|
|
91
91
|
confidence,
|
|
92
92
|
}
|
|
93
|
-
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns estimated context window token limit for model identifier.
|
|
97
|
+
*/
|
|
98
|
+
export function getFallbackContextLength(model) {
|
|
99
|
+
const m = String(model || '').toLowerCase()
|
|
100
|
+
if (m.includes('claude-3-5') || m.includes('claude-3-7') || m.includes('claude-3-opus')) return 200000
|
|
101
|
+
if (m.includes('claude')) return 200000
|
|
102
|
+
if (m.includes('gemini-1.5') || m.includes('gemini-2.0') || m.includes('gemini-exp')) return 1000000
|
|
103
|
+
if (m.includes('gpt-4.1') || m.includes('gpt-4o') || m.includes('o1') || m.includes('o3')) return 128000
|
|
104
|
+
if (m.includes('qwen-2.5') || m.includes('qwq')) return 128000
|
|
105
|
+
if (m.includes('deepseek')) return 128000
|
|
106
|
+
return 128000
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Calculates context window saturation metrics and compaction risk.
|
|
111
|
+
*
|
|
112
|
+
* @param {Array<{ turn: number, uncachedInputTokens?: number, cacheReadTokens?: number }>} turns
|
|
113
|
+
* @param {object} [tariff]
|
|
114
|
+
* @param {object} [route]
|
|
115
|
+
* @returns {{ currentTokens: number, contextLength: number, saturationPercent: number, riskLevel: 'low' | 'moderate' | 'high', nextTurnEstUsd: number }}
|
|
116
|
+
*/
|
|
117
|
+
export function calculateContextMetrics(turns, tariff, route) {
|
|
118
|
+
const lastTurn = (Array.isArray(turns) && turns.length > 0) ? turns[turns.length - 1] : null
|
|
119
|
+
const currentTokens = lastTurn
|
|
120
|
+
? (Number(lastTurn.uncachedInputTokens || 0) + Number(lastTurn.cacheReadTokens || 0))
|
|
121
|
+
: 0
|
|
122
|
+
|
|
123
|
+
const contextLength = (tariff && Number(tariff.contextLength)) || getFallbackContextLength(route?.model)
|
|
124
|
+
const saturationPercent = contextLength > 0
|
|
125
|
+
? Math.min(100, Math.round((currentTokens / contextLength) * 100))
|
|
126
|
+
: 0
|
|
127
|
+
|
|
128
|
+
let riskLevel = 'low'
|
|
129
|
+
if (saturationPercent >= 80) {
|
|
130
|
+
riskLevel = 'high'
|
|
131
|
+
} else if (saturationPercent >= 50) {
|
|
132
|
+
riskLevel = 'moderate'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rates = tariff?.schedule?.base || {}
|
|
136
|
+
const inputRate = Number(rates.input) || 0.14
|
|
137
|
+
const nextTurnEstUsd = currentTokens > 0
|
|
138
|
+
? Number(((currentTokens * inputRate) / 1e6).toFixed(6))
|
|
139
|
+
: 0
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
currentTokens,
|
|
143
|
+
contextLength,
|
|
144
|
+
saturationPercent,
|
|
145
|
+
riskLevel,
|
|
146
|
+
nextTurnEstUsd,
|
|
147
|
+
}
|
|
148
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -91,6 +91,25 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
'turn.modal_title': 'Message Token & Cost Inspector',
|
|
92
92
|
'turn.tokens_total': 'Turn Tokens',
|
|
93
93
|
'turn.cost_title': 'Turn Cost',
|
|
94
|
+
'turn.reasoning': 'Thinking / Reasoning',
|
|
95
|
+
'context.title': 'Context Window',
|
|
96
|
+
'context.saturation': 'Context Saturation',
|
|
97
|
+
'context.risk_low': 'Healthy',
|
|
98
|
+
'context.risk_moderate': 'Moderate',
|
|
99
|
+
'context.risk_high': 'Compaction Risk',
|
|
100
|
+
'context.next_turn': 'Next turn base: ≈ {cost}',
|
|
101
|
+
'context.compaction_warning': 'Context saturation over 80%. High compaction and prompt fee risk.',
|
|
102
|
+
|
|
103
|
+
'privacy.toggle': 'Streamer / Privacy Mode',
|
|
104
|
+
'privacy.enabled': 'Privacy mode active (amounts masked)',
|
|
105
|
+
'field.privacyMode': 'Privacy / Streamer Mode',
|
|
106
|
+
'hint.privacyMode': 'Mask currency amounts with asterisks to safely share screens or recordings.',
|
|
107
|
+
'field.showHeaderChip': 'Show Cost Chip in Header',
|
|
108
|
+
'hint.showHeaderChip': 'Display the live cost meter chip in the chat conversation header.',
|
|
109
|
+
'field.showMessageAction': 'Show Token Inspector on Messages',
|
|
110
|
+
'hint.showMessageAction': 'Display the lightning token inspector button on assistant messages.',
|
|
111
|
+
'notice.hidden_access': 'Cost metering is active in background. Full statistics and session data remain available in this settings panel and via the /cost command.',
|
|
112
|
+
|
|
94
113
|
'turn.cache_hit_rate': 'Cache Hit',
|
|
95
114
|
'turn.duration': 'Duration',
|
|
96
115
|
'turn.uncached_in': 'Uncached Input',
|
|
@@ -209,6 +228,25 @@ window.__ModuleLoader__.load({
|
|
|
209
228
|
'turn.modal_title': '单次回复 Token 与成本详情',
|
|
210
229
|
'turn.tokens_total': '本轮 Token 总量',
|
|
211
230
|
'turn.cost_title': '单次成本',
|
|
231
|
+
'turn.reasoning': '思考推理 Token',
|
|
232
|
+
'context.title': '上下文窗口',
|
|
233
|
+
'context.saturation': '上下文饱和度',
|
|
234
|
+
'context.risk_low': '充足',
|
|
235
|
+
'context.risk_moderate': '适中',
|
|
236
|
+
'context.risk_high': '压缩风险',
|
|
237
|
+
'context.next_turn': '下轮基础成本:≈ {cost}',
|
|
238
|
+
'context.compaction_warning': '上下文占用超过 80%,存在压缩丢忆与费用激增风险。',
|
|
239
|
+
|
|
240
|
+
'privacy.toggle': '主播/隐私模式',
|
|
241
|
+
'privacy.enabled': '隐私模式已开启(金额已隐藏)',
|
|
242
|
+
'field.privacyMode': '隐私/主播模式',
|
|
243
|
+
'hint.privacyMode': '隐藏所有金额显示(替换为星号),便于录屏、直播或截图分享。',
|
|
244
|
+
'field.showHeaderChip': '在顶部显示消费小组件',
|
|
245
|
+
'hint.showHeaderChip': '在对话顶部标题栏显示实时费用与费率倒计时小组件。',
|
|
246
|
+
'field.showMessageAction': '在单条消息旁显示 Token 详情按钮',
|
|
247
|
+
'hint.showMessageAction': '在助手回复的消息操作栏中显示闪电 Token 详情按钮。',
|
|
248
|
+
'notice.hidden_access': '后台费用计量与记录正常运行。即使隐藏顶部组件,仍可在此设置面板或通过 /cost 命令随时查看完整统计。',
|
|
249
|
+
|
|
212
250
|
'turn.cache_hit_rate': '缓存命中率',
|
|
213
251
|
'turn.duration': '耗时',
|
|
214
252
|
'turn.uncached_in': '未命中输入',
|
|
@@ -381,6 +419,19 @@ window.__ModuleLoader__.load({
|
|
|
381
419
|
.dcm-proj-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
382
420
|
.dcm-proj-val { font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
383
421
|
|
|
422
|
+
.dcm-odometer { display: inline-block; font-variant-numeric: tabular-nums; transition: transform 0.15s ease-out; }
|
|
423
|
+
@media (prefers-reduced-motion: reduce) { .dcm-odometer { transition: none !important; } }
|
|
424
|
+
.dcm-context-gauge { margin: 8px 0; padding: 8px 10px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 60%, transparent); border: 1px solid var(--dsw-alias-border-l2); display: flex; flex-direction: column; gap: 5px; }
|
|
425
|
+
.dcm-context-header { display: flex; justify-content: space-between; align-items: center; font-size: 11px; }
|
|
426
|
+
.dcm-context-bar-bg { width: 100%; height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 90%, transparent); overflow: hidden; }
|
|
427
|
+
.dcm-context-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s ease-out; }
|
|
428
|
+
.dcm-context-footer { display: flex; justify-content: space-between; font-size: 10.5px; color: var(--dsw-alias-label-secondary); }
|
|
429
|
+
.dcm-risk-badge { padding: 1px 5px; border-radius: 8px; font-size: 10px; font-weight: 500; }
|
|
430
|
+
.dcm-risk-low { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); color: var(--dsw-alias-state-success-primary); }
|
|
431
|
+
.dcm-risk-moderate { background: color-mix(in srgb, var(--dsw-alias-state-warning-primary) 15%, transparent); color: var(--dsw-alias-state-warning-primary); }
|
|
432
|
+
.dcm-risk-high { background: color-mix(in srgb, var(--dsw-alias-state-danger-primary) 15%, transparent); color: var(--dsw-alias-state-danger-primary); }
|
|
433
|
+
|
|
434
|
+
|
|
384
435
|
.dcm-chip-alert-peak { box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-warning-primary) 50%, transparent); animation: dcm-pulse-peak 2s infinite ease-in-out; }
|
|
385
436
|
.dcm-chip-alert-offpeak { box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-success-primary) 50%, transparent); animation: dcm-pulse-offpeak 2s infinite ease-in-out; }
|
|
386
437
|
@keyframes dcm-pulse-peak { 0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--dsw-alias-state-warning-primary) 30%, transparent); } 50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--dsw-alias-state-warning-primary) 70%, transparent); } }
|
|
@@ -700,6 +751,9 @@ window.__ModuleLoader__.load({
|
|
|
700
751
|
alertBeforeSwitch: stored.alertBeforeSwitch !== undefined ? Boolean(stored.alertBeforeSwitch) : true,
|
|
701
752
|
alertLeadMinutes: stored.alertLeadMinutes !== undefined ? String(stored.alertLeadMinutes) : '5',
|
|
702
753
|
browserNotifications: stored.browserNotifications !== undefined ? Boolean(stored.browserNotifications) : false,
|
|
754
|
+
privacyMode: stored.privacyMode !== undefined ? Boolean(stored.privacyMode) : false,
|
|
755
|
+
showHeaderChip: stored.showHeaderChip !== undefined ? Boolean(stored.showHeaderChip) : true,
|
|
756
|
+
showMessageAction: stored.showMessageAction !== undefined ? Boolean(stored.showMessageAction) : true,
|
|
703
757
|
useOpenRouter: stored.useOpenRouter !== undefined ? Boolean(stored.useOpenRouter) : true,
|
|
704
758
|
refreshHours: stored.refreshHours !== undefined ? String(stored.refreshHours) : '24',
|
|
705
759
|
})
|
|
@@ -1207,13 +1261,64 @@ window.__ModuleLoader__.load({
|
|
|
1207
1261
|
return str
|
|
1208
1262
|
}
|
|
1209
1263
|
|
|
1264
|
+
function useAnimatedNumber(target, enabled = true) {
|
|
1265
|
+
const [display, setDisplay] = React.useState(target)
|
|
1266
|
+
const animRef = React.useRef(null)
|
|
1267
|
+
const currentRef = React.useRef(target)
|
|
1268
|
+
|
|
1269
|
+
React.useEffect(() => {
|
|
1270
|
+
const prefersReduced = typeof window !== 'undefined' &&
|
|
1271
|
+
window.matchMedia &&
|
|
1272
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
1273
|
+
|
|
1274
|
+
if (prefersReduced || !enabled || typeof target !== 'number' || isNaN(target)) {
|
|
1275
|
+
currentRef.current = target
|
|
1276
|
+
setDisplay(target)
|
|
1277
|
+
return
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const start = currentRef.current
|
|
1281
|
+
const diff = target - start
|
|
1282
|
+
if (Math.abs(diff) < 0.00001) return
|
|
1283
|
+
|
|
1284
|
+
const duration = Math.min(800, Math.max(300, Math.abs(diff) * 1000))
|
|
1285
|
+
const startTime = performance.now()
|
|
1286
|
+
|
|
1287
|
+
const step = (now) => {
|
|
1288
|
+
const elapsed = now - startTime
|
|
1289
|
+
const progress = Math.min(1, elapsed / duration)
|
|
1290
|
+
const ease = 1 - Math.pow(1 - progress, 3)
|
|
1291
|
+
const current = start + diff * ease
|
|
1292
|
+
currentRef.current = current
|
|
1293
|
+
setDisplay(current)
|
|
1294
|
+
|
|
1295
|
+
if (progress < 1) {
|
|
1296
|
+
animRef.current = requestAnimationFrame(step)
|
|
1297
|
+
} else {
|
|
1298
|
+
currentRef.current = target
|
|
1299
|
+
setDisplay(target)
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (animRef.current) cancelAnimationFrame(animRef.current)
|
|
1304
|
+
animRef.current = requestAnimationFrame(step)
|
|
1305
|
+
|
|
1306
|
+
return () => {
|
|
1307
|
+
if (animRef.current) cancelAnimationFrame(animRef.current)
|
|
1308
|
+
}
|
|
1309
|
+
}, [target, enabled])
|
|
1310
|
+
|
|
1311
|
+
return display
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1210
1314
|
function rate(value) {
|
|
1211
1315
|
if (!value) return '—'
|
|
1212
1316
|
const text = value < 0.1 ? value.toFixed(3) : value.toFixed(2)
|
|
1213
1317
|
return '$' + text.replace(/(\\.\\d*?)0+$/, '$1').replace(/\\.$/, '')
|
|
1214
1318
|
}
|
|
1215
1319
|
|
|
1216
|
-
function money(value, currency) {
|
|
1320
|
+
function money(value, currency, privacy = false) {
|
|
1321
|
+
if (privacy) return currency + '••••'
|
|
1217
1322
|
const n = Number(value) || 0
|
|
1218
1323
|
return currency + n.toFixed(Math.abs(n) >= 100 ? 0 : 2)
|
|
1219
1324
|
}
|
|
@@ -1309,7 +1414,7 @@ window.__ModuleLoader__.load({
|
|
|
1309
1414
|
|
|
1310
1415
|
const tip = (t ? t('sparkline.steps') : 'step') + ' ' + (item.turn || idx + 1) +
|
|
1311
1416
|
(item.step ? '.' + item.step : '') + ': ' + tokens(tok) +
|
|
1312
|
-
(stepCost > 0 ? ' (≈ ' + money(stepCost, currency) + ')' : '')
|
|
1417
|
+
(stepCost > 0 ? ' (≈ ' + money(stepCost, currency, isPrivacy) + ')' : '')
|
|
1313
1418
|
|
|
1314
1419
|
return h('rect', {
|
|
1315
1420
|
key: 'bar-' + idx,
|
|
@@ -1474,6 +1579,16 @@ window.__ModuleLoader__.load({
|
|
|
1474
1579
|
const locale = useActiveLocale(ctx)
|
|
1475
1580
|
const t = props.t || makeT(locale === 'zh' ? zh : en, en)
|
|
1476
1581
|
const [open, setOpen] = React.useState(false)
|
|
1582
|
+
const configForms = getSettingsScope(ctx)
|
|
1583
|
+
const scope = React.useMemo(() => (configForms ? configForms.get(NS) : undefined), [configForms])
|
|
1584
|
+
const snapshot = React.useSyncExternalStore(
|
|
1585
|
+
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
1586
|
+
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
|
|
1587
|
+
React.useCallback(() => ({ status: 'loading' }), []),
|
|
1588
|
+
)
|
|
1589
|
+
const stored = (snapshot && snapshot.value) || {}
|
|
1590
|
+
if (stored.showMessageAction === false) return null
|
|
1591
|
+
|
|
1477
1592
|
const useProjection = props.useProjection || (ctx && ctx.useProjection)
|
|
1478
1593
|
const byModel = useProjection ? useProjection('costByModel') : undefined
|
|
1479
1594
|
|
|
@@ -1501,6 +1616,8 @@ window.__ModuleLoader__.load({
|
|
|
1501
1616
|
: (uncached + hit > 0 ? Math.round((hit / (uncached + hit)) * 100) : 0)
|
|
1502
1617
|
const durMs = Number(tr.durationMs) || 0
|
|
1503
1618
|
const cost = Number(tr.costDeltaUsd) || 0
|
|
1619
|
+
const reasoning = Number(tr.reasoningTokens) || 0
|
|
1620
|
+
const isPrivacy = Boolean(stored.privacyMode)
|
|
1504
1621
|
|
|
1505
1622
|
const fmtDur = (ms) => {
|
|
1506
1623
|
if (!ms || ms <= 0) return '0s'
|
|
@@ -1515,7 +1632,7 @@ window.__ModuleLoader__.load({
|
|
|
1515
1632
|
key: 'btn',
|
|
1516
1633
|
type: 'button',
|
|
1517
1634
|
className: 'dcm-msg-action-btn',
|
|
1518
|
-
title: t('turn.modal_title'),
|
|
1635
|
+
title: t('turn.modal_title') + (reasoning > 0 ? (' (🧠 ' + tokens(reasoning) + ')') : ''),
|
|
1519
1636
|
onClick: () => setOpen(true),
|
|
1520
1637
|
}, [
|
|
1521
1638
|
React.createElement('span', { key: 'icon' }, '⚡'),
|
|
@@ -1554,7 +1671,7 @@ window.__ModuleLoader__.load({
|
|
|
1554
1671
|
]),
|
|
1555
1672
|
React.createElement('div', { key: 's4', className: 'dcm-inspector-stat' }, [
|
|
1556
1673
|
React.createElement('div', { className: 'dcm-inspector-stat-lbl' }, t('turn.cost_title')),
|
|
1557
|
-
React.createElement('div', { className: 'dcm-inspector-stat-val' }, cost > 0 ? ('$' + cost.toFixed(4)) : '—'),
|
|
1674
|
+
React.createElement('div', { className: 'dcm-inspector-stat-val' }, isPrivacy ? '$••••' : (cost > 0 ? ('$' + cost.toFixed(4)) : '—')),
|
|
1558
1675
|
]),
|
|
1559
1676
|
]),
|
|
1560
1677
|
React.createElement('div', { key: 'breakdown', style: { display: 'flex', flexDirection: 'column', gap: 6, fontSize: 11.5, marginTop: 8 } }, [
|
|
@@ -1570,6 +1687,10 @@ window.__ModuleLoader__.load({
|
|
|
1570
1687
|
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('turn.output')),
|
|
1571
1688
|
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, tokens(out)),
|
|
1572
1689
|
]),
|
|
1690
|
+
reasoning > 0 ? React.createElement('div', { key: 'b-reasoning', style: { display: 'flex', justifyContent: 'space-between', color: 'var(--dsw-alias-brand-primary)' } }, [
|
|
1691
|
+
React.createElement('span', null, '🧠 ' + t('turn.reasoning')),
|
|
1692
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, tokens(reasoning)),
|
|
1693
|
+
]) : null,
|
|
1573
1694
|
]),
|
|
1574
1695
|
])
|
|
1575
1696
|
) : null
|
|
@@ -1580,6 +1701,13 @@ window.__ModuleLoader__.load({
|
|
|
1580
1701
|
// ------------------------------------------------------------ CostMeter
|
|
1581
1702
|
|
|
1582
1703
|
|
|
1704
|
+
function AnimatedCost(props) {
|
|
1705
|
+
const { value = 0, currency = '$', isPrivacy = false, className = '' } = props || {}
|
|
1706
|
+
const animated = useAnimatedNumber(Number(value) || 0, !isPrivacy)
|
|
1707
|
+
if (isPrivacy) return React.createElement('span', { className }, currency + '••••')
|
|
1708
|
+
return React.createElement('span', { className }, money(animated, currency))
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1583
1711
|
function CostMeter(props) {
|
|
1584
1712
|
const ctx = props.ctx
|
|
1585
1713
|
const locale = useActiveLocale(ctx)
|
|
@@ -1595,6 +1723,7 @@ window.__ModuleLoader__.load({
|
|
|
1595
1723
|
const [showAllModels, setShowAllModels] = React.useState(false)
|
|
1596
1724
|
const [showTurnInspector, setShowTurnInspector] = React.useState(false)
|
|
1597
1725
|
const [selectedCurrency, setSelectedCurrency] = React.useState(null)
|
|
1726
|
+
const [selectedPrivacy, setSelectedPrivacy] = React.useState(null)
|
|
1598
1727
|
const [exportingPng, setExportingPng] = React.useState(false)
|
|
1599
1728
|
const lastAlertRef = React.useRef(null)
|
|
1600
1729
|
const box = React.useRef(null)
|
|
@@ -1685,8 +1814,11 @@ window.__ModuleLoader__.load({
|
|
|
1685
1814
|
|
|
1686
1815
|
if (!state) return null
|
|
1687
1816
|
|
|
1688
|
-
const turns = (byModel?.turns && byModel.turns.length > 0) ? byModel.turns : (state.turns || [])
|
|
1689
1817
|
const stored = (settingsSnap && settingsSnap.value) || {}
|
|
1818
|
+
const showHeaderChip = stored.showHeaderChip !== undefined ? Boolean(stored.showHeaderChip) : (state.showHeaderChip !== false)
|
|
1819
|
+
if (!showHeaderChip) return null
|
|
1820
|
+
|
|
1821
|
+
const turns = (byModel?.turns && byModel.turns.length > 0) ? byModel.turns : (state.turns || [])
|
|
1690
1822
|
const CURRENCY_LIST = [{ symbol: '$', code: 'USD' }, { symbol: '¥', code: 'CNY' }, { symbol: '₽', code: 'RUB' }, { symbol: '€', code: 'EUR' }]
|
|
1691
1823
|
const activeCurrency = selectedCurrency || (stored.currency !== undefined ? String(stored.currency) : (state.currency || '$'))
|
|
1692
1824
|
const fxRates = state.fxRates || { USD: 1, RUB: 80, CNY: 7.15, EUR: 0.92 }
|
|
@@ -1695,6 +1827,8 @@ window.__ModuleLoader__.load({
|
|
|
1695
1827
|
? Number(fxRates[normCode])
|
|
1696
1828
|
: (stored.usdRate !== undefined && !isNaN(Number(stored.usdRate)) ? Number(stored.usdRate) : (Number(state.usdRate) || 1))
|
|
1697
1829
|
const currency = activeCurrency
|
|
1830
|
+
const isPrivacy = selectedPrivacy !== null ? selectedPrivacy : (stored.privacyMode !== undefined ? Boolean(stored.privacyMode) : Boolean(state.privacyMode))
|
|
1831
|
+
const formatMoney = (val) => money(val, currency, isPrivacy)
|
|
1698
1832
|
|
|
1699
1833
|
const handleCurrencySelect = (sym) => {
|
|
1700
1834
|
setSelectedCurrency(sym)
|
|
@@ -1833,7 +1967,7 @@ window.__ModuleLoader__.load({
|
|
|
1833
1967
|
return [
|
|
1834
1968
|
h('div', { key: label + '-l', className: 'dcm-td' }, label),
|
|
1835
1969
|
h('div', { key: label + '-n', className: 'dcm-num' }, tokens(n)),
|
|
1836
|
-
h('div', { key: label + '-v', className: 'dcm-cost' },
|
|
1970
|
+
h('div', { key: label + '-v', className: 'dcm-cost' }, formatMoney(usd * fx)),
|
|
1837
1971
|
]
|
|
1838
1972
|
}
|
|
1839
1973
|
|
|
@@ -1879,12 +2013,12 @@ window.__ModuleLoader__.load({
|
|
|
1879
2013
|
const copyReport = () => {
|
|
1880
2014
|
const lines = [
|
|
1881
2015
|
'=== ' + (activeModelName || (state.route ? state.route.model : 'Cost Meter Report')) + ' ===',
|
|
1882
|
-
t('tokens.session_total') + ' ' + (hasUsage ?
|
|
2016
|
+
t('tokens.session_total') + ' ' + (hasUsage ? formatMoney(total) : '—') + (budgetThreshold > 0 ? ' (limit: ' + money(budgetThreshold * fx, currency) + ')' : ''),
|
|
1883
2017
|
'Tier: ' + (flat ? 'Flat' : (cheapNow ? t('offpeak') : t('peak'))),
|
|
1884
2018
|
'Rates (1M): input ' + rate(active.input) + ', output ' + rate(active.output) + ', cache ' + rate(active.cacheHit),
|
|
1885
2019
|
]
|
|
1886
2020
|
if (cacheSavedAmount > 0) {
|
|
1887
|
-
lines.push('Cache saved: ≈ ' +
|
|
2021
|
+
lines.push('Cache saved: ≈ ' + formatMoney(cacheSavedAmount) + ' (' + cacheSavedPercent + '%)')
|
|
1888
2022
|
}
|
|
1889
2023
|
if (hasUsage && usage) {
|
|
1890
2024
|
lines.push('Tokens: in ' + tokens(usage.inputTokens) + ', out ' + tokens(usage.outputTokens) + ', cache hit ' + tokens(usage.cacheReadTokens))
|
|
@@ -1927,13 +2061,13 @@ window.__ModuleLoader__.load({
|
|
|
1927
2061
|
className: 'dcm-chip' + (displayMode === 'compact' ? ' dcm-chip-compact' : '') + (alertActive ? (alertDirection === 'peak' ? ' dcm-chip-alert-peak' : ' dcm-chip-alert-offpeak') : ''),
|
|
1928
2062
|
onClick: () => setOpen((v) => !v),
|
|
1929
2063
|
title: (budgetExceeded
|
|
1930
|
-
? t('budget.exceeded', { limit:
|
|
2064
|
+
? t('budget.exceeded', { limit: formatMoney(budgetThreshold * fx) })
|
|
1931
2065
|
: (flat ? 'Flat tariff' : cheapNow ? t('offpeak') : t('peak'))) +
|
|
1932
|
-
(cacheSavedAmount > 0 ? ' · ' + t('savings.cache_saved', { amount:
|
|
2066
|
+
(cacheSavedAmount > 0 ? ' · ' + t('savings.cache_saved', { amount: formatMoney(cacheSavedAmount), percent: cacheSavedPercent }) : ''),
|
|
1933
2067
|
|
|
1934
2068
|
}, [
|
|
1935
2069
|
h('span', { key: 'dot', className: dotClass }),
|
|
1936
|
-
h(
|
|
2070
|
+
hasUsage ? h(AnimatedCost, { key: 'sum', value: total, currency, isPrivacy, className: 'dcm-odometer' }) : h('span', { key: 'sum' }, '—'),
|
|
1937
2071
|
left ? h('span', { key: 'left', className: 'dcm-chip-timer' }, left) : null,
|
|
1938
2072
|
]),
|
|
1939
2073
|
|
|
@@ -1973,11 +2107,11 @@ window.__ModuleLoader__.load({
|
|
|
1973
2107
|
|
|
1974
2108
|
cacheSavedAmount > 0
|
|
1975
2109
|
? h('div', { key: 'cache-saved-box', className: 'dcm-cache-box', style: { marginTop: 6 } },
|
|
1976
|
-
t('savings.cache_saved', { amount:
|
|
2110
|
+
t('savings.cache_saved', { amount: formatMoney(cacheSavedAmount), percent: cacheSavedPercent }))
|
|
1977
2111
|
: null,
|
|
1978
2112
|
|
|
1979
2113
|
budgetExceeded ? h('div', { key: 'budget-warn', className: 'dcm-alert dcm-alert-err', style: { marginTop: 6, padding: '6px 10px', fontSize: 11 } },
|
|
1980
|
-
t('budget.exceeded', { limit:
|
|
2114
|
+
t('budget.exceeded', { limit: formatMoney(budgetThreshold * fx) })) : null,
|
|
1981
2115
|
|
|
1982
2116
|
renderSparkline(turns, {
|
|
1983
2117
|
h,
|
|
@@ -2009,10 +2143,12 @@ window.__ModuleLoader__.load({
|
|
|
2009
2143
|
? tr.cacheHitRate
|
|
2010
2144
|
: (uncached + hit > 0 ? Math.round((hit / (uncached + hit)) * 100) : 0)
|
|
2011
2145
|
const cost = Number(tr.costDeltaUsd) || 0
|
|
2146
|
+
const reasoning = Number(tr.reasoningTokens) || 0
|
|
2147
|
+
const isPrivacy = Boolean(stored.privacyMode)
|
|
2012
2148
|
return h('div', { key: 'tr-' + idx, className: 'dcm-turn-row' }, [
|
|
2013
2149
|
h('span', { key: 'tn', style: { fontWeight: 500 } }, 'T#' + tr.turn + (tr.step ? '.' + tr.step : '')),
|
|
2014
2150
|
h('span', { key: 'tok', style: { color: 'var(--dsw-alias-label-secondary)' } }, tokens(tok) + ' tok (' + hitRate + '% hit)'),
|
|
2015
|
-
h('span', { key: 'c', style: { fontWeight: 600 } }, cost > 0 ?
|
|
2151
|
+
h('span', { key: 'c', style: { fontWeight: 600 } }, cost > 0 ? formatMoney(cost * fx) : '—'),
|
|
2016
2152
|
])
|
|
2017
2153
|
})
|
|
2018
2154
|
) : null,
|
|
@@ -2026,11 +2162,11 @@ window.__ModuleLoader__.load({
|
|
|
2026
2162
|
return h('div', { key: 'proj-box', className: 'dcm-proj-box' }, [
|
|
2027
2163
|
h('span', { key: 'burn', className: 'dcm-proj-item' }, [
|
|
2028
2164
|
h('span', null, t('projection.burn_rate') + ' '),
|
|
2029
|
-
h('span', { className: 'dcm-proj-val' },
|
|
2165
|
+
h('span', { className: 'dcm-proj-val' }, formatMoney(burnRate) + t('projection.per_hour')),
|
|
2030
2166
|
]),
|
|
2031
2167
|
h('span', { key: 'proj', className: 'dcm-proj-item' }, [
|
|
2032
2168
|
h('span', null, t('projection.projected') + ' '),
|
|
2033
|
-
h('span', { className: 'dcm-proj-val' }, '≈ ' +
|
|
2169
|
+
h('span', { className: 'dcm-proj-val' }, '≈ ' + formatMoney(projectedTotal)),
|
|
2034
2170
|
]),
|
|
2035
2171
|
])
|
|
2036
2172
|
})(),
|
|
@@ -2102,7 +2238,7 @@ window.__ModuleLoader__.load({
|
|
|
2102
2238
|
color: m.known ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
2103
2239
|
},
|
|
2104
2240
|
},
|
|
2105
|
-
m.known ? (hasUsage ?
|
|
2241
|
+
m.known ? (hasUsage ? formatMoney(m.cost) : '—') : t('tokens.not_priced'),
|
|
2106
2242
|
),
|
|
2107
2243
|
],
|
|
2108
2244
|
),
|
|
@@ -2122,7 +2258,7 @@ window.__ModuleLoader__.load({
|
|
|
2122
2258
|
|
|
2123
2259
|
h('div', { key: 'total', className: 'dcm-total' }, [
|
|
2124
2260
|
h('span', { key: 'l' }, t('tokens.session_total')),
|
|
2125
|
-
h('span', { key: 'v' }, hasUsage ? '≈ ' +
|
|
2261
|
+
h('span', { key: 'v' }, hasUsage ? '≈ ' + formatMoney(total) : '—'),
|
|
2126
2262
|
]),
|
|
2127
2263
|
|
|
2128
2264
|
hasUsage ? null : h('div', { key: 'nodata', className: 'dcm-panel-note', style: { paddingTop: 6 } },
|
package/lib/commands.js
CHANGED
|
@@ -60,6 +60,10 @@ export function formatCostReceipt(data) {
|
|
|
60
60
|
`| **Output Tokens** | ${formatNumber(output)} |`,
|
|
61
61
|
]
|
|
62
62
|
|
|
63
|
+
if (tokens.reasoningTokens > 0) {
|
|
64
|
+
lines.push(`| **Thinking / Reasoning** | 🧠 ${formatNumber(tokens.reasoningTokens)} |`)
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
if (cacheWrite > 0) {
|
|
64
68
|
lines.push(`| **Cache Write** | ${formatNumber(cacheWrite)} |`)
|
|
65
69
|
}
|
package/lib/export.js
CHANGED
|
@@ -40,6 +40,7 @@ export function generateSessionCsv(data) {
|
|
|
40
40
|
'Cache_Read_Tokens',
|
|
41
41
|
'Cache_Hit_Rate_%',
|
|
42
42
|
'Output_Tokens',
|
|
43
|
+
'Reasoning_Tokens',
|
|
43
44
|
'Total_Tokens',
|
|
44
45
|
'Cost_USD',
|
|
45
46
|
'Cost_Local',
|
|
@@ -51,6 +52,7 @@ export function generateSessionCsv(data) {
|
|
|
51
52
|
let sumUncached = 0
|
|
52
53
|
let sumHit = 0
|
|
53
54
|
let sumOut = 0
|
|
55
|
+
let sumReasoning = 0
|
|
54
56
|
let sumTotalTok = 0
|
|
55
57
|
let sumCostUsd = 0
|
|
56
58
|
|
|
@@ -60,6 +62,7 @@ export function generateSessionCsv(data) {
|
|
|
60
62
|
const uncached = Number(t.uncachedInputTokens) || 0
|
|
61
63
|
const hit = Number(t.cacheReadTokens) || 0
|
|
62
64
|
const out = Number(t.outputTokens) || 0
|
|
65
|
+
const reasoning = Number(t.reasoningTokens) || 0
|
|
63
66
|
const totalTok = uncached + hit + out
|
|
64
67
|
const hitRate = t.cacheHitRate !== undefined
|
|
65
68
|
? t.cacheHitRate
|
|
@@ -70,6 +73,7 @@ export function generateSessionCsv(data) {
|
|
|
70
73
|
sumUncached += uncached
|
|
71
74
|
sumHit += hit
|
|
72
75
|
sumOut += out
|
|
76
|
+
sumReasoning += reasoning
|
|
73
77
|
sumTotalTok += totalTok
|
|
74
78
|
sumCostUsd += costUsd
|
|
75
79
|
|
|
@@ -83,6 +87,7 @@ export function generateSessionCsv(data) {
|
|
|
83
87
|
hit,
|
|
84
88
|
hitRate,
|
|
85
89
|
out,
|
|
90
|
+
reasoning,
|
|
86
91
|
totalTok,
|
|
87
92
|
costUsd.toFixed(6),
|
|
88
93
|
costLocal.toFixed(4),
|
|
@@ -103,6 +108,7 @@ export function generateSessionCsv(data) {
|
|
|
103
108
|
sumHit,
|
|
104
109
|
overallHitRate,
|
|
105
110
|
sumOut,
|
|
111
|
+
sumReasoning,
|
|
106
112
|
sumTotalTok,
|
|
107
113
|
(totalCostUsd || sumCostUsd).toFixed(6),
|
|
108
114
|
((totalCostUsd || sumCostUsd) * fx).toFixed(4),
|
|
@@ -137,14 +143,17 @@ export function generateSessionJson(data) {
|
|
|
137
143
|
let sumUncached = 0
|
|
138
144
|
let sumHit = 0
|
|
139
145
|
let sumOut = 0
|
|
146
|
+
let sumReasoning = 0
|
|
140
147
|
|
|
141
148
|
const turnsFormatted = turns.map((t) => {
|
|
142
149
|
const uncached = Number(t.uncachedInputTokens) || 0
|
|
143
150
|
const hit = Number(t.cacheReadTokens) || 0
|
|
144
151
|
const out = Number(t.outputTokens) || 0
|
|
152
|
+
const reasoning = Number(t.reasoningTokens) || 0
|
|
145
153
|
sumUncached += uncached
|
|
146
154
|
sumHit += hit
|
|
147
155
|
sumOut += out
|
|
156
|
+
sumReasoning += reasoning
|
|
148
157
|
return {
|
|
149
158
|
turn: t.turn,
|
|
150
159
|
step: t.step ?? 0,
|
|
@@ -153,6 +162,7 @@ export function generateSessionJson(data) {
|
|
|
153
162
|
uncachedInput: uncached,
|
|
154
163
|
cacheRead: hit,
|
|
155
164
|
output: out,
|
|
165
|
+
reasoningTokens: reasoning,
|
|
156
166
|
total: uncached + hit + out,
|
|
157
167
|
cacheHitRate: t.cacheHitRate !== undefined ? t.cacheHitRate : (uncached + hit > 0 ? Math.round((hit / (uncached + hit)) * 100) : 0),
|
|
158
168
|
},
|
|
@@ -180,6 +190,7 @@ export function generateSessionJson(data) {
|
|
|
180
190
|
uncachedInput: sumUncached,
|
|
181
191
|
cacheRead: sumHit,
|
|
182
192
|
output: sumOut,
|
|
193
|
+
reasoningTokens: sumReasoning,
|
|
183
194
|
total: sumUncached + sumHit + sumOut,
|
|
184
195
|
cacheHitRate: overallHitRate,
|
|
185
196
|
},
|
|
@@ -230,7 +241,7 @@ export function renderSummaryCardToCanvas(canvas, data) {
|
|
|
230
241
|
} = data || {}
|
|
231
242
|
|
|
232
243
|
const totalCost = costUsd * fx
|
|
233
|
-
const costFormatted = totalCost >= 1 ? `${currency}${totalCost.toFixed(2)}` : (totalCost > 0 ? `${currency}${totalCost.toFixed(4)}` : `${currency}0.00`)
|
|
244
|
+
const costFormatted = data?.privacyMode ? `${currency}****` : (totalCost >= 1 ? `${currency}${totalCost.toFixed(2)}` : (totalCost > 0 ? `${currency}${totalCost.toFixed(4)}` : `${currency}0.00`))
|
|
234
245
|
const uncached = Number(tokens.uncachedInputTokens || tokens.inputTokens) || 0
|
|
235
246
|
const hit = Number(tokens.cacheReadTokens) || 0
|
|
236
247
|
const out = Number(tokens.outputTokens) || 0
|
package/lib/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export * from './fx.js'
|
|
|
33
33
|
export * from './alerts.js'
|
|
34
34
|
export * from './commands.js'
|
|
35
35
|
export * from './export.js'
|
|
36
|
+
export * from './analytics.js'
|
|
36
37
|
export { STATE_PATH, REFRESH_PATH } from './routes.js'
|
|
37
38
|
import { indexCatalog, resolveTariff, matchModel, unwrapVolatile, calculateCacheSavings } from './tariff.js'
|
|
38
39
|
import { usageByModelProjection, getLastTurns, resetLastTurns } from './projection.js'
|
|
@@ -131,6 +132,9 @@ export function normalizeConfig(raw) {
|
|
|
131
132
|
alertBeforeSwitch: u.alertBeforeSwitch !== false,
|
|
132
133
|
alertLeadMinutes: Math.max(1, Math.min(30, Number(u.alertLeadMinutes) || 5)),
|
|
133
134
|
browserNotifications: Boolean(u.browserNotifications),
|
|
135
|
+
privacyMode: Boolean(u.privacyMode),
|
|
136
|
+
showHeaderChip: u.showHeaderChip !== false,
|
|
137
|
+
showMessageAction: u.showMessageAction !== false,
|
|
134
138
|
useOpenRouter: u.useOpenRouter !== false,
|
|
135
139
|
refreshHours: Number(u.refreshHours) || 24,
|
|
136
140
|
deepseekPeakPrices: u.deepseekPeakPrices || {},
|
|
@@ -162,6 +166,9 @@ export const Config = z.object({
|
|
|
162
166
|
alertBeforeSwitch: z.boolean().description('Pre-switch notification and visual alert before tariff change.').default(true).volatile(),
|
|
163
167
|
alertLeadMinutes: z.number().description('Minutes before tariff change to trigger pre-switch alert.').default(5).volatile(),
|
|
164
168
|
browserNotifications: z.boolean().description('Enable desktop browser notifications for tariff switches.').default(false).volatile(),
|
|
169
|
+
privacyMode: z.boolean().description('Streamer/Privacy mode: mask financial spend with asterisks while retaining token stats.').default(false).volatile(),
|
|
170
|
+
showHeaderChip: z.boolean().description('Show the cost meter chip in the conversation header.').default(true).volatile(),
|
|
171
|
+
showMessageAction: z.boolean().description('Show the token inspector action button on assistant messages.').default(true).volatile(),
|
|
165
172
|
deepseekPeakPrices: z.dict(PriceRow).description('Override the built-in DeepSeek peak rates, by model id.').default({}).volatile(),
|
|
166
173
|
useOpenRouter: z.boolean().description('Pull rates and discount windows from the public OpenRouter catalog.').default(true).volatile(),
|
|
167
174
|
refreshHours: z.number().description('How often to refetch the catalog.').default(24).volatile(),
|
package/lib/projection.js
CHANGED
|
@@ -20,6 +20,7 @@ export const bucketSchema = zod.object({
|
|
|
20
20
|
outputTokens: zod.number().nonnegative(),
|
|
21
21
|
cacheReadTokens: zod.number().nonnegative(),
|
|
22
22
|
cacheWriteTokens: zod.number().nonnegative(),
|
|
23
|
+
reasoningTokens: zod.number().nonnegative().optional(),
|
|
23
24
|
}).strict()
|
|
24
25
|
|
|
25
26
|
export const turnRecordSchema = zod.object({
|
|
@@ -30,6 +31,7 @@ export const turnRecordSchema = zod.object({
|
|
|
30
31
|
outputTokens: zod.number().nonnegative().optional(),
|
|
31
32
|
cacheReadTokens: zod.number().nonnegative().optional(),
|
|
32
33
|
cacheWriteTokens: zod.number().nonnegative().optional(),
|
|
34
|
+
reasoningTokens: zod.number().nonnegative().optional(),
|
|
33
35
|
costDeltaUsd: zod.number().nonnegative().optional(),
|
|
34
36
|
timestamp: zod.number().nonnegative(),
|
|
35
37
|
durationMs: zod.number().nonnegative().optional(),
|
|
@@ -38,11 +40,19 @@ export const turnRecordSchema = zod.object({
|
|
|
38
40
|
|
|
39
41
|
/** Pull the four buckets out of one provider usage record. */
|
|
40
42
|
export function bucketsFromUsage(usage) {
|
|
43
|
+
const reasoningTokens = Number(
|
|
44
|
+
usage?.completion_tokens_details?.reasoning_tokens ||
|
|
45
|
+
usage?.reasoningTokens ||
|
|
46
|
+
usage?.reasoning_tokens ||
|
|
47
|
+
usage?.details?.reasoning_tokens ||
|
|
48
|
+
0
|
|
49
|
+
) || 0
|
|
41
50
|
return {
|
|
42
51
|
uncachedInputTokens: Number(usage.inputTokens) || 0,
|
|
43
52
|
outputTokens: Number(usage.outputTokens) || 0,
|
|
44
53
|
cacheReadTokens: Number(usage.cacheReadTokens) || 0,
|
|
45
54
|
cacheWriteTokens: Number(usage.cacheWriteTokens) || 0,
|
|
55
|
+
reasoningTokens,
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -180,6 +190,7 @@ export const usageByModelProjection = {
|
|
|
180
190
|
outputTokens: buckets.outputTokens,
|
|
181
191
|
cacheReadTokens: buckets.cacheReadTokens,
|
|
182
192
|
cacheWriteTokens: buckets.cacheWriteTokens,
|
|
193
|
+
reasoningTokens: buckets.reasoningTokens || 0,
|
|
183
194
|
costDeltaUsd: nextTurns[existingIndex].costDeltaUsd || 0,
|
|
184
195
|
timestamp,
|
|
185
196
|
durationMs: nextTurns[existingIndex].durationMs || durationMs,
|
|
@@ -194,6 +205,7 @@ export const usageByModelProjection = {
|
|
|
194
205
|
outputTokens: buckets.outputTokens,
|
|
195
206
|
cacheReadTokens: buckets.cacheReadTokens,
|
|
196
207
|
cacheWriteTokens: buckets.cacheWriteTokens,
|
|
208
|
+
reasoningTokens: buckets.reasoningTokens || 0,
|
|
197
209
|
costDeltaUsd: 0,
|
|
198
210
|
timestamp,
|
|
199
211
|
durationMs,
|
package/lib/routes.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { registerPluginUpdater } from './updater.js'
|
|
4
4
|
import { getLastTurns } from './projection.js'
|
|
5
|
-
import { calculateBurnRateAndProjection } from './analytics.js'
|
|
5
|
+
import { calculateBurnRateAndProjection, calculateContextMetrics } from './analytics.js'
|
|
6
6
|
import { getCachedFxState } from './fx.js'
|
|
7
7
|
|
|
8
8
|
export const STATE_PATH = '/dsh-cost-meter/state'
|
|
@@ -105,12 +105,19 @@ export function registerRoutes(ctx, options) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const turnsList = getLastTurns()
|
|
109
|
+
const fxRateVal = Number(cfg?.usdRate) > 0 ? Number(cfg.usdRate) : 1
|
|
110
|
+
const ctxMetrics = calculateContextMetrics(turnsList, own, route)
|
|
111
|
+
|
|
108
112
|
const payloadObj = {
|
|
109
113
|
currency: typeof cfg?.currency === 'string' && cfg.currency ? cfg.currency : '$',
|
|
110
114
|
usdRate: Number(cfg?.usdRate) > 0 ? Number(cfg.usdRate) : 1,
|
|
111
115
|
fxRates: { USD: 1, ...(getCachedFxState().rates || {}) },
|
|
112
116
|
displayTimeZone: typeof cfg?.displayTimeZone === 'string' && cfg.displayTimeZone ? cfg.displayTimeZone : 'Europe/Moscow',
|
|
113
117
|
budgetThreshold: Number(cfg?.budgetThreshold) || 0,
|
|
118
|
+
privacyMode: Boolean(cfg?.privacyMode),
|
|
119
|
+
showHeaderChip: cfg?.showHeaderChip !== false,
|
|
120
|
+
showMessageAction: cfg?.showMessageAction !== false,
|
|
114
121
|
displayMode: cfg?.displayMode || 'auto',
|
|
115
122
|
alertBeforeSwitch: cfg?.alertBeforeSwitch !== false,
|
|
116
123
|
alertLeadMinutes: Number(cfg?.alertLeadMinutes) || 5,
|
|
@@ -126,6 +133,14 @@ export function registerRoutes(ctx, options) {
|
|
|
126
133
|
catalogError: catalog.error,
|
|
127
134
|
catalogModelsCount: Object.keys((catalog.index && catalog.index.byId) || {}).length,
|
|
128
135
|
schedule: own.schedule,
|
|
136
|
+
contextMetrics: {
|
|
137
|
+
currentTokens: ctxMetrics.currentTokens,
|
|
138
|
+
contextLength: ctxMetrics.contextLength,
|
|
139
|
+
saturationPercent: ctxMetrics.saturationPercent,
|
|
140
|
+
riskLevel: ctxMetrics.riskLevel,
|
|
141
|
+
nextTurnEstUsd: ctxMetrics.nextTurnEstUsd,
|
|
142
|
+
nextTurnEstLocal: ctxMetrics.nextTurnEstUsd * fxRateVal,
|
|
143
|
+
},
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
const serialized = JSON.stringify(payloadObj)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cost-meter",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.29",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a session cost chip in the conversation header - live token usage by bucket, peak/off-peak tariff, and a countdown to the next tariff switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|