@kenz1117/dsh-ui-usage-billing 1.6.5 → 1.6.6

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/lib/index.js CHANGED
@@ -3548,7 +3548,7 @@ function isFormatUnsupportedError(error) {
3548
3548
  * @returns the aggregator holding the per-session fold cache.
3549
3549
  */
3550
3550
  function createUsageAggregator(persistence, options = {}) {
3551
- const subscriptionMatcher = options.subscriptionProviders === void 0 ? new Set(DEFAULT_SUBSCRIPTION_PROVIDERS) : new Set(options.subscriptionProviders);
3551
+ const subscriptionMatcher = options.subscriptionMatcher ?? (options.subscriptionProviders === void 0 ? new Set(DEFAULT_SUBSCRIPTION_PROVIDERS) : new Set(options.subscriptionProviders));
3552
3552
  const officialProviderIds = options.officialProviderIds === void 0 ? void 0 : new Set(options.officialProviderIds);
3553
3553
  const routeAliases = options.routeAliases ?? {};
3554
3554
  const maxCacheSessions = options.maxCacheSessions ?? 400;
@@ -6406,10 +6406,34 @@ function minmaxDisplayName(provider) {
6406
6406
  /** MiniMax Token Plan / Coding Plan 的 key 判别:普通按量 API key 以 `sk-` 开头,
6407
6407
  * Token Plan(订阅套餐)key 以 `sk-cp-` 开头。据此区分一个 key 是订阅还是按量,
6408
6408
  * 供查询失败时给出「key 类型 / region」定向提示,避免把按量 key 误当 token-plan
6409
- * 查询后只剩一句笼统的「响应异常」。 */
6409
+ * 查询后只剩一句笼统的「响应异常」;也是聚合层订阅豁免的判定依据(同一路由 id
6410
+ * 按量/套餐两用,豁免只能按 key)。 */
6410
6411
  function isMiniMaxTokenPlanKey(apiKey) {
6411
6412
  return apiKey.startsWith("sk-cp-");
6412
6413
  }
6414
+ /** MiniMax 系路由 id(官方目录 minimax / minimax-cn 与本插件早期命名 minimax-token-plan[-cn])。 */
6415
+ function isMiniMaxProviderId(providerId) {
6416
+ return /^minimax(?:-cn|-token-plan|-token-plan-cn)?$/i.test(providerId);
6417
+ }
6418
+ /**
6419
+ * 逐路由判定 MiniMax 系路由是否 Coding Plan:按该路由自己的 key(显式 apiKeyEnv
6420
+ * 优先,缺省按路由约定 ref)解析后看 `sk-cp-` 前缀。同一路由 id(如官方目录的
6421
+ * minimax-cn)按量用户与套餐用户两用,订阅豁免不能按路由 id 一刀切——此前聚合
6422
+ * 层默认集合干脆不含 minimax,套餐用户的用量被无条件按量计费。解析不出 key
6423
+ * 的路由判为 false(保守:不豁免)。导出供测试:无文件 I/O。
6424
+ * @param providers - llm-pi-ai providers 视图。
6425
+ * @param resolveRef - 凭据 seam:ref 名 → 非空 key 值。
6426
+ * @returns MiniMax 系路由 id → 是否 Coding Plan。
6427
+ */
6428
+ async function classifyMiniMaxPlanRoutes(providers, resolveRef) {
6429
+ const out = /* @__PURE__ */ new Map();
6430
+ for (const [route, view] of Object.entries(providers)) {
6431
+ if (!isMiniMaxProviderId(route)) continue;
6432
+ const hit = await firstResolvedRef([...typeof view?.apiKeyEnv === "string" && view.apiKeyEnv !== "" ? [view.apiKeyEnv] : [], ...routeKeyRefCandidates(route)], resolveRef);
6433
+ out.set(route, hit !== void 0 && isMiniMaxTokenPlanKey(hit.value));
6434
+ }
6435
+ return out;
6436
+ }
6413
6437
  /** 构造 MiniMax Token Plan 查询端点:避免 base 已含 `/v1` 时拼出 `/v1/v1/...`。 */
6414
6438
  function miniMaxEndpoint(base) {
6415
6439
  const trimmed = base.replace(/\/+$/, "");
@@ -8077,8 +8101,19 @@ function apply(ctx, config = {}) {
8077
8101
  }
8078
8102
  return ids;
8079
8103
  };
8104
+ const subscriptionRoutes = new Set(config.subscriptionProviders ?? DEFAULT_SUBSCRIPTION_PROVIDERS);
8105
+ const explicitSubscriptionRoutes = new Set(config.subscriptionProviders ?? []);
8106
+ const refreshMiniMaxPlanRoutes = async () => {
8107
+ try {
8108
+ const classified = await classifyMiniMaxPlanRoutes(await readPiAiProviders(ctx.settings), refResolver(ctx.credentials));
8109
+ for (const [route, isPlan] of classified) if (isPlan) subscriptionRoutes.add(route);
8110
+ else if (!explicitSubscriptionRoutes.has(route)) subscriptionRoutes.delete(route);
8111
+ } catch (error) {
8112
+ swallowed("minimax-plan-classify-failed", error);
8113
+ }
8114
+ };
8080
8115
  const aggregator = createUsageAggregator(adaptSessionPersistence(ctx.sessionPersistence), {
8081
- ...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
8116
+ subscriptionMatcher: subscriptionRoutes,
8082
8117
  ...config.routeAliases === void 0 ? {} : { routeAliases: config.routeAliases },
8083
8118
  resolveRoutes: () => readPiAiProviderRoutes(ctx.settings),
8084
8119
  resolveRegisteredProviders: registeredProvidersOf,
@@ -8087,6 +8122,7 @@ function apply(ctx, config = {}) {
8087
8122
  ...config.searchCallEstimateCny === void 0 ? {} : { searchCallEstimateCny: config.searchCallEstimateCny },
8088
8123
  ledger: ledgerStore
8089
8124
  });
8125
+ refreshMiniMaxPlanRoutes();
8090
8126
  ctx.effect(() => () => {
8091
8127
  aggregator.flush();
8092
8128
  }, "usage-billing: ledger flush on dispose");
@@ -8434,6 +8470,7 @@ function apply(ctx, config = {}) {
8434
8470
  quotas: []
8435
8471
  };
8436
8472
  const refreshQuotas = async () => {
8473
+ await refreshMiniMaxPlanRoutes();
8437
8474
  const { keys, identified } = await resolveSubscriptionKeys(ctx.settings, ctx.credentials);
8438
8475
  const piProviders = await readPiAiProviders(ctx.settings);
8439
8476
  const rows = [...await collectSubscriptions(keys, identified.filter((item) => item.adapter).map((item) => {
@@ -132,6 +132,13 @@ export declare function sumSiteBuckets(bySite: Readonly<Record<string, {
132
132
  export interface AggregateOptions {
133
133
  /** 订阅制 provider id 列表;缺省按订阅卡同款 id 判定(`isSubscriptionProviderId`)。 */
134
134
  subscriptionProviders?: readonly string[];
135
+ /**
136
+ * 订阅判定集合(**引用语义**,优先于 subscriptionProviders):聚合器直接持有
137
+ * 传入的 Set 实例,调用方可在运行期原地增删——同一路由 id 按量/套餐两用的
138
+ * 厂商(MiniMax:`sk-cp-` 前缀 key 才是 Coding Plan)只能按 key 动态判定;
139
+ * 集合内容变化经 configFingerprint(按内容序列化)自动触发历史行全量重折。
140
+ */
141
+ subscriptionMatcher?: ReadonlySet<string>;
135
142
  /** 官方渠道 provider id 列表;缺省按 {@link officialChannelOf} 判定(通道 origin 优先)。 */
136
143
  officialProviderIds?: readonly string[];
137
144
  /**
@@ -1,12 +1,15 @@
1
1
  /**
2
- * LiveCostBar: the session-scope cost ticker mounted on the composer's dock,
2
+ * LiveCostBar: the session-scope cost ticker mounted beside the composer,
3
3
  * showing the current session's accumulated spend and the latest turn's cost.
4
4
  *
5
- * It rides `conversation.composer.dock` (the stats-line family seat under the
6
- * composer card, same posture as ui-conversation's own StatsLine), so it stays
7
- * visible while working without opening the full dashboard. Data comes from the
8
- * same `/api/billing/usage-stats` endpoint the dashboard polls; the bar reads
9
- * the current session id from the session-scope standard kit and matches
5
+ * It is registered on two host seats and renders in exactly one of them per the
6
+ * position preference: `conversation.composer.dock` (the stats-line family seat
7
+ * under the composer card, same posture as ui-conversation's own StatsLine) for
8
+ * 「下方」, and `conversation.input.dock` (the full-width seat above the composer
9
+ * card, where the host's todo panel and queue dock live) for 「上方」. Either way
10
+ * it stays visible while working without opening the full dashboard. Data comes
11
+ * from the same `/api/billing/usage-stats` endpoint the dashboard polls; the bar
12
+ * reads the current session id from the session-scope standard kit and matches
10
13
  * `bySession` (session total) and `byTurn` (latest turn cost). Rendering is a
11
14
  * pure function of props and polled data, never a side effect.
12
15
  *
@@ -16,6 +19,7 @@
16
19
  * visible without opening the dashboard.
17
20
  */
18
21
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
22
+ import type { LiveCostBarPrefs } from './usage-billing-settings.ts';
19
23
  import type { UsageBillingKey } from './locales.ts';
20
24
  /** The usage-stats shape the composer bar needs (a thin slice, not the whole doc). */
21
25
  export interface LiveStats {
@@ -85,18 +89,48 @@ export declare function lowQuotaChips(quotas: readonly QuotaSlice[], threshold?:
85
89
  * @returns 该会话最近一轮的归因模型键;无轮次或轮行未带模型时 undefined。
86
90
  */
87
91
  export declare function sessionModelOf(stats: LiveStats | null, sessionId: string | undefined): string | undefined;
88
- /** Props: the framework's session identity plus the owning dock's locale seat. */
92
+ /** 胶囊位置偏好(设置 Tab 三选)。 */
93
+ type CapsulePosition = LiveCostBarPrefs['position'];
94
+ /** 胶囊条的宿主座位:above=input.dock(卡片上方);below=composer.dock(卡片下方)。 */
95
+ export type CapsuleSeat = Exclude<CapsulePosition, 'toolbar'>;
96
+ /**
97
+ * 宿主 SessionSnapshot 里决定 hero(空白新会话)态的字段薄切片。input.dock 座位
98
+ * 的 owner 快照(InputZone.session)按此结构读取。
99
+ */
100
+ export interface SessionPhaseSlice {
101
+ readonly blank: boolean;
102
+ readonly running: boolean;
103
+ readonly promptAttempted: boolean;
104
+ readonly awaitingFirstTurn: boolean;
105
+ }
106
+ /**
107
+ * 会话是否处于宿主的 hero(空白)阶段:复刻 ui-conversation `conversationPhase`
108
+ * 的 blank 分支——无正在运行的轮次、用户尚未尝试提交、且会话仍空白或等待首轮。
109
+ * 宿主在 hero 态不渲染 composer.dock(输入框下方座位),但 input.dock 会渲染;
110
+ * 「上方」座位借此与「下方」保持同样的显隐语义,空白会话页不浮一只空壳药丸。
111
+ * 导出供测试:纯函数。
112
+ * @param session - 会话快照薄切片。
113
+ */
114
+ export declare function isBlankPhase(session: SessionPhaseSlice): boolean;
115
+ /** Props: the framework's session identity plus the owning seat's locale and identity. */
89
116
  export interface LiveCostBarProps {
90
117
  /** Current Session identity supplied by the session-scope standard kit. */
91
118
  sessionId: SessionId;
92
- /** The owning dock's locale seat (bound to the billing NS). */
119
+ /** The owning seat's locale seat (bound to the billing NS). */
93
120
  t: (key: UsageBillingKey) => string;
121
+ /** Which host seat this instance occupies; it renders only when the position pref matches. */
122
+ seat: CapsuleSeat;
123
+ /** Owner snapshot the input.dock seat receives (InputZone.session); absent on composer.dock. */
124
+ session?: SessionPhaseSlice;
94
125
  }
95
126
  /**
96
- * Render the live cost ticker for the current session.
97
- * @param props - framework session identity and locale.
127
+ * Render the live cost ticker for the current session in one host seat.
128
+ * @param props - framework session identity, locale, the seat occupied, and
129
+ * (input.dock only) the owner session snapshot.
98
130
  */
99
- export declare function LiveCostBar({ sessionId, t }: LiveCostBarProps): React.ReactNode;
131
+ export declare function LiveCostBar({ sessionId, t, seat, session }: LiveCostBarProps): React.ReactNode;
132
+ /** Props of the tool-row chip: session identity and locale only (its seat is fixed to input.right). */
133
+ export type LiveCostChipProps = Pick<LiveCostBarProps, 'sessionId' | 't'>;
100
134
  /**
101
135
  * Render the compact inline chip for the composer tool row (position
102
136
  * 「模型选择前」). Occupies a fixed minimal footprint — tier glyph + session
@@ -106,5 +140,6 @@ export declare function LiveCostBar({ sessionId, t }: LiveCostBarProps): React.R
106
140
  * amber/red instead of growing it.
107
141
  * @param props - framework session identity and locale.
108
142
  */
109
- export declare function LiveCostChip({ sessionId, t }: LiveCostBarProps): React.ReactNode;
143
+ export declare function LiveCostChip({ sessionId, t }: LiveCostChipProps): React.ReactNode;
144
+ export {};
110
145
  //# sourceMappingURL=live-cost.d.ts.map
@@ -98,7 +98,7 @@ export declare function saveSiteListPrefs(prefs: SiteListPrefs): void;
98
98
  export interface LiveCostBarPrefs {
99
99
  /** 是否显示输入框下方的即时代费条胶囊(默认 true:保持历史行为)。 */
100
100
  show: boolean;
101
- /** 胶囊位置:toolbar = 输入框内部工具行内联 chip(默认,issue #47);below = 输入框下方;above = 输入框上方。 */
101
+ /** 胶囊位置:toolbar = 输入框内部工具行内联 chip(默认,issue #47);below = 输入框下方(宿主 composer.dock 座位);above = 输入框上方(宿主 input.dock 座位,issue #101)。 */
102
102
  position: 'below' | 'above' | 'toolbar';
103
103
  }
104
104
  /** 默认即时代费条偏好:显示在输入框内部(工具行内联 chip,issue #47 反馈「上方/下方」都打断输入视线)。 */
@@ -102,6 +102,25 @@ export declare function identifySubscriptionPlans(providers: Record<string, Subs
102
102
  * @returns 窗口列表;无可用窗口时为空数组。
103
103
  */
104
104
  export declare function parseMiniMaxRemains(body: unknown): SubscriptionWindow[];
105
+ /** MiniMax Token Plan / Coding Plan 的 key 判别:普通按量 API key 以 `sk-` 开头,
106
+ * Token Plan(订阅套餐)key 以 `sk-cp-` 开头。据此区分一个 key 是订阅还是按量,
107
+ * 供查询失败时给出「key 类型 / region」定向提示,避免把按量 key 误当 token-plan
108
+ * 查询后只剩一句笼统的「响应异常」;也是聚合层订阅豁免的判定依据(同一路由 id
109
+ * 按量/套餐两用,豁免只能按 key)。 */
110
+ export declare function isMiniMaxTokenPlanKey(apiKey: string): boolean;
111
+ /** MiniMax 系路由 id(官方目录 minimax / minimax-cn 与本插件早期命名 minimax-token-plan[-cn])。 */
112
+ export declare function isMiniMaxProviderId(providerId: string): boolean;
113
+ /**
114
+ * 逐路由判定 MiniMax 系路由是否 Coding Plan:按该路由自己的 key(显式 apiKeyEnv
115
+ * 优先,缺省按路由约定 ref)解析后看 `sk-cp-` 前缀。同一路由 id(如官方目录的
116
+ * minimax-cn)按量用户与套餐用户两用,订阅豁免不能按路由 id 一刀切——此前聚合
117
+ * 层默认集合干脆不含 minimax,套餐用户的用量被无条件按量计费。解析不出 key
118
+ * 的路由判为 false(保守:不豁免)。导出供测试:无文件 I/O。
119
+ * @param providers - llm-pi-ai providers 视图。
120
+ * @param resolveRef - 凭据 seam:ref 名 → 非空 key 值。
121
+ * @returns MiniMax 系路由 id → 是否 Coding Plan。
122
+ */
123
+ export declare function classifyMiniMaxPlanRoutes(providers: Readonly<Record<string, SubscriptionRouteConfigView | undefined>>, resolveRef: (name: string) => Promise<string | undefined>): Promise<ReadonlyMap<string, boolean>>;
105
124
  /**
106
125
  * 解析 OpenRouter `/api/v1/credits` 响应:已用% = total_usage / total_credits。
107
126
  * 导出供测试:纯函数。
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": "1.6.5",
4
+ "version": "1.6.6",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",