dsh-plugin-usage 0.2.0 → 0.2.2
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 +38 -12
- package/image-1.png +0 -0
- package/image.png +0 -0
- package/lib/client/index.js +57 -14
- package/lib/host/fold.js +142 -35
- package/lib/host/index.js +44 -13
- package/lib/host/rpc.js +136 -14
- package/lib/host/scan.js +108 -15
- package/lib/host/settings.js +37 -99
- package/lib/host/state.js +14 -3
- package/package.json +16 -7
package/lib/host/settings.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Price table + cost computation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Persistence is the official plugin-config path: `index.js` exports a
|
|
5
|
+
* schemastery `Config`, so DSH materializes a form under the plugin entry's
|
|
6
|
+
* id (`usage`) in the Plugins settings tab and stores values in the profile
|
|
7
|
+
* patch. There is deliberately no private `settings.yaml` / settings-service
|
|
8
|
+
* registration here — DSH 0.2.x `ctx.settings` (SettingsForms) only offers
|
|
9
|
+
* `describe/update/replace/mutate/configure`, and hand-rolled namespaces are
|
|
10
|
+
* not reachable from the client config surface.
|
|
11
|
+
*
|
|
12
|
+
* The store below stays the in-memory view the aggregation code reads.
|
|
8
13
|
*/
|
|
9
14
|
export const DEFAULT_CURRENCY = '¥';
|
|
10
15
|
export const DEFAULT_PRICES = {
|
|
@@ -13,17 +18,42 @@ export const DEFAULT_PRICES = {
|
|
|
13
18
|
'deepseek-v3.1': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
|
|
14
19
|
'deepseek-v4': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
|
|
15
20
|
'deepseek-v4-flash': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
21
|
+
'deepseek-v4.1-flash': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
22
|
+
'deepseek-v4.1-flash-1017': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
23
|
+
// DSH's routed default model id (`magpie` provider) aliases the same route;
|
|
24
|
+
// without this entry every session bills as "unpriced" on a stock install.
|
|
25
|
+
'group/auto-deepseek-v4-1-flash': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
16
26
|
'deepseek-reasoner': { inputPerM: 0.55, outputPerM: 2.19, cacheReadPerM: 0.14, cacheWritePerM: 0.55 },
|
|
17
27
|
'deepseek-r1': { inputPerM: 0.55, outputPerM: 2.19, cacheReadPerM: 0.14, cacheWritePerM: 0.55 },
|
|
18
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Shallow-merge configured prices over the defaults.
|
|
31
|
+
*
|
|
32
|
+
* @param {unknown} incoming - configured per-model prices.
|
|
33
|
+
* @returns {Record<string, object>} complete price table.
|
|
34
|
+
*/
|
|
35
|
+
export function mergePrices(incoming) {
|
|
36
|
+
const merged = Object.assign({}, DEFAULT_PRICES);
|
|
37
|
+
if (incoming && typeof incoming === 'object' && !Array.isArray(incoming)) {
|
|
38
|
+
for (const [model, price] of Object.entries(incoming)) {
|
|
39
|
+
if (price && typeof price === 'object') {
|
|
40
|
+
merged[model] = {
|
|
41
|
+
inputPerM: Number(price.inputPerM) || 0,
|
|
42
|
+
outputPerM: Number(price.outputPerM) || 0,
|
|
43
|
+
cacheReadPerM: Number(price.cacheReadPerM) || 0,
|
|
44
|
+
cacheWritePerM: Number(price.cacheWritePerM) || 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
19
51
|
export function createPriceStore(defaultPrices, defaultCurrency) {
|
|
20
52
|
const prices = Object.assign({}, defaultPrices);
|
|
21
53
|
let currency = defaultCurrency || DEFAULT_CURRENCY;
|
|
22
54
|
const store = {
|
|
23
55
|
prices,
|
|
24
56
|
get currency() { return currency; },
|
|
25
|
-
/** Write-through slot wired by initSettings(); null without a settings service. */
|
|
26
|
-
persist: null,
|
|
27
57
|
/** Merge incoming per-model prices (each field coerced to a number). */
|
|
28
58
|
setPrices(incoming) {
|
|
29
59
|
if (!incoming || typeof incoming !== 'object')
|
|
@@ -38,15 +68,10 @@ export function createPriceStore(defaultPrices, defaultCurrency) {
|
|
|
38
68
|
};
|
|
39
69
|
}
|
|
40
70
|
}
|
|
41
|
-
if (typeof store.persist === 'function')
|
|
42
|
-
store.persist({ prices: Object.assign({}, prices) });
|
|
43
71
|
},
|
|
44
72
|
setCurrency(v) {
|
|
45
|
-
if (typeof v === 'string' && v.length > 0)
|
|
73
|
+
if (typeof v === 'string' && v.length > 0)
|
|
46
74
|
currency = v;
|
|
47
|
-
if (typeof store.persist === 'function')
|
|
48
|
-
store.persist({ currency });
|
|
49
|
-
}
|
|
50
75
|
},
|
|
51
76
|
/** Look up a price by `provider/model` composite first, then bare model. */
|
|
52
77
|
priceOf(r) {
|
|
@@ -73,90 +98,3 @@ export function createPriceStore(defaultPrices, defaultCurrency) {
|
|
|
73
98
|
};
|
|
74
99
|
return store;
|
|
75
100
|
}
|
|
76
|
-
const USAGE_PRICES_NS = 'usage.prices';
|
|
77
|
-
/** JSON-schema-shaped descriptor for the usage.prices settings section. */
|
|
78
|
-
const PRICE_SECTION_DESCRIPTOR = {
|
|
79
|
-
type: 'object',
|
|
80
|
-
properties: {
|
|
81
|
-
prices: {
|
|
82
|
-
type: 'object',
|
|
83
|
-
description: 'Per-model prices (cost per 1M tokens, in `currency`).',
|
|
84
|
-
additionalProperties: {
|
|
85
|
-
type: 'object',
|
|
86
|
-
properties: {
|
|
87
|
-
inputPerM: { type: 'number' },
|
|
88
|
-
outputPerM: { type: 'number' },
|
|
89
|
-
cacheReadPerM: { type: 'number' },
|
|
90
|
-
cacheWritePerM: { type: 'number' },
|
|
91
|
-
},
|
|
92
|
-
required: ['inputPerM', 'outputPerM', 'cacheReadPerM', 'cacheWritePerM'],
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
currency: { type: 'string' },
|
|
96
|
-
},
|
|
97
|
-
required: ['prices', 'currency'],
|
|
98
|
-
};
|
|
99
|
-
/** Apply a resolved settings section onto the in-memory price store. */
|
|
100
|
-
function applySection(store, section) {
|
|
101
|
-
if (!section || typeof section !== 'object')
|
|
102
|
-
return;
|
|
103
|
-
if (typeof store.setPrices === 'function' && section.prices && typeof section.prices === 'object' && !Array.isArray(section.prices)) {
|
|
104
|
-
store.setPrices(section.prices);
|
|
105
|
-
}
|
|
106
|
-
if (typeof store.setCurrency === 'function' && typeof section.currency === 'string')
|
|
107
|
-
store.setCurrency(section.currency);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* P0-1: wire the price store to the official `ctx.settings` namespace
|
|
111
|
-
* (durable, shown in the official Plugins config tab) when available.
|
|
112
|
-
* Registers `usage.prices`, seeds the store from the resolved section, and
|
|
113
|
-
* writes every setPrices/setCurrency mutation through. Falls back to the
|
|
114
|
-
* in-memory store whenever the settings service is missing or unavailable.
|
|
115
|
-
*/
|
|
116
|
-
export function initSettings(ctx, store) {
|
|
117
|
-
if (!ctx || typeof ctx.get !== 'function' || !store || typeof store !== 'object')
|
|
118
|
-
return;
|
|
119
|
-
let settings;
|
|
120
|
-
try {
|
|
121
|
-
settings = ctx.get('settings');
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (!settings || typeof settings.register !== 'function')
|
|
127
|
-
return;
|
|
128
|
-
let scope;
|
|
129
|
-
try {
|
|
130
|
-
scope = settings.register(USAGE_PRICES_NS, PRICE_SECTION_DESCRIPTOR, { base: { prices: DEFAULT_PRICES, currency: DEFAULT_CURRENCY }, applies: 'live' });
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return; // registration failed — keep the current in-memory behavior
|
|
134
|
-
}
|
|
135
|
-
store.persist = (patch) => {
|
|
136
|
-
try {
|
|
137
|
-
if (scope && typeof scope.update === 'function')
|
|
138
|
-
return scope.update(USAGE_PRICES_NS, patch);
|
|
139
|
-
if (settings && typeof settings.update === 'function')
|
|
140
|
-
return settings.update(USAGE_PRICES_NS, patch);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
// Never throw when the settings scope is unavailable.
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
let section;
|
|
147
|
-
try {
|
|
148
|
-
if (scope && typeof scope.get === 'function')
|
|
149
|
-
section = scope.get();
|
|
150
|
-
else if (settings && typeof settings.get === 'function')
|
|
151
|
-
section = settings.get(USAGE_PRICES_NS);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
section = undefined;
|
|
155
|
-
}
|
|
156
|
-
if (section && typeof section.then === 'function') {
|
|
157
|
-
section.then((resolved) => applySection(store, resolved)).catch(() => { });
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
applySection(store, section);
|
|
161
|
-
}
|
|
162
|
-
}
|
package/lib/host/state.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mutable runtime state capsule for the usage host half.
|
|
3
3
|
*
|
|
4
|
-
* - records: folded usage records, one per assistant
|
|
4
|
+
* - records: folded usage records, one per billed assistant settlement
|
|
5
|
+
* (`assistant/message` or `assistant/attempt`) that reported usage.
|
|
5
6
|
* - watermarks: sessionId -> last folded event seq.
|
|
7
|
+
* - revisions: sessionId -> last persisted revision folded (opaque equality
|
|
8
|
+
* token from `sessionPersistence.list()`); lets the slow scan skip sessions
|
|
9
|
+
* whose log did not change.
|
|
6
10
|
* - backfill: progress counters surfaced through query results.
|
|
7
11
|
* - prices: the price store from settings.js.
|
|
8
|
-
* -
|
|
12
|
+
* - lastSample: sessionId -> { turn, step, record } — the replacement slot the
|
|
13
|
+
* official tokenUsage projection keeps, so a later sample for the same
|
|
14
|
+
* (turn, step) replaces in place and `llm/retry-started` clears it.
|
|
15
|
+
* - route: sessionId -> { provider, model } from the latest request route,
|
|
16
|
+
* used to attribute settlements that carry no `message.source`
|
|
17
|
+
* (`assistant/attempt` never does).
|
|
9
18
|
* - index: incremental aggregation index (aggregate.js), kept in sync by
|
|
10
19
|
* fold.js and consumed by query.js.
|
|
11
20
|
*/
|
|
@@ -13,9 +22,11 @@ export function createState(priceStore) {
|
|
|
13
22
|
return {
|
|
14
23
|
records: [],
|
|
15
24
|
watermarks: new Map(),
|
|
25
|
+
revisions: new Map(),
|
|
16
26
|
backfill: { status: 'idle', total: 0, done: 0 },
|
|
17
27
|
prices: priceStore,
|
|
18
|
-
|
|
28
|
+
lastSample: new Map(),
|
|
29
|
+
route: new Map(),
|
|
19
30
|
index: null, // wired by apply() after createUsageIndex(state)
|
|
20
31
|
};
|
|
21
32
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-usage",
|
|
3
3
|
"description": "Token usage statistics for DeepSeek Harness — daily/custom-range views, model filters, call counts and cost, inspired by CC Switch statistics.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"lib",
|
|
24
|
-
"cordis.patch.yml"
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"image.png",
|
|
26
|
+
"image-1.png"
|
|
25
27
|
],
|
|
26
28
|
"keywords": [
|
|
27
29
|
"dsh",
|
|
@@ -32,7 +34,12 @@
|
|
|
32
34
|
"statistics",
|
|
33
35
|
"cost"
|
|
34
36
|
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22",
|
|
39
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
40
|
+
},
|
|
35
41
|
"dsh": {
|
|
42
|
+
"manifestVersion": 1,
|
|
36
43
|
"bundle": {
|
|
37
44
|
"patch": "./cordis.patch.yml"
|
|
38
45
|
},
|
|
@@ -45,12 +52,14 @@
|
|
|
45
52
|
"platform": "web"
|
|
46
53
|
}
|
|
47
54
|
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@deepseek-ai/schemastery": "~3.18.5"
|
|
57
|
+
},
|
|
48
58
|
"peerDependencies": {
|
|
49
|
-
"@deepseek-ai/cordis": "
|
|
50
|
-
"react": "^18.2.0"
|
|
59
|
+
"@deepseek-ai/cordis": "~4.0.5"
|
|
51
60
|
},
|
|
52
61
|
"devDependencies": {
|
|
53
|
-
"@deepseek-ai/cordis": "
|
|
62
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
54
63
|
"@types/react": "~18.3.1",
|
|
55
64
|
"react": "^18.2.0",
|
|
56
65
|
"typescript": "^5.5.0"
|
|
@@ -59,6 +68,6 @@
|
|
|
59
68
|
"build": "tsc -p tsconfig.json && node scripts/build-client.js",
|
|
60
69
|
"test": "node --test tests/*.test.js",
|
|
61
70
|
"watch": "tsc -p tsconfig.json --watch",
|
|
62
|
-
"prepublishOnly": "npm run build"
|
|
71
|
+
"prepublishOnly": "npm run build && npm test"
|
|
63
72
|
}
|
|
64
|
-
}
|
|
73
|
+
}
|