@goodandready/dsh-cost-meter 0.8.24 → 0.8.25
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/client.js +48 -17
- package/lib/projection.js +5 -0
- package/lib/routes.js +13 -4
- package/lib/tariff.js +13 -5
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -909,7 +909,7 @@ window.__ModuleLoader__.load({
|
|
|
909
909
|
),
|
|
910
910
|
React.createElement('div', { className: 'dcm-stat-box' },
|
|
911
911
|
React.createElement('span', { className: 'dcm-stat-val', style: { fontSize: 12 } },
|
|
912
|
-
diagState && diagState.route ? (diagState.route.
|
|
912
|
+
diagState && diagState.route ? (diagState.route.model || diagState.route.provider) : 'default'
|
|
913
913
|
),
|
|
914
914
|
React.createElement('span', { className: 'dcm-stat-lbl' }, t('diag.route')),
|
|
915
915
|
),
|
|
@@ -1184,6 +1184,7 @@ window.__ModuleLoader__.load({
|
|
|
1184
1184
|
const useProjection = props.useProjection
|
|
1185
1185
|
const usage = useProjection ? useProjection('tokenUsage') : undefined
|
|
1186
1186
|
const byModel = useProjection ? useProjection('costByModel') : undefined
|
|
1187
|
+
const modelSelection = useProjection ? useProjection('modelSelection') : undefined
|
|
1187
1188
|
const [state, setState] = React.useState(null)
|
|
1188
1189
|
const [open, setOpen] = React.useState(false)
|
|
1189
1190
|
const [now, setNow] = React.useState(() => new Date())
|
|
@@ -1212,11 +1213,39 @@ window.__ModuleLoader__.load({
|
|
|
1212
1213
|
m.usage.cacheReadTokens || m.usage.cacheWriteTokens)
|
|
1213
1214
|
const usedKeys = used.map((m) => m.provider + '/' + m.model).sort().join(',')
|
|
1214
1215
|
|
|
1216
|
+
// Determine active model from session modelSelection, committed route, or used models
|
|
1217
|
+
const activeSelection = modelSelection?.next || modelSelection?.lastUsed
|
|
1218
|
+
const activeModelName = activeSelection?.model
|
|
1219
|
+
|| byModel?.route?.model
|
|
1220
|
+
|| (used.length > 0 ? used[used.length - 1].model : undefined)
|
|
1221
|
+
|| (state?.route?.model)
|
|
1222
|
+
|| ''
|
|
1223
|
+
|
|
1224
|
+
const activeProvider = activeSelection?.provider
|
|
1225
|
+
|| byModel?.route?.provider
|
|
1226
|
+
|| (used.length > 0 ? used[used.length - 1].provider : undefined)
|
|
1227
|
+
|| (state?.route?.provider)
|
|
1228
|
+
|| ''
|
|
1229
|
+
|
|
1230
|
+
const activeRouteKey = activeModelName
|
|
1231
|
+
? (activeProvider ? activeProvider + '/' + activeModelName : activeModelName)
|
|
1232
|
+
: ''
|
|
1233
|
+
|
|
1215
1234
|
const reqSeq = React.useRef(0)
|
|
1216
1235
|
|
|
1217
|
-
const load = React.useCallback((keys, signal) => {
|
|
1236
|
+
const load = React.useCallback((keys, currentRoute, signal) => {
|
|
1218
1237
|
const seq = ++reqSeq.current
|
|
1219
|
-
const
|
|
1238
|
+
const params = new URLSearchParams()
|
|
1239
|
+
if (currentRoute) {
|
|
1240
|
+
params.set('currentRoute', currentRoute)
|
|
1241
|
+
params.append('route', currentRoute)
|
|
1242
|
+
}
|
|
1243
|
+
if (keys) {
|
|
1244
|
+
params.set('models', keys)
|
|
1245
|
+
keys.split(',').forEach((k) => { if (k) params.append('route', k) })
|
|
1246
|
+
}
|
|
1247
|
+
const qs = params.toString()
|
|
1248
|
+
const query = qs ? '?' + qs : ''
|
|
1220
1249
|
fetchWithTimeout(STATE_PATH + query, { signal, headers: { accept: 'application/json' } })
|
|
1221
1250
|
.then((r) => (r.ok ? r.json() : null))
|
|
1222
1251
|
.then((data) => {
|
|
@@ -1227,15 +1256,15 @@ window.__ModuleLoader__.load({
|
|
|
1227
1256
|
|
|
1228
1257
|
React.useEffect(() => {
|
|
1229
1258
|
const controller = new AbortController()
|
|
1230
|
-
load(usedKeys, controller.signal)
|
|
1259
|
+
load(usedKeys, activeRouteKey, controller.signal)
|
|
1231
1260
|
return () => controller.abort()
|
|
1232
|
-
}, [load, usedKeys])
|
|
1261
|
+
}, [load, usedKeys, activeRouteKey])
|
|
1233
1262
|
React.useEffect(() => {
|
|
1234
1263
|
if (!open) return
|
|
1235
1264
|
const controller = new AbortController()
|
|
1236
|
-
load(usedKeys, controller.signal)
|
|
1265
|
+
load(usedKeys, activeRouteKey, controller.signal)
|
|
1237
1266
|
return () => controller.abort()
|
|
1238
|
-
}, [open, load, usedKeys])
|
|
1267
|
+
}, [open, load, usedKeys, activeRouteKey])
|
|
1239
1268
|
React.useEffect(() => {
|
|
1240
1269
|
const id = setInterval(() => setNow(new Date()), 15000)
|
|
1241
1270
|
return () => clearInterval(id)
|
|
@@ -1255,7 +1284,10 @@ window.__ModuleLoader__.load({
|
|
|
1255
1284
|
const fx = stored.usdRate !== undefined && !isNaN(Number(stored.usdRate)) ? Number(stored.usdRate) : (Number(state.usdRate) || 1)
|
|
1256
1285
|
const budgetThreshold = stored.budgetThreshold !== undefined ? Number(stored.budgetThreshold) : (Number(state.budgetThreshold) || 0)
|
|
1257
1286
|
const displayMode = stored.displayMode !== undefined ? stored.displayMode : (state.displayMode || 'auto')
|
|
1258
|
-
const
|
|
1287
|
+
const schedules = state.schedules || {}
|
|
1288
|
+
const schedule = (activeRouteKey && schedules[activeRouteKey])
|
|
1289
|
+
|| (activeModelName && schedules[activeModelName])
|
|
1290
|
+
|| state.schedule
|
|
1259
1291
|
const hasUsage = usage !== undefined && usage !== null
|
|
1260
1292
|
const minute = now.getUTCHours() * 60 + now.getUTCMinutes()
|
|
1261
1293
|
|
|
@@ -1271,7 +1303,7 @@ window.__ModuleLoader__.load({
|
|
|
1271
1303
|
]),
|
|
1272
1304
|
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
1273
1305
|
h('div', { key: 't', className: 'dcm-panel-title' },
|
|
1274
|
-
|
|
1306
|
+
activeModelName || (state.route ? state.route.model : t('routeUnknown'))),
|
|
1275
1307
|
h('div', { key: 'n', className: 'dcm-panel-note' },
|
|
1276
1308
|
state.catalogError
|
|
1277
1309
|
? t('catalogUnavailable') + state.catalogError
|
|
@@ -1286,11 +1318,10 @@ window.__ModuleLoader__.load({
|
|
|
1286
1318
|
const leftMinutes = minutesUntilChange(schedule, minute)
|
|
1287
1319
|
const left = clock(leftMinutes)
|
|
1288
1320
|
|
|
1289
|
-
const schedules = state.schedules || {}
|
|
1290
1321
|
const perModel = used.map((m) => {
|
|
1291
1322
|
const key = m.provider + '/' + m.model
|
|
1292
|
-
const own = schedules[key]
|
|
1293
|
-
const ownSchedule = own && own.schedule
|
|
1323
|
+
const own = schedules[key] || schedules[m.model]
|
|
1324
|
+
const ownSchedule = own && (own.schedule || own)
|
|
1294
1325
|
return {
|
|
1295
1326
|
key,
|
|
1296
1327
|
model: m.model,
|
|
@@ -1361,8 +1392,8 @@ window.__ModuleLoader__.load({
|
|
|
1361
1392
|
let totalSavedUsd = 0
|
|
1362
1393
|
let totalMissedUsd = 0
|
|
1363
1394
|
for (const m of perModel) {
|
|
1364
|
-
const own = schedules[m.key]
|
|
1365
|
-
const sched = own && own.schedule ? own.schedule : schedule
|
|
1395
|
+
const own = schedules[m.key] || schedules[m.model]
|
|
1396
|
+
const sched = own && (own.schedule || own) ? (own.schedule || own) : schedule
|
|
1366
1397
|
if (sched) {
|
|
1367
1398
|
const res = cacheSavingsBySlots(m.slots || (m.usage ? { 0: m.usage } : {}), sched)
|
|
1368
1399
|
totalSavedUsd += res.savedUsd
|
|
@@ -1386,7 +1417,7 @@ window.__ModuleLoader__.load({
|
|
|
1386
1417
|
// Copy summary report to clipboard
|
|
1387
1418
|
const copyReport = () => {
|
|
1388
1419
|
const lines = [
|
|
1389
|
-
'=== ' + (state.route ? state.route.model : 'Cost Meter Report') + ' ===',
|
|
1420
|
+
'=== ' + (activeModelName || (state.route ? state.route.model : 'Cost Meter Report')) + ' ===',
|
|
1390
1421
|
t('tokens.session_total') + ' ' + (hasUsage ? money(total, currency) : '—') + (budgetThreshold > 0 ? ' (limit: ' + money(budgetThreshold * fx, currency) + ')' : ''),
|
|
1391
1422
|
'Tier: ' + (flat ? 'Flat' : (cheapNow ? t('offpeak') : t('peak'))),
|
|
1392
1423
|
'Rates (1M): input ' + rate(active.input) + ', output ' + rate(active.output) + ', cache ' + rate(active.cacheHit),
|
|
@@ -1424,7 +1455,7 @@ window.__ModuleLoader__.load({
|
|
|
1424
1455
|
// Display models with collapse support if > 3 models
|
|
1425
1456
|
const visibleModels = (perModel.length
|
|
1426
1457
|
? perModel
|
|
1427
|
-
: (
|
|
1458
|
+
: (activeModelName ? [{ key: activeRouteKey || activeModelName, model: activeModelName, known: Boolean(schedule), cost: 0 }] : [])
|
|
1428
1459
|
)
|
|
1429
1460
|
const shouldCollapse = visibleModels.length > 3 && !showAllModels
|
|
1430
1461
|
const renderedModels = shouldCollapse ? visibleModels.slice(0, 3) : visibleModels
|
|
@@ -1447,7 +1478,7 @@ window.__ModuleLoader__.load({
|
|
|
1447
1478
|
|
|
1448
1479
|
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
1449
1480
|
h('div', { key: 'title', className: 'dcm-panel-title' }, [
|
|
1450
|
-
h('span', { key: 'model-name' }, state.route ? state.route.model : 'model'),
|
|
1481
|
+
h('span', { key: 'model-name' }, activeModelName || (state.route ? state.route.model : 'model')),
|
|
1451
1482
|
flat ? null : h('span', { key: 'tier', style: { fontSize: 11.5, opacity: 0.85 } }, cheapNow ? t('offpeak') : t('peak')),
|
|
1452
1483
|
]),
|
|
1453
1484
|
|
package/lib/projection.js
CHANGED
|
@@ -86,6 +86,10 @@ export function slotOf(timeMs) {
|
|
|
86
86
|
export const modelSlotsSchema = zod.record(zod.string(), bucketSchema)
|
|
87
87
|
|
|
88
88
|
export const costByModelViewSchema = zod.object({
|
|
89
|
+
route: zod.object({
|
|
90
|
+
provider: zod.string(),
|
|
91
|
+
model: zod.string(),
|
|
92
|
+
}).nullable().optional(),
|
|
89
93
|
models: zod.array(zod.object({
|
|
90
94
|
provider: zod.string(),
|
|
91
95
|
model: zod.string(),
|
|
@@ -194,6 +198,7 @@ export const usageByModelProjection = {
|
|
|
194
198
|
wire: {
|
|
195
199
|
viewSchema: costByModelViewSchema,
|
|
196
200
|
view: (state) => ({
|
|
201
|
+
...(state.route ? { route: state.route } : {}),
|
|
197
202
|
models: Object.keys(state.byKey).map((key) => {
|
|
198
203
|
const slash = key.indexOf('/')
|
|
199
204
|
return {
|
package/lib/routes.js
CHANGED
|
@@ -80,18 +80,27 @@ export function registerRoutes(ctx, options) {
|
|
|
80
80
|
void refresh()
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const
|
|
83
|
+
const url = new URL(req.url, 'http://localhost')
|
|
84
|
+
const currentQueryRoute = url.searchParams.get('currentRoute') || url.searchParams.get('model')
|
|
85
|
+
const parsedCurrent = currentQueryRoute ? splitRoute(currentQueryRoute) : null
|
|
86
|
+
const route = parsedCurrent ?? (defaultRoute(ctx) ?? null)
|
|
84
87
|
const own = getCachedTariff(cfg, route)
|
|
85
88
|
|
|
86
89
|
// Also resolve tariffs for any session routes requested in query params
|
|
87
90
|
const schedules = {}
|
|
88
|
-
const
|
|
89
|
-
|
|
91
|
+
const extraRoutes = [
|
|
92
|
+
...url.searchParams.getAll('route'),
|
|
93
|
+
...url.searchParams.getAll('models').flatMap((m) => m.split(',')),
|
|
94
|
+
]
|
|
90
95
|
for (const key of extraRoutes) {
|
|
96
|
+
if (!key) continue
|
|
91
97
|
const parsed = splitRoute(key)
|
|
92
98
|
if (parsed) {
|
|
93
99
|
const res = getCachedTariff(cfg, parsed)
|
|
94
|
-
if (res && res.schedule)
|
|
100
|
+
if (res && res.schedule) {
|
|
101
|
+
schedules[key] = res.schedule
|
|
102
|
+
if (parsed.model) schedules[parsed.model] = res.schedule
|
|
103
|
+
}
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
package/lib/tariff.js
CHANGED
|
@@ -139,8 +139,16 @@ export function matchModel(index, route, modelMap) {
|
|
|
139
139
|
if (byLowerId[lower]) return byLowerId[lower]
|
|
140
140
|
if (byLowerId[key.toLowerCase()]) return byLowerId[key.toLowerCase()]
|
|
141
141
|
const bySuffix = index.bySuffix || {}
|
|
142
|
-
|
|
143
|
-
if (!list || !list.length)
|
|
142
|
+
let list = bySuffix[lower]
|
|
143
|
+
if (!list || !list.length) {
|
|
144
|
+
// Prefix fallback for short names like "beam" -> "beam-1.0-501b-a23b"
|
|
145
|
+
const prefixKey = Object.keys(bySuffix).find((k) => k.startsWith(lower + '-') || k.startsWith(lower + '.'))
|
|
146
|
+
if (prefixKey && bySuffix[prefixKey]?.length) {
|
|
147
|
+
list = bySuffix[prefixKey]
|
|
148
|
+
} else {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
}
|
|
144
152
|
if (list.length === 1) return list[0]
|
|
145
153
|
|
|
146
154
|
// Multiple catalog entries share the same model suffix: disambiguate by provider
|
|
@@ -155,8 +163,8 @@ export function matchModel(index, route, modelMap) {
|
|
|
155
163
|
})
|
|
156
164
|
if (matched) return matched
|
|
157
165
|
}
|
|
158
|
-
//
|
|
159
|
-
return
|
|
166
|
+
// When provider does not match or is absent, fall back to the first catalog candidate for this model
|
|
167
|
+
return list[0]
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
/** Build lookup tables once per fetch; the raw catalog is ~700 KB. */
|
|
@@ -273,4 +281,4 @@ export function calculateCacheSavings(usage, rates) {
|
|
|
273
281
|
return { savedUsd, readTokens, percent }
|
|
274
282
|
}
|
|
275
283
|
|
|
276
|
-
|
|
284
|
+
|
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.25",
|
|
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",
|