@goodandready/dsh-cost-meter 0.8.16 → 0.8.18

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
@@ -961,6 +961,28 @@ window.__ModuleLoader__.load({
961
961
  return usd
962
962
  }
963
963
 
964
+ function cacheSavingsBySlots(slots, schedule) {
965
+ let savedUsd = 0
966
+ let missedUsd = 0
967
+ let readTokens = 0
968
+ for (const key of Object.keys(slots || {})) {
969
+ const minute = (Number(key) * 30 + 15) % 1440
970
+ const rates = ratesAt(schedule, minute)
971
+ const u = slots[key] || {}
972
+ const cRead = Number(u.cacheReadTokens) || 0
973
+ if (cRead > 0 && rates && rates.input > rates.cacheHit) {
974
+ readTokens += cRead
975
+ const missed = (cRead / 1e6) * (rates.input || 0)
976
+ const hit = (cRead / 1e6) * (rates.cacheHit || 0)
977
+ savedUsd += Math.max(0, missed - hit)
978
+ missedUsd += missed
979
+ }
980
+ }
981
+ const percent = missedUsd > 0 ? Math.round((savedUsd / missedUsd) * 100) : 0
982
+ return { savedUsd, missedUsd, readTokens, percent }
983
+ }
984
+
985
+
964
986
  function spendUsd(usage, rates) {
965
987
  const u = usage || {}
966
988
  const perMillion =
@@ -1193,17 +1215,36 @@ window.__ModuleLoader__.load({
1193
1215
  .join(', ')
1194
1216
  const nowLocal = hhmmInZone(minute, zone, now)
1195
1217
 
1196
- // Calculate cache savings amount
1218
+ // Calculate cache savings amount across all used models (#92)
1197
1219
  let cacheSavedAmount = 0
1198
1220
  let cacheSavedPercent = 0
1199
- if (hasUsage && usage && Number(usage.cacheReadTokens) > 0 && active.input > active.cacheHit) {
1200
- const tokensRead = Number(usage.cacheReadTokens)
1201
- const missedCost = (tokensRead / 1e6) * active.input
1202
- const hitCost = (tokensRead / 1e6) * active.cacheHit
1203
- cacheSavedAmount = Math.max(0, (missedCost - hitCost) * fx)
1204
- cacheSavedPercent = missedCost > 0 ? Math.round(((missedCost - hitCost) / missedCost) * 100) : 0
1221
+ if (hasUsage) {
1222
+ if (perModel.length > 0) {
1223
+ let totalSavedUsd = 0
1224
+ let totalMissedUsd = 0
1225
+ for (const m of perModel) {
1226
+ const own = schedules[m.key]
1227
+ const sched = own && own.schedule ? own.schedule : schedule
1228
+ if (sched) {
1229
+ const res = cacheSavingsBySlots(m.slots || (m.usage ? { 0: m.usage } : {}), sched)
1230
+ totalSavedUsd += res.savedUsd
1231
+ totalMissedUsd += res.missedUsd
1232
+ }
1233
+ }
1234
+ if (totalSavedUsd > 0) {
1235
+ cacheSavedAmount = totalSavedUsd * fx
1236
+ cacheSavedPercent = totalMissedUsd > 0 ? Math.round((totalSavedUsd / totalMissedUsd) * 100) : 0
1237
+ }
1238
+ } else if (usage && Number(usage.cacheReadTokens) > 0 && active.input > active.cacheHit) {
1239
+ const tokensRead = Number(usage.cacheReadTokens)
1240
+ const missedCost = (tokensRead / 1e6) * active.input
1241
+ const hitCost = (tokensRead / 1e6) * active.cacheHit
1242
+ cacheSavedAmount = Math.max(0, (missedCost - hitCost) * fx)
1243
+ cacheSavedPercent = missedCost > 0 ? Math.round(((missedCost - hitCost) / missedCost) * 100) : 0
1244
+ }
1205
1245
  }
1206
1246
 
1247
+
1207
1248
  // Copy summary report to clipboard
1208
1249
  const copyReport = () => {
1209
1250
  const lines = [
@@ -1255,9 +1296,11 @@ window.__ModuleLoader__.load({
1255
1296
  key: 'chip', type: 'button',
1256
1297
  className: 'dcm-chip',
1257
1298
  onClick: () => setOpen((v) => !v),
1258
- title: budgetExceeded
1299
+ title: (budgetExceeded
1259
1300
  ? t('budget.exceeded', { limit: money(budgetThreshold * fx, currency) })
1260
- : (flat ? 'Flat tariff' : cheapNow ? t('offpeak') : t('peak')),
1301
+ : (flat ? 'Flat tariff' : cheapNow ? t('offpeak') : t('peak'))) +
1302
+ (cacheSavedAmount > 0 ? ' · ' + t('savings.cache_saved', { amount: money(cacheSavedAmount, currency), percent: cacheSavedPercent }) : ''),
1303
+
1261
1304
  }, [
1262
1305
  h('span', { key: 'dot', className: dotClass }),
1263
1306
  h('span', { key: 'sum' }, hasUsage ? '≈ ' + money(total, currency) : '—'),
@@ -1480,8 +1523,12 @@ window.__ModuleLoader__.load({
1480
1523
  minutesUntilChange,
1481
1524
  extremes,
1482
1525
  ratesAt,
1526
+ spendBySlots,
1527
+ cacheSavingsBySlots,
1483
1528
  fetchWithTimeout,
1484
1529
  }
1530
+
1485
1531
  return module.exports
1486
1532
  },
1487
1533
  })
1534
+
package/lib/index.js CHANGED
@@ -21,19 +21,16 @@ import z from '@deepseek-ai/schemastery'
21
21
 
22
22
  // Ensure .volatile() helper exists on Schemastery schema prototype for DSH 0.2
23
23
  try {
24
- const schemaProto = Object.getPrototypeOf(z.boolean())
25
- if (schemaProto && typeof schemaProto.volatile !== 'function') {
26
- schemaProto.volatile = function () {
27
- return typeof this.extra === 'function' ? this.extra('volatile', true) : this
28
- }
24
+ const p = Object.getPrototypeOf(z.boolean())
25
+ if (p && typeof p.volatile !== 'function') {
26
+ p.volatile = function () { return typeof this.extra === 'function' ? this.extra('volatile', true) : this }
29
27
  }
30
- } catch {
31
- // safe fallback
32
- }
28
+ } catch {}
29
+
33
30
  export * from './tariff.js'
34
31
  export * from './projection.js'
35
32
  export { STATE_PATH, REFRESH_PATH } from './routes.js'
36
- import { indexCatalog, resolveTariff, matchModel } from './tariff.js'
33
+ import { indexCatalog, resolveTariff, matchModel, unwrapVolatile } from './tariff.js'
37
34
  import { usageByModelProjection } from './projection.js'
38
35
 
39
36
  export const name = '@goodandready/dsh-cost-meter'
@@ -43,12 +40,30 @@ const NS = 'dsh-cost-meter'
43
40
  export function isLoopback(value) {
44
41
  const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
45
42
  return address === 'localhost' || address === 'localhost.' || address === '::1'
46
- || address?.startsWith('127.') === true
47
- || address?.startsWith('::ffff:127.') === true
43
+ || address?.startsWith('127.') === true || address?.startsWith('::ffff:127.') === true
44
+ }
45
+
46
+ export function getConnection(ctx) {
47
+ try {
48
+ const f = ctx?.reflect?.get
49
+ ? (ctx.reflect.get('connection') || ctx?.connection)
50
+ : (ctx?.get ? (ctx.get('connection') || ctx?.connection) : Reflect.get(ctx || {}, 'connection'))
51
+ return f || (ctx?.root ? getConnection(ctx.root) : undefined)
52
+ } catch {
53
+ return undefined
54
+ }
48
55
  }
49
56
 
50
- export function isTrustedCaller(request) {
57
+ export function isTrustedCaller(request, ctx = null) {
51
58
  if (!request) return false
59
+ const conn = getConnection(ctx) || request?.connectionService || request?.connection
60
+ if (conn && typeof conn.requestRejection === 'function') {
61
+ try {
62
+ if (conn.requestRejection(request) !== undefined) return false
63
+ } catch {
64
+ return false
65
+ }
66
+ }
52
67
  const remote = request.socket?.remoteAddress || request.connection?.remoteAddress
53
68
  const site = request.headers?.['sec-fetch-site']
54
69
  if (site === 'cross-site') return false
@@ -95,6 +110,24 @@ export function isTrustedCaller(request) {
95
110
  return false
96
111
  }
97
112
 
113
+ export function normalizeConfig(raw) {
114
+ const u = unwrapVolatile(raw) || {}
115
+ return {
116
+ ...u,
117
+ currency: typeof u.currency === 'string' && u.currency ? u.currency : '$',
118
+ usdRate: Number(u.usdRate) > 0 ? Number(u.usdRate) : 1,
119
+ displayTimeZone: typeof u.displayTimeZone === 'string' && u.displayTimeZone ? u.displayTimeZone : 'Europe/Moscow',
120
+ budgetThreshold: Number(u.budgetThreshold) || 0,
121
+ useOpenRouter: u.useOpenRouter !== false,
122
+ refreshHours: Number(u.refreshHours) || 24,
123
+ deepseekPeakPrices: u.deepseekPeakPrices || {},
124
+ modelMap: u.modelMap || {},
125
+ prices: u.prices || {},
126
+ manualPeakWindowsUtc: Array.isArray(u.manualPeakWindowsUtc) ? u.manualPeakWindowsUtc : [],
127
+ manualOffPeakMultiplier: Number(u.manualOffPeakMultiplier) || 1,
128
+ }
129
+ }
130
+
98
131
  const CATALOG_URL = 'https://openrouter.ai/api/v1/models'
99
132
  const CACHE_FILE = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'storages', 'dsh-cost-meter-catalog.json')
100
133
 
@@ -109,47 +142,15 @@ const PriceRow = z.object({
109
142
  export const Config = z.object({
110
143
  currency: z.string().description('Currency symbol shown in the widget.').default('$').volatile(),
111
144
  usdRate: z.number().description('Units of `currency` per USD. 1 keeps everything in USD.').default(1).volatile(),
112
- displayTimeZone: z
113
- .string()
114
- .description('IANA zone the widget shows tariff hours in.')
115
- .default('Europe/Moscow')
116
- .volatile(),
117
- budgetThreshold: z
118
- .number()
119
- .description('Optional session budget threshold in USD. 0 disables threshold warnings.')
120
- .default(0)
121
- .volatile(),
122
- deepseekPeakPrices: z
123
- .dict(PriceRow)
124
- .description('Override the built-in DeepSeek peak rates, by model id.')
125
- .default({})
126
- .volatile(),
127
- useOpenRouter: z
128
- .boolean()
129
- .description('Pull rates and discount windows from the public OpenRouter catalog.')
130
- .default(true)
131
- .volatile(),
145
+ displayTimeZone: z.string().description('IANA zone the widget shows tariff hours in.').default('Europe/Moscow').volatile(),
146
+ budgetThreshold: z.number().description('Optional session budget threshold in USD. 0 disables threshold warnings.').default(0).volatile(),
147
+ deepseekPeakPrices: z.dict(PriceRow).description('Override the built-in DeepSeek peak rates, by model id.').default({}).volatile(),
148
+ useOpenRouter: z.boolean().description('Pull rates and discount windows from the public OpenRouter catalog.').default(true).volatile(),
132
149
  refreshHours: z.number().description('How often to refetch the catalog.').default(24).volatile(),
133
- modelMap: z
134
- .dict(z.string())
135
- .description('Route override: "provider/model" -> OpenRouter model id.')
136
- .default({})
137
- .volatile(),
138
- prices: z
139
- .dict(PriceRow)
140
- .description('Manual rates by "provider/model" (or "*"). Win over the catalog.')
141
- .default({})
142
- .volatile(),
143
- manualPeakWindowsUtc: z
144
- .array(z.string())
145
- .description('Peak windows HH:MM-HH:MM UTC, used only with manual prices.')
146
- .default([])
147
- .volatile(),
148
- manualOffPeakMultiplier: z
149
- .number()
150
- .description('Multiplier outside the manual peak windows.')
151
- .default(1)
152
- .volatile(),
150
+ modelMap: z.dict(z.string()).description('Route override: "provider/model" -> OpenRouter model id.').default({}).volatile(),
151
+ prices: z.dict(PriceRow).description('Manual rates by "provider/model" (or "*"). Win over the catalog.').default({}).volatile(),
152
+ manualPeakWindowsUtc: z.array(z.string()).description('Peak windows HH:MM-HH:MM UTC, used only with manual prices.').default([]).volatile(),
153
+ manualOffPeakMultiplier: z.number().description('Multiplier outside the manual peak windows.').default(1).volatile(),
153
154
  })
154
155
 
155
156
  // Per-model usage projection is defined in and exported from ./projection.js
@@ -180,9 +181,8 @@ export function splitRoute(key) {
180
181
  }
181
182
 
182
183
  export function apply(ctx, config) {
183
- let current = config
184
+ let current = normalizeConfig(config)
184
185
  let catalog = { index: { byId: {}, byLowerId: {}, bySuffix: {} }, fetchedAt: 0, error: null }
185
- let lastManualRefreshAt = 0
186
186
 
187
187
  // Server-side cache for resolved tariffs: key -> { schedule, source, matchedId }
188
188
  const resolveCache = new Map()
@@ -255,14 +255,14 @@ export function apply(ctx, config) {
255
255
  }
256
256
  const row = readRow()
257
257
  if (row?.value && typeof row.value === 'object') {
258
- current = { ...config, ...row.value }
258
+ current = normalizeConfig({ ...config, ...row.value })
259
259
  clearResolveCache()
260
260
  }
261
261
  ctx.on('settings/document-updated', (ns) => {
262
262
  if (ns && ns !== NS) return
263
263
  const fresh = readRow()
264
264
  if (fresh?.value && typeof fresh.value === 'object') {
265
- current = { ...config, ...fresh.value }
265
+ current = normalizeConfig({ ...config, ...fresh.value })
266
266
  }
267
267
  clearResolveCache()
268
268
  })
@@ -282,7 +282,8 @@ export function apply(ctx, config) {
282
282
  refresh,
283
283
  splitRoute,
284
284
  defaultRoute,
285
- getCurrentConfig: () => current ?? config,
285
+ getCurrentConfig: () => (current ? normalizeConfig(current) : normalizeConfig(config)),
286
286
  getCatalog: () => catalog,
287
287
  })
288
288
  }
289
+
package/lib/routes.js CHANGED
@@ -26,7 +26,7 @@ export function registerRoutes(ctx, options) {
26
26
  path: REFRESH_PATH,
27
27
  method: 'POST',
28
28
  handler: async (req, res) => {
29
- if (!isTrustedCaller(req)) {
29
+ if (!isTrustedCaller(req, ctx)) {
30
30
  res.writeHead(403, { 'Content-Type': 'application/json' })
31
31
  res.end(JSON.stringify({ error: 'forbidden' }))
32
32
  return
@@ -61,7 +61,7 @@ export function registerRoutes(ctx, options) {
61
61
  path: STATE_PATH,
62
62
  method: 'GET',
63
63
  handler: async (req, res) => {
64
- if (!isTrustedCaller(req)) {
64
+ if (!isTrustedCaller(req, ctx)) {
65
65
  res.writeHead(403, { 'Content-Type': 'application/json' })
66
66
  res.end(JSON.stringify({ error: 'forbidden' }))
67
67
  return
@@ -94,10 +94,10 @@ export function registerRoutes(ctx, options) {
94
94
  }
95
95
 
96
96
  const payloadObj = {
97
- currency: cfg.currency,
98
- usdRate: cfg.usdRate,
99
- displayTimeZone: cfg.displayTimeZone || 'Europe/Moscow',
100
- budgetThreshold: Number(cfg.budgetThreshold) || 0,
97
+ currency: typeof cfg?.currency === 'string' && cfg.currency ? cfg.currency : '$',
98
+ usdRate: Number(cfg?.usdRate) > 0 ? Number(cfg.usdRate) : 1,
99
+ displayTimeZone: typeof cfg?.displayTimeZone === 'string' && cfg.displayTimeZone ? cfg.displayTimeZone : 'Europe/Moscow',
100
+ budgetThreshold: Number(cfg?.budgetThreshold) || 0,
101
101
  route,
102
102
  source: own.source,
103
103
  matchedId: own.matchedId,
@@ -140,3 +140,4 @@ export function registerRoutes(ctx, options) {
140
140
  }), 'dsh-cost-meter: updater')
141
141
  }
142
142
  }
143
+
package/lib/tariff.js CHANGED
@@ -252,3 +252,25 @@ export function resolveTariff(rawCfg, index, route) {
252
252
  }
253
253
  return { schedule: null, source: 'none', matchedId: null }
254
254
  }
255
+
256
+ /**
257
+ * Calculate saved cost from prompt cache hits vs regular input price (#92).
258
+ * Returns { savedUsd, readTokens, percent }
259
+ */
260
+ export function calculateCacheSavings(usage, rates) {
261
+ const u = usage || {}
262
+ const r = rates || {}
263
+ const readTokens = Number(u.cacheReadTokens) || 0
264
+ const inputRate = Number(r.input) || 0
265
+ const cacheHitRate = Number(r.cacheHit) || 0
266
+ if (readTokens <= 0 || inputRate <= cacheHitRate) {
267
+ return { savedUsd: 0, readTokens: 0, percent: 0 }
268
+ }
269
+ const missedUsd = (readTokens / 1e6) * inputRate
270
+ const hitUsd = (readTokens / 1e6) * cacheHitRate
271
+ const savedUsd = Math.max(0, missedUsd - hitUsd)
272
+ const percent = missedUsd > 0 ? Math.round((savedUsd / missedUsd) * 100) : 0
273
+ return { savedUsd, readTokens, percent }
274
+ }
275
+
276
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cost-meter",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
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",