dsh-token-total 1.0.0 → 1.0.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 +5 -4
- package/lib/client.js +566 -159
- package/lib/index.js +355 -54
- package/lib/types/client/ContextUsageEntry.d.ts +22 -0
- package/lib/types/client/context-meter-core.d.ts +26 -0
- package/lib/types/client/context-takeover.d.ts +4 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/session-usage-core.d.ts +9 -0
- package/lib/types/client/use-session-usage.d.ts +2 -18
- package/lib/types/collector.d.ts +27 -12
- package/lib/types/context-types.d.ts +23 -2
- package/lib/types/model-names.d.ts +6 -0
- package/lib/types/query.d.ts +8 -0
- package/lib/types/session-usage.d.ts +8 -9
- package/lib/types/store.d.ts +32 -0
- package/lib/types/wire.d.ts +30 -1
- package/package.json +1 -1
- package/src/client/ContextUsageEntry.module.css +20 -0
- package/src/client/ContextUsageEntry.tsx +89 -0
- package/src/client/StatsLineEnhanced.tsx +17 -35
- package/src/client/TokenTotalPanel.module.css +72 -5
- package/src/client/TokenTotalPanel.tsx +204 -56
- package/src/client/context-meter-core.ts +54 -0
- package/src/client/context-takeover.ts +35 -0
- package/src/client/index.tsx +21 -3
- package/src/client/locales.ts +4 -0
- package/src/client/session-usage-core.ts +54 -0
- package/src/client/use-session-usage.ts +45 -98
- package/src/collector.ts +138 -67
- package/src/context-types.ts +17 -2
- package/src/index.ts +29 -0
- package/src/model-names.ts +34 -0
- package/src/query.ts +84 -13
- package/src/routes.ts +20 -2
- package/src/session-usage.ts +50 -38
- package/src/store.ts +136 -0
- package/src/wire.ts +24 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import { type ContextBreakdownLike, type ContextPressureLike } from './context-meter-core.ts';
|
|
3
|
+
export declare const CONTEXT_LOCALE_NS = "tokenTotalContext";
|
|
4
|
+
declare const keys: readonly ["label", "used", "system", "tools", "messages", "aria", "thousand", "million"];
|
|
5
|
+
export type ContextLocaleKey = typeof keys[number];
|
|
6
|
+
export declare const CONTEXT_LOCALES: Record<string, Record<ContextLocaleKey, string>>;
|
|
7
|
+
type ProjectionMap = {
|
|
8
|
+
contextPressure: ContextPressureLike;
|
|
9
|
+
contextBreakdown: ContextBreakdownLike;
|
|
10
|
+
};
|
|
11
|
+
type UseContextProjection = <K extends keyof ProjectionMap & string>(key: K) => ProjectionMap[K] | undefined;
|
|
12
|
+
export interface ContextUsageEntryProps extends PropsLocale<typeof CONTEXT_LOCALE_NS> {
|
|
13
|
+
useProjection: UseContextProjection;
|
|
14
|
+
installContextTakeover?: (anchor: HTMLElement) => () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function ContextUsageEntry({ useProjection, t, installContextTakeover }: ContextUsageEntryProps): JSX.Element | null;
|
|
17
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
18
|
+
interface LocaleNamespaceMap {
|
|
19
|
+
tokenTotalContext: ContextLocaleKey;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ContextPressureLike {
|
|
2
|
+
projectedTokens?: number;
|
|
3
|
+
pressureTokens?: number;
|
|
4
|
+
contextWindow?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ContextBreakdownLike {
|
|
7
|
+
systemTokens: number;
|
|
8
|
+
toolsTokens: number;
|
|
9
|
+
messageTokens: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ContextOccupancy {
|
|
12
|
+
percent: number;
|
|
13
|
+
usedTokens: number;
|
|
14
|
+
contextWindow: number;
|
|
15
|
+
}
|
|
16
|
+
/** Mirrors the host meter's occupancy calculation exactly. */
|
|
17
|
+
export declare function contextOccupancy(pressure: ContextPressureLike | undefined): ContextOccupancy | null;
|
|
18
|
+
export declare function contextBreakdownTotal(breakdown: ContextBreakdownLike | undefined): number;
|
|
19
|
+
export declare function contextBreakdownSegments(percent: number, breakdown: ContextBreakdownLike | undefined): Array<{
|
|
20
|
+
key: string;
|
|
21
|
+
width: number;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function formatContextTokens(value: number, units: {
|
|
24
|
+
thousand: string;
|
|
25
|
+
million: string;
|
|
26
|
+
}): string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The host ContextMeter is hard-coded beside composer.dock, not a slot.
|
|
2
|
+
* Suppress only that recognizable sibling while our replacement is mounted;
|
|
3
|
+
* never hide a whole dock, slot wrapper, or another plugin's readout. */
|
|
4
|
+
export declare function installContextTakeover(anchor: HTMLElement): () => void;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** The locale namespace this plugin owns. */
|
|
8
8
|
export declare const LOCALE_NS = "tokenTotal";
|
|
9
9
|
/** The typed translator key union for the panel's locale seat. */
|
|
10
|
-
export type TokenTotalKey = 'nav' | 'back' | 'range' | 'rangePreset.today' | 'rangePreset.3' | 'rangePreset.7' | 'rangePreset.30' | 'rangePreset.90' | 'rangeCustom' | 'from' | 'to' | 'refresh' | 'loading' | 'tokens' | 'tokensHint' | 'tokensUnit' | 'inputTokens' | 'outputTokens' | 'cacheTokens' | 'cachedTokens' | 'sessions' | 'requests' | 'activeDays' | 'cacheRate' | 'cacheRateHint' | 'cacheHitRate' | 'hitRateLegend' | 'topModel' | 'topModelHint' | 'heatmap' | 'heatLess' | 'heatMore' | 'dailyTrend' | 'hourlyTrend' | 'trendLimited' | 'usageRanking' | 'byModel' | 'byProvider' | 'tokensColumn' | 'cacheRateColumn' | 'providerModels' | 'other' | 'total' | 'percent' | 'asOf' | 'empty' | 'stats.counts' | 'stats.tokensPerSecond' | 'stats.cacheHit' | 'stats.cacheMiss' | 'stats.dialog.title' | 'stats.dialog.usageTitle' | 'stats.dialog.llmTime' | 'stats.dialog.toolTime' | 'stats.dialog.ttft' | 'stats.dialog.speed' | 'stats.dialog.main' | 'stats.dialog.subagents' | 'stats.dialog.subagentCount' | 'stats.dialog.treeTotal' | 'message.turnUsage.count' | 'message.turnUsage.cacheHit' | 'message.turnUsage.input' | 'message.turnUsage.cacheRead' | 'message.turnUsage.cacheWrite' | 'message.turnUsage.output';
|
|
10
|
+
export type TokenTotalKey = 'nav' | 'back' | 'range' | 'rangePreset.today' | 'rangePreset.3' | 'rangePreset.7' | 'rangePreset.30' | 'rangePreset.90' | 'rangeCustom' | 'from' | 'to' | 'refresh' | 'loading' | 'tokens' | 'tokensHint' | 'tokensUnit' | 'inputTokens' | 'outputTokens' | 'cacheTokens' | 'cachedTokens' | 'sessions' | 'requests' | 'requestsFailed' | 'activeDays' | 'cacheRate' | 'cacheRateHint' | 'cacheHitRate' | 'hitRateLegend' | 'topModel' | 'topModelHint' | 'heatmap' | 'heatLess' | 'heatMore' | 'dailyTrend' | 'hourlyTrend' | 'trendLimited' | 'usageRanking' | 'byModel' | 'byProvider' | 'tokensColumn' | 'cacheRateColumn' | 'providerModels' | 'other' | 'total' | 'percent' | 'asOf' | 'empty' | 'stats.counts' | 'stats.tokensPerSecond' | 'stats.cacheHit' | 'stats.cacheMiss' | 'stats.dialog.title' | 'stats.dialog.usageTitle' | 'stats.dialog.llmTime' | 'stats.dialog.toolTime' | 'stats.dialog.ttft' | 'stats.dialog.speed' | 'stats.dialog.main' | 'stats.dialog.subagents' | 'stats.dialog.subagentCount' | 'stats.dialog.treeTotal' | 'message.turnUsage.count' | 'message.turnUsage.cacheHit' | 'message.turnUsage.input' | 'message.turnUsage.cacheRead' | 'message.turnUsage.cacheWrite' | 'message.turnUsage.output';
|
|
11
11
|
export declare const en: Record<TokenTotalKey, string>;
|
|
12
12
|
export declare const zh: Record<TokenTotalKey, string>;
|
|
13
13
|
export declare const zhTW: Record<TokenTotalKey, string>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Pure billing helpers shared by the session-tree hook and composer pills. */
|
|
2
|
+
import type { SessionUsageTotals, SessionUsageTree } from '../wire.ts';
|
|
3
|
+
/** Add the root and descendants once; tree.total is already that sum. */
|
|
4
|
+
export declare function combinedTotals(tree: SessionUsageTree, self?: SessionUsageTotals): SessionUsageTotals;
|
|
5
|
+
/** One consistent split for the pill, cache hit rate and every dialog row. */
|
|
6
|
+
export declare function sessionUsageForDisplay(native: SessionUsageTotals | undefined, tree: SessionUsageTree | null): SessionUsageTree;
|
|
7
|
+
export declare function hasAnyTokens(totals: SessionUsageTotals | undefined): boolean;
|
|
8
|
+
export declare function billedTotal(totals: SessionUsageTotals): number;
|
|
9
|
+
export declare function cacheHitPercentOf(totals: SessionUsageTotals): number | null;
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import type { SessionUsageTotals, SessionUsageTree } from '../wire.ts';
|
|
2
|
-
|
|
3
|
-
export declare function combinedTotals(tree: SessionUsageTree): SessionUsageTotals;
|
|
4
|
-
/** Whether a totals record carries any billable token at all. */
|
|
5
|
-
export declare function hasAnyTokens(totals: SessionUsageTotals | undefined): boolean;
|
|
6
|
-
/** Total billed tokens of a flat totals record (prompt side + completion). */
|
|
7
|
-
export declare function billedTotal(totals: SessionUsageTotals): number;
|
|
8
|
-
/** Cache hit rate over the prompt side, or null when nothing was prompted. */
|
|
9
|
-
export declare function cacheHitPercentOf(totals: SessionUsageTotals): number | null;
|
|
2
|
+
export { billedTotal, cacheHitPercentOf, combinedTotals, hasAnyTokens } from './session-usage-core.ts';
|
|
10
3
|
export interface SessionTreeReading {
|
|
11
4
|
tree: SessionUsageTree | null;
|
|
12
|
-
/** main +
|
|
5
|
+
/** Authoritative log-based main + all descendant usage after the tree loads. */
|
|
13
6
|
totals: SessionUsageTotals | null;
|
|
14
7
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Subscribe to one session's whole-tree usage.
|
|
17
|
-
*
|
|
18
|
-
* `signature` is any string that changes when the native usage figure moves;
|
|
19
|
-
* passing it in keeps this hook out of the projection seat and lets the caller
|
|
20
|
-
* decide what "a new call settled" means for its own pill. A session that turns
|
|
21
|
-
* out to have subagents additionally re-asks on a slow beat, because subagent
|
|
22
|
-
* spend lands in a different session log and no native figure moves for it.
|
|
23
|
-
*/
|
|
24
8
|
export declare function useSessionUsage(sessionId: string | undefined, signature: string): SessionTreeReading;
|
package/lib/types/collector.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Two sources feed the same fold:
|
|
4
4
|
*
|
|
5
5
|
* - live: `session/event` subscription — every committed session event
|
|
6
|
-
* reaches this plugin after the fact.
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
* provider/model route. A completed call's usage is attributed to the model
|
|
6
|
+
* reaches this plugin after the fact. Assistant messages expose final usage,
|
|
7
|
+
* and modern message/attempt streams retain raw usage even on failed calls.
|
|
8
|
+
* Legacy `assistant/chunk` reports an early sample; `request/context` carries
|
|
9
|
+
* the provider/model route. A completed call's usage is attributed to the model
|
|
10
10
|
* the request used — in priority order: the message's own `source`
|
|
11
11
|
* (authoritative per call), then the session's last-known route, which
|
|
12
12
|
* comes from observed `request/context` events OR is lazily seeded from
|
|
@@ -27,11 +27,10 @@
|
|
|
27
27
|
* >= 0.1.2-rc.1, 0 for a non-forked session), and the replay skips it —
|
|
28
28
|
* the parent's own backfill already counted those events.
|
|
29
29
|
*
|
|
30
|
-
* Idempotence: within ONE session, the same (turn, step)
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* why the store observably keeps the FIRST emission. Cross-session
|
|
30
|
+
* Idempotence: within ONE session, repeated reports for the same (turn, step)
|
|
31
|
+
* emit only signed bucket corrections: the accumulated usage is last-wins.
|
|
32
|
+
* A started retry closes that replacement slot because its predecessor was a
|
|
33
|
+
* separately billed attempt. Cross-session
|
|
35
34
|
* (turn, step) collisions are NOT deduped: two sessions both own a turn 1 /
|
|
36
35
|
* step 1, and each must count. The store's per-day rows accumulate, so
|
|
37
36
|
* re-folding the same log must not add twice: live events are only folded
|
|
@@ -75,6 +74,17 @@ export declare class UsageFold {
|
|
|
75
74
|
* carries no (new) usage or turn marker. */
|
|
76
75
|
fold(ev: UsageSessionEvent): UsageSample | null;
|
|
77
76
|
private replaceSample;
|
|
77
|
+
/**
|
|
78
|
+
* Fold one `assistant/attempt` — a provider call that failed. Tokens follow
|
|
79
|
+
* the same last-report-wins rule as a committed message (the attempt's
|
|
80
|
+
* stream is the call's final word, so a usage chunk seen mid-stream is
|
|
81
|
+
* replaced or corrected, never added twice), but the call must ALWAYS be
|
|
82
|
+
* counted as one failure: `replaceSample` drops an all-zero first report,
|
|
83
|
+
* which is correct for the token buckets and would silently lose the
|
|
84
|
+
* failure. A failed call that reported no usage therefore contributes a
|
|
85
|
+
* zero-token failure marker instead of a null.
|
|
86
|
+
*/
|
|
87
|
+
private attemptSample;
|
|
78
88
|
/**
|
|
79
89
|
* Fold one `compaction/summary` — the one place a provider call's usage
|
|
80
90
|
* reaches the log OUTSIDE the agent loop. The summarization call produces no
|
|
@@ -201,8 +211,13 @@ export declare class UsageCollector {
|
|
|
201
211
|
* untouched, so a user who upgraded never loses day history because a
|
|
202
212
|
* session log was pruned. */
|
|
203
213
|
backfillHours(persistence: UsageSessionPersistence, sessions: UsageSessionStore, signal?: AbortSignal): Promise<void>;
|
|
204
|
-
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
214
|
+
/** The failure upgrade pass: replay every session the FAILURE cursors have
|
|
215
|
+
* not seen, counting only failed calls. Neither the day nor the hour
|
|
216
|
+
* cursor covers this counter (both were full when it arrived), and the
|
|
217
|
+
* replay must not re-add tokens or requests. */
|
|
218
|
+
backfillFailures(persistence: UsageSessionPersistence, sessions: UsageSessionStore, signal?: AbortSignal): Promise<void>;
|
|
219
|
+
/** Shared replay engine behind {@link backfill}, {@link backfillHours} and
|
|
220
|
+
* {@link backfillFailures}. `target` selects the cursor pair, the write
|
|
221
|
+
* function, and the cursor the completed sessions land in. */
|
|
207
222
|
private scan;
|
|
208
223
|
}
|
|
@@ -151,6 +151,22 @@ export interface UsageLlmService {
|
|
|
151
151
|
id: string;
|
|
152
152
|
name: string;
|
|
153
153
|
}[]>;
|
|
154
|
+
/** The provider catalog: route id -> the display name the model selector
|
|
155
|
+
* shows. Synchronous on every host line this plugin supports (the llm
|
|
156
|
+
* runtime's own `listProviders`, mirrored by the dsh-llm service). Optional:
|
|
157
|
+
* a host without it leaves every ranked provider row labelled by its raw
|
|
158
|
+
* historical route id. */
|
|
159
|
+
listProviders?(): readonly {
|
|
160
|
+
id: string;
|
|
161
|
+
name: string;
|
|
162
|
+
}[];
|
|
163
|
+
/** The configured-provider view (the models page's own list), whose
|
|
164
|
+
* `displayName` is the same label. Read as a fallback for a route that
|
|
165
|
+
* `listProviders` omits. */
|
|
166
|
+
listConfigurableProviders?(): readonly {
|
|
167
|
+
provider: string;
|
|
168
|
+
displayName: string;
|
|
169
|
+
}[];
|
|
154
170
|
}
|
|
155
171
|
/** The route a session's log records for its model calls (mirror of
|
|
156
172
|
* dsh-agent-loop RequestContext). */
|
|
@@ -167,8 +183,13 @@ export interface UsageSessionHandle {
|
|
|
167
183
|
id: string;
|
|
168
184
|
cwd?: string;
|
|
169
185
|
createdAt?: number;
|
|
170
|
-
/**
|
|
171
|
-
|
|
186
|
+
/** Modern hosts retain creation metadata on Session.header, not as getters. */
|
|
187
|
+
header?: UsageSessionHeader;
|
|
188
|
+
/** Exact fork prefix, distinct from the resume/construction seed length. */
|
|
189
|
+
inheritedEventCount?: number;
|
|
190
|
+
/** Immutable current live log; avoids the persistence backend's batching lag. */
|
|
191
|
+
snapshotEvents?(): readonly UsageSessionEvent[];
|
|
192
|
+
/** Legacy direct parent; modern hosts expose header.parentSession instead. */
|
|
172
193
|
parentSession?: string;
|
|
173
194
|
/** 0 for a root session; the delegation depth for a subagent. */
|
|
174
195
|
delegationDepth?: number;
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* This is query-time metadata: no store wipe/replay is needed, and a provider
|
|
3
3
|
* rename applies to both the ranking and every trend bucket on refresh. */
|
|
4
4
|
import type { UsageLlmService } from './context-types.ts';
|
|
5
|
+
/** Resolve provider route ids to the labels the model selector shows. Both
|
|
6
|
+
* catalogs are presentational and synchronous on the host runtime, so this is
|
|
7
|
+
* a plain in-process read rather than a per-request lookup: a missing service,
|
|
8
|
+
* a throwing adapter, or an unexpected promise all resolve to an empty map and
|
|
9
|
+
* every surface falls back to the raw historical route id. */
|
|
10
|
+
export declare function resolveProviderNames(llm?: UsageLlmService): Record<string, string>;
|
|
5
11
|
/** Resolve each distinct route once per range query. Unknown/deleted providers
|
|
6
12
|
* fall back to their bare historical model names in aggregateSamples(). */
|
|
7
13
|
export declare function resolveModelNames(modelRefs: Iterable<string>, llm?: UsageLlmService): Promise<Map<string, string>>;
|
package/lib/types/query.d.ts
CHANGED
|
@@ -41,6 +41,14 @@ export interface UsageSample {
|
|
|
41
41
|
cacheWriteTokens: number;
|
|
42
42
|
turn?: boolean;
|
|
43
43
|
request?: boolean;
|
|
44
|
+
/** True when this sample also counts one FAILED provider call. The host
|
|
45
|
+
* appends `assistant/attempt` (instead of `assistant/message`) when a
|
|
46
|
+
* call's stream ends with an error or is cancelled before any content is
|
|
47
|
+
* committed, so that event is the exact boundary of a failed call. The
|
|
48
|
+
* marker is ORTHOGONAL to `request` and to the buckets: a failed call was
|
|
49
|
+
* still opened by its own `step/start` (so it is already one request) and
|
|
50
|
+
* a stream that died mid-flight may still report usage. */
|
|
51
|
+
failed?: boolean;
|
|
44
52
|
}
|
|
45
53
|
export interface RangeFilter {
|
|
46
54
|
from: string;
|
|
@@ -26,18 +26,17 @@ import type { UsageSessionPersistence, UsageSessionStore } from './context-types
|
|
|
26
26
|
import type { SessionUsageTree } from './wire.ts';
|
|
27
27
|
export type { SessionUsageTotals, SessionUsageTree } from './wire.ts';
|
|
28
28
|
export declare class SessionUsageService {
|
|
29
|
-
/**
|
|
30
|
-
* live event on ANY of them (a subagent's own event changes its ancestors'
|
|
31
|
-
* totals too) can invalidate exactly the affected trees. */
|
|
29
|
+
/** Short-lived tree reads; any live event can invalidate tree membership. */
|
|
32
30
|
private cache;
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/** Any event can introduce a new descendant while an async read is running. */
|
|
32
|
+
private generation;
|
|
33
|
+
/** Invalidate conservatively: an unseen session may be a newly-created child
|
|
34
|
+
* of ANY cached root, so membership-only invalidation is not safe. */
|
|
35
|
+
invalidate(_sessionId: string): void;
|
|
37
36
|
/** Resolve the whole session tree rooted at `rootId` and sum its usage.
|
|
38
37
|
* `self` is the root's own usage, `subagents` every descendant's, `total`
|
|
39
|
-
* the sum.
|
|
40
|
-
*
|
|
38
|
+
* the sum. Unreadable logs reject rather than publishing partial totals;
|
|
39
|
+
* callers can retain their last good display while surfacing the error. */
|
|
41
40
|
tree(persistence: UsageSessionPersistence, sessions: UsageSessionStore | undefined, rootId: string, signal?: AbortSignal): Promise<SessionUsageTree>;
|
|
42
41
|
private listHeaders;
|
|
43
42
|
}
|
package/lib/types/store.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface UsageCounters {
|
|
|
23
23
|
cacheReadTokens: number;
|
|
24
24
|
cacheWriteTokens: number;
|
|
25
25
|
requests: number;
|
|
26
|
+
/** Provider calls in the bucket that FAILED (error / cancelled before any
|
|
27
|
+
* content): a subset of `requests`, never larger than it. Rows persisted
|
|
28
|
+
* before the counter existed may lack it; the schema normalizes that to 0
|
|
29
|
+
* and every subsequent fold writes the number back. */
|
|
30
|
+
failures: number;
|
|
26
31
|
turns: number;
|
|
27
32
|
lastSeen: number;
|
|
28
33
|
}
|
|
@@ -43,6 +48,7 @@ export declare const usageDayRowSchema: z.ZodObject<{
|
|
|
43
48
|
cacheReadTokens: z.ZodNumber;
|
|
44
49
|
cacheWriteTokens: z.ZodNumber;
|
|
45
50
|
requests: z.ZodNumber;
|
|
51
|
+
failures: z.ZodDefault<z.ZodNumber>;
|
|
46
52
|
turns: z.ZodNumber;
|
|
47
53
|
lastSeen: z.ZodNumber;
|
|
48
54
|
}, z.core.$strip>;
|
|
@@ -55,6 +61,7 @@ export declare const usageHourRowSchema: z.ZodObject<{
|
|
|
55
61
|
cacheReadTokens: z.ZodNumber;
|
|
56
62
|
cacheWriteTokens: z.ZodNumber;
|
|
57
63
|
requests: z.ZodNumber;
|
|
64
|
+
failures: z.ZodDefault<z.ZodNumber>;
|
|
58
65
|
turns: z.ZodNumber;
|
|
59
66
|
lastSeen: z.ZodNumber;
|
|
60
67
|
}, z.core.$strip>;
|
|
@@ -81,12 +88,17 @@ export declare const usageHistoryDomain: {
|
|
|
81
88
|
hoursBackfilledSessions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
82
89
|
hoursLiveFirstSeq: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
83
90
|
hoursReady: z.ZodOptional<z.ZodBoolean>;
|
|
91
|
+
failuresBackfilledSessions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
92
|
+
failuresLiveFirstSeq: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
93
|
+
failuresReady: z.ZodOptional<z.ZodBoolean>;
|
|
84
94
|
}, z.core.$strip>;
|
|
85
95
|
initial: {
|
|
86
96
|
backfilledSessions: string[];
|
|
87
97
|
liveFirstSeq: {};
|
|
88
98
|
hoursBackfilledSessions: string[];
|
|
89
99
|
hoursLiveFirstSeq: {};
|
|
100
|
+
failuresBackfilledSessions: string[];
|
|
101
|
+
failuresLiveFirstSeq: {};
|
|
90
102
|
};
|
|
91
103
|
};
|
|
92
104
|
tables: {
|
|
@@ -160,6 +172,21 @@ export declare class UsageStore {
|
|
|
160
172
|
hoursReady(): Promise<boolean>;
|
|
161
173
|
/** Mark the hour history complete; the upgrade pass then never reruns. */
|
|
162
174
|
markHoursReady(): Promise<void>;
|
|
175
|
+
/** Session ids already replayed into the FAILURE counter (third cursor
|
|
176
|
+
* pair, same reason as the hour pair: the day and hour cursors were
|
|
177
|
+
* already full when the counter arrived). */
|
|
178
|
+
failuresSeenSessions(): Promise<Set<string>>;
|
|
179
|
+
/** Per-session seq of the first LIVE-observed event, failure-counter share. */
|
|
180
|
+
failuresLiveSequences(): Promise<Map<string, number>>;
|
|
181
|
+
/** Persist failure-pass replay progress (merges; serialized on the same
|
|
182
|
+
* chain as every other cursor write). */
|
|
183
|
+
markFailuresSeenSessions(ids: Iterable<string>): Promise<void>;
|
|
184
|
+
/** Record the failure-pass live boundary per session (earliest wins). */
|
|
185
|
+
markFailuresLiveSequences(entries: Iterable<readonly [string, number]>): Promise<void>;
|
|
186
|
+
/** Whether the failure counter covers every replayable session. */
|
|
187
|
+
failuresReady(): Promise<boolean>;
|
|
188
|
+
/** Mark the failure history complete; the upgrade pass then never reruns. */
|
|
189
|
+
markFailuresReady(): Promise<void>;
|
|
163
190
|
/** Drop every row and the whole cursor, re-bounding each named session at
|
|
164
191
|
* its WIPE-TIME WATERMARK (the store-rebuild escape hatch for corrupted
|
|
165
192
|
* history or attribution-logic upgrades).
|
|
@@ -198,6 +225,11 @@ export declare class UsageStore {
|
|
|
198
225
|
* day cursors are already advanced, so replaying through `record()` would
|
|
199
226
|
* double every day row. */
|
|
200
227
|
recordHours(sample: UsageSample): Promise<void>;
|
|
228
|
+
/** Fold ONLY the failure count of a sample into the day and hour rows. The
|
|
229
|
+
* failure upgrade pass uses this: its replay walks logs the token passes
|
|
230
|
+
* already consumed, so the tokens and the request marker must not be added
|
|
231
|
+
* a second time — only the failed-call counter may move. */
|
|
232
|
+
recordFailure(sample: UsageSample): Promise<void>;
|
|
201
233
|
/** The row identity a sample lands on: turn markers use the synthetic
|
|
202
234
|
* (turns) model, unlabelled calls the (unknown) one. */
|
|
203
235
|
private targetsOf;
|
package/lib/types/wire.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface DailyTokenUsage {
|
|
|
17
17
|
byModel: Record<string, number>;
|
|
18
18
|
byProvider: Record<string, number>;
|
|
19
19
|
requests: number;
|
|
20
|
+
failures: number;
|
|
20
21
|
turns: number;
|
|
21
22
|
cacheHit: number;
|
|
22
23
|
cacheMiss: number;
|
|
@@ -29,6 +30,13 @@ export interface ModelTokenUsage {
|
|
|
29
30
|
provider: string;
|
|
30
31
|
/** All source providers; optional for compatibility with older responses. */
|
|
31
32
|
providers?: string[];
|
|
33
|
+
/** Per-provider token split of this one model, largest first — the ranked
|
|
34
|
+
* row's hover detail ("which providers served this model"). Optional so a
|
|
35
|
+
* pre-cross-breakdown host still renders. */
|
|
36
|
+
providerTokens?: Array<{
|
|
37
|
+
provider: string;
|
|
38
|
+
tokens: number;
|
|
39
|
+
}>;
|
|
32
40
|
tokens: number;
|
|
33
41
|
percent: number;
|
|
34
42
|
/** Cached input tokens this model served (rate numerator). */
|
|
@@ -43,6 +51,13 @@ export interface ProviderTokenUsage {
|
|
|
43
51
|
percent: number;
|
|
44
52
|
cacheHit: number;
|
|
45
53
|
cacheMiss: number;
|
|
54
|
+
/** Per-model token split this provider served, largest first — the ranked
|
|
55
|
+
* row's hover detail ("which models this provider served"). Optional so a
|
|
56
|
+
* pre-cross-breakdown host still renders. */
|
|
57
|
+
modelTokens?: Array<{
|
|
58
|
+
model: string;
|
|
59
|
+
tokens: number;
|
|
60
|
+
}>;
|
|
46
61
|
}
|
|
47
62
|
/** The full aggregate the panel renders for one time range. */
|
|
48
63
|
export interface TokenTotalRange {
|
|
@@ -56,6 +71,12 @@ export interface TokenTotalRange {
|
|
|
56
71
|
/** Cached prompt tokens: cache reads + cache writes (the cache bucket of `tokens`). */
|
|
57
72
|
cacheTokens: number;
|
|
58
73
|
requests: number;
|
|
74
|
+
/** Provider calls in the range that FAILED: the stream ended with an error
|
|
75
|
+
* or the call was cancelled before any content was committed. A subset of
|
|
76
|
+
* `requests` (a failed call was still one call), and — like `requests` —
|
|
77
|
+
* independent of the token buckets: a stream that died mid-flight may
|
|
78
|
+
* still have reported usage. */
|
|
79
|
+
failures: number;
|
|
59
80
|
turns: number;
|
|
60
81
|
cacheHit: number;
|
|
61
82
|
cacheMiss: number;
|
|
@@ -68,6 +89,11 @@ export interface TokenTotalRange {
|
|
|
68
89
|
daily: DailyTokenUsage[];
|
|
69
90
|
models: ModelTokenUsage[];
|
|
70
91
|
providers: ProviderTokenUsage[];
|
|
92
|
+
/** Provider route id -> the display name the model selector shows. Filled by
|
|
93
|
+
* the host route from the llm service metadata; a provider missing here (a
|
|
94
|
+
* deleted route, an older host) renders as its raw id. Optional so a host
|
|
95
|
+
* that cannot resolve names still serves a valid payload. */
|
|
96
|
+
providerNames?: Record<string, string>;
|
|
71
97
|
}
|
|
72
98
|
/** The usage statistics panel aggregate request. */
|
|
73
99
|
export interface TokenTotalRequest {
|
|
@@ -87,8 +113,11 @@ export interface SessionUsageTotals {
|
|
|
87
113
|
* The composer stats row renders this so a delegating session shows its whole
|
|
88
114
|
* spend (the native `tokenUsage` projection only covers the main agent). */
|
|
89
115
|
export interface SessionUsageTree {
|
|
90
|
-
/** The root session's own
|
|
116
|
+
/** The root session's own usage, including logged compaction calls. */
|
|
91
117
|
self: SessionUsageTotals;
|
|
118
|
+
/** Root-only compaction spend already included in `self`, but absent from
|
|
119
|
+
* the native `tokenUsage` projection. Optional for older host responses. */
|
|
120
|
+
selfCompaction?: SessionUsageTotals;
|
|
92
121
|
/** Sum over every descendant (subagent) session. */
|
|
93
122
|
subagents: SessionUsageTotals;
|
|
94
123
|
/** self + subagents. */
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.root { display: inline-flex; align-items: center; min-width: 0; font: inherit; font-variant-numeric: tabular-nums; line-height: inherit; }
|
|
2
|
+
.trigger { display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box; max-width: 100%; padding: 1px 8px; border: 0; border-radius: 24px; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; line-height: inherit; white-space: nowrap; cursor: pointer; }
|
|
3
|
+
.trigger svg { flex: none; }
|
|
4
|
+
.track { fill: none; stroke: var(--dsw-alias-separator-primary); stroke-width: 1.5; }
|
|
5
|
+
.fill { fill: none; stroke: var(--dsw-alias-label-tertiary); stroke-width: 2; stroke-linecap: round; }
|
|
6
|
+
.trigger:hover, .trigger[aria-expanded='true'] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }
|
|
7
|
+
.panel { position: fixed; z-index: 1100; box-sizing: border-box; width: max-content; min-width: min(300px, calc(100vw - 24px)); max-width: min(440px, calc(100vw - 24px)); padding: 16px; border-radius: 12px; background: var(--dsw-specific-menu); backdrop-filter: var(--dsw-menu-backdrop-filter); box-shadow: var(--dsw-elevation-prominent); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
8
|
+
.header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: var(--dsw-alias-label-primary); font-weight: 500; }
|
|
9
|
+
.percent { font-variant-numeric: tabular-nums; }
|
|
10
|
+
.figures { margin-left: auto; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
11
|
+
.bar { display: flex; width: 100%; height: 8px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-separator-primary); }
|
|
12
|
+
.segment { height: 100%; }
|
|
13
|
+
.systemTokens { background: var(--dsw-alias-label-primary); }
|
|
14
|
+
.toolsTokens { background: var(--dsw-alias-label-secondary); }
|
|
15
|
+
.messageTokens { background: var(--dsw-alias-label-tertiary); }
|
|
16
|
+
.rows { display: grid; gap: 6px; margin: 12px 0 0; color: var(--dsw-alias-label-tertiary); }
|
|
17
|
+
.row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
18
|
+
.row dt { display: flex; align-items: center; gap: 8px; }
|
|
19
|
+
.row dd { margin: 0; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
|
|
20
|
+
.swatch { width: 8px; height: 8px; border-radius: 2px; }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createPortal } from 'react-dom'
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useAnchoredPosition, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
5
|
+
import { MEASURE_STYLE } from './stat-dialog.ts'
|
|
6
|
+
import { contextBreakdownSegments, contextOccupancy, formatContextTokens, type ContextBreakdownLike, type ContextPressureLike } from './context-meter-core.ts'
|
|
7
|
+
import css from './ContextUsageEntry.module.css'
|
|
8
|
+
|
|
9
|
+
export const CONTEXT_LOCALE_NS = 'tokenTotalContext'
|
|
10
|
+
const keys = ['label', 'used', 'system', 'tools', 'messages', 'aria', 'thousand', 'million'] as const
|
|
11
|
+
export type ContextLocaleKey = typeof keys[number]
|
|
12
|
+
export const CONTEXT_LOCALES: Record<string, Record<ContextLocaleKey, string>> = {
|
|
13
|
+
zh: { label: '上下文', used: '已用上下文', system: '系统提示词', tools: '工具', messages: '对话', aria: '上下文占用 {percent}', thousand: '{value}千', million: '{value}百万' },
|
|
14
|
+
en: { label: 'Context', used: 'Context used', system: 'System', tools: 'Tools', messages: 'Conversation', aria: 'Context usage {percent}', thousand: '{value}K', million: '{value}M' },
|
|
15
|
+
'zh-TW': { label: '上下文', used: '已用上下文', system: '系統提示詞', tools: '工具', messages: '對話', aria: '上下文使用量 {percent}', thousand: '{value}千', million: '{value}百萬' },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ProjectionMap = {
|
|
19
|
+
contextPressure: ContextPressureLike
|
|
20
|
+
contextBreakdown: ContextBreakdownLike
|
|
21
|
+
}
|
|
22
|
+
type UseContextProjection = <K extends keyof ProjectionMap & string>(key: K) => ProjectionMap[K] | undefined
|
|
23
|
+
|
|
24
|
+
export interface ContextUsageEntryProps extends PropsLocale<typeof CONTEXT_LOCALE_NS> {
|
|
25
|
+
useProjection: UseContextProjection
|
|
26
|
+
installContextTakeover?: (anchor: HTMLElement) => () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ContextUsageEntry({ useProjection, t, installContextTakeover }: ContextUsageEntryProps): JSX.Element | null {
|
|
30
|
+
const pressure = useProjection('contextPressure')
|
|
31
|
+
const breakdown = useProjection('contextBreakdown')
|
|
32
|
+
const context = contextOccupancy(pressure)
|
|
33
|
+
const [open, setOpen] = useState(false)
|
|
34
|
+
const rootRef = useRef<HTMLSpanElement | null>(null)
|
|
35
|
+
const panelRef = useRef<HTMLDivElement | null>(null)
|
|
36
|
+
const position = useAnchoredPosition({ open: open && context !== null, anchorRef: rootRef, panelRef, side: 'top', gap: 8, margin: 12 })
|
|
37
|
+
useDismissOnOutsidePointer(rootRef, open && context !== null, setOpen, panelRef)
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const anchor = rootRef.current
|
|
40
|
+
if (anchor === null || context === null || installContextTakeover === undefined) return
|
|
41
|
+
return installContextTakeover(anchor)
|
|
42
|
+
}, [context !== null, installContextTakeover])
|
|
43
|
+
useEffect(() => { if (context === null && open) setOpen(false) }, [context, open])
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!open || context === null) return
|
|
46
|
+
const onKeyDown = (event: KeyboardEvent): void => { if (event.key === 'Escape') setOpen(false) }
|
|
47
|
+
document.addEventListener('keydown', onKeyDown)
|
|
48
|
+
return () => document.removeEventListener('keydown', onKeyDown)
|
|
49
|
+
}, [context, open])
|
|
50
|
+
if (context === null) return null
|
|
51
|
+
const percent = context.percent
|
|
52
|
+
const reading = `${percent}%`
|
|
53
|
+
const total = breakdown === undefined ? 0 : breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens
|
|
54
|
+
const segments = contextBreakdownSegments(percent, breakdown)
|
|
55
|
+
const format = (value: number): string => formatContextTokens(value, {
|
|
56
|
+
thousand: t('thousand'), million: t('million'),
|
|
57
|
+
})
|
|
58
|
+
const label = (part: 'system' | 'tools' | 'messages'): string => t(part)
|
|
59
|
+
return (
|
|
60
|
+
<span ref={rootRef} className={css.root} data-dsh-token-total-stat="context">
|
|
61
|
+
<button type="button" className={css.trigger} aria-label={t('aria', { percent: reading })} aria-haspopup="dialog" aria-expanded={open} onClick={() => setOpen(!open)}>
|
|
62
|
+
<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
|
|
63
|
+
<circle className={css.track} cx="7" cy="7" r="5.5" />
|
|
64
|
+
<circle className={css.fill} cx="7" cy="7" r="5.5" strokeDasharray={`${2 * Math.PI * 5.5 * percent / 100} ${2 * Math.PI * 5.5}`} transform="rotate(-90 7 7)" />
|
|
65
|
+
</svg>
|
|
66
|
+
<span>{reading}</span>
|
|
67
|
+
</button>
|
|
68
|
+
{open && createPortal(
|
|
69
|
+
<div ref={panelRef} className={css.panel} style={position ?? MEASURE_STYLE} role="dialog" aria-label={t('used')}>
|
|
70
|
+
<div className={css.header}>
|
|
71
|
+
<span>{t('used')}</span><span className={css.percent}>{reading}</span>
|
|
72
|
+
<span className={css.figures}>{`~${format(context.usedTokens)} / ${format(context.contextWindow)}`}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div className={css.bar}>{segments.map(segment => <div key={segment.key} className={`${css.segment} ${css[segment.key] ?? ''}`} style={{ width: `${segment.width}%` }} />)}</div>
|
|
75
|
+
{breakdown !== undefined && total > 0 && <dl className={css.rows}>
|
|
76
|
+
{(['systemTokens', 'toolsTokens', 'messageTokens'] as const).map((key, index) => <div className={css.row} key={key}>
|
|
77
|
+
<dt><span className={`${css.swatch} ${css[key]}`} />{label((['system', 'tools', 'messages'] as const)[index])}</dt>
|
|
78
|
+
<dd>{`~${format(breakdown[key])}`}</dd>
|
|
79
|
+
</div>)}
|
|
80
|
+
</dl>}
|
|
81
|
+
</div>, document.body,
|
|
82
|
+
)}
|
|
83
|
+
</span>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
88
|
+
interface LocaleNamespaceMap { tokenTotalContext: ContextLocaleKey }
|
|
89
|
+
}
|