@goodandready/dsh-cost-meter 0.8.16 → 0.8.17
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/index.js +58 -57
- package/lib/routes.js +7 -6
- package/package.json +1 -1
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
|
|
25
|
-
if (
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
|
100
|
-
budgetThreshold: Number(cfg
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cost-meter",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
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",
|