@kenz1117/dsh-ui-usage-billing 0.2.1 → 0.2.3

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
@@ -1,27 +1,11 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- //#region lib/types/client/pricing.js
5
- /**
6
- * Billing engine: per-model price tables and token-usage cost estimation.
7
- *
8
- * Each model's price table uses its NATIVE currency: domestic providers
9
- * (DeepSeek, 智谱, 通义…) publish RMB prices and store them directly;
10
- * overseas providers (OpenAI, Google, xAI, Meta) publish USD.
11
- * Cost is always computed and displayed in CNY — only USD-priced models go
12
- * through the exchange rate, never domestic ones.
13
- *
14
- * Google-style two-band billing is modeled per model: Gemini's Flex tier
15
- * prices spare-capacity traffic at -50%; DeepSeek V4 splits peak
16
- * (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
17
- * mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
18
- */
19
- /**
20
- * USD → CNY rate for display. Source: China Foreign Exchange Trade System
21
- * mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
22
- * USD-priced models — domestic models never pass through this rate.
23
- */
24
- const USD_TO_CNY = 6.79;
4
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
+ /** 当前汇率:实时覆盖优先,缺省回退内置固定值。 */
6
+ function currentRate() {
7
+ return 6.79;
8
+ }
25
9
  /** Default share of traffic assumed to fall in the peak band (0..1). */
26
10
  const DEFAULT_PEAK_SHARE = .5;
27
11
  /**
@@ -500,11 +484,11 @@ const MODEL_CATALOG = [
500
484
  ];
501
485
  /** Lookup a model by its stats key; falls back to the generic `other` entry. */
502
486
  function modelOf(key) {
503
- const found = MODEL_CATALOG.find((entry) => entry.key === key);
504
- if (found !== void 0) return found;
505
- const fallback = MODEL_CATALOG.at(-1);
506
- if (fallback !== void 0) return fallback;
507
- throw new Error("MODEL_CATALOG must not be empty");
487
+ return MODEL_CATALOG.find((entry) => entry.key === key) ?? (() => {
488
+ const fallback = MODEL_CATALOG.at(-1);
489
+ if (fallback !== void 0) return fallback;
490
+ throw new Error("MODEL_CATALOG must not be empty");
491
+ })();
508
492
  }
509
493
  /**
510
494
  * Price one band's token usage in CNY. The stats `input` field is the TOTAL
@@ -518,12 +502,18 @@ function priceBandCost(band, buckets, currency) {
518
502
  const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
519
503
  const hit = Math.min(buckets.cacheHit, buckets.input);
520
504
  const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
521
- return currency === "USD" ? raw * USD_TO_CNY : raw;
505
+ return currency === "USD" ? raw * currentRate() : raw;
522
506
  }
523
507
  /**
524
508
  * Estimate the CNY cost of one model's token usage, mixing the peak and
525
509
  * off-peak bands by the given peak share (flat-priced models cost the same in
526
510
  * both bands).
511
+ *
512
+ * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
513
+ * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
514
+ * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
515
+ * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
516
+ * 时段只能按比例估算,而非逐请求判定。
527
517
  * @param entry - the catalog entry whose prices apply.
528
518
  * @param buckets - token usage counts.
529
519
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
@@ -673,6 +663,253 @@ async function aggregateUsage(persistence, options = {}) {
673
663
  };
674
664
  }
675
665
  //#endregion
666
+ //#region lib/types/balance.js
667
+ /**
668
+ * Account-balance queries for the billing dashboard.
669
+ *
670
+ * Only providers with a public balance endpoint can report one. Today that is
671
+ * DeepSeek (`GET https://api.deepseek.com/user/balance`, Bearer 鉴权); the
672
+ * other mainstream providers (OpenAI, 智谱, 通义, Kimi…) expose no standard
673
+ * balance API, so their rows in the model table show an unavailable state.
674
+ * The lookup map below is the extension point for future providers.
675
+ */
676
+ /** Abort a balance fetch when the upstream hangs beyond this budget. */
677
+ const FETCH_TIMEOUT_MS$1 = 8e3;
678
+ /** DeepSeek 官方余额接口(官方文档 api-docs.deepseek.com/api/get-user-balance)。 */
679
+ const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
680
+ /** 数字归一化:接口返回的余额是字符串(如 `"110.00"`),统一转 number。 */
681
+ function toNumber(value) {
682
+ if (typeof value === "number" && Number.isFinite(value)) return value;
683
+ if (typeof value === "string") {
684
+ const parsed = Number(value);
685
+ return Number.isFinite(parsed) ? parsed : void 0;
686
+ }
687
+ }
688
+ /**
689
+ * Query the DeepSeek account balance through the configured credential.
690
+ * @param ctx - host context carrying the credentials seam.
691
+ * @param apiKeyEnv - credential reference resolving the DeepSeek API key.
692
+ * @returns the balance row, or an error row when the key/endpoint misbehaves.
693
+ */
694
+ async function queryDeepSeek(ctx, apiKeyEnv) {
695
+ const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
696
+ if (hit === void 0) return {
697
+ provider: "deepseek",
698
+ displayName: "DeepSeek",
699
+ error: "unconfigured"
700
+ };
701
+ const controller = new AbortController();
702
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$1);
703
+ try {
704
+ const response = await fetch(DEEPSEEK_BALANCE_URL, {
705
+ headers: {
706
+ accept: "application/json",
707
+ authorization: `Bearer ${hit.value}`
708
+ },
709
+ signal: controller.signal
710
+ });
711
+ if (response.status === 401 || response.status === 403) return {
712
+ provider: "deepseek",
713
+ displayName: "DeepSeek",
714
+ error: "unauthorized"
715
+ };
716
+ if (!response.ok) return {
717
+ provider: "deepseek",
718
+ displayName: "DeepSeek",
719
+ error: "unreachable"
720
+ };
721
+ const data = await response.json();
722
+ const info = (Array.isArray(data.balance_infos) ? data.balance_infos : [])[0];
723
+ const currency = typeof info?.currency === "string" ? info.currency : void 0;
724
+ const totalBalance = toNumber(info?.total_balance);
725
+ const grantedBalance = toNumber(info?.granted_balance);
726
+ const toppedUpBalance = toNumber(info?.topped_up_balance);
727
+ const isAvailable = typeof data.is_available === "boolean" ? data.is_available : void 0;
728
+ return {
729
+ provider: "deepseek",
730
+ displayName: "DeepSeek",
731
+ ...currency !== void 0 ? { currency } : {},
732
+ ...totalBalance !== void 0 ? { totalBalance } : {},
733
+ ...grantedBalance !== void 0 ? { grantedBalance } : {},
734
+ ...toppedUpBalance !== void 0 ? { toppedUpBalance } : {},
735
+ ...isAvailable !== void 0 ? { isAvailable } : {}
736
+ };
737
+ } catch {
738
+ return {
739
+ provider: "deepseek",
740
+ displayName: "DeepSeek",
741
+ error: "unreachable"
742
+ };
743
+ } finally {
744
+ clearTimeout(timer);
745
+ }
746
+ }
747
+ const QUERIERS = [{
748
+ provider: "deepseek",
749
+ querier: queryDeepSeek
750
+ }];
751
+ /**
752
+ * Query every configured provider's account balance.
753
+ * @param ctx - host context carrying the credentials seam.
754
+ * @param balanceApiKeyEnv - credential reference for the DeepSeek key.
755
+ * @returns the balance rows (one per provider).
756
+ */
757
+ async function queryBalances(ctx, balanceApiKeyEnv) {
758
+ return await Promise.all(QUERIERS.map(({ querier }) => querier(ctx, balanceApiKeyEnv)));
759
+ }
760
+ //#endregion
761
+ //#region lib/types/pricing-fetch.js
762
+ /**
763
+ * One-shot live pricing refresh for the billing dashboard.
764
+ *
765
+ * Fetches the USD → CNY mid rate and the OpenRouter model price list, maps
766
+ * matched models onto the built-in catalog keys, and returns the combined
767
+ * LivePricing. Every fetch failure degrades to the built-in values: the node
768
+ * half caches whatever succeeded and the browser dashboard falls back to the
769
+ * catalog for the rest — a total outage answers `{ source: 'builtin' }`.
770
+ */
771
+ /** Abort a fetch when the upstream hangs beyond this budget. */
772
+ const FETCH_TIMEOUT_MS = 8e3;
773
+ /**
774
+ * USD → CNY 汇率源,按顺序尝试:国内可达的腾讯财经行情(免 key、`~` 分隔
775
+ * 第 4 个字段为价格)优先,国外 open.er-api.com 兜底。任一源失败自动落到
776
+ * 下一个;全部失败由调用方降级内置汇率。
777
+ */
778
+ const RATE_SOURCES = [{
779
+ url: "https://qt.gtimg.cn/q=whUSDCNY",
780
+ parse: (text) => {
781
+ const price = /"([^"]*)"/.exec(text)?.[1]?.split("~")[3];
782
+ return price !== void 0 && price !== "" ? Number(price) : void 0;
783
+ }
784
+ }, {
785
+ url: "https://open.er-api.com/v6/latest/USD",
786
+ parse: (text) => {
787
+ try {
788
+ const cny = JSON.parse(text).rates?.CNY;
789
+ return typeof cny === "number" && Number.isFinite(cny) && cny > 0 ? cny : void 0;
790
+ } catch {
791
+ return;
792
+ }
793
+ }
794
+ }];
795
+ /** OpenRouter's public model list: per-token USD prices, no key needed. */
796
+ const ROUTER_URL = "https://openrouter.ai/api/v1/models";
797
+ /**
798
+ * Built-in catalog key → OpenRouter model-id candidates. Matching prefers an
799
+ * exact id, then a single strong substring hit (the router id contains the
800
+ * hint); an ambiguous hit is skipped so the built-in price stays
801
+ * authoritative. Hints are provider+generation words the router ids carry —
802
+ * correct or extend them as the market moves.
803
+ */
804
+ const ROUTER_ID_HINTS = {
805
+ "flash": ["deepseek-v4-flash", "deepseek-v4"],
806
+ "pro": ["deepseek-v4-pro"],
807
+ "glm": ["glm-5"],
808
+ "qwen-3.8-max": ["qwen-3.8-max", "qwen3.8-max"],
809
+ "qwen-max": ["qwen-max"],
810
+ "qwen-plus": ["qwen-plus"],
811
+ "gemini-pro": ["gemini-3-pro", "gemini-pro"],
812
+ "gemini-flash": ["gemini-3-flash", "gemini-flash"],
813
+ "gpt-5.6-sol": ["gpt-5.6-sol"],
814
+ "gpt-5.6-terra": ["gpt-5.6-terra"],
815
+ "gpt-5.6-luna": ["gpt-5.6-luna"],
816
+ "grok": ["grok-4"],
817
+ "llama": ["llama-4"],
818
+ "kimi": ["kimi-k2"]
819
+ };
820
+ /** GET a URL's text body with a hard timeout; null on any failure. */
821
+ async function fetchText(url) {
822
+ const controller = new AbortController();
823
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
824
+ try {
825
+ const response = await fetch(url, { signal: controller.signal });
826
+ if (!response.ok) return null;
827
+ return await response.text();
828
+ } catch {
829
+ return null;
830
+ } finally {
831
+ clearTimeout(timer);
832
+ }
833
+ }
834
+ /** GET a JSON endpoint with a hard timeout; null on any failure. */
835
+ async function fetchJson(url) {
836
+ const text = await fetchText(url);
837
+ if (text === null) return null;
838
+ try {
839
+ return JSON.parse(text);
840
+ } catch {
841
+ return null;
842
+ }
843
+ }
844
+ /** Latest USD → CNY rate from the first working source, or undefined when none respond. */
845
+ async function fetchRate() {
846
+ for (const source of RATE_SOURCES) {
847
+ const text = await fetchText(source.url);
848
+ if (text === null) continue;
849
+ const value = source.parse(text);
850
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return value;
851
+ }
852
+ }
853
+ /** OpenRouter model rows with usable USD unit prices, or undefined on failure. */
854
+ async function fetchRouterModels() {
855
+ const data = await fetchJson(ROUTER_URL);
856
+ if (data === null || typeof data !== "object") return void 0;
857
+ const list = data.data;
858
+ if (!Array.isArray(list)) return void 0;
859
+ const models = [];
860
+ for (const item of list) {
861
+ if (item === null || typeof item !== "object") continue;
862
+ const { id, pricing } = item;
863
+ if (typeof id !== "string" || pricing === null || typeof pricing !== "object") continue;
864
+ const { prompt, completion } = pricing;
865
+ if (typeof prompt !== "number" || typeof completion !== "number") continue;
866
+ if (!Number.isFinite(prompt) || !Number.isFinite(completion)) continue;
867
+ models.push({
868
+ id,
869
+ input: prompt * 1e6,
870
+ output: completion * 1e6
871
+ });
872
+ }
873
+ return models;
874
+ }
875
+ /** Match one catalog key's candidates: exact id first, then a single strong substring hit. */
876
+ function matchRouterModel(hints, models) {
877
+ const exact = models.find((model) => hints.some((hint) => model.id === hint));
878
+ if (exact !== void 0) return exact;
879
+ const strong = models.filter((model) => hints.some((hint) => hint.length >= 8 && model.id.includes(hint)));
880
+ if (strong.length !== 1) return void 0;
881
+ return strong[0];
882
+ }
883
+ /** Map router matches onto catalog keys; undefined when nothing matched. */
884
+ function buildPrices(models) {
885
+ const result = {};
886
+ for (const [key, hints] of Object.entries(ROUTER_ID_HINTS)) {
887
+ const hit = matchRouterModel(hints, models);
888
+ if (hit === void 0) continue;
889
+ result[key] = {
890
+ input: hit.input,
891
+ cacheHit: hit.input * .1,
892
+ output: hit.output
893
+ };
894
+ }
895
+ return Object.keys(result).length > 0 ? result : void 0;
896
+ }
897
+ /**
898
+ * Fetch the live pricing once at boot. Both upstreams run in parallel; a
899
+ * failure in either degrades independently to the built-in value.
900
+ * @returns the live pricing snapshot (builtin when everything failed).
901
+ */
902
+ async function fetchLivePricing() {
903
+ const [rate, models] = await Promise.all([fetchRate(), fetchRouterModels()]);
904
+ const prices = models === void 0 ? void 0 : buildPrices(models);
905
+ if (rate === void 0 && prices === void 0) return { source: "builtin" };
906
+ return {
907
+ source: "live",
908
+ ...rate !== void 0 ? { rate } : {},
909
+ ...prices !== void 0 ? { prices } : {}
910
+ };
911
+ }
912
+ //#endregion
676
913
  //#region lib/types/index.js
677
914
  /**
678
915
  * Usage billing surface plugin, node half.
@@ -685,8 +922,16 @@ async function aggregateUsage(persistence, options = {}) {
685
922
  * missing file answers `{ error }` so the dashboard shows zeros, never
686
923
  * fabricated samples.
687
924
  */
925
+ /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
926
+ const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
927
+ /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
928
+ const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
688
929
  /** Required services: the web server and the persisted session log store. */
689
- const inject = ["webServer", "sessionPersistence"];
930
+ const inject = [
931
+ "webServer",
932
+ "sessionPersistence",
933
+ "credentials"
934
+ ];
690
935
  /**
691
936
  * Host plugin body: serve real aggregated usage to the browser dashboard.
692
937
  * @param ctx - host context carrying webServer and sessionPersistence.
@@ -700,6 +945,36 @@ function apply(ctx, config = {}) {
700
945
  join(cwd, ".dsh-usage-stats.json"),
701
946
  join(homedir(), ".dsh/.dsh-usage-stats.json")
702
947
  ].filter((path) => typeof path === "string" && path.length > 0);
948
+ let live = { source: "builtin" };
949
+ const refreshPricing = async () => {
950
+ live = await fetchLivePricing();
951
+ };
952
+ refreshPricing();
953
+ ctx.effect(() => {
954
+ const timer = setInterval(() => {
955
+ refreshPricing();
956
+ }, PRICING_REFRESH_INTERVAL_MS);
957
+ return () => {
958
+ clearInterval(timer);
959
+ };
960
+ }, "usage-billing: pricing refresh timer");
961
+ ctx.effect(() => ctx.webServer.register({
962
+ kind: "exact",
963
+ path: "/api/billing/pricing",
964
+ handler: async (_req, res) => {
965
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
966
+ res.end(JSON.stringify(live));
967
+ }
968
+ }), "usage-billing: pricing route");
969
+ ctx.effect(() => ctx.webServer.register({
970
+ kind: "exact",
971
+ path: "/api/billing/balance",
972
+ handler: async (_req, res) => {
973
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
974
+ const balances = await queryBalances(ctx, config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV);
975
+ res.end(JSON.stringify({ balances }));
976
+ }
977
+ }), "usage-billing: balance route");
703
978
  ctx.effect(() => ctx.webServer.register({
704
979
  kind: "exact",
705
980
  path: "/api/billing/usage-stats",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Account-balance queries for the billing dashboard.
3
+ *
4
+ * Only providers with a public balance endpoint can report one. Today that is
5
+ * DeepSeek (`GET https://api.deepseek.com/user/balance`, Bearer 鉴权); the
6
+ * other mainstream providers (OpenAI, 智谱, 通义, Kimi…) expose no standard
7
+ * balance API, so their rows in the model table show an unavailable state.
8
+ * The lookup map below is the extension point for future providers.
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import type { ProviderBalance } from './pricing-shared.ts';
12
+ /**
13
+ * Query every configured provider's account balance.
14
+ * @param ctx - host context carrying the credentials seam.
15
+ * @param balanceApiKeyEnv - credential reference for the DeepSeek key.
16
+ * @returns the balance rows (one per provider).
17
+ */
18
+ export declare function queryBalances(ctx: Context, balanceApiKeyEnv: string): Promise<readonly ProviderBalance[]>;
19
+ //# sourceMappingURL=balance.d.ts.map
@@ -1,9 +1,11 @@
1
1
  /**
2
- * TrendChart: dependency-free SVG stacked bar chart of daily cost per model.
3
- * Each day's column stacks every model's share in its brand color, so the
4
- * total trend and the per-model composition are visible at once. A hover
5
- * crosshair shows the day's model breakdown. No chart library the surface
6
- * stays self-contained and offline.
2
+ * TrendChart: dependency-free SVG chart of daily cost + calls.
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.
7
+ * A hover crosshair shows the day's model breakdown. No chart library — the
8
+ * surface stays self-contained and offline.
7
9
  */
8
10
  /** One model's legend identity: key, display name, and brand color. */
9
11
  export interface TrendSeriesModel {
@@ -11,7 +13,7 @@ export interface TrendSeriesModel {
11
13
  key: string;
12
14
  /** Human-readable model name. */
13
15
  name: string;
14
- /** Resolved brand color for the stack segment and legend swatch. */
16
+ /** Resolved brand color for the bar and legend swatch (empty = single-color fallback). */
15
17
  color: string;
16
18
  }
17
19
  /** One day row fed to the chart. */
@@ -20,15 +22,15 @@ export interface TrendPoint {
20
22
  date: string;
21
23
  /** Total cost that day. */
22
24
  cost: number;
23
- /** API calls that day. */
25
+ /** API calls that day (total across models). */
24
26
  calls: number;
25
- /** Per-model cost that day (stats key → CNY); absent entries stack zero. */
27
+ /** Per-model cost that day (stats key → CNY); absent entries plot zero. */
26
28
  byModel?: Readonly<Record<string, number>>;
27
29
  }
28
30
  /**
29
- * Render the daily per-model stacked cost chart.
31
+ * Render the daily grouped cost bars plus the total-calls line.
30
32
  * @param props.data - sorted daily rows (ascending date).
31
- * @param props.models - the model legend, in stack order (bottom first).
33
+ * @param props.models - the model legend, in bar order.
32
34
  */
33
35
  export declare function TrendChart({ data, models }: {
34
36
  data: readonly TrendPoint[];
@@ -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';
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';
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>;
@@ -12,12 +12,29 @@
12
12
  * (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate. The estimator
13
13
  * mixes both bands by a configured peak share ({@link DEFAULT_PEAK_SHARE}).
14
14
  */
15
+ import type { LivePricing } from '../pricing-shared.ts';
15
16
  /**
16
17
  * USD → CNY rate for display. Source: China Foreign Exchange Trade System
17
18
  * mid-rate 6.7878 on 2026-08-14; rounded to 6.79. Only applies to overseas
18
19
  * USD-priced models — domestic models never pass through this rate.
20
+ * The node half may refresh this at boot via `/api/billing/pricing`; until a
21
+ * live rate arrives the built-in value stays in force.
19
22
  */
20
23
  export declare const USD_TO_CNY = 6.79;
24
+ /**
25
+ * Apply the node half's live pricing snapshot. Absent fields keep the
26
+ * built-in catalog and rate; callers never fabricate values.
27
+ * @param pricing - the `/api/billing/pricing` response.
28
+ */
29
+ export declare function applyLivePricing(pricing: LivePricing): void;
30
+ /**
31
+ * 当前生效的 USD → CNY 汇率及其来源:live = 启动时实时拉取成功,
32
+ * builtin = 实时拉取失败、正在用内置默认值。
33
+ */
34
+ export declare function getRateInfo(): {
35
+ rate: number;
36
+ live: boolean;
37
+ };
21
38
  /** Default share of traffic assumed to fall in the peak band (0..1). */
22
39
  export declare const DEFAULT_PEAK_SHARE = 0.5;
23
40
  /**
@@ -98,6 +115,12 @@ export declare function resolveToken(name: string): string;
98
115
  * Estimate the CNY cost of one model's token usage, mixing the peak and
99
116
  * off-peak bands by the given peak share (flat-priced models cost the same in
100
117
  * both bands).
118
+ *
119
+ * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
120
+ * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
121
+ * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
122
+ * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
123
+ * 时段只能按比例估算,而非逐请求判定。
101
124
  * @param entry - the catalog entry whose prices apply.
102
125
  * @param buckets - token usage counts.
103
126
  * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
@@ -16,6 +16,8 @@ export interface UsageBillingConfig {
16
16
  statsPath?: string;
17
17
  /** 订阅制(coding / token / agent plan)provider id 列表;默认 kimi-coding、xiaomi-token-plan-cn。 */
18
18
  subscriptionProviders?: string[];
19
+ /** 余额查询用的 DeepSeek 凭据引用(环境变量名);默认 DEEPSEEK_API_KEY。 */
20
+ balanceApiKeyEnv?: string;
19
21
  }
20
22
  /** Required services: the web server and the persisted session log store. */
21
23
  export declare const inject: string[];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * One-shot live pricing refresh for the billing dashboard.
3
+ *
4
+ * Fetches the USD → CNY mid rate and the OpenRouter model price list, maps
5
+ * matched models onto the built-in catalog keys, and returns the combined
6
+ * LivePricing. Every fetch failure degrades to the built-in values: the node
7
+ * half caches whatever succeeded and the browser dashboard falls back to the
8
+ * catalog for the rest — a total outage answers `{ source: 'builtin' }`.
9
+ */
10
+ import type { LivePricing } from './pricing-shared.ts';
11
+ /**
12
+ * Fetch the live pricing once at boot. Both upstreams run in parallel; a
13
+ * failure in either degrades independently to the built-in value.
14
+ * @returns the live pricing snapshot (builtin when everything failed).
15
+ */
16
+ export declare function fetchLivePricing(): Promise<LivePricing>;
17
+ //# sourceMappingURL=pricing-fetch.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Live-pricing wire types shared by the node half (which fetches and caches
3
+ * once at boot) and the browser half (which applies the overrides on top of
4
+ * the built-in catalog). An absent field means the built-in value still
5
+ * applies — the dashboard degrades to the catalog, never to fabricated data.
6
+ */
7
+ /** One model's unit prices as reported by the router, in USD per 1M tokens. */
8
+ export interface LivePrice {
9
+ /** Uncached input price per 1M tokens. */
10
+ input: number;
11
+ /** Cache-hit input price per 1M tokens (estimated as 10% of input; the router list carries no cache band). */
12
+ cacheHit: number;
13
+ /** Output price per 1M tokens. */
14
+ output: number;
15
+ }
16
+ /** Response of `/api/billing/pricing` consumed by the dashboard. */
17
+ export interface LivePricing {
18
+ /** live = at least one fetch succeeded; builtin = full fallback to the catalog. */
19
+ source: 'live' | 'builtin';
20
+ /** USD → CNY mid rate (present when the rate fetch succeeded). */
21
+ rate?: number;
22
+ /** Overrides keyed by built-in catalog key (present when router matches succeeded). */
23
+ prices?: Record<string, LivePrice>;
24
+ }
25
+ /** 余额查询失败的原因,前端据此显示文案。 */
26
+ export type BalanceError = 'unconfigured' | 'unauthorized' | 'unreachable';
27
+ /** 一个提供方的账户余额(`/api/billing/balance` 的一行)。 */
28
+ export interface ProviderBalance {
29
+ /** 提供方 id(小写,如 `deepseek`),与模型表 provider 匹配用。 */
30
+ provider: string;
31
+ /** 显示名(如 `DeepSeek`)。 */
32
+ displayName: string;
33
+ /** 余额币种(CNY / USD)。 */
34
+ currency?: string;
35
+ /** 总可用余额(含赠金与充值)。 */
36
+ totalBalance?: number;
37
+ /** 未过期赠金余额。 */
38
+ grantedBalance?: number;
39
+ /** 充值余额。 */
40
+ toppedUpBalance?: number;
41
+ /** 余额是否足以继续调用。 */
42
+ isAvailable?: boolean;
43
+ /** 未配置/鉴权失败/网络不可达等失败原因;缺省 = 查询成功。 */
44
+ error?: BalanceError;
45
+ }
46
+ /** Response of `/api/billing/balance` consumed by the dashboard. */
47
+ export interface BalanceResponse {
48
+ balances: readonly ProviderBalance[];
49
+ }
50
+ //# sourceMappingURL=pricing-shared.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.1",
4
+ "version": "0.2.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },