@goodandready/dsh-cost-meter 0.8.9 → 0.8.10

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.
Files changed (2) hide show
  1. package/lib/index.js +96 -37
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -38,21 +38,40 @@ export function isLoopback(value) {
38
38
  }
39
39
 
40
40
  export function isTrustedCaller(request) {
41
- const remote = request?.socket?.remoteAddress || request?.connection?.remoteAddress
42
- if (isLoopback(remote)) return true
43
- const site = request?.headers?.['sec-fetch-site']
44
- if (site === 'same-origin' || site === 'same-site') return true
41
+ if (!request) return false
42
+ const remote = request.socket?.remoteAddress || request.connection?.remoteAddress
43
+ const site = request.headers?.['sec-fetch-site']
45
44
  if (site === 'cross-site') return false
46
- const origin = request?.headers?.['origin']
47
- const host = request?.headers?.['host']
48
- if (origin && host) {
45
+
46
+ const host = request.headers?.['x-forwarded-host'] || request.headers?.['host']
47
+ const origin = request.headers?.['origin']
48
+ if (origin) {
49
49
  try {
50
50
  const url = new URL(origin)
51
- if (url.host === host) return true
52
- } catch (e) {
53
- /* invalid url */
51
+ if (host && url.host.toLowerCase() === host.toLowerCase()) return true
52
+ if (isLoopback(url.hostname) && isLoopback(remote)) return true
53
+ return false
54
+ } catch {
55
+ return false
56
+ }
57
+ }
58
+
59
+ const referer = request.headers?.['referer']
60
+ if (referer) {
61
+ try {
62
+ const url = new URL(referer)
63
+ if (host && url.host.toLowerCase() === host.toLowerCase()) return true
64
+ if (isLoopback(url.hostname) && isLoopback(remote)) return true
65
+ return false
66
+ } catch {
67
+ return false
54
68
  }
55
69
  }
70
+
71
+ if (site === 'same-site') return false
72
+ if (site === 'same-origin') return true
73
+ if (isLoopback(remote)) return true
74
+
56
75
  return false
57
76
  }
58
77
 
@@ -167,15 +186,35 @@ export function slotOf(timeMs) {
167
186
  return Math.floor((d.getUTCHours() * 60 + d.getUTCMinutes()) / 30)
168
187
  }
169
188
 
170
- const usageByModelProjection = {
189
+ const modelSlotsSchema = zod.record(zod.string(), bucketSchema)
190
+
191
+ const costByModelViewSchema = zod.object({
192
+ models: zod.array(zod.object({
193
+ provider: zod.string(),
194
+ model: zod.string(),
195
+ slots: modelSlotsSchema,
196
+ }).strict()),
197
+ }).strict()
198
+
199
+ const costByModelStateSchema = zod.object({
200
+ route: zod.object({
201
+ provider: zod.string(),
202
+ model: zod.string(),
203
+ }).nullable(),
204
+ byKey: zod.record(zod.string(), modelSlotsSchema),
205
+ last: zod.object({
206
+ turn: zod.number().int().nonnegative(),
207
+ step: zod.number().int().nonnegative(),
208
+ key: zod.string(),
209
+ slot: zod.string(),
210
+ buckets: bucketSchema,
211
+ }).nullable(),
212
+ }).strict()
213
+
214
+ export const usageByModelProjection = {
171
215
  key: 'costByModel',
172
- schema: zod.object({
173
- models: zod.array(zod.object({
174
- provider: zod.string(),
175
- model: zod.string(),
176
- slots: zod.record(zod.string(), bucketSchema),
177
- })),
178
- }).strict(),
216
+ stateVersion: 2,
217
+ stateSchema: costByModelStateSchema,
179
218
  init: () => ({ route: null, byKey: {}, last: null }),
180
219
  apply: (state, event) => {
181
220
  if (event.type === 'request/header') {
@@ -211,17 +250,19 @@ const usageByModelProjection = {
211
250
  last: { turn: sample.turn, step: sample.step, key, slot, buckets },
212
251
  }
213
252
  },
214
- view: (state) => ({
215
- models: Object.keys(state.byKey).map((key) => {
216
- const slash = key.indexOf('/')
217
- return {
218
- provider: key.slice(0, slash),
219
- model: key.slice(slash + 1),
220
- slots: state.byKey[key],
221
- }
253
+ wire: {
254
+ viewSchema: costByModelViewSchema,
255
+ view: (state) => ({
256
+ models: Object.keys(state.byKey).map((key) => {
257
+ const slash = key.indexOf('/')
258
+ return {
259
+ provider: key.slice(0, slash),
260
+ model: key.slice(slash + 1),
261
+ slots: state.byKey[key],
262
+ }
263
+ }),
222
264
  }),
223
- }),
224
- stateVersion: 2,
265
+ },
225
266
  }
226
267
 
227
268
  /** The agent's default route, read off settings descriptors when available. */
@@ -309,19 +350,37 @@ export function apply(ctx, config) {
309
350
  }
310
351
  }
311
352
 
312
- // Live config from the GUI, with the composition entry as the base layer.
313
- ctx.inject(['settings'], (sctx) => {
314
- const scope = sctx.settings.register(NS, Config, { base: config })
315
- current = scope.get() ?? config
316
- scope.watch((next) => {
317
- current = next
353
+ // Live config reaction from GUI / settings service
354
+ ctx.on('settings/updated', (ns, next) => {
355
+ if (ns === NS && next) {
356
+ current = { ...config, ...next }
357
+ clearResolveCache()
358
+ }
359
+ })
360
+
361
+ ctx.on('settings/document-updated', (ns) => {
362
+ if (!ns || ns === NS) {
318
363
  clearResolveCache()
319
- })
364
+ }
365
+ })
366
+
367
+ ctx.inject(['settings'], (sctx) => {
368
+ if (typeof sctx.settings?.describe === 'function') {
369
+ try {
370
+ const desc = sctx.settings.describe(NS)
371
+ if (desc?.value && typeof desc.value === 'object') {
372
+ current = { ...config, ...desc.value }
373
+ clearResolveCache()
374
+ }
375
+ } catch {
376
+ /* namespace might not have descriptor yet */
377
+ }
378
+ }
320
379
  })
321
380
 
322
381
  // Hook the session projection so the widget can show spend breakdown.
323
- ctx.inject(['chatSessionProjections'], (pctx) => {
324
- pctx.chatSessionProjections.register(usageByModelProjection)
382
+ ctx.inject(['sessionProjections'], (pctx) => {
383
+ pctx.sessionProjections.register(usageByModelProjection)
325
384
  })
326
385
 
327
386
  // Route 1: POST /dsh-cost-meter/refresh (manual catalog reload)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cost-meter",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
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",