dsh-usage-badge 0.1.2 → 0.1.4
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/README.md +31 -12
- package/assets/usage-dialog.png +0 -0
- package/docs/design-notes.md +172 -39
- package/lib/buckets.js +115 -0
- package/lib/cache-store.js +633 -0
- package/lib/client.js +1413 -169
- package/lib/fold.js +82 -178
- package/lib/holidays.js +5 -4
- package/lib/index.js +246 -30
- package/lib/official-pricing.js +14 -3
- package/lib/pricing.js +188 -20
- package/package.json +2 -2
package/lib/buckets.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-usage-badge — the token bucket model and its stored form.
|
|
3
|
+
*
|
|
4
|
+
* A bucket is one (provider, model) route on one day: token totals, a 24-slot hourly
|
|
5
|
+
* breakdown for the day chart, and the raw per-request records that let the pricing layer
|
|
6
|
+
* apply a different multiplier to each request rather than pricing a day flat.
|
|
7
|
+
*
|
|
8
|
+
* This module owns that shape and its JSON representation, because two very different
|
|
9
|
+
* places need to agree on it: the folder builds buckets while reading session logs, and the
|
|
10
|
+
* cache store reads them back years later. Keeping the codec here means a store never has to
|
|
11
|
+
* know what a bucket is — it moves `{ date: { routeKey: bucket } }` slices in and out.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-usage-badge/buckets
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { initHourly } from './pricing.js'
|
|
17
|
+
|
|
18
|
+
/** Bucket key and display name used when a log records no provider/model. */
|
|
19
|
+
export const UNKNOWN = 'unknown'
|
|
20
|
+
|
|
21
|
+
/** A fresh, empty bucket for one route. */
|
|
22
|
+
export function emptyBucket(provider, model) {
|
|
23
|
+
return {
|
|
24
|
+
provider,
|
|
25
|
+
model,
|
|
26
|
+
input: 0,
|
|
27
|
+
cacheRead: 0,
|
|
28
|
+
cacheWrite: 0,
|
|
29
|
+
output: 0,
|
|
30
|
+
requests: 0,
|
|
31
|
+
hourly: initHourly(),
|
|
32
|
+
usageRecords: [],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add `source` into `target`.
|
|
38
|
+
*
|
|
39
|
+
* A missing per-request list marks a legacy aggregate row: mixing one into a
|
|
40
|
+
* bucket that has a list would silently drop the aggregate's share from the
|
|
41
|
+
* context-tier price, so the list is dropped to `null` for the whole bucket
|
|
42
|
+
* instead, and that bucket falls back to flat pricing. Appending is looped rather
|
|
43
|
+
* than spread because one day bucket can hold hundreds of thousands of requests,
|
|
44
|
+
* which would overflow the argument list long before it exhausted memory.
|
|
45
|
+
*/
|
|
46
|
+
export function addBucket(target, source) {
|
|
47
|
+
target.input += Number(source?.input) || 0
|
|
48
|
+
target.cacheRead += Number(source?.cacheRead) || 0
|
|
49
|
+
target.cacheWrite += Number(source?.cacheWrite) || 0
|
|
50
|
+
target.output += Number(source?.output) || 0
|
|
51
|
+
target.requests += Number(source?.requests) || 0
|
|
52
|
+
if (!Array.isArray(target.hourly)) target.hourly = initHourly()
|
|
53
|
+
if (Array.isArray(source?.hourly)) {
|
|
54
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
55
|
+
const from = source.hourly[hour]
|
|
56
|
+
const to = target.hourly[hour]
|
|
57
|
+
if (!from || !to) continue
|
|
58
|
+
to.input += Number(from.input) || 0
|
|
59
|
+
to.cacheRead += Number(from.cacheRead) || 0
|
|
60
|
+
to.cacheWrite += Number(from.cacheWrite) || 0
|
|
61
|
+
to.output += Number(from.output) || 0
|
|
62
|
+
to.requests += Number(from.requests) || 0
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(target.usageRecords) || !Array.isArray(source?.usageRecords)) {
|
|
66
|
+
target.usageRecords = null
|
|
67
|
+
} else {
|
|
68
|
+
for (const usage of source.usageRecords) {
|
|
69
|
+
if (usage && typeof usage === 'object') target.usageRecords.push(usage)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One day, as storable JSON: `{ routeKey: bucket }`.
|
|
76
|
+
*
|
|
77
|
+
* The bucket objects are handed over by reference; every store either stringifies the slice
|
|
78
|
+
* immediately or replaces the whole entry, and the folder never mutates a bucket it has
|
|
79
|
+
* already handed over (a re-fold builds fresh ones), so aliasing is safe and avoids copying
|
|
80
|
+
* a day's worth of per-request records on every save.
|
|
81
|
+
*/
|
|
82
|
+
export function serializeDay(dayObj) {
|
|
83
|
+
const routes = {}
|
|
84
|
+
for (const [key, bucket] of dayObj ?? []) routes[key] = bucket
|
|
85
|
+
return routes
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The inverse of {@link serializeDay}, rebuilding the buckets' invariants as it goes. */
|
|
89
|
+
export function deserializeDay(routes) {
|
|
90
|
+
const dayObj = new Map()
|
|
91
|
+
for (const [key, source] of Object.entries(routes ?? {})) {
|
|
92
|
+
if (!source || typeof source !== 'object') continue
|
|
93
|
+
const bucket = emptyBucket(String(source.provider ?? UNKNOWN), String(source.model ?? UNKNOWN))
|
|
94
|
+
addBucket(bucket, source)
|
|
95
|
+
dayObj.set(key, bucket)
|
|
96
|
+
}
|
|
97
|
+
return dayObj
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A whole session's days, as storable JSON: `{ date: { routeKey: bucket } }`. */
|
|
101
|
+
export function serializeDays(days) {
|
|
102
|
+
const plain = {}
|
|
103
|
+
for (const [date, dayObj] of days ?? []) plain[date] = serializeDay(dayObj)
|
|
104
|
+
return plain
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The inverse of {@link serializeDays}. */
|
|
108
|
+
export function deserializeDays(plain) {
|
|
109
|
+
const days = new Map()
|
|
110
|
+
for (const [date, routes] of Object.entries(plain ?? {})) {
|
|
111
|
+
if (!routes || typeof routes !== 'object') continue
|
|
112
|
+
days.set(date, deserializeDay(routes))
|
|
113
|
+
}
|
|
114
|
+
return days
|
|
115
|
+
}
|