@rayadesu/dsh-llm-billing 0.3.16 → 0.3.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.
@@ -7,7 +7,7 @@
7
7
  * `billingTodaySpend` projection cell; cold sessions are answered from the
8
8
  * zero-I/O projection-cache row whenever that row's own day is not the
9
9
  * queried one, and otherwise resolved through one detached local fold over
10
- * a full `inspect`. Persisted revisions gate every cold read, so a session
10
+ * a full `open` + handle read. Persisted revisions gate every cold read, so a session
11
11
  * whose log did not change since the last resolution costs nothing — and a
12
12
  * failed resolution is remembered by revision instead of being retried on
13
13
  * every scan.
@@ -38,258 +38,29 @@
38
38
  * cut, and `apply` skips events below it), so the eager cell is correct for a
39
39
  * fork child; the cold path skips the projection cache for a seeded session
40
40
  * (its cached row may predate the boundary) and folds its own events with the
41
- * durable cut instead. The boundary is the durable session state, read across
42
- * both DSH runtime families — a resumed fork child keeps its original
41
+ * durable cut instead. The boundary is the durable session state, read from the
42
+ * live-session and handle surfaces — a resumed fork child keeps its original
43
43
  * boundary and an unseeded session stays at 0.
44
44
  *
45
45
  * The live `Session` log surface changed in 0.1.2-alpha.4: `Session.events`
46
46
  * was removed and replaced by `Session.snapshotEvents()` / `ownEvents()`, and
47
47
  * `SessionHeader.seedLength` moved to `Session.inheritedEventCount` (the
48
- * persistence `inspect` result carries it alongside `meta`). Reads go through
49
- * {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept both
50
- * families structurally, so the scanner runs on the ≤ 0.1.1-rc.2 npm baseline
51
- * and on the newer runtime.
48
+ * opened `SessionHandle` carries it alongside `header`). Reads go through
49
+ * {@link liveSessionEvents} / {@link forkBoundaryOf}, which accept the
50
+ * live-session and handle shapes structurally, so the scanner runs on the
51
+ * older `events` runtime and on the newer `snapshotEvents` runtime.
52
52
  * @module @rayadesu/dsh-llm-billing/today-spend
53
53
  */
54
- import { beijingDayKey, beijingPartsOf, BillingFolder, emptyTodaySpend, forkBoundaryOf, isSeededSession, mergeTodaySpend } from "./billing.js";
54
+ import { beijingDayRangeOfKey, beijingPartsOf, BillingFolder, emptyTodaySpend, forkBoundaryOf, isSeededSession, mergeTodaySpend } from "./billing.js";
55
55
  import { BILLING_UNIT_KEY, foldOwnBilling } from "./projection.js";
56
- /**
57
- * Fold one session's durable display title: the latest `session/title`
58
- * event's text (last-wins, matching the `title` projection), or `null` before
59
- * the first title lands. The fold runs over the complete log, so an explicit
60
- * user rename is picked up as soon as its event commits.
61
- * @param events - one session's complete event log.
62
- * @returns the session's current title, or `null` when untitled.
63
- */
64
- export function foldSessionTitle(events) {
65
- for (let index = events.length - 1; index >= 0; index--) {
66
- const event = events[index];
67
- // `session/title` joined the SessionEvent union after the npm
68
- // 0.1.1-rc.2 baseline this package builds against; read its payload
69
- // through the structural escape hatch (runtime logs carry it).
70
- if (event.type !== 'session/title')
71
- continue;
72
- const data = event.data;
73
- return typeof data.title === 'string' ? data.title : null;
74
- }
75
- return null;
76
- }
77
- /**
78
- * Whether one session's durable header marks it as a subagent child. Either
79
- * marker is enough: `origin` is DSH's navigation classification and
80
- * `delegationDepth` is its persisted recursion budget, so a header carrying
81
- * only the depth (or only the origin) is still a delegation child. A session
82
- * created without either — an ordinary session, a user fork, or a cold resume
83
- * — is top-level.
84
- * @param header - the session's lineage slice; `undefined` reads as top-level.
85
- * @returns true when the session was created as a subagent child.
86
- */
87
- export function isSubagentSession(header) {
88
- if (header === undefined)
89
- return false;
90
- return header.origin === 'subagent' || (header.delegationDepth ?? 0) > 0;
91
- }
92
- /**
93
- * The top-level session one session's ranking row belongs to: the session
94
- * itself for a top-level session, and for a subagent child the first ancestor
95
- * up the `parentSession` chain that is not itself a subagent child. A
96
- * multi-generation delegation (a subagent that spawned subagents) therefore
97
- * lands on the same root row as its parent, and a child whose parent header is
98
- * unknown is attributed to the parent id its own header names — the parent is
99
- * authoritative even when its log is not part of this scan.
100
- * @param id - the session whose row is being attributed.
101
- * @param lineage - lineage of every session this scan saw, by id.
102
- * @returns the session id whose ranking row the input belongs to.
103
- */
104
- export function topLevelSessionOf(id, lineage) {
105
- let current = id;
106
- // A malformed log could claim a delegation cycle; each step visits a
107
- // distinct ancestor, so a repeat ends the walk instead of spinning.
108
- const seen = new Set([current]);
109
- for (;;) {
110
- const header = lineage.get(current);
111
- if (!isSubagentSession(header))
112
- return current;
113
- const parent = header?.parentSession;
114
- if (parent === undefined || seen.has(parent))
115
- return current;
116
- seen.add(parent);
117
- current = parent;
118
- }
119
- }
120
- /**
121
- * Fold every subagent child's row into the top-level row it belongs to
122
- * ({@link topLevelSessionOf}), so the ranking lists conversations rather than
123
- * every delegation a conversation started. A child's spend is added to its
124
- * ancestor's `total`; the ancestor's `ownTotal` keeps its own spend only. A
125
- * top-level session whose own day was empty but whose subagents priced
126
- * something still gets a row (with `ownTotal` 0), carrying the title the scan
127
- * resolved for it in `titles`.
128
- * @param rows - one row per session that priced something today (own spends).
129
- * @param lineage - lineage of every session this scan saw, by id.
130
- * @param titles - resolved display titles by session id; a session absent from
131
- * the map has no resolved title and its created row reports `null`.
132
- * @returns the merged rows, sorted by `total` descending.
133
- */
134
- export function rollUpSubagentSpend(rows, lineage, titles = new Map()) {
135
- const merged = new Map();
136
- for (const row of rows) {
137
- const target = topLevelSessionOf(row.sessionId, lineage);
138
- const carried = merged.get(target);
139
- if (carried === undefined) {
140
- // A row's own session keeps its own total as `ownTotal`; a row created
141
- // for an ancestor that priced nothing today carries zero there.
142
- merged.set(target, target === row.sessionId
143
- ? row
144
- : { sessionId: target, title: titles.get(target) ?? null, total: row.total, ownTotal: 0 });
145
- continue;
146
- }
147
- merged.set(target, { ...carried, total: carried.total + row.total });
148
- }
149
- return [...merged.values()].sort((left, right) => right.total - left.total);
150
- }
151
- /**
152
- * Read one live session's complete event log across both runtime families.
153
- * @throws when the session exposes neither the legacy `events` snapshot nor
154
- * the newer `snapshotEvents()` reader — an unknown runtime surface must
155
- * fail loudly rather than silently price an empty log.
156
- */
157
- export function liveSessionEvents(session) {
158
- if (session.events !== undefined)
159
- return session.events;
160
- // Optional-call form keeps the receiver bound (snapshotEvents uses `this`).
161
- if (session.snapshotEvents !== undefined)
162
- return session.snapshotEvents();
163
- throw new Error('llm-billing: session log surface is neither Session.events nor Session.snapshotEvents');
164
- }
165
- /**
166
- * Unwrap a handle read across both return shapes.
167
- * @param read - the handle's read result.
168
- * @returns the event array.
169
- */
170
- export function handleReadEvents(read) {
171
- // `Array.isArray` does not narrow readonly arrays out of a union, so the
172
- // branches are asserted explicitly.
173
- if (Array.isArray(read))
174
- return read;
175
- return read.events;
176
- }
177
- function isHandlePersistence(persistence) {
178
- return typeof persistence.open === 'function';
179
- }
180
- /**
181
- * List every stored session snapshot across both persistence runtime families:
182
- * `listSnapshots` (≤ 0.1.1-rc.2) or `list` (0.1.2-alpha.5+).
183
- * @param persistence - the persistence service slice.
184
- * @returns one snapshot per stored session.
185
- */
186
- export function persistenceListSnapshots(persistence) {
187
- return isHandlePersistence(persistence)
188
- ? persistence.list()
189
- : persistence.listSnapshots();
190
- }
191
- /**
192
- * Read one stored session's complete event log and durable inherited boundary
193
- * across both persistence runtime families: legacy `inspect` (≤ 0.1.1-rc.2)
194
- * or `open` + handle `read` (0.1.2-alpha.5+; the handle is closed after the
195
- * read). Both throw when the session does not exist.
196
- * @param persistence - the persistence service slice.
197
- * @param id - the stored session to read.
198
- * @returns the session's complete event log plus its inherited-prefix boundary.
199
- */
200
- export async function persistenceInspect(persistence, id) {
201
- if (isHandlePersistence(persistence)) {
202
- const handle = await persistence.open(id, 'read');
203
- try {
204
- return { events: handleReadEvents(await handle.read()), seedLength: forkBoundaryOf(handle) };
205
- }
206
- finally {
207
- await handle.close();
208
- }
209
- }
210
- const inspection = await persistence.inspect(id);
211
- return { events: inspection.events, seedLength: forkBoundaryOf(inspection) };
212
- }
213
- /**
214
- * Bounded parallel fan-out: run `run` over `items` with at most `limit` in
215
- * flight. A shared index counter hands each worker its next job, so the
216
- * dispatch is O(n) overall (array `shift()` would be O(n) per pop).
217
- */
218
- async function withConcurrency(items, limit, run) {
219
- const total = items.length;
220
- let next = 0;
221
- await Promise.all(Array.from({ length: Math.min(limit, total) }, async () => {
222
- for (let job = next; job < total; job = next) {
223
- next += 1;
224
- await run(items[job]);
225
- }
226
- }));
227
- }
228
- /**
229
- * The A1 cache: one Beijing-day key + a 60s window, an in-flight promise that
230
- * coalesces concurrent misses, and a `force` bypass for the manual refresh
231
- * path. Cross-day invalidation is automatic (the day key changes); a failed
232
- * scan leaves the previous value in place and retries on the next call.
233
- * @typeParam T - the cached aggregate's value shape (the spend or its
234
- * per-session breakdown).
235
- */
236
- export class TodaySpendCache {
237
- scan;
238
- ttlMs;
239
- now;
240
- cachedDayKey;
241
- cachedValue;
242
- cachedAt = 0;
243
- inFlight;
244
- /**
245
- * @param scan - the aggregate computation behind a miss.
246
- * @param ttlMs - time window in milliseconds (default 60 000).
247
- * @param now - clock source (injectable for tests).
248
- */
249
- constructor(scan, ttlMs = 60_000, now = () => new Date()) {
250
- this.scan = scan;
251
- this.ttlMs = ttlMs;
252
- this.now = now;
253
- }
254
- /**
255
- * Read today's spend, cached per Beijing day within the TTL window.
256
- * @param force - bypass the time window (manual refresh); the day-key gate
257
- * and the in-flight coalescing still apply to non-force callers.
258
- * @returns today's spend.
259
- */
260
- get(force = false) {
261
- const now = this.now();
262
- const dayKey = beijingDayKey(now);
263
- if (!force && this.cachedDayKey === dayKey && this.cachedValue !== undefined
264
- && now.getTime() - this.cachedAt < this.ttlMs) {
265
- return Promise.resolve(this.cachedValue);
266
- }
267
- // A scan already in flight is fresh by definition, so a forced caller
268
- // joins it instead of starting a second pass.
269
- if (this.inFlight !== undefined)
270
- return this.inFlight;
271
- const run = (async () => {
272
- try {
273
- const value = await this.scan(dayKey);
274
- this.cachedDayKey = dayKey;
275
- this.cachedValue = value;
276
- this.cachedAt = now.getTime();
277
- return value;
278
- }
279
- finally {
280
- this.inFlight = undefined;
281
- }
282
- })();
283
- this.inFlight = run;
284
- return run;
285
- }
286
- }
287
- /** Max session-ids kept in the scanner's cold-resolution cache before eviction. */
288
- export const COLD_RESOLVE_CACHE_LIMIT = 1024;
289
- /** Max session-ids kept in the scanner's cold-failure cache before eviction. */
290
- export const COLD_FAILED_CACHE_LIMIT = 1024;
291
- /** Bounded parallel fan-out for cold-session resolution. */
292
- export const COLD_RESOLVE_CONCURRENCY = 8;
56
+ import { foldSessionTitle, isSubagentSession, rollUpSubagentSpend } from "./session-lineage.js";
57
+ import { COLD_FAILED_CACHE_LIMIT, COLD_FAILED_RETRY_MS, COLD_RESOLVE_CACHE_LIMIT, COLD_RESOLVE_CONCURRENCY, SCAN_YIELD_SESSIONS, withConcurrency, yieldToEventLoop } from "./cache.js";
58
+ import { liveSessionEvents, persistenceInspect, persistenceListSnapshots } from "./persistence.js";
59
+ // Re-exported so the package's import surface is unchanged: these names were
60
+ // declared here before the module was split.
61
+ export * from "./session-lineage.js";
62
+ export * from "./persistence.js";
63
+ export * from "./cache.js";
293
64
  /**
294
65
  * Bounded-map eviction: drop the oldest inserted entry once `size` reached
295
66
  * `limit`. Evicting one entry (instead of clearing) keeps the other sessions'
@@ -367,7 +138,12 @@ export class TodaySpendScanner {
367
138
  * from the aggregate and the ranking on every scan after the first.
368
139
  */
369
140
  coldResolved = new Map();
370
- /** Cold sessions whose resolution failed: id → revision (retried only when the log changes). */
141
+ /**
142
+ * Cold sessions whose resolution failed, id → the revision it failed at plus
143
+ * when. Keyed by revision too, so an unchanged log is not re-read every scan,
144
+ * but the retry is TIME-boxed: a purely revision-keyed failure would hide a
145
+ * session permanently when its log never changes again.
146
+ */
371
147
  coldFailed = new Map();
372
148
  constructor(deps) {
373
149
  this.deps = deps;
@@ -410,7 +186,10 @@ export class TodaySpendScanner {
410
186
  * otherwise.
411
187
  *
412
188
  * The aggregate sums every priced session, subagents included — the ranking's
413
- * subagent roll-up only regroups rows, so neither total moves.
189
+ * subagent roll-up only regroups rows, so neither total moves. Both paths
190
+ * fold in slices and hand the host's event loop back every
191
+ * {@link SCAN_YIELD_SESSIONS} sessions, so a long scan never starves the
192
+ * GUI's own round trips.
414
193
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
415
194
  * @returns the aggregate plus per-session rows sorted by cost descending.
416
195
  */
@@ -427,14 +206,14 @@ export class TodaySpendScanner {
427
206
  * own latest priced day is NOT the queried day: the row then proves the
428
207
  * session contributed nothing to the queried day, so the log is never read.
429
208
  * When the row IS the queried day (or no usable row exists) the session is
430
- * inspected and folded locally, because the row may trail the log (a crash
209
+ * opened and folded locally, because the row may trail the log (a crash
431
210
  * between the last checkpoint and the session's last event).
432
211
  *
433
212
  * A cache-served value carries no title (the ladder only stores projection
434
213
  * values), so such rows report `title: null`. A SEEDED session (fork child)
435
214
  * skips the cache entirely: its cached row was folded over the inherited
436
- * prefix too, so it always detaches through inspect with the durable
437
- * boundary (the inspect result's inherited count or `meta.seedLength`,
215
+ * prefix too, so it always detaches through a handle read with the durable
216
+ * boundary (the handle's `inheritedEventCount` or `header.seedLength`,
438
217
  * depending on the runtime family) applied to the local fold.
439
218
  * @param header - the listed session header (the cache identity witness).
440
219
  * @param seeded - whether the session carries a fork-inherited prefix.
@@ -508,11 +287,23 @@ export class TodaySpendScanner {
508
287
  adopt(header.id, resolved);
509
288
  continue;
510
289
  }
511
- if (this.coldFailed.get(header.id) === revision)
290
+ // Skipped while the SAME revision is still inside its retry window; once
291
+ // the window lapses the read is attempted again even though nothing
292
+ // changed, which is what rescues a transient failure.
293
+ const failed = this.coldFailed.get(header.id);
294
+ if (failed !== undefined && failed.revision === revision
295
+ && Date.now() - failed.at < COLD_FAILED_RETRY_MS)
512
296
  continue;
513
297
  pending.push({ header, revision, seeded });
514
298
  }
299
+ let resolvedCount = 0;
515
300
  await withConcurrency(pending, COLD_RESOLVE_CONCURRENCY, async ({ header, revision, seeded }) => {
301
+ // A resolution answered straight from the projection cache performs no
302
+ // I/O, so without this the fan-out would run a long microtask chain that
303
+ // never reaches the event loop's poll phase.
304
+ resolvedCount += 1;
305
+ if (resolvedCount % SCAN_YIELD_SESSIONS === 0)
306
+ await yieldToEventLoop();
516
307
  const resolved = await this.resolveCold(header, seeded, dayKey);
517
308
  if (resolved !== undefined) {
518
309
  this.coldFailed.delete(header.id);
@@ -520,10 +311,16 @@ export class TodaySpendScanner {
520
311
  }
521
312
  else if (persistenceAvailable) {
522
313
  evictOldest(this.coldFailed, COLD_FAILED_CACHE_LIMIT);
523
- this.coldFailed.set(header.id, revision);
314
+ this.coldFailed.set(header.id, { revision, at: Date.now() });
524
315
  }
525
316
  });
317
+ let adopted = 0;
526
318
  for (const { header } of pending) {
319
+ // The adopt below merges synchronously; a day with many cold sessions
320
+ // must not fold them all in one uninterrupted slice.
321
+ adopted += 1;
322
+ if (adopted % SCAN_YIELD_SESSIONS === 0)
323
+ await yieldToEventLoop();
527
324
  const resolved = this.coldResolved.get(header.id);
528
325
  if (resolved !== undefined)
529
326
  adopt(header.id, resolved);
@@ -550,6 +347,14 @@ export class TodaySpendScanner {
550
347
  const liveIds = new Set();
551
348
  let collected = 0;
552
349
  let truncated = false;
350
+ // The queried day as numbers — one conversion per scan instead of a date
351
+ // string per event, since the counting below walks every event of every
352
+ // log. A key that does not parse falls back to the string test rather than
353
+ // silently claiming no event belongs to the day.
354
+ const dayRange = beijingDayRangeOfKey(dayKey);
355
+ const onDay = dayRange === undefined
356
+ ? (event) => beijingPartsOf(event.time).dayKey === dayKey
357
+ : (event) => event.time >= dayRange.start && event.time < dayRange.end;
553
358
  /** Price one complete log, announce it, and return what to remember. */
554
359
  const collect = (id, events, seedLength, lineage) => {
555
360
  const folder = new BillingFolder(billing, catalog, seedLength);
@@ -557,7 +362,7 @@ export class TodaySpendScanner {
557
362
  // The cap counts the queried day's events; the fold still sees every
558
363
  // event up to the cap (model tracking and attempt replacement need
559
364
  // the surrounding events).
560
- if (beijingPartsOf(event.time).dayKey === dayKey) {
365
+ if (onDay(event)) {
561
366
  collected += 1;
562
367
  if (collected > maxEvents) {
563
368
  truncated = true;
@@ -574,7 +379,14 @@ export class TodaySpendScanner {
574
379
  if (sessions !== undefined) {
575
380
  const store = sessions();
576
381
  if (store !== undefined) {
382
+ let folded = 0;
577
383
  for (const session of store.list()) {
384
+ // The fold below walks this session's whole log synchronously on the
385
+ // host's main loop; hand the loop back every few sessions so the GUI's
386
+ // own round trips still get served during a long scan.
387
+ folded += 1;
388
+ if (folded % SCAN_YIELD_SESSIONS === 0)
389
+ await yieldToEventLoop();
578
390
  liveIds.add(session.id);
579
391
  collect(session.id, liveSessionEvents(session), forkBoundaryOf(session), session.header ?? {});
580
392
  if (truncated)
@@ -618,7 +430,7 @@ export class TodaySpendScanner {
618
430
  /**
619
431
  * Projection path, one pass for both outputs: eager cells for live sessions
620
432
  * (title folded from the live log, so a rename is reflected immediately),
621
- * revision-gated cold ladder for the rest (title resolved on inspect, `null`
433
+ * revision-gated cold ladder for the rest (title resolved on the handle read, `null`
622
434
  * when answered from the projection cache). A fork child's cell covers its
623
435
  * inherited prefix, so its own-events fold supplies both outputs. Lineage
624
436
  * (which session delegated which) comes from the same headers the boundary
@@ -640,7 +452,14 @@ export class TodaySpendScanner {
640
452
  if (sessions !== undefined) {
641
453
  const store = sessions();
642
454
  if (store !== undefined) {
455
+ let folded = 0;
643
456
  for (const { session, state } of this.liveBillingEntries(store, projectionsService)) {
457
+ // The title fold below materialises and walks this session's whole
458
+ // log synchronously on the host's main loop; hand the loop back every
459
+ // few sessions so the GUI's own round trips still get served.
460
+ folded += 1;
461
+ if (folded % SCAN_YIELD_SESSIONS === 0)
462
+ await yieldToEventLoop();
644
463
  liveIds.add(session.id);
645
464
  lineage.set(session.id, session.header ?? {});
646
465
  const born = session.header?.createdAt;
@@ -65,6 +65,26 @@ export interface DeepSeekSessionSpend {
65
65
  total: number;
66
66
  /** One row per model that reported usage AND has a pricing row; empty when the session has no priced usage. */
67
67
  models: readonly DeepSeekSessionSpendModel[];
68
+ /**
69
+ * Samples that carried usage but have NO pricing row, and so contribute
70
+ * nothing to {@link total} — so a session that only ever used an unlisted
71
+ * model does not read as "no usage" beside a today row that says otherwise.
72
+ */
73
+ unpriced?: DeepSeekUnpricedUsage | undefined;
74
+ }
75
+ /**
76
+ * Provider-reported usage that could NOT be billed: it matched no pricing row,
77
+ * and no rate is ever guessed from a similar-looking model id. Purely advisory
78
+ * — it never changes a spend's `total`, and the whole field is omitted when
79
+ * every sample was priced against its own row.
80
+ */
81
+ export interface DeepSeekUnpricedUsage {
82
+ /** Number of usage samples affected. */
83
+ events: number;
84
+ /** Token count affected: input + cache read + cache write + output tokens. */
85
+ tokens: number;
86
+ /** The wire model ids affected, in first-seen order. */
87
+ models: readonly string[];
68
88
  }
69
89
  /** The billed spend of every session on one Beijing-time calendar day. */
70
90
  export interface DeepSeekTodaySpend {
@@ -72,6 +92,14 @@ export interface DeepSeekTodaySpend {
72
92
  total: number;
73
93
  /** One row per model that reported usage AND has a pricing row; empty when today has no priced usage. */
74
94
  models: readonly DeepSeekSessionSpendModel[];
95
+ /**
96
+ * Samples that carried usage but have NO pricing row, and so contribute
97
+ * nothing to {@link total}. Without this, an upstream model that ships before
98
+ * its rate row reads exactly like "no usage" (see MiMo-V2.6, which arrived
99
+ * before the table knew it) — the difference is what this field exists to
100
+ * report. Add the row under `billing.models` to price these.
101
+ */
102
+ unpriced?: DeepSeekUnpricedUsage | undefined;
75
103
  }
76
104
  /** One session's billed spend on one Beijing-time calendar day. */
77
105
  export interface DeepSeekTodaySessionSpend {
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Billing Core",
4
+ "description": "DeepSeek account balance and session/today spend behind the billing Remote (peak/off-peak pricing)."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "计费核心",
4
+ "description": "余额、峰谷计价与会话/今日花费(billing Remote 提供方)。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@rayadesu/dsh-llm-billing",
3
3
  "description": "Standalone DeepSeek account-balance and session-spend provider exposed through the billing Remote",
4
- "version": "0.3.16",
4
+ "icon": "./icon.svg",
5
+ "version": "0.3.17",
5
6
  "publishConfig": {
6
7
  "access": "public"
7
8
  },
@@ -39,9 +40,12 @@
39
40
  "default": "./lib/typert.remote-client.js"
40
41
  },
41
42
  "./src/*": "./src/*",
43
+ "./locale/*.json": "./locale/*.json",
42
44
  "./package.json": "./package.json"
43
45
  },
44
46
  "files": [
47
+ "locale/*.json",
48
+ "icon.svg",
45
49
  "lib/index.js",
46
50
  "lib/invariant.js",
47
51
  "lib/types/**/*.js",