@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 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.provider + '/' + diagState.route.model) : 'default'
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 query = keys ? '?models=' + encodeURIComponent(keys) : ''
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 schedule = state.schedule
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
- state.route ? state.route.provider + ' / ' + state.route.model : t('routeUnknown')),
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
- : (state.route ? [{ key: state.route.provider + '/' + state.route.model, model: state.route.model, known: Boolean(schedule), cost: 0 }] : [])
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 route = defaultRoute(ctx) ?? null
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 url = new URL(req.url, 'http://localhost')
89
- const extraRoutes = url.searchParams.getAll('route')
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) schedules[key] = 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
- const list = bySuffix[lower]
143
- if (!list || !list.length) return undefined
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
- // Ambiguous suffix with no provider match: do not guess arbitrarily
159
- return undefined
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.24",
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",