@kenz1117/dsh-ui-usage-billing 0.2.6 → 0.4.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.
@@ -11,16 +11,14 @@
11
11
  */
12
12
  import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
13
13
  import type { TokenUsage } from '@deepseek-ai/dsh-llm';
14
+ import { MODEL_KEY_ALIASES } from './client/pricing.ts';
15
+ export { MODEL_KEY_ALIASES };
14
16
  /**
15
- * Real provider model ids map to their billing-catalog keys. Unknown ids stay
16
- * as-is and price zero (they are not in the catalog; subscription-plan routes
17
- * like kimi-coding / token plans fall here and therefore cost nothing).
18
- */
19
- export declare const MODEL_KEY_ALIASES: Readonly<Record<string, string>>;
20
- /**
21
- * 走订阅套餐(coding / token / agent plan)的 provider id:这些通道的调用
22
- * 按套餐计费,不再按 token 计费,因此即使模型 id 与计费表撞名也一律豁免。
23
- * 部署可在 plugin config 的 `subscriptionProviders` 中覆盖。
17
+ * 走订阅套餐(coding / token plan / opencode 订阅)的 provider id:这些通道的
18
+ * 调用按套餐计费,不再按 token 计费,因此即使模型 id 与计费表撞名也一律豁免。
19
+ * pi-ai 内置提供方对齐(含各地区变体:qwen/xiaomi token-plan、opencode
20
+ * opencode-go、zai-coding-cn);部署可在 plugin config 的 `subscriptionProviders`
21
+ * 中覆盖。
24
22
  */
25
23
  export declare const DEFAULT_SUBSCRIPTION_PROVIDERS: readonly string[];
26
24
  /** Aggregation tuning options. */
@@ -36,6 +34,8 @@ export interface ModelUsage {
36
34
  cacheHit: number;
37
35
  cacheMiss: number;
38
36
  cost: number;
37
+ /** 该模型本次统计的所有调用是否都走订阅通道(coding/token plan);混合通道不置位。 */
38
+ plan?: boolean;
39
39
  }
40
40
  /** Zeroed usage accumulator. */
41
41
  export declare function emptyUsage(): ModelUsage;
@@ -47,15 +47,22 @@ export declare function emptyUsage(): ModelUsage;
47
47
  * @param usage - the provider-reported usage of one call.
48
48
  * @param key - the billing-catalog key this call belongs to.
49
49
  * @param subscription - whether the call went through a subscription plan; such calls never cost money.
50
+ * @param timeMs - the call's wall-clock time (epoch ms); drives peak/off-peak pricing.
50
51
  */
51
- export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean): void;
52
+ export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number): void;
52
53
  /** Local-time date stamp (the host runs in the user's timezone). */
53
54
  export declare function dayStamp(time: number): string;
55
+ /** cwd 未知时工作区聚合的占位名(UI 显示 em dash,保持语言无关)。 */
56
+ export declare const UNKNOWN_WORKSPACE_NAME = "\u2014";
57
+ /** 工作区名:取 cwd 的末级目录名;无 cwd 时返回 {@link UNKNOWN_WORKSPACE_NAME}。 */
58
+ export declare function workspaceNameOf(cwd: string | undefined): string;
54
59
  /**
55
60
  * The persistence surface the aggregate reads: enough of
56
- * `SessionPersistence` to list sessions and read each log once.
61
+ * `SessionPersistence` to list sessions and read each log once; `locate`
62
+ * is optional — backends exposing it give the incremental cache a cheap
63
+ * invalidation stamp (artifact mtime + size), others always re-fold.
57
64
  */
58
- export type UsagePersistence = Pick<SessionPersistence, 'list' | 'readFrom'>;
65
+ export type UsagePersistence = Pick<SessionPersistence, 'list' | 'readFrom'> & Partial<Pick<SessionPersistence, 'locate'>>;
59
66
  /** The usage-stats document served to the billing dashboard. */
60
67
  export interface UsageStatsDocument {
61
68
  version: number;
@@ -66,9 +73,110 @@ export interface UsageStatsDocument {
66
73
  byDay: Record<string, ModelUsage>;
67
74
  /** 模型 × 日期 二维统计:趋势图按模型堆叠的输入([date][modelKey])。 */
68
75
  byDayModels: Record<string, Record<string, ModelUsage>>;
76
+ /** 会话明细:按费用倒序,封顶 {@link SESSION_ROW_LIMIT} 行;旧快照可能缺失。 */
77
+ bySession: SessionUsageRow[];
78
+ /** 每轮费用明细:按起始时间倒序,封顶 {@link TURN_ROW_LIMIT} 行;旧快照可能缺失。 */
79
+ byTurn?: TurnUsageRow[];
80
+ /** 工作区聚合:按 cwd 末级目录归并,按费用倒序;旧快照可能缺失。 */
81
+ byWorkspace?: WorkspaceUsageRow[];
82
+ }
83
+ /** 会话明细行:仪表盘「会话明细」面板的数据源。 */
84
+ export interface SessionUsageRow {
85
+ /** 会话 id(字符串形式)。 */
86
+ id: string;
87
+ /** 日志里最新的 session/title 文本;无标题事件时缺失。 */
88
+ title?: string;
89
+ /** 会话创建时的工作目录(项目路径);未知时缺失。 */
90
+ cwd?: string;
91
+ calls: number;
92
+ cost: number;
93
+ /** 最后一个事件的时间戳(毫秒)。 */
94
+ lastActive: number;
95
+ }
96
+ /** 每轮费用明细行:仪表盘「每轮费用」图的数据源。 */
97
+ export interface TurnUsageRow {
98
+ /** 会话 id(字符串形式):不同会话的轮次号相互独立,展示时需区分。 */
99
+ sessionId: string;
100
+ /** 会话内轮次号。 */
101
+ turn: number;
102
+ /** 归因模型 key(计费目录键;未收录原样保留)。 */
103
+ model: string;
104
+ input: number;
105
+ output: number;
106
+ cacheHit: number;
107
+ cacheMiss: number;
108
+ /** 该轮成本(人民币元,按调用时刻精确判高峰/空闲档)。 */
109
+ cost: number;
110
+ /** 轮起始时刻(毫秒)。 */
111
+ startedAt: number;
112
+ /** 轮结束时刻(毫秒);未结束轮缺失。 */
113
+ endedAt?: number;
69
114
  }
115
+ /** 会话内折叠的每轮行(不含 sessionId,合并时补齐)。 */
116
+ type SessionTurnRow = Omit<TurnUsageRow, 'sessionId'>;
117
+ /** 工作区聚合行:按会话 cwd 的末级目录归并。 */
118
+ export interface WorkspaceUsageRow {
119
+ /** 目录末级名;cwd 未知的会话归入「未命名」。 */
120
+ name: string;
121
+ calls: number;
122
+ cost: number;
123
+ input: number;
124
+ output: number;
125
+ /** 该工作区最近一次活跃时刻(毫秒)。 */
126
+ lastActive: number;
127
+ }
128
+ /** 会话明细行的响应封顶:控制 payload 体积,重度用户的完整长尾不逐行下发。 */
129
+ export declare const SESSION_ROW_LIMIT = 100;
130
+ /** 每轮费用行的响应封顶:同样控制 payload 体积。 */
131
+ export declare const TURN_ROW_LIMIT = 200;
132
+ /** 聚合文档的短 TTL(毫秒):合并密集轮询,TTL 内直接复用上次的合并结果。 */
133
+ export declare const AGGREGATE_TTL_MS = 5000;
134
+ /** One persisted session's folded usage plus drill-down metadata. */
135
+ interface SessionFold {
136
+ total: ModelUsage;
137
+ byModel: Map<string, ModelUsage>;
138
+ byDay: Map<string, ModelUsage>;
139
+ byDayModels: Map<string, Map<string, ModelUsage>>;
140
+ /** 每个模型 key 在本会话内走订阅通道的调用数(合并时跨会话累加判定 plan)。 */
141
+ planCalls: Map<string, number>;
142
+ /** 每轮费用明细(按轮次号升序,不含 sessionId);sessionId 在合并时补齐。 */
143
+ turns: SessionTurnRow[];
144
+ /** 日志里最新的 session/title 文本(无标题事件时 undefined)。 */
145
+ title?: string;
146
+ /** 最后一个事件的时间戳(毫秒);空日志为 0。 */
147
+ lastActive: number;
148
+ }
149
+ /**
150
+ * Fold one session's events into a {@link SessionFold}. 每个 LLM 调用归属到
151
+ * 其前置 request/header 记录的模型;同时提取最新会话标题、最后活跃时间,
152
+ * 并按轮次折叠每轮费用明细(turn/start → turn/end;调用按 (turn) 归组)。
153
+ * @param events - the session's persisted events in log order.
154
+ * @param subscriptionProviders - provider ids billed through subscription plans.
155
+ * @returns the per-session fold (cached by the incremental aggregator).
156
+ */
157
+ export declare function foldSession(events: readonly {
158
+ type: string;
159
+ time: number;
160
+ data: never;
161
+ }[], subscriptionProviders: ReadonlySet<string>): SessionFold;
162
+ /**
163
+ * 增量聚合器:按会话缓存折叠结果,用日志文件的 mtime+size 作失效键——
164
+ * 日志没动的会话直接复用,只有写过的会话重新折叠;整份文档另有短 TTL
165
+ * 合并密集轮询。缓存活在内存里(进程重启后首次全量折叠一次)。
166
+ */
167
+ export interface UsageAggregator {
168
+ /** Aggregate current usage, reusing cached per-session folds when their logs are untouched. */
169
+ aggregate(): Promise<UsageStatsDocument>;
170
+ }
171
+ /**
172
+ * Create the incremental usage aggregator.
173
+ * @param persistence - the session persistence service.
174
+ * @param options - aggregation tuning (e.g. subscription-plan providers).
175
+ * @returns the aggregator holding the per-session fold cache.
176
+ */
177
+ export declare function createUsageAggregator(persistence: UsagePersistence, options?: AggregateOptions): UsageAggregator;
70
178
  /**
71
- * Aggregate real usage from every persisted session log.
179
+ * Aggregate real usage from every persisted session log (one-shot, no cache).
72
180
  * @param persistence - the session persistence service.
73
181
  * @param options - aggregation tuning (e.g. subscription-plan providers).
74
182
  * @returns the usage-stats document (same shape the dashboard expects).
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * TrendChart: dependency-free SVG chart of daily cost + calls.
3
3
  *
4
- * The columns are GROUPED per model — one bar per model per day, each in its
5
- * brand color, so per-model cost is directly comparable. The blue line is the
6
- * total call volume across all models, plotted on its own right-hand axis.
4
+ * The columns are STACKED per day — one bar per day, with each model's cost
5
+ * as a colored segment inside the bar, so the daily total reads at a glance
6
+ * and the model mix stays visible. The blue line is the total call volume
7
+ * across all models, plotted on its own right-hand axis.
7
8
  * A hover crosshair shows the day's model breakdown. No chart library — the
8
9
  * surface stays self-contained and offline.
9
10
  */
@@ -28,7 +29,7 @@ export interface TrendPoint {
28
29
  byModel?: Readonly<Record<string, number>>;
29
30
  }
30
31
  /**
31
- * Render the daily grouped cost bars plus the total-calls line.
32
+ * Render the daily stacked cost bars plus the total-calls line.
32
33
  * @param props.data - sorted daily rows (ascending date).
33
34
  * @param props.models - the model legend, in bar order.
34
35
  */
@@ -9,8 +9,9 @@
9
9
  * before real data arrives the dashboard shows an empty (zero) snapshot,
10
10
  * never fabricated samples.
11
11
  */
12
- import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
13
13
  import type { SidebarFooterActionOwnerProps } from '@deepseek-ai/dsh-client-ui-sidebar/client';
14
+ import type { createBillingBudgetStore } from './budget-store.ts';
14
15
  import { NS } from './locales.ts';
15
16
  /** Model-connectivity health reported by the host model directory probe. */
16
17
  export interface ModelHealth {
@@ -27,10 +28,35 @@ export interface ModelHealth {
27
28
  /** Display names of providers whose catalog probe failed. */
28
29
  badProviders: readonly string[];
29
30
  }
30
- /** Full props type for the UsageBilling component. */
31
- type UsageBillingProps = PropsRuntime<'sidebar.footer.action'> & SidebarFooterActionOwnerProps & InjectFace<{
31
+ /**
32
+ * The dashboard's display names (中文厂商名) never equal the provider names a
33
+ * user actually configures (deepseek, zhipu, qwen…), so the dot match also
34
+ * accepts a bidirectional substring hit and a display-name alias list.
35
+ * 导出供一致性守卫测试:catalog 每个厂商都必须在此登记(Custom 除外),
36
+ * 防止新增厂商漏配导致健康绿灯不亮。
37
+ */
38
+ export declare const PROVIDER_ALIASES: Readonly<Record<string, readonly string[]>>;
39
+ /**
40
+ * 从真实 model id 反推提供方显示名:目录未收录的模型(key 落回「其他」)
41
+ * 只靠 entry.provider(Custom)永远点不亮健康灯,这里用厂商别名对 model id
42
+ * 做强匹配(别名作为完整 id / 前缀 / 独立段)与弱匹配(长别名子串),
43
+ * 命中即显示厂商名并点亮健康点;无命中保持 Custom。
44
+ * 导出供守卫测试:短别名(mi/yi)仅允许前缀形式,防止 minimax 等误吞。
45
+ */
46
+ export declare function providerFromModelKey(modelKey: string): string | undefined;
47
+ /** 组件注入面:探活 + 计费指标写入(billing 自身写入,主题插件经服务读取)。 */
48
+ export interface UsageBillingInjected {
32
49
  checkModels: () => Promise<ModelHealth>;
33
- }> & PropsLocale<typeof NS>;
50
+ publishCosts: (costs: {
51
+ todayCost: number;
52
+ monthCost: number;
53
+ }) => void;
54
+ registerOpen: (handler: () => void) => () => void;
55
+ }
56
+ /** 预算 store 的 props 份额(useStore 读取 + actions 写面)。 */
57
+ type BillingBudgetStoreProps = PropsStore<ReturnType<typeof createBillingBudgetStore>>;
58
+ /** Full props type for the UsageBilling component. */
59
+ type UsageBillingProps = PropsRuntime<'sidebar.footer.action'> & SidebarFooterActionOwnerProps & InjectFace<UsageBillingInjected> & PropsRenderSlots<'billing.dashboard.decor'> & BillingBudgetStoreProps & PropsLocale<typeof NS>;
34
60
  /**
35
61
  * UsageBilling: sidebar trigger plus the billing dashboard modal.
36
62
  * @param props - framework-provided sidebar and locale props.
@@ -5,9 +5,40 @@
5
5
  * Displays compact cost/token/cache metrics in the sidebar footer, above the
6
6
  * Settings button, plus a model-health dot (green when any connected model
7
7
  * route responds). Expands to a detailed dashboard panel on click.
8
+ *
9
+ * 与主题插件(如 acid-zine)的协作走 slot 与服务:billing 声明装饰孔位
10
+ *(billing.dashboard.decor)并注册计费指标服务(ctx.billingMetrics),主题
11
+ * 插件主动注入装饰视觉、消费费用数据——billing 不反向依赖任何主题包。
8
12
  */
9
13
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
10
14
  import { type UsageBillingKey } from './locales.ts';
15
+ import { type BillingMetricsService } from './billing-service.ts';
16
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
+ interface SlotMap {
18
+ /**
19
+ * Dashboard 弹窗内的装饰孔位:主题插件(如 acid-zine)按 position 锚点
20
+ * 注入 MacDots、撕角便签、胶带标题、条码等 ZINE 元素。kind=list,可多个
21
+ * 注册者并列;未注入时走 billing 默认视觉。
22
+ */
23
+ 'billing.dashboard.decor': {
24
+ kind: 'list';
25
+ scope: 'root';
26
+ owner: BillingDashboardDecorOwnerProps;
27
+ };
28
+ }
29
+ }
30
+ /** Dashboard 装饰的锚点位置:head/headTitle=窗口标题区;hero=主数字卡;trend/models=面板标题;footer=面板底部。 */
31
+ export type BillingDecorPosition = 'head' | 'headTitle' | 'hero' | 'trend' | 'models' | 'footer';
32
+ /** Dashboard 装饰组件收到的所有者数据:当前锚点。 */
33
+ export interface BillingDashboardDecorOwnerProps {
34
+ position: BillingDecorPosition;
35
+ }
36
+ declare module '@deepseek-ai/cordis' {
37
+ interface Context {
38
+ /** 计费指标服务(billing 插件提供;主题插件可选消费)。 */
39
+ billingMetrics?: BillingMetricsService;
40
+ }
41
+ }
11
42
  declare module '@deepseek-ai/dsh-client-ui-slots' {
12
43
  interface LocaleNamespaceMap {
13
44
  /** The usage billing surface's copy. */
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 计费指标服务(ctx.billingMetrics):把 UsageBilling 的实时费用摘要与
3
+ * 弹窗打开能力开放给其他插件(如 acid-zine 主题的贴纸层)消费。
4
+ *
5
+ * 依赖方向为「billing 提供服务、主题适配消费」:billing 插件是唯一的
6
+ * 写入方(组件通过 inject 的 publishCosts / registerOpen 写入),消费方
7
+ * 只读费用快照并触发弹窗打开——billing 不反向依赖任何主题包。
8
+ */
9
+ /** 计费摘要(精简版,仅供外部展示;金额为人民币元)。 */
10
+ export interface BillingCosts {
11
+ todayCost: number;
12
+ monthCost: number;
13
+ }
14
+ /** 消费方(如酸-zine 贴纸层)可用的只读接口。 */
15
+ export interface BillingMetricsService {
16
+ /** 当前费用快照;从未发布过则 undefined。 */
17
+ readCosts(): BillingCosts | undefined;
18
+ /**
19
+ * 订阅费用更新;立即收到当前值一次(若无值则为 undefined)。
20
+ * @param listener - 费用回调。
21
+ * @returns 退订函数。
22
+ */
23
+ subscribeCosts(listener: (costs: BillingCosts | undefined) => void): () => void;
24
+ /** 打开计费仪表盘(若 billing 弹窗已挂载)。 */
25
+ openDashboard(): void;
26
+ }
27
+ /** 服务运行时:在只读接口之上追加写入入口,仅 apply 与组件使用。 */
28
+ export interface BillingMetricsRuntime extends BillingMetricsService {
29
+ /** 发布最新费用摘要(UsageBilling 组件每次渲染数据变化时调用)。 */
30
+ publishCosts(costs: BillingCosts): void;
31
+ /**
32
+ * 注册弹窗打开回调;组件卸载时应解除。
33
+ * @param handler - 打开弹窗的处理函数。
34
+ * @returns 解除注册的函数。
35
+ */
36
+ registerOpen(handler: () => void): () => void;
37
+ }
38
+ /** 创建计费指标运行时(apply 内调用,随插件纤维存活)。 */
39
+ export declare function createBillingMetrics(): BillingMetricsRuntime;
40
+ //# sourceMappingURL=billing-service.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 预算偏好 store:本月预算的开关与金额。
3
+ *
4
+ * 用户在仪表盘里用开关控制预算条显隐、用数字输入框设置金额;状态经框架
5
+ * store 引擎持久化到 localStorage(persist key 即存储身份),重启后保留。
6
+ * 宿主 Config 的 monthlyBudget 仅作为金额未设置时的默认值,用户输入优先。
7
+ */
8
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
9
+ /** 预算偏好状态。 */
10
+ export interface BudgetPrefsState {
11
+ /** 预算条开关:关 = 只显示标题行与开关,不显示进度。 */
12
+ enabled: boolean;
13
+ /** 用户设置的月度预算(人民币元);0 = 未设置(回退到宿主默认值)。 */
14
+ amount: number;
15
+ /** 最近一次超支通知的日期戳(YYYY-MM-DD):超支通知每天最多一次,跨重启生效。 */
16
+ lastAlertDay: string;
17
+ /** 最近一次余额不足通知的日期戳(YYYY-MM-DD):余额告警同样每天最多一次。 */
18
+ lastBalanceAlertDay: string;
19
+ }
20
+ /** 预算偏好的完整写面(组件只能经这些 action 写入);type 别名以兼容 ActionsDecl 的索引签名约束。 */
21
+ export type BudgetPrefsActions = {
22
+ setEnabled: (d: BudgetPrefsState, on: boolean) => void;
23
+ setAmount: (d: BudgetPrefsState, value: number) => void;
24
+ markAlerted: (d: BudgetPrefsState, day: string) => void;
25
+ markBalanceAlerted: (d: BudgetPrefsState, day: string) => void;
26
+ };
27
+ /**
28
+ * Declare the budget-preferences store handle.
29
+ * @returns the store handle for the register call's store seat.
30
+ */
31
+ export declare function createBillingBudgetStore(): EngineStoreHandle<BudgetPrefsState, BudgetPrefsActions>;
32
+ //# sourceMappingURL=budget-store.d.ts.map
@@ -8,4 +8,6 @@
8
8
  export { inject, apply } from './apply.ts';
9
9
  export { UsageBilling } from './UsageBilling.tsx';
10
10
  export type { UsageBillingKey } from './locales.ts';
11
+ export type { BillingCosts, BillingMetricsService } from './billing-service.ts';
12
+ export type { BillingDecorPosition, BillingDashboardDecorOwnerProps } from './apply.ts';
11
13
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /** Locale dictionaries for the usage billing surface. */
2
- export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trendEmpty' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.totalCost' | 'billing.calls' | 'billing.cacheHitRate' | 'billing.tokens' | 'billing.inputTokens' | 'billing.outputTokens' | 'billing.avgCost' | 'billing.trend' | 'billing.trend7d' | 'billing.trend30d' | 'billing.trendEmpty' | 'billing.budget' | 'billing.sessions' | 'billing.project' | 'billing.lastActive' | 'billing.sessionOverflow' | 'billing.budgetOverBody' | 'billing.models' | 'billing.estimated' | 'billing.actual' | 'billing.pricing' | 'billing.showPricing' | 'billing.hidePricing' | 'billing.pricePerM' | 'billing.input' | 'billing.output' | 'billing.cacheHit' | 'billing.peak' | 'billing.offPeak' | 'billing.flat' | 'billing.peakHours' | 'billing.band' | 'billing.openDashboard' | 'billing.close' | 'billing.lastUpdated' | 'billing.noData' | 'billing.todayRate' | 'billing.rateLive' | 'billing.rateBuiltin' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.balanceDays' | 'billing.balanceLowBody';
3
3
  export declare const NS = "usageBilling";
4
4
  export declare const zh: Record<UsageBillingKey, string>;
5
5
  export declare const en: Record<UsageBillingKey, string>;
@@ -37,16 +37,21 @@ export declare function getRateInfo(): {
37
37
  };
38
38
  /** Default share of traffic assumed to fall in the peak band (0..1). */
39
39
  export declare const DEFAULT_PEAK_SHARE = 0.5;
40
+ /** 计费时段档位:高峰 / 空闲(官方 DeepSeek 刊例价:高峰 = 空闲 × 2)。 */
41
+ export type PriceTierId = 'peak' | 'offPeak';
42
+ /** 成本显示币种:人民币(国内模型直价)/ 美元(国外模型直价或换算显示)。 */
43
+ export type CostCurrency = 'cny' | 'usd';
40
44
  /**
41
- * Model keys served through a subscription plan (e.g. a coding plan or topic
42
- * plan) instead of metered per-token API billing. Usage through these routes
43
- * costs no tokens: the estimator treats them as ¥0 and the billing table
44
- * labels them 订阅包含. Add any model key your deployment serves through a
45
- * plan here; leave empty when every route is pay-as-you-go.
45
+ * 高峰时段判定(北京时间,UTC+8,无夏令时):09:00–12:00、14:00–18:00。
46
+ * @param beijingHour - 北京时间的小时数(0–23)。
46
47
  */
47
- export declare const SUBSCRIPTION_PLAN_KEYS: readonly string[];
48
- /** Whether one stats model key is billed through a subscription plan. */
49
- export declare function isSubscriptionPlan(key: string): boolean;
48
+ export declare function isPeakHour(beijingHour: number): boolean;
49
+ /**
50
+ * 由时刻(epoch 毫秒)推断计费时段;时刻未知/非法时按高峰计(保守:未知
51
+ * 时刻不低估成本,与社区 dsh-usage-chart 的 tierAt 语义一致)。
52
+ * @param timeMs - Unix epoch 毫秒;null/undefined/NaN 视为未知。
53
+ */
54
+ export declare function tierAt(timeMs: number | null | undefined): PriceTierId;
50
55
  /** Usage buckets consumed by one model (counts in raw tokens). */
51
56
  export interface TokenUsageBuckets {
52
57
  /** Uncached input tokens. */
@@ -107,6 +112,13 @@ export interface ModelEntry {
107
112
  * from 2026-08-17, and Gemini's Flex tier discounts spare-capacity traffic.
108
113
  */
109
114
  export declare const MODEL_CATALOG: readonly ModelEntry[];
115
+ /**
116
+ * 真实 provider model id → 计费目录键(`MODEL_CATALOG[].key`)的映射。未知 id
117
+ * 原样保留并落回 `other`(未知模型不估算费用)。聚合层(aggregate.ts)在折叠时
118
+ * 用同一张表把日志里的 model id 归并为目录键,客户端渲染(`modelOf`)也按它
119
+ * 解析,两侧共用一份映射,避免同一模型两侧不一致导致「未收录」。
120
+ */
121
+ export declare const MODEL_KEY_ALIASES: Readonly<Record<string, string>>;
110
122
  /** Lookup a model by its stats key; falls back to the generic `other` entry. */
111
123
  export declare function modelOf(key: string): ModelEntry;
112
124
  /** Resolve a price-table row by its CSS variable name (theme token or fallback color). */
@@ -127,8 +139,25 @@ export declare function resolveToken(name: string): string;
127
139
  * @returns the estimated cost in CNY.
128
140
  */
129
141
  export declare function computeCost(entry: ModelEntry, buckets: TokenUsageBuckets, peakShare?: number): number;
130
- /** Format a CNY amount with adaptive precision. */
131
- export declare function formatMoney(cny: number): string;
142
+ /**
143
+ * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
144
+ * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
145
+ * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。
146
+ * @param entry - the catalog entry whose prices apply.
147
+ * @param buckets - token usage counts.
148
+ * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
149
+ * @param peakShare - fallback mix used only when `timeMs` is missing.
150
+ * @returns the estimated cost in the entry's native currency.
151
+ */
152
+ export declare function computeCostAt(entry: ModelEntry, buckets: TokenUsageBuckets, timeMs: number | null | undefined, peakShare?: number): number;
153
+ /** 人民币 → 美元(显示换算用):1 USD = {@link USD_TO_CNY} CNY。 */
154
+ export declare function cnyToUsd(cny: number): number;
155
+ /**
156
+ * Format an amount with adaptive precision and the given currency symbol.
157
+ * @param amount - the amount (CNY by default; pass `usd` for dollar display).
158
+ * @param currency - display currency; default `cny`.
159
+ */
160
+ export declare function formatMoney(amount: number, currency?: CostCurrency): string;
132
161
  /**
133
162
  * Format a per-1M-token price in its native currency (free when the rate is
134
163
  * zero): CNY for domestic models, USD for overseas ones.
@@ -10,17 +10,38 @@
10
10
  * fabricated samples.
11
11
  */
12
12
  import type { Context } from '@deepseek-ai/cordis';
13
+ import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
14
+ import type { SettingsProvider } from '@deepseek-ai/dsh-settings';
15
+ import { type IdentifiedSubscriptionPlan, type SubscriptionKeys, type SubscriptionPlanConfig } from './subscriptions.ts';
13
16
  /** Plugin configuration. */
14
17
  export interface UsageBillingConfig {
15
18
  /** Absolute path to a `.dsh-usage-stats.json` fallback file. */
16
19
  statsPath?: string;
17
20
  /** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
18
21
  subscriptionProviders?: string[];
22
+ /** 订阅套餐额度适配器(kimi / zai / opencode-go);默认全部内置。 */
23
+ subscriptionPlans?: readonly SubscriptionPlanConfig[];
19
24
  /** 余额查询用的 DeepSeek 凭据引用(环境变量名);默认 DEEPSEEK_API_KEY。 */
20
25
  balanceApiKeyEnv?: string;
26
+ /** 月度预算(人民币元);设置后随 usage-stats 下发,仪表盘显示预算进度条。 */
27
+ monthlyBudget?: number;
28
+ /** 余额不足告警阈值(人民币元):余额低于此值时仪表盘每天提醒一次;
29
+ 不设置则客户端按默认阈值(50 元)兜底。 */
30
+ lowBalanceThreshold?: number;
21
31
  }
22
- /** Required services: the web server and the persisted session log store. */
32
+ /** Required services: the web server, the persisted session log store, and user settings. */
23
33
  export declare const inject: string[];
34
+ /**
35
+ * 解析订阅适配器需要的 API Key:从 llm-pi-ai 设置的 `providers.<id>.apiKeyEnv`
36
+ * 读引用(如 kimi-coding → KIMI_CODING_API_KEY),再经凭据 seam 解析成实际值。
37
+ * 同时识别出用户配置了 key 的订阅套餐(供面板只显示已识别的)。
38
+ * @param settings - the settings service (reads the llm-pi-ai namespace).
39
+ * @param credentials - the credentials service (resolves the env refs).
40
+ */
41
+ export declare function resolveSubscriptionKeys(settings: SettingsProvider, credentials: CredentialProvider): Promise<{
42
+ keys: SubscriptionKeys;
43
+ identified: IdentifiedSubscriptionPlan[];
44
+ }>;
24
45
  /**
25
46
  * Host plugin body: serve real aggregated usage to the browser dashboard.
26
47
  * @param ctx - host context carrying webServer and sessionPersistence.
@@ -47,4 +47,43 @@ export interface ProviderBalance {
47
47
  export interface BalanceResponse {
48
48
  balances: readonly ProviderBalance[];
49
49
  }
50
+ /** Quota query result status; the dashboard maps each to a row state. */
51
+ export type SubscriptionStatus = 'ok' | 'not-configured' | 'unauthorized' | 'rate-limited' | 'unavailable' | 'invalid-response';
52
+ /** One quota window (session / weekly / monthly / billing). */
53
+ export interface SubscriptionWindow {
54
+ kind: 'session' | 'weekly' | 'monthly' | 'billing';
55
+ /** Used share in percent (0–100, one decimal). */
56
+ usedPercent: number;
57
+ /** Remaining share in percent (0–100, one decimal). */
58
+ remainingPercent: number;
59
+ /** ISO reset time; absent when the provider reports none. */
60
+ resetsAt?: string;
61
+ /** Absolute remaining amount; present when the provider reports one. */
62
+ remaining?: number;
63
+ }
64
+ /** One provider's subscription plan quota row. */
65
+ export interface SubscriptionQuota {
66
+ /** Adapter id: `kimi` / `zai` / `opencode-go`. */
67
+ provider: string;
68
+ /** Human display name (e.g. Kimi For Coding). */
69
+ displayName: string;
70
+ /** Plan label; absent when the provider did not name one. */
71
+ plan?: string;
72
+ status: SubscriptionStatus;
73
+ /** Quota windows, newest-window first; empty when the query failed. */
74
+ windows: readonly SubscriptionWindow[];
75
+ }
76
+ /** Response of `/api/billing/subscriptions`. */
77
+ export interface SubscriptionResponse {
78
+ quotas: readonly SubscriptionQuota[];
79
+ }
80
+ /** Config for one subscription adapter (validated in apply). */
81
+ export interface SubscriptionPlanConfig {
82
+ /** Adapter id: `kimi` / `zai` / `opencode-go`. */
83
+ provider: string;
84
+ /** API base URL override; defaults to the provider's public endpoint. */
85
+ baseUrl?: string;
86
+ /** Z.ai region override; defaults to the settings-namespace `zaiRegion`. */
87
+ region?: 'global' | 'bigmodel-cn';
88
+ }
50
89
  //# sourceMappingURL=pricing-shared.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Subscription-plan quota polling (node half): how much of each coding/token
3
+ * plan is left. The billing dashboard already exempts subscription providers
4
+ * from per-token cost; this module surfaces the REMAINING quota so the user
5
+ * sees plan headroom instead of a blank row.
6
+ *
7
+ * The panel shows only the plans the user actually configured: adapters with
8
+ * a known quota API (Kimi, Z.ai, OpenCode Go) query the remaining amount;
9
+ * other subscription providers the harness recognizes (volcengine / baidu /
10
+ * qwen / xiaomi token plans, agent plans…) are identified and listed with a
11
+ * "no quota API" marker rather than hidden. API keys come from the `llm-pi-ai`
12
+ * settings namespace (`apiKeyEnv` refs) resolved through the credentials seam.
13
+ */
14
+ import type { SubscriptionPlanConfig, SubscriptionQuota } from './pricing-shared.ts';
15
+ /** 订阅适配器需要的凭据(来自 llm-pi-ai 设置命名空间)。 */
16
+ export interface SubscriptionKeys {
17
+ /** Kimi For Coding API key。 */
18
+ kimiApiKey: string;
19
+ /** Z.ai API key。 */
20
+ zaiApiKey: string;
21
+ /** OpenCode Go API key。 */
22
+ opencodeApiKey: string;
23
+ /** Z.ai 区域(global / bigmodel-cn)。 */
24
+ zaiRegion: 'global' | 'bigmodel-cn';
25
+ }
26
+ /** 空凭据:全部未配置时的初始值。 */
27
+ export declare const EMPTY_SUBSCRIPTION_KEYS: SubscriptionKeys;
28
+ /** 已识别的一个订阅套餐(用户在 llm-pi-ai 里配置了 key 的订阅类 provider)。 */
29
+ export interface IdentifiedSubscriptionPlan {
30
+ /** llm-pi-ai 的 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
31
+ provider: string;
32
+ /** 显示名(映射表命中则用映射,否则用 id)。 */
33
+ displayName: string;
34
+ /** 是否有额度查询适配器。 */
35
+ adapter: boolean;
36
+ /** 适配器区域覆盖(zai-coding-cn → bigmodel-cn)。 */
37
+ region?: 'global' | 'bigmodel-cn';
38
+ }
39
+ /** 是否是订阅类 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
40
+ export declare function isSubscriptionProviderId(providerId: string): boolean;
41
+ /**
42
+ * 从 llm-pi-ai 设置里识别订阅套餐:带订阅类 id 且配置了 apiKeyEnv 的 provider。
43
+ * @param providers - the `providers` map of the llm-pi-ai settings namespace.
44
+ * @returns identified plans in configuration order.
45
+ */
46
+ export declare function identifySubscriptionPlans(providers: Record<string, {
47
+ apiKeyEnv?: string;
48
+ } | undefined> | undefined): IdentifiedSubscriptionPlan[];
49
+ /**
50
+ * Collect quota for the given plans concurrently (adapter-backed plans only;
51
+ * identified plans without an adapter are surfaced by the caller as "no
52
+ * quota API" rows).
53
+ * @param keys - the API keys from the llm-pi-ai settings namespace.
54
+ * @param plans - adapter-backed plans to poll; empty by default.
55
+ * @param timeoutMs - per-request timeout; defaults to 15s.
56
+ * @returns the quotas in plan order (unknown providers degrade to `unavailable`).
57
+ */
58
+ export declare function collectSubscriptions(keys: SubscriptionKeys, plans?: readonly SubscriptionPlanConfig[], timeoutMs?: number): Promise<readonly SubscriptionQuota[]>;
59
+ //# sourceMappingURL=subscriptions.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "0.2.6",
4
+ "version": "0.4.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -56,10 +56,13 @@
56
56
  "@deepseek-ai/dsh-client-ui-sidebar": "*",
57
57
  "@deepseek-ai/dsh-client-ui-primitives": "*",
58
58
  "@deepseek-ai/dsh-client-ui-slots": "*",
59
+ "@deepseek-ai/dsh-credentials": "*",
59
60
  "@deepseek-ai/dsh-invariants": "*",
60
61
  "@deepseek-ai/dsh-llm": "*",
61
62
  "@deepseek-ai/dsh-session": "*",
62
63
  "@deepseek-ai/dsh-session-persistence": "*",
64
+ "@deepseek-ai/dsh-settings": "*",
65
+ "@deepseek-ai/schemastery": "*",
63
66
  "@deepseek-ai/cordis": "*",
64
67
  "react": "^18.2.0"
65
68
  },
@@ -73,6 +76,7 @@
73
76
  "@deepseek-ai/dsh-client-ui-conversation": "*",
74
77
  "@deepseek-ai/dsh-client-ui-primitives": "*",
75
78
  "@deepseek-ai/dsh-client-ui-slots": "*",
79
+ "@deepseek-ai/dsh-client-web-react": "*",
76
80
  "@deepseek-ai/dsh-invariants": "*",
77
81
  "@testing-library/react": "^16.1.0",
78
82
  "@types/react": "~18.3.1",