@kenz1117/dsh-ui-usage-billing 0.9.6 → 0.9.8

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.
@@ -27,13 +27,52 @@ export declare const DEFAULT_SUBSCRIPTION_PROVIDERS: readonly string[];
27
27
  * 调用与费用分桶展示;部署可由配置覆盖(见 {@link AggregateOptions})。
28
28
  */
29
29
  export declare function isOfficialProvider(provider: string): boolean;
30
+ /** 一个 provider 路由的站点视图(来自 llm-pi-ai providers 的 baseURL)。 */
31
+ export interface ProviderRouteView {
32
+ /** 该路由配置的端点地址;无值 = 直连厂商(非中转站)。 */
33
+ baseURL?: string;
34
+ }
35
+ /** 站点归属分类:site=中转站(有 baseURL origin);direct=直连;unknown=未知路由(配置已删)。 */
36
+ export type SiteKind = 'site' | 'direct' | 'unknown';
37
+ /** 一个 provider 路由归类后的站点引用。 */
38
+ export interface SiteRef {
39
+ kind: SiteKind;
40
+ /** 站点归一化 origin(仅 site)。 */
41
+ origin?: string;
42
+ /** 原 provider 路由名。 */
43
+ provider: string;
44
+ }
45
+ /** 由 baseURL 归一化出站点 origin(协议 + 主机 + 端口);解析失败回退原值。 */
46
+ export declare function siteOriginOf(baseURL: string): string;
47
+ /**
48
+ * 把一个 provider 路由归类为站点引用。判定顺序(与路由在 provider 配置里的状态一致):
49
+ * - 路由存在于当前配置且配了 baseURL → 中转站 `site`(按 origin 归组,同站多 key 合并);
50
+ * - 路由存在于当前配置但无 baseURL → 厂商直连 `direct`;
51
+ * - 路由不在当前配置里 → `unknown`(改过名 / 删除过,是「读不到」而非「直连」)。
52
+ * @param provider - 会话日志里的 provider 路由名(request/header 的 `config.provider`)。
53
+ * @param routes - 当前 provider 路由视图(来自 llm-pi-ai providers)。
54
+ */
55
+ export declare function siteRefOf(provider: string, routes: Readonly<Record<string, ProviderRouteView>>): SiteRef;
56
+ /** 站点桶的稳定 key:`site:<origin>` 与 `direct:<provider>` 分开,`unknown` 单一桶。 */
57
+ export declare function siteBucketKey(ref: SiteRef): string;
30
58
  /** Aggregation tuning options. */
31
59
  export interface AggregateOptions {
32
60
  /** 订阅制 provider id 列表;默认 {@link DEFAULT_SUBSCRIPTION_PROVIDERS}。 */
33
61
  subscriptionProviders?: readonly string[];
34
62
  /** 官方渠道 provider id 列表;默认按 {@link isOfficialProvider} 判定(DeepSeek 官方直连)。 */
35
63
  officialProviderIds?: readonly string[];
64
+ /** 每会话折叠缓存的上限(默认 {@link DEFAULT_MAX_CACHE_SESSIONS});
65
+ * 超限时按最近使用先后淘汰最久未用的会话,防长期运行内存膨胀。 */
66
+ maxCacheSessions?: number;
67
+ /** 中转站归组来源:返回当前 provider 路由视图(llm-pi-ai providers 的 baseURL)。
68
+ * 每次聚合时调用取最新值;缺省时全部路由按「未知路由」处理(无配置发现)。 */
69
+ resolveRoutes?: () => Readonly<Record<string, ProviderRouteView>>;
70
+ /** 工作区标题解析:给定会话 cwd 返回项目显示标题;undefined = 回退到 cwd 末级目录名
71
+ * (host 的 workspaceRegistry 为可选依赖,缺失时不注入,行为保持不变)。 */
72
+ resolveWorkspaceTitle?: (cwd: string) => string | undefined;
36
73
  }
74
+ /** 每会话折叠缓存默认上限:超过则按 LRU 淘汰(P1-6 峰值内存治理)。 */
75
+ export declare const DEFAULT_MAX_CACHE_SESSIONS = 400;
37
76
  /** One model's aggregated usage plus estimated cost in CNY. */
38
77
  export interface ModelUsage {
39
78
  calls: number;
@@ -96,6 +135,12 @@ export interface UsageStatsDocument {
96
135
  byTurn?: TurnUsageRow[];
97
136
  /** 工作区聚合:按 cwd 末级目录归并,按费用倒序;旧快照可能缺失。 */
98
137
  byWorkspace?: WorkspaceUsageRow[];
138
+ /**
139
+ * 中转站归组:按 provider 路由归类到站点(有 baseURL 按 origin 归组)、直连、未知路由;
140
+ * key 为 {@link siteBucketKey} 的稳定值(`site:<origin>` / `direct:<provider>` / `unknown`)。
141
+ * 旧快照可能缺失。
142
+ */
143
+ bySite?: Record<string, ModelUsage>;
99
144
  /**
100
145
  * 按角色费用归因(人民币元):助手输出成本为实测计价;输入成本按会话内
101
146
  * 用户消息 / 工具结果的文本长度占比启发式摊分(日志无角色级 token 实测,
@@ -222,6 +267,8 @@ interface SessionFold {
222
267
  byModel: Map<string, ModelUsage>;
223
268
  byDay: Map<string, ModelUsage>;
224
269
  byDayModels: Map<string, Map<string, ModelUsage>>;
270
+ /** 中转站归组:按 provider 路由归类到站点/直连/未知路由(key = {@link siteBucketKey})。 */
271
+ bySite: Map<string, ModelUsage>;
225
272
  /** 每个模型 key 在本会话内走订阅通道的调用数(合并时跨会话累加判定 plan)。 */
226
273
  planCalls: Map<string, number>;
227
274
  /** 每轮费用明细(按轮次号升序,不含 sessionId);sessionId 在合并时补齐。 */
@@ -263,7 +310,7 @@ export declare function foldSession(events: readonly {
263
310
  type: string;
264
311
  time: number;
265
312
  data: never;
266
- }[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>): SessionFold;
313
+ }[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>): SessionFold;
267
314
  /**
268
315
  * 增量聚合器:按会话缓存折叠结果,用日志文件的 mtime+size 作失效键——
269
316
  * 日志没动的会话直接复用,只有写过的会话重新折叠;整份文档另有短 TTL
@@ -2,12 +2,11 @@
2
2
  * Account-balance queries for the billing dashboard.
3
3
  *
4
4
  * Only providers with a public balance endpoint can report one. Today that is
5
- * DeepSeek (`GET https://api.deepseek.com/user/balance`) and Moonshot/Kimi
6
- * (`GET https://api.moonshot.cn/v1/users/me/balance`), both Bearer 鉴权 with a
7
- * documented JSON shape; the other mainstream providers expose no standard
8
- * balance API (or require a non-Bearer auth flow), so their rows in the model
9
- * table show an unavailable state. The lookup map below is the extension point
10
- * for future providers.
5
+ * DeepSeek, Moonshot/Kimi, StepFun, SiliconFlow, and xAI (Grok) — all Bearer
6
+ * 鉴权 with a documented JSON shape; the other mainstream providers expose no
7
+ * standard balance API (or require a non-Bearer auth flow), so their rows in
8
+ * the model table show an unavailable state. The lookup map below is the
9
+ * extension point for future providers.
11
10
  *
12
11
  * API keys are read from the `llm-pi-ai` settings namespace (`providers.<id>.apiKeyEnv`),
13
12
  * the same source the subscription adapter uses, so a deployment configures a
@@ -0,0 +1,45 @@
1
+ /**
2
+ * PerfPanel: per-model latency/perf table + per-hour TTFT/generation-speed curve.
3
+ *
4
+ * Reads the optional `perf` field of the usage-stats document (aggregated by
5
+ * the host from session logs). Renders a per-model table of TTFT mean/P50/P90,
6
+ * generation speed, total latency and estimated-step count, plus a small
7
+ * dependency-free SVG twin-series hourly curve (TTFT in ms on the left axis,
8
+ * tokens/s on the right). Absent `perf` (older snapshot or stream-less logs)
9
+ * renders an empty state; the panel never fabricates samples.
10
+ */
11
+ import type { TrendSeriesModel } from './TrendChart.tsx';
12
+ import type { UsageBillingKey } from './locales.ts';
13
+ /** 每模型性能统计(与服务端 `ModelPerf` 同形)。 */
14
+ export interface PerfModelData {
15
+ samples: number;
16
+ ttftAvg: number;
17
+ ttftP50: number;
18
+ ttftP90: number;
19
+ tpsAvg?: number;
20
+ latencyAvg: number;
21
+ estimatedSamples: number;
22
+ }
23
+ /** 每小时性能统计(与服务端 `HourPerf` 同形)。 */
24
+ export interface PerfHourData {
25
+ samples: number;
26
+ ttftAvg: number;
27
+ tpsAvg?: number;
28
+ }
29
+ /** 性能指标文档(服务端可选 `perf` 字段;旧快照缺失)。 */
30
+ export interface ClientPerf {
31
+ byModel: Record<string, PerfModelData>;
32
+ byHour: Record<string, PerfHourData>;
33
+ }
34
+ /**
35
+ * Render the performance panel.
36
+ * @param props.perf - the optional perf doc; `undefined`/empty renders an empty state.
37
+ * @param props.models - model legend (key/name/color) for the table swatches and curve legend.
38
+ * @param props.t - locale function.
39
+ */
40
+ export declare function PerfPanel({ perf, models, t, }: {
41
+ perf: ClientPerf | undefined;
42
+ models: readonly TrendSeriesModel[];
43
+ t: (key: UsageBillingKey) => string;
44
+ }): React.ReactNode;
45
+ //# sourceMappingURL=PerfPanel.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PluginInfoCard: 「设置 → 插件信息」卡——展示头像/作者、GitHub 仓库、版本号、
3
+ * 许可证等插件元信息。版本号来自服务端 usage-stats 的 `pluginVersion`(读自
4
+ * 包 package.json),其余元信息静态来自 `plugin-info.ts`。无版本号时显示 em dash。
5
+ */
6
+ import type { UsageBillingKey } from './locales.ts';
7
+ /** 信息卡 props:locale 函数 + 版本号(服务端下发,可为空)。 */
8
+ export declare function PluginInfoCard({ t, version }: {
9
+ t: (key: UsageBillingKey) => string;
10
+ version: string | undefined;
11
+ }): React.ReactNode;
12
+ //# sourceMappingURL=PluginInfoCard.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * TokenPanel: 「Token」分区——把 token 从费用里独立出来洞察。
3
+ * 三个板块 + 导出,全部由 `UsageStats` 的 byDay/byModel/total 派生,服务端零改动:
4
+ * 1. 每日 Token 堆叠趋势(未命中输入 / 缓存命中 / 输出[含 reasoning]),7/30 天切换;
5
+ * 2. 模型 Token 总量排行 + 占比;
6
+ * 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)。
7
+ */
8
+ import type { UsageBillingKey } from './locales.ts';
9
+ import { type CostCurrency } from './pricing.ts';
10
+ import type { UsageStats } from './UsageBilling.tsx';
11
+ /**
12
+ * Token 洞察面板。
13
+ * @param props.stats - usage-stats 文档(byDay/byModel/total)。
14
+ * @param props.trendDays - 每日 token 窗口(7/30 天)。
15
+ * @param props.onTrendDays - 切换趋势窗口。
16
+ */
17
+ export declare function TokenPanel(props: {
18
+ stats: UsageStats;
19
+ currency: CostCurrency;
20
+ trendDays: 7 | 30;
21
+ onTrendDays: (d: 7 | 30) => void;
22
+ t: (key: UsageBillingKey) => string;
23
+ }): React.ReactNode;
24
+ //# sourceMappingURL=TokenPanel.d.ts.map
@@ -11,6 +11,7 @@
11
11
  */
12
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 ClientPerf } from './PerfPanel.tsx';
14
15
  import type { createBillingBudgetStore } from './budget-store.ts';
15
16
  import { type CatalogModel } from './pricing.ts';
16
17
  import { NS, type UsageBillingKey } from './locales.ts';
@@ -32,7 +33,7 @@ export interface ModelHealth {
32
33
  catalog?: readonly CatalogModel[];
33
34
  }
34
35
  /** 仪表盘分区 Tab id。 */
35
- export type DashboardTab = 'overview' | 'trends' | 'providers' | 'details' | 'pricing' | 'settings';
36
+ export type DashboardTab = 'overview' | 'token' | 'trends' | 'providers' | 'pricing' | 'settings';
36
37
  /**
37
38
  * Tab 定义(顺序即渲染顺序):概览=主数字/KPI/热力图,趋势=趋势图/每轮费用,
38
39
  * 明细=厂商计费与订阅,统计=工作区/会话明细,费率=模型单价表,设置=预算与峰谷提醒。
@@ -83,6 +84,16 @@ export declare function peakOffpeakCost(turns: readonly {
83
84
  peak: number;
84
85
  offPeak: number;
85
86
  };
87
+ /** 近 7 天费用序列(含今天,缺日补 0):触发卡 hover 速览的迷你柱数据源。
88
+ * 导出供测试:纯函数(日期取本地时区)。 */
89
+ export declare function activeDaysOf(byDay: Record<string, {
90
+ cost: number;
91
+ }>): number;
92
+ /** 连续使用天数:从今天往前连续「有调用记录」的天数;今天无记录则为 0。
93
+ * 导出供测试:纯函数(日期取本地时区)。 */
94
+ export declare function streakDaysOf(byDay: Record<string, {
95
+ cost: number;
96
+ }>, now?: number): number;
86
97
  /**
87
98
  * 近 7 天费用序列(含今天,缺日补 0):触发卡 hover 速览的迷你柱数据源。
88
99
  * 导出供测试:纯函数(日期取本地时区)。
@@ -95,6 +106,111 @@ export declare function lastSevenDays(byDay: Record<string, {
95
106
  date: string;
96
107
  cost: number;
97
108
  }[];
109
+ /** 会话明细行(与服务端 SessionUsageRow 同形;旧快照可能缺失整个 bySession)。 */
110
+ interface SessionBillingRow {
111
+ id: string;
112
+ title?: string;
113
+ cwd?: string;
114
+ calls: number;
115
+ cost: number;
116
+ lastActive: number;
117
+ }
118
+ /** Usage stats structure from `.dsh-usage-stats.json`. */
119
+ export interface UsageStats {
120
+ /** 服务端聚合时间戳(毫秒);旧快照可能缺失。 */
121
+ updatedAt?: number;
122
+ /** 月度预算(人民币元):宿主 Config 注入;未配置时不渲染预算条。 */
123
+ budget?: number;
124
+ /** 余额不足告警阈值(人民币元):宿主 Config 注入;未配置时客户端用默认值。 */
125
+ lowBalanceThreshold?: number;
126
+ /** 会话明细(按费用倒序,服务端已封顶);旧快照可能缺失。 */
127
+ bySession?: readonly SessionBillingRow[];
128
+ total: {
129
+ calls: number;
130
+ input: number;
131
+ output: number;
132
+ cacheHit: number;
133
+ cacheMiss: number;
134
+ cost: number;
135
+ /** 输出中的 reasoning(思考)token;已含在 output 内。 */
136
+ reasoning: number;
137
+ };
138
+ byModel: Record<string, {
139
+ calls: number;
140
+ input: number;
141
+ output: number;
142
+ cacheHit: number;
143
+ cacheMiss: number;
144
+ cost: number;
145
+ reasoning: number;
146
+ /** Billed through a subscription plan (no per-token cost). */
147
+ plan?: boolean;
148
+ /** 走官方 DeepSeek 直连的调用数(其余为第三方);旧快照可能缺失。 */
149
+ officialCalls?: number;
150
+ /** 走官方渠道的费用(CNY);旧快照可能缺失。 */
151
+ officialCost?: number;
152
+ }>;
153
+ byDay: Record<string, {
154
+ calls: number;
155
+ input: number;
156
+ output: number;
157
+ cacheHit: number;
158
+ cacheMiss: number;
159
+ cost: number;
160
+ reasoning: number;
161
+ }>;
162
+ /** 模型 × 日期 二维统计(趋势图堆叠柱的输入);旧快照可能缺失,渲染时降级为单色柱。 */
163
+ byDayModels?: Record<string, Record<string, {
164
+ calls: number;
165
+ input: number;
166
+ output: number;
167
+ cacheHit: number;
168
+ cacheMiss: number;
169
+ cost: number;
170
+ }>>;
171
+ /** 每轮费用明细(服务端聚合路径恒带);旧快照可能缺失。 */
172
+ byTurn?: readonly {
173
+ sessionId: string;
174
+ turn: number;
175
+ model: string;
176
+ input: number;
177
+ output: number;
178
+ cacheHit: number;
179
+ cacheMiss: number;
180
+ cost: number;
181
+ startedAt: number;
182
+ endedAt?: number;
183
+ }[];
184
+ /** 工作区聚合(按 cwd 末级目录);旧快照可能缺失。 */
185
+ byWorkspace?: readonly {
186
+ name: string;
187
+ calls: number;
188
+ cost: number;
189
+ input: number;
190
+ output: number;
191
+ lastActive: number;
192
+ }[];
193
+ /** 中转站归组(key = `site:<origin>` / `direct:<provider>` / `unknown`);旧快照可能缺失。 */
194
+ bySite?: Record<string, {
195
+ calls: number;
196
+ input: number;
197
+ output: number;
198
+ cacheHit: number;
199
+ cacheMiss: number;
200
+ cost: number;
201
+ reasoning: number;
202
+ }>;
203
+ /** 按角色费用归因(估算口径:输出实测,输入按消息长度摊分);旧快照可能缺失。 */
204
+ byRole?: {
205
+ user: number;
206
+ assistant: number;
207
+ tool: number;
208
+ };
209
+ /** 性能指标(TTFT/生成速度/总延迟)按模型与按小时;旧快照可能缺失。 */
210
+ perf?: ClientPerf;
211
+ /** 插件版本号(服务端读自包 package.json;旧快照缺失)。 */
212
+ pluginVersion?: string;
213
+ }
98
214
  /** 组件注入面:探活 + 计费指标写入(billing 自身写入,主题插件经服务读取)。 */
99
215
  export interface UsageBillingInjected {
100
216
  checkModels: () => Promise<ModelHealth>;
@@ -20,16 +20,18 @@ export interface HeatmapDay {
20
20
  value: number;
21
21
  }
22
22
  /**
23
- * Render the month heatmap.
23
+ * Render the month or year heatmap.
24
24
  * @param props.days - daily cost rows (keys are `YYYY-MM-DD`).
25
25
  * @param props.currency - display currency for the hover amount.
26
26
  * @param props.now - anchor date (defaults to today); injectable for tests.
27
27
  * @param props.t - locale function (used for the legend labels).
28
+ * @param props.range - `month` (calendar month) or `year` (last 52 weeks, GitHub style).
28
29
  */
29
- export declare function UsageHeatmap({ days, currency, now, t }: {
30
+ export declare function UsageHeatmap({ days, currency, now, t, range }: {
30
31
  days: readonly HeatmapDay[];
31
32
  currency: CostCurrency;
32
33
  now?: Date;
34
+ range?: 'month' | 'year';
33
35
  t: (key: 'billing.costAbbr' | 'billing.noData' | 'billing.heatmapLess' | 'billing.heatmapMore') => string;
34
36
  }): React.ReactNode;
35
37
  //# sourceMappingURL=heatmap.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.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | '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.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | '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.pricingTip' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.anomaly' | 'billing.workspaces' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakShareHint' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost';
2
+ export type UsageBillingKey = 'billing.title' | 'billing.subtitle' | 'billing.cost' | 'billing.todayCost' | 'billing.monthCost' | 'billing.yearCost' | 'billing.monthProjected' | 'billing.liveTurn' | 'billing.liveSession' | '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.budgetTierBody' | 'billing.models' | 'billing.providerBilling' | '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.pricingTip' | 'billing.balance' | 'billing.balanceUnconfigured' | 'billing.balanceUnauthorized' | 'billing.balanceUnreachable' | 'billing.uncatalogued' | 'billing.estimatedPricing' | 'billing.balanceDays' | 'billing.balanceLowBody' | 'billing.subscriptions' | 'billing.subscriptionNotConfigured' | 'billing.subscriptionUnauthorized' | 'billing.subscriptionUnavailable' | 'billing.subscriptionInvalid' | 'billing.subscriptionRateLimited' | 'billing.subscriptionSession' | 'billing.subscriptionWeekly' | 'billing.subscriptionMonthly' | 'billing.subscriptionBilling' | 'billing.subscriptionRemaining' | 'billing.subscriptionExhausted' | 'billing.subscriptionReset' | 'billing.subscriptionNoApi' | 'billing.heatmapLess' | 'billing.heatmapMore' | 'billing.currency' | 'billing.currencyCny' | 'billing.currencyUsd' | 'billing.heatmap' | 'billing.rounds' | 'billing.anomaly' | 'billing.workspaces' | 'billing.plan' | 'billing.remaining' | 'billing.unknownModel' | 'billing.model' | 'billing.currentRound' | 'billing.costAbbr' | 'billing.tabOverview' | 'billing.tabTrends' | 'billing.tabProviders' | 'billing.tabDetails' | 'billing.tabPricing' | 'billing.tabSettings' | 'billing.settingsHead' | 'billing.settingsHint' | 'billing.budgetHint' | 'billing.peakAlertHint' | 'billing.peakAlertDescPeak' | 'billing.peakAlertDescOff' | 'billing.export' | 'billing.exportCsvDay' | 'billing.exportCsvSession' | 'billing.exportJson' | 'billing.peakShare' | 'billing.peakShareHint' | 'billing.weekCost' | 'billing.roleCost' | 'billing.roleUser' | 'billing.roleAssistant' | 'billing.roleTool' | 'billing.roleHint' | 'billing.tierPeak' | 'billing.tierOff' | 'billing.tierToPeak' | 'billing.tierToOff' | 'billing.tierAlertEnterPeak' | 'billing.tierAlertEnterOff' | 'billing.peakAlertTitlePeak' | 'billing.peakAlertTitleOff' | 'billing.peakAlert' | 'billing.peakAlertLeadMin' | 'billing.peakAlertPosCorner' | 'billing.peakAlertPosCenter' | 'billing.peakAlertModePeak' | 'billing.peakAlertModeOff' | 'billing.peakAlertModeBoth' | 'billing.peakAlertWebNotify' | 'billing.peakAlertPreview' | 'billing.planTypeCode' | 'billing.planTypeToken' | 'billing.subscriptionFeePerMonth' | 'billing.triggerToday' | 'billing.triggerMonth' | 'billing.subscriptionIncluded' | 'billing.free' | 'billing.official' | 'billing.thirdParty' | 'billing.officialCost' | 'billing.thirdPartyCost' | 'billing.perfSamples' | 'billing.perfTtft' | 'billing.perfP50' | 'billing.perfP90' | 'billing.perfTps' | 'billing.perfLatency' | 'billing.perfEstimated' | 'billing.perfEmpty' | 'billing.perfTpsUnit' | 'billing.perfTitle' | 'billing.perfHint' | 'billing.heatmapYear' | 'billing.heatmapMonth' | 'billing.activeDays' | 'billing.streakDays' | 'billing.subscriptionAutoDetect' | 'billing.pluginInfo' | 'billing.pluginName' | 'billing.pluginDescription' | 'billing.pluginVersion' | 'billing.pluginAuthor' | 'billing.pluginRepository' | 'billing.pluginNpm' | 'billing.pluginLicense' | 'billing.tabToken' | 'billing.tokenExport' | 'billing.tokenExportCsv' | 'billing.tokenCacheHitRate' | 'billing.tokenReasoningShare' | 'billing.tokenReasoningShort' | 'billing.tokenIo' | 'billing.tokenPeak' | 'billing.tokenDaily' | 'billing.tokenByModel' | 'billing.tokenMiss' | 'billing.tokenHit' | 'billing.tokenOutput' | 'billing.tokenTotal' | 'billing.tokenShare' | 'billing.usageStatsTool' | 'billing.usageStatsToolHint' | 'billing.balanceGranted' | 'billing.balanceTopped' | 'billing.balanceDaily' | 'billing.balanceDaysLong' | 'billing.balanceDaysUnit' | 'billing.popTodayModel' | 'billing.popNoConsumption' | 'billing.popQuotaAlert' | 'billing.popRiskNone' | 'billing.popTitle' | 'billing.popDirectLead' | 'billing.popSubLead' | 'billing.popBalanceNormal' | 'billing.popBalanceLow' | 'billing.popQuotaNormal' | 'billing.popQuotaLow' | 'billing.alertBalanceLow' | 'billing.alertQuotaLow' | 'billing.panelRelay' | 'billing.relaySite' | 'billing.relayDirect' | 'billing.relayUnknown' | 'billing.panelRelayQuota' | 'billing.relayBalance' | 'billing.relayNoQuota' | 'billing.relayWindowUsed' | 'billing.relayKindNewApi' | 'billing.relayKindSub2Api' | 'billing.relayKindUnknown' | 'billing.relayCalls';
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>;
@@ -33,11 +33,17 @@ export interface PlanTier {
33
33
  /** 额度口径的人话描述(官方未公布精确额度时)。 */
34
34
  label?: string;
35
35
  }
36
- /** One plan row: provider id (after alias) → plan shape + optional subscription fee. */
36
+ /** 原生币订阅月费(金额 + 币种 + 计费周期)。 */
37
+ export interface SubscriptionMount {
38
+ amount: number;
39
+ currency: 'CNY' | 'USD';
40
+ period: 'month' | 'week' | 'day';
41
+ }
42
+ /** One plan row: provider id (after plan alias) → plan shape + optional subscription fee. */
37
43
  export interface PlanKnowledgeEntry {
38
44
  type: PlanType;
39
- /** 订阅月费(人民币元);code 计划用,计入「本月预计」。 */
40
- subscriptionCny?: number;
45
+ /** 原生币订阅月费(如 $10/月);code 计划用,折算后计入「本月预计」。 */
46
+ subscription?: SubscriptionMount;
41
47
  /** 自动识别的档位月费与周期额度口径(订阅卡片展示)。 */
42
48
  tier?: PlanTier;
43
49
  }
@@ -46,10 +52,23 @@ export interface PlanKnowledgeEntry {
46
52
  * 覆盖我们实际会识别到的订阅通道;其余按量 API 不计入此表(默认 token)。
47
53
  */
48
54
  export declare const PLAN_KNOWLEDGE: Readonly<Record<string, PlanKnowledgeEntry>>;
55
+ /**
56
+ * 订阅/plan provider id 变体 → PLAN_KNOWLEDGE 规范键(引用 dsh-spend 的别名归一化)。
57
+ * 部署配置的订阅 provider id 写法不一(glm/zhipu/bigmodel、ark/volcengine、
58
+ * kimi/moonshot、xiaomi…),先归一化再匹配,提升"自动识别"覆盖率。
59
+ * 注意:裸 qwen/dashscope/tongyi 等是按量 API(token 计费)而非订阅,不归一到
60
+ * 订阅键——只有显式 token-plan 后缀才由 SUBSCRIPTION_ID_RE 判定为订阅。
61
+ */
62
+ export declare const PLAN_PROVIDER_ALIASES: Readonly<Record<string, string>>;
63
+ /** 归一化订阅 provider id:别名命中则映射到规范键,否则原样返回。 */
64
+ export declare function normalizePlanProvider(providerId: string): string;
49
65
  /** provider id(llm-pi-ai 设置键)→ plan 知识;未命中默认 token。 */
50
66
  export declare function planTypeOf(providerId: string): PlanType;
51
- /** 订阅月费(人民币元);非 code 或未配置时为 0。 */
52
- export declare function subscriptionCnyOf(providerId: string): number;
67
+ /**
68
+ * 订阅月费折算为人民币:原生币 × 实时汇率(USD→CNY)。汇率缺失时返回 0,
69
+ * 避免用假设汇率造成失真(跨币种保护:金额统一折成 CNY 再相加)。
70
+ */
71
+ export declare function subscriptionFeeCnyOf(providerId: string, rate: number | undefined): number;
53
72
  /** 自动识别的档位月费与周期额度口径(订阅卡片展示);无档位知识返回 undefined。 */
54
73
  export declare function tierInfoOf(providerId: string): PlanTier | undefined;
55
74
  export interface FallbackRate {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 插件元信息(「设置 → 插件信息卡」展示):名称 / 描述 / 作者 / 仓库 / 许可证。
3
+ * 版本号不含在此——由服务端从自身 package.json 读取(pluginVersion),
4
+ * 这样发布版版本与源码单一来源,不会两处不同步。
5
+ */
6
+ /** npm 包名。 */
7
+ export declare const PLUGIN_NAME = "@kenz1117/dsh-ui-usage-billing";
8
+ /** 一句话描述。 */
9
+ export declare const PLUGIN_DESCRIPTION = "Usage billing dashboard for DeepSeek Harness";
10
+ /** 作者显示名。 */
11
+ export declare const PLUGIN_AUTHOR_NAME = "KenZ";
12
+ /** 作者 GitHub 账号(仓库/作者页链接)。 */
13
+ export declare const PLUGIN_AUTHOR_HANDLE = "kenz1117";
14
+ /** 源码仓库(可点击)。 */
15
+ export declare const PLUGIN_REPOSITORY = "https://github.com/kenz1117/dsh-ui-usage-billing";
16
+ /** npm 包主页(可点击)。 */
17
+ export declare const PLUGIN_NPM_URL = "https://www.npmjs.com/package/@kenz1117/dsh-ui-usage-billing";
18
+ /** 许可证。 */
19
+ export declare const PLUGIN_LICENSE = "MIT";
20
+ //# sourceMappingURL=plugin-info.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * usage-stats 工具开关的共享设置契约(node 与 client 两端共用)。
3
+ *
4
+ * 宿主把已注册的设置命名空间服务给浏览器;node 半区在 apply 阶段读一次该值决定
5
+ * 是否向模型注入 `usage_stats` 工具(工具注入是启动期决策,改开关后重载应用生效),
6
+ * client 半区在「设置」Tab 渲染开关并写入同一命名空间。缺省的默认行为是关闭——
7
+ * 避免该工具默认占用模型每次请求的上下文(coding 场景通常在仪表盘看用量)。
8
+ */
9
+ /** 设置命名空间 id(小写 kebab-case)。 */
10
+ export declare const BILLING_SETTINGS_NAMESPACE = "ui-usage-billing";
11
+ /** 该命名空间下用户可编辑的字段名。 */
12
+ export declare const ENABLE_USAGE_STATS_TOOL_FIELD = "enableUsageStatsTool";
13
+ /** 该命名空间下用户可编辑的子集。 */
14
+ export interface UsageBillingSettings {
15
+ /** 是否向模型注入 `usage_stats` 动态工具(默认 false:不注入)。 */
16
+ enableUsageStatsTool: boolean;
17
+ }
18
+ /** 默认值:工具不注入(贴合 issue 诉求)。 */
19
+ export declare const DEFAULT_ENABLE_USAGE_STATS_TOOL = false;
20
+ //# sourceMappingURL=usage-billing-settings.d.ts.map
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { Context } from '@deepseek-ai/cordis';
13
13
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
14
- import type { SettingsProvider } from '@deepseek-ai/dsh-settings';
14
+ import { type SettingsProvider } from '@deepseek-ai/dsh-settings';
15
15
  import type { CustomBalanceConfig, SubscriptionPlanConfig } from './pricing-shared.ts';
16
16
  import { type IdentifiedSubscriptionPlan, type SubscriptionKeys } from './subscriptions.ts';
17
17
  /** Plugin configuration. */
@@ -31,11 +31,37 @@ export interface UsageBillingConfig {
31
31
  lowBalanceThreshold?: number;
32
32
  /** 自定义 Provider 余额查询(任意 HTTP 端点 + extract 规则,适配 NewApi/LiteLLM 等)。 */
33
33
  customBalances?: readonly CustomBalanceConfig[];
34
+ /** `usage_stats` 工具注入的组合 base(默认 false:不注入);与设置命名空间同字段,
35
+ * 作为用户设置(设置 Tab 开关)的组合兜底。该工具占用每次请求的上下文,coding 场景多在仪表盘查看。 */
36
+ enableUsageStatsTool?: boolean;
34
37
  }
35
38
  /** Required services: the web server, the persisted session log store, and user settings. */
36
39
  export declare const inject: string[];
37
40
  /**
38
- * 解析订阅适配器需要的 API Key:从 llm-pi-ai 设置的 `providers.<id>.apiKeyEnv`
41
+ * 读取 llm-pi-ai 设置的 `providers` 字典(`<route> → { apiKeyEnv? }`)。
42
+ * 余额查询复用同一份来源:部署为某个 provider 配一次 key,多个 surface 共享。
43
+ * @param settings - the settings service (reads the llm-pi-ai namespace).
44
+ * @returns the providers dict; empty when the namespace is unreadable.
45
+ */
46
+ /** 一个 llm-pi-ai provider 路由的读取视图:只取三块——apiKeyEnv(凭据引用)、
47
+ * baseURL(中转站零配置发现的来源)、displayName(站点显示名)。 */
48
+ export interface PiAiProviderRoute {
49
+ apiKeyEnv?: string;
50
+ baseURL?: string;
51
+ displayName?: string;
52
+ }
53
+ /** 同步读取 provider 路由的 baseURL 视图(中转站零配置发现来源)。
54
+ * `settings.describe` 是同步调用,聚合器每次折叠取最新站点映射,无需缓存/过期。
55
+ * 注意:返回**全部可读路由**(baseURL 可选),聚合层据此区分「路由存在但无
56
+ * baseURL=直连」与「路由已删除=未知路由」两种不同归属。
57
+ * @param settings - the settings service (reads the llm-pi-ai namespace).
58
+ * @returns `<route> → { baseURL? }`;命名空间不可读时返回空。
59
+ */
60
+ export declare function readPiAiProviderRoutes(settings: SettingsProvider): Readonly<Record<string, {
61
+ baseURL?: string;
62
+ }>>;
63
+ /**
64
+ * 解析订阅适配器需要的 API Key:从 llm-pi-ai 设置的 `providers.<id>.apiKeyEnv`
39
65
  * 读引用(如 kimi-coding → KIMI_CODING_API_KEY),再经凭据 seam 解析成实际值。
40
66
  * 同时识别出用户配置了 key 的订阅套餐(供面板只显示已识别的)。
41
67
  * @param settings - the settings service (reads the llm-pi-ai namespace).
@@ -38,8 +38,10 @@ export interface ExtraModelPrice {
38
38
  provider: string;
39
39
  price: LivePrice;
40
40
  }
41
- /** 余额查询失败的原因,前端据此显示文案。 */
42
- export type BalanceError = 'unconfigured' | 'unauthorized' | 'unreachable';
41
+ /** 余额查询失败的原因,前端据此显示文案。
42
+ * `invalid` 表示上游返回 2xx 但响应结构偏离预期(接口改版/漂移),
43
+ * 与"网络不可达"(unreachable)区分,便于定位是上游变更还是网络问题。 */
44
+ export type BalanceError = 'unconfigured' | 'unauthorized' | 'unreachable' | 'invalid';
43
45
  /** 一个提供方的账户余额(`/api/billing/balance` 的一行)。 */
44
46
  export interface ProviderBalance {
45
47
  /** 提供方 id(小写,如 `deepseek`),与模型表 provider 匹配用。 */
@@ -101,6 +103,27 @@ export interface SubscriptionQuota {
101
103
  export interface SubscriptionResponse {
102
104
  quotas: readonly SubscriptionQuota[];
103
105
  }
106
+ /** 中转站程序类型;`unknown` = 两个端点都没读出额度。 */
107
+ export type RelayKind = 'new-api' | 'sub2api' | 'unknown';
108
+ /** 中转站额度的一行(`/api/billing/relay-quotas` 的一行)。 */
109
+ export interface RelayQuota {
110
+ /** llm-pi-ai providers 路由名。 */
111
+ route: string;
112
+ /** 站点 origin(baseURL 归一化)。 */
113
+ origin: string;
114
+ /** 站点显示名。 */
115
+ displayName: string;
116
+ kind: RelayKind;
117
+ status: SubscriptionStatus;
118
+ /** 上游明确携带的钱包余额(不猜币种,币种由上游决定)。 */
119
+ balance?: number;
120
+ /** 解析出的滚动额度窗口(used/total 比值)。 */
121
+ windows?: readonly SubscriptionWindow[];
122
+ }
123
+ /** Response of `/api/billing/relay-quotas`. */
124
+ export interface RelayResponse {
125
+ quotas: readonly RelayQuota[];
126
+ }
104
127
  /** Config for one subscription adapter (validated in apply). */
105
128
  export interface SubscriptionPlanConfig {
106
129
  /** Adapter id: `kimi` / `zai` / `opencode-go`. */
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 中转站额度查询(node 半区):识别并读取 New API 系与 Sub2API 的「余额 / 额度窗口」。
3
+ *
4
+ * 适用场景:用户把某条 llm-pi-ai provider 路由的 `baseURL` 指向第三方中转站
5
+ * (New API / One API / VoAPI / Sub2API 等)。这类站点不卖官方余额,卖的是
6
+ * 按 key 的额度(used/total)或多个滚动窗口。本模块对**配了 baseURL 且有
7
+ * apiKeyEnv** 的路由逐个探测两个已知端点,能解析出额度就返回;解析不出的
8
+ * 静默标记 unavailable,绝不臆造金额(与 balance/subscriptions 一致的姿态)。
9
+ *
10
+ * 探测顺序:先 Sub2API `/v1/usage`(标准化程度高),再 New API `/api/status`;
11
+ * 404 = 不是该套程序,继续试下一种;401/403 = 是但 key 不对(unauthorized);
12
+ * 网络/5xx 走熔断门短路一段时间。同一站点多把 key 是独立额度,分别列出。
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import type { RelayQuota, SubscriptionWindow } from './pricing-shared.ts';
16
+ /** 一个待探测的中转站路由(来自 llm-pi-ai providers 的 baseURL + apiKeyEnv)。 */
17
+ export interface RelayRoute {
18
+ /** llm-pi-ai providers 路由名(key)。 */
19
+ route: string;
20
+ /** 该路由配置的端点地址(origin 来源)。 */
21
+ baseURL: string;
22
+ /** 该路由解析到的 apiKeyEnv 引用。 */
23
+ apiKeyEnv: string;
24
+ /** 站点显示名;缺省用路由名。 */
25
+ displayName?: string;
26
+ }
27
+ /**
28
+ * 解析 Sub2API `/v1/usage` 响应:能取到 balance 或 quota/used 就识别为 sub2api。
29
+ * 三种形态(窗口 / 分组 / 钱包余额)都宽容处理:有 `quota/total` 给出窗口,
30
+ * 有 `balance` 给出余额,两者可同时存在。
31
+ * @param data - `/v1/usage` 的 JSON 响应。
32
+ * @returns 解析结果;两者都取不到返回 null(不是 Sub2API 或响应漂移)。
33
+ */
34
+ export declare function parseSub2ApiUsage(data: unknown): {
35
+ balance?: number;
36
+ windows?: readonly SubscriptionWindow[];
37
+ } | null;
38
+ /**
39
+ * 解析 New API `/api/status` 响应:New API 系(One API / VoAPI 分支)的额度是
40
+ * 按记录行的 ratio(已用比例)。只给出窗口,不猜金额(币种防猜)。
41
+ * @param data - `/api/status` 的 JSON 响应。
42
+ * @returns 窗口;取不到比例返回 null(响应漂移)。
43
+ */
44
+ export declare function parseNewApiStatus(data: unknown): {
45
+ windows?: readonly SubscriptionWindow[];
46
+ } | null;
47
+ /**
48
+ * 查询单个中转站路由的额度。先试 Sub2API,再试 New API;任一读出额度即返回。
49
+ * @param ctx - host context carrying the credentials seam.
50
+ * @param route - 待探测的路由(baseURL + apiKeyEnv)。
51
+ * @returns 该路由的一行额度结果(status 标记成败)。
52
+ */
53
+ export declare function queryRelayQuota(ctx: Context, route: RelayRoute): Promise<RelayQuota>;
54
+ /**
55
+ * 批量查询多个中转站路由的额度(每个独立成败,互不影响)。
56
+ * @param ctx - host context carrying the credentials seam.
57
+ * @param routes - 配了 baseURL 且 apiKeyEnv 有值的路由列表。
58
+ * @returns 每个路由一行的额度结果。
59
+ */
60
+ export declare function queryRelayQuotas(ctx: Context, routes: readonly RelayRoute[]): Promise<readonly RelayQuota[]>;
61
+ //# sourceMappingURL=relay.d.ts.map