dsh-plugin-usage 0.2.0 → 0.3.0
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 +41 -110
- package/icon.svg +11 -0
- package/image-1.png +0 -0
- package/image.png +0 -0
- package/lib/client/index.js +671 -44
- package/lib/host/fold.js +142 -35
- package/lib/host/index.js +44 -13
- package/lib/host/query.js +24 -4
- package/lib/host/rpc.js +136 -14
- package/lib/host/scan.js +108 -15
- package/lib/host/settings.js +76 -99
- package/lib/host/state.js +14 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +21 -8
package/lib/host/scan.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live/persisted session scanning + startup backfill.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* DSH 0.2.x session accessors (verified against @deepseek-ai/dsh-session and
|
|
5
|
+
* @deepseek-ai/dsh-session-persistence 0.2.0-rc.2 / 0.2.1-rc.2):
|
|
6
|
+
*
|
|
7
|
+
* - live: `ctx.sessions.list()` -> `Session`, events via
|
|
8
|
+
* `session.snapshotEvents(fromSeq)`.
|
|
9
|
+
* - persisted: `ctx.sessionPersistence.list()` -> `{ header, revision,
|
|
10
|
+
* eventCount?, sizeBytes? }`, then `open(id, 'read')` and
|
|
11
|
+
* `handle.read(offset)`; the handle carries
|
|
12
|
+
* `inheritedEventCount` and must be closed.
|
|
13
|
+
*
|
|
14
|
+
* A forked session replays its parent's prefix; `inheritedEventCount` is the
|
|
15
|
+
* cut, so folding starts there and the parent is never billed twice.
|
|
16
|
+
*
|
|
17
|
+
* Optimization lives here: bounded-concurrency backfill, per-session failure
|
|
18
|
+
* backoff, revision-based skip (unchanged logs are not re-read), progress
|
|
19
|
+
* callbacks, and watermark repair when a log shrank.
|
|
7
20
|
*/
|
|
8
21
|
import { foldNew } from './fold.js';
|
|
9
22
|
/** How many persisted headers are read concurrently per batch. */
|
|
@@ -17,28 +30,94 @@ const BATCH_SIZE = 8;
|
|
|
17
30
|
const backoffBySession = new Map();
|
|
18
31
|
/** True while a scanPersisted pass is in flight; overlapping passes are dropped. */
|
|
19
32
|
let persistedInFlight = false;
|
|
33
|
+
/**
|
|
34
|
+
* Events of one live session at or after `fromSeq`.
|
|
35
|
+
*
|
|
36
|
+
* @param {any} session - live Session.
|
|
37
|
+
* @param {number} fromSeq - first seq to return.
|
|
38
|
+
* @returns {ReadonlyArray<object>} events in log order.
|
|
39
|
+
*/
|
|
40
|
+
export function liveEvents(session, fromSeq) {
|
|
41
|
+
if (session === null || typeof session !== 'object')
|
|
42
|
+
return [];
|
|
43
|
+
if (typeof session.snapshotEvents === 'function')
|
|
44
|
+
return session.snapshotEvents(fromSeq);
|
|
45
|
+
// Legacy pre-0.2 sessions exposed the whole log as `events`.
|
|
46
|
+
if (Array.isArray(session.events)) {
|
|
47
|
+
return session.events.filter((event) => !event || typeof event.seq !== 'number' || event.seq >= fromSeq);
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
20
51
|
/** Scan live in-memory sessions (cheap, every tick). */
|
|
21
52
|
export function scanLive(ctx, state) {
|
|
22
53
|
const sessions = ctx.get('sessions');
|
|
23
|
-
if (sessions === undefined)
|
|
54
|
+
if (sessions === undefined || sessions === null)
|
|
24
55
|
return;
|
|
25
56
|
for (const s of sessions.list()) {
|
|
26
57
|
if (!s || !s.id)
|
|
27
58
|
continue;
|
|
28
59
|
const wm = state.watermarks.get(s.id) ?? -1;
|
|
29
|
-
|
|
60
|
+
// Never bill the fork-inherited prefix to the child.
|
|
61
|
+
const inherited = typeof s.inheritedEventCount === 'number' ? s.inheritedEventCount : 0;
|
|
62
|
+
const from = Math.max(wm + 1, inherited);
|
|
63
|
+
const next = foldNew(state, s.id, liveEvents(s, from), wm);
|
|
30
64
|
if (next > wm)
|
|
31
65
|
state.watermarks.set(s.id, next);
|
|
32
66
|
}
|
|
33
67
|
}
|
|
68
|
+
/** Id of one persistence listing snapshot (`{header:{id}}` in 0.2.x). */
|
|
69
|
+
function snapshotId(snap) {
|
|
70
|
+
if (!snap || typeof snap !== 'object')
|
|
71
|
+
return undefined;
|
|
72
|
+
if (snap.header && typeof snap.header.id === 'string')
|
|
73
|
+
return snap.header.id;
|
|
74
|
+
if (typeof snap.id === 'string')
|
|
75
|
+
return snap.id;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read events for one persisted session at or after `fromSeq`, preferring the
|
|
80
|
+
* 0.2.x handle API and falling back to a legacy `readFrom` backend.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} persistence - sessionPersistence service.
|
|
83
|
+
* @param {string} id - session id.
|
|
84
|
+
* @param {number} fromSeq - first seq to return.
|
|
85
|
+
* @returns {Promise<{ events: ReadonlyArray<object>, inherited: number }>} events + fork cut.
|
|
86
|
+
*/
|
|
87
|
+
export async function readSessionEvents(persistence, id, fromSeq) {
|
|
88
|
+
if (typeof persistence.open === 'function') {
|
|
89
|
+
const handle = await persistence.open(id, 'read');
|
|
90
|
+
try {
|
|
91
|
+
const inherited = typeof handle.inheritedEventCount === 'number' ? handle.inheritedEventCount : 0;
|
|
92
|
+
const page = await handle.read(Math.max(fromSeq, inherited));
|
|
93
|
+
return { events: (page && page.events) || [], inherited };
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (handle && typeof handle.close === 'function') {
|
|
97
|
+
try {
|
|
98
|
+
await handle.close();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// A close failure must not turn a successful read into a failed scan.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof persistence.readFrom === 'function') {
|
|
107
|
+
const page = await persistence.readFrom(id, fromSeq);
|
|
108
|
+
return { events: (page && page.events) || [], inherited: 0 };
|
|
109
|
+
}
|
|
110
|
+
return { events: [], inherited: 0 };
|
|
111
|
+
}
|
|
34
112
|
/**
|
|
35
113
|
* Incremental scan of persisted sessions (slow cadence). Headers are read in
|
|
36
|
-
* small concurrent batches;
|
|
37
|
-
* exponentially growing number of passes instead of
|
|
114
|
+
* small concurrent batches; unchanged revisions are skipped; a failing session
|
|
115
|
+
* is backed off for an exponentially growing number of passes instead of
|
|
116
|
+
* aborting the pass.
|
|
38
117
|
*/
|
|
39
118
|
export async function scanPersisted(ctx, state, options) {
|
|
40
119
|
const sp = ctx.get('sessionPersistence');
|
|
41
|
-
if (sp === undefined)
|
|
120
|
+
if (sp === undefined || sp === null)
|
|
42
121
|
return;
|
|
43
122
|
if (persistedInFlight)
|
|
44
123
|
return; // previous pass still running: skip this tick
|
|
@@ -65,22 +144,36 @@ export async function scanPersisted(ctx, state, options) {
|
|
|
65
144
|
}
|
|
66
145
|
}
|
|
67
146
|
/** Read + fold one persisted session header; bounded by the batch caller. */
|
|
68
|
-
async function processHeader(sp, state,
|
|
69
|
-
|
|
147
|
+
async function processHeader(sp, state, snap, options) {
|
|
148
|
+
const id = snapshotId(snap);
|
|
149
|
+
if (id === undefined)
|
|
70
150
|
return;
|
|
71
|
-
const id = h.id;
|
|
72
151
|
const backoff = backoffBySession.get(id);
|
|
73
152
|
if (backoff && backoff.skipLeft > 0) {
|
|
74
153
|
backoff.skipLeft -= 1; // decay the skip count one pass at a time
|
|
75
154
|
return;
|
|
76
155
|
}
|
|
77
|
-
const wm = state.watermarks.get(id) ?? -1;
|
|
78
156
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
157
|
+
const revision = snap && snap.revision;
|
|
158
|
+
if (revision !== undefined && Object.is(state.revisions.get(id), revision)) {
|
|
159
|
+
// The backend reports the same change token: the log is unchanged.
|
|
160
|
+
backoffBySession.delete(id);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let wm = state.watermarks.get(id) ?? -1;
|
|
164
|
+
// A crash-repaired or migrated log can shrink below our watermark; the
|
|
165
|
+
// backend reports the current event count, so repair instead of stalling.
|
|
166
|
+
if (typeof snap.eventCount === 'number' && wm >= snap.eventCount) {
|
|
167
|
+
wm = -1;
|
|
168
|
+
state.watermarks.set(id, -1);
|
|
169
|
+
state.lastSample.delete(id);
|
|
170
|
+
}
|
|
171
|
+
const { events } = await readSessionEvents(sp, id, wm + 1);
|
|
81
172
|
const next = foldNew(state, id, events, wm);
|
|
82
173
|
if (next > wm)
|
|
83
174
|
state.watermarks.set(id, next);
|
|
175
|
+
if (revision !== undefined)
|
|
176
|
+
state.revisions.set(id, revision);
|
|
84
177
|
backoffBySession.delete(id); // success resets the consecutive-failure chain
|
|
85
178
|
}
|
|
86
179
|
catch (error) {
|
|
@@ -107,7 +200,7 @@ async function processHeader(sp, state, h, options) {
|
|
|
107
200
|
/** Full history scan, once at startup. */
|
|
108
201
|
export async function runBackfill(ctx, state, options) {
|
|
109
202
|
const sp = ctx.get('sessionPersistence');
|
|
110
|
-
if (sp === undefined) {
|
|
203
|
+
if (sp === undefined || sp === null) {
|
|
111
204
|
state.backfill.status = 'done';
|
|
112
205
|
fireBackfillDone(options, state);
|
|
113
206
|
return;
|
package/lib/host/settings.js
CHANGED
|
@@ -1,29 +1,98 @@
|
|
|
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 = '¥';
|
|
15
|
+
/**
|
|
16
|
+
* Currency the price table is denominated in.
|
|
17
|
+
*
|
|
18
|
+
* DEFAULT_PRICES carries DeepSeek's USD list price per 1M tokens (0.27 input /
|
|
19
|
+
* 1.10 output / 0.07 cache-read for `deepseek-chat`, which is exactly 2 / 8 /
|
|
20
|
+
* 0.5 CNY at the fixed 7:1). The table is never rewritten at runtime: a display
|
|
21
|
+
* switch converts on the way out, so a query can never pollute the next one.
|
|
22
|
+
*/
|
|
23
|
+
export const PRICE_UNIT_CURRENCY = 'USD';
|
|
24
|
+
/** Display currencies; `perUsd` is how many units of it one USD buys. */
|
|
25
|
+
export const CURRENCIES = {
|
|
26
|
+
USD: { code: 'USD', symbol: '$', perUsd: 1 },
|
|
27
|
+
CNY: { code: 'CNY', symbol: '¥', perUsd: 7 },
|
|
28
|
+
};
|
|
29
|
+
function currencyCodeOf(value) {
|
|
30
|
+
const raw = typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
31
|
+
if (raw === 'USD' || raw === '$' || raw === 'US$')
|
|
32
|
+
return 'USD';
|
|
33
|
+
if (raw === 'CNY' || raw === '¥' || raw === 'RMB' || raw === 'CN¥')
|
|
34
|
+
return 'CNY';
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a configured currency (ISO code or the bare symbol the store has
|
|
39
|
+
* always held) to a CURRENCIES entry. Unknown values fall back to `fallback`,
|
|
40
|
+
* then to the default display currency — never throw on user input.
|
|
41
|
+
*
|
|
42
|
+
* @param {unknown} value
|
|
43
|
+
* @param {unknown} [fallback]
|
|
44
|
+
*/
|
|
45
|
+
export function currencyOf(value, fallback) {
|
|
46
|
+
const hit = CURRENCIES[currencyCodeOf(value)];
|
|
47
|
+
if (hit)
|
|
48
|
+
return hit;
|
|
49
|
+
const fb = CURRENCIES[currencyCodeOf(fallback)];
|
|
50
|
+
if (fb)
|
|
51
|
+
return fb;
|
|
52
|
+
return CURRENCIES[currencyCodeOf(DEFAULT_CURRENCY)] || CURRENCIES.CNY;
|
|
53
|
+
}
|
|
10
54
|
export const DEFAULT_PRICES = {
|
|
11
55
|
'deepseek-chat': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
|
|
12
56
|
'deepseek-v3': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
|
|
13
57
|
'deepseek-v3.1': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
|
|
14
58
|
'deepseek-v4': { inputPerM: 0.27, outputPerM: 1.1, cacheReadPerM: 0.07, cacheWritePerM: 0.27 },
|
|
15
59
|
'deepseek-v4-flash': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
60
|
+
'deepseek-v4.1-flash': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
61
|
+
'deepseek-v4.1-flash-1017': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
62
|
+
// DSH's routed default model id (`magpie` provider) aliases the same route;
|
|
63
|
+
// without this entry every session bills as "unpriced" on a stock install.
|
|
64
|
+
'group/auto-deepseek-v4-1-flash': { inputPerM: 0.14, outputPerM: 0.56, cacheReadPerM: 0.04, cacheWritePerM: 0.14 },
|
|
16
65
|
'deepseek-reasoner': { inputPerM: 0.55, outputPerM: 2.19, cacheReadPerM: 0.14, cacheWritePerM: 0.55 },
|
|
17
66
|
'deepseek-r1': { inputPerM: 0.55, outputPerM: 2.19, cacheReadPerM: 0.14, cacheWritePerM: 0.55 },
|
|
18
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Shallow-merge configured prices over the defaults.
|
|
70
|
+
*
|
|
71
|
+
* @param {unknown} incoming - configured per-model prices.
|
|
72
|
+
* @returns {Record<string, object>} complete price table.
|
|
73
|
+
*/
|
|
74
|
+
export function mergePrices(incoming) {
|
|
75
|
+
const merged = Object.assign({}, DEFAULT_PRICES);
|
|
76
|
+
if (incoming && typeof incoming === 'object' && !Array.isArray(incoming)) {
|
|
77
|
+
for (const [model, price] of Object.entries(incoming)) {
|
|
78
|
+
if (price && typeof price === 'object') {
|
|
79
|
+
merged[model] = {
|
|
80
|
+
inputPerM: Number(price.inputPerM) || 0,
|
|
81
|
+
outputPerM: Number(price.outputPerM) || 0,
|
|
82
|
+
cacheReadPerM: Number(price.cacheReadPerM) || 0,
|
|
83
|
+
cacheWritePerM: Number(price.cacheWritePerM) || 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return merged;
|
|
89
|
+
}
|
|
19
90
|
export function createPriceStore(defaultPrices, defaultCurrency) {
|
|
20
91
|
const prices = Object.assign({}, defaultPrices);
|
|
21
92
|
let currency = defaultCurrency || DEFAULT_CURRENCY;
|
|
22
93
|
const store = {
|
|
23
94
|
prices,
|
|
24
95
|
get currency() { return currency; },
|
|
25
|
-
/** Write-through slot wired by initSettings(); null without a settings service. */
|
|
26
|
-
persist: null,
|
|
27
96
|
/** Merge incoming per-model prices (each field coerced to a number). */
|
|
28
97
|
setPrices(incoming) {
|
|
29
98
|
if (!incoming || typeof incoming !== 'object')
|
|
@@ -38,15 +107,10 @@ export function createPriceStore(defaultPrices, defaultCurrency) {
|
|
|
38
107
|
};
|
|
39
108
|
}
|
|
40
109
|
}
|
|
41
|
-
if (typeof store.persist === 'function')
|
|
42
|
-
store.persist({ prices: Object.assign({}, prices) });
|
|
43
110
|
},
|
|
44
111
|
setCurrency(v) {
|
|
45
|
-
if (typeof v === 'string' && v.length > 0)
|
|
112
|
+
if (typeof v === 'string' && v.length > 0)
|
|
46
113
|
currency = v;
|
|
47
|
-
if (typeof store.persist === 'function')
|
|
48
|
-
store.persist({ currency });
|
|
49
|
-
}
|
|
50
114
|
},
|
|
51
115
|
/** Look up a price by `provider/model` composite first, then bare model. */
|
|
52
116
|
priceOf(r) {
|
|
@@ -73,90 +137,3 @@ export function createPriceStore(defaultPrices, defaultCurrency) {
|
|
|
73
137
|
};
|
|
74
138
|
return store;
|
|
75
139
|
}
|
|
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/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/bennett-dsh/dsh-plugin-usage.git"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
|
+
"icon": "./icon.svg",
|
|
11
12
|
"main": "lib/host/index.js",
|
|
12
13
|
"exports": {
|
|
13
14
|
".": {
|
|
@@ -17,11 +18,16 @@
|
|
|
17
18
|
"default": "./lib/client/index.js"
|
|
18
19
|
},
|
|
19
20
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
20
|
-
"./package.json": "./package.json"
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./locale/*.json": "./locale/*.json"
|
|
21
23
|
},
|
|
22
24
|
"files": [
|
|
23
25
|
"lib",
|
|
24
|
-
"cordis.patch.yml"
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"icon.svg",
|
|
28
|
+
"locale",
|
|
29
|
+
"image.png",
|
|
30
|
+
"image-1.png"
|
|
25
31
|
],
|
|
26
32
|
"keywords": [
|
|
27
33
|
"dsh",
|
|
@@ -32,7 +38,12 @@
|
|
|
32
38
|
"statistics",
|
|
33
39
|
"cost"
|
|
34
40
|
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22",
|
|
43
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
44
|
+
},
|
|
35
45
|
"dsh": {
|
|
46
|
+
"manifestVersion": 1,
|
|
36
47
|
"bundle": {
|
|
37
48
|
"patch": "./cordis.patch.yml"
|
|
38
49
|
},
|
|
@@ -45,12 +56,14 @@
|
|
|
45
56
|
"platform": "web"
|
|
46
57
|
}
|
|
47
58
|
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@deepseek-ai/schemastery": "~3.18.5"
|
|
61
|
+
},
|
|
48
62
|
"peerDependencies": {
|
|
49
|
-
"@deepseek-ai/cordis": "
|
|
50
|
-
"react": "^18.2.0"
|
|
63
|
+
"@deepseek-ai/cordis": "~4.0.5"
|
|
51
64
|
},
|
|
52
65
|
"devDependencies": {
|
|
53
|
-
"@deepseek-ai/cordis": "
|
|
66
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
54
67
|
"@types/react": "~18.3.1",
|
|
55
68
|
"react": "^18.2.0",
|
|
56
69
|
"typescript": "^5.5.0"
|
|
@@ -59,6 +72,6 @@
|
|
|
59
72
|
"build": "tsc -p tsconfig.json && node scripts/build-client.js",
|
|
60
73
|
"test": "node --test tests/*.test.js",
|
|
61
74
|
"watch": "tsc -p tsconfig.json --watch",
|
|
62
|
-
"prepublishOnly": "npm run build"
|
|
75
|
+
"prepublishOnly": "npm run build && npm test"
|
|
63
76
|
}
|
|
64
|
-
}
|
|
77
|
+
}
|