@linxin666/dsh-usage 0.4.4 → 0.4.5

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
@@ -118,7 +118,7 @@ function mountOnce(packageName, fn) {
118
118
  return mount;
119
119
  }
120
120
  //#endregion
121
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.0-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
121
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
122
122
  /**
123
123
  * Duplicate-install-safe nominal primitive helpers.
124
124
  *
@@ -141,7 +141,7 @@ function brandString(value) {
141
141
  return value;
142
142
  }
143
143
  //#endregion
144
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.2.0-rc.1_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-inva_1a647ad178a224e363f109a5f72e2237/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
144
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-inva_13783eb73d2217c5a4a149b781a9cc49/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
145
145
  /**
146
146
  * Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
147
147
  * *references* to secrets — environment-variable names — while providers own
@@ -310,6 +310,7 @@ function bearer(apiKey) {
310
310
  }
311
311
  const DEEPSEEK = {
312
312
  ids: ["deepseek", "deepseek-official"],
313
+ familyOnlyIds: ["deepseek-account"],
313
314
  displayName: "DeepSeek",
314
315
  balance: {
315
316
  origin: "https://api.deepseek.com",
@@ -729,11 +730,20 @@ function balanceAppliesToRoute(adapter, baseURL) {
729
730
  /**
730
731
  * Whether a provider route belongs to the official DeepSeek family: the only
731
732
  * family with a spend price book and a settings-section-owned env credential
732
- * (llm-deepseek) rather than a pi-ai profile. Drives the env fallback in
733
- * credential resolution and the fold-time cost stamping.
733
+ * (llm-deepseek) rather than a pi-ai profile. Drives the fold-time cost
734
+ * stamping, the client fold-time spend estimate, and the whale-yuan bank.
735
+ *
736
+ * Family membership is wider than the probe directory. The account route
737
+ * (deepseek-account) bills the same account but authenticates with the account
738
+ * token, so it prices and mints while `adapterFor()` still returns undefined
739
+ * for it and no probe or env-credential fallback can reach it. Every caller of
740
+ * this predicate is an accounting decision; the credential and probing
741
+ * decisions read `adapterFor()` instead, so the two never widen together
742
+ * (issue #1772).
734
743
  */
735
744
  function isDeepSeekProviderRoute(provider) {
736
- return adapterFor(provider) === DEEPSEEK;
745
+ if (adapterFor(provider) === DEEPSEEK) return true;
746
+ return DEEPSEEK.familyOnlyIds?.includes(provider) === true;
737
747
  }
738
748
  /**
739
749
  * Best-effort human message from a provider error body, for the per-provider
@@ -777,6 +787,252 @@ function foldAliasRoutes(routes) {
777
787
  });
778
788
  }
779
789
  //#endregion
790
+ //#region src/core/holidays.ts
791
+ /**
792
+ * Chinese public-holiday dates, Beijing calendar days, as `YYYY-MM-DD`.
793
+ *
794
+ * The DeepSeek peak rule bills a public holiday off-peak in full, and the
795
+ * State Council publishes these dates per year, so they cannot be derived
796
+ * from a rule. This module is that data; `pricing.ts` keeps the arithmetic.
797
+ *
798
+ * Adjusted workdays are deliberately absent: the provider counts the
799
+ * calendar day, so an adjusted Saturday or Sunday is off-peak exactly like
800
+ * any other weekend and listing it would price it as peak.
801
+ *
802
+ * Source: the yearly schedules in bastengao/chinese-holidays-node
803
+ * (`data/<year>.json`, MIT). Only the `holiday` entries are taken; that
804
+ * package's runtime code (which fetches over the network) and its
805
+ * dependencies are not vendored.
806
+ *
807
+ * Coverage ends at the newest year below. A year with no entry is priced as
808
+ * if it had no holidays, which is the documented default of
809
+ * {@link PublicHolidayCalendar} in `pricing.ts`.
810
+ * @module @linxin666/dsh-usage/core/holidays
811
+ */
812
+ /** Public-holiday dates keyed by Beijing calendar year. */
813
+ const CHINESE_PUBLIC_HOLIDAYS = {
814
+ "2020": [
815
+ "2020-01-24",
816
+ "2020-01-25",
817
+ "2020-01-26",
818
+ "2020-01-27",
819
+ "2020-01-28",
820
+ "2020-01-29",
821
+ "2020-01-30",
822
+ "2020-04-04",
823
+ "2020-04-05",
824
+ "2020-04-06",
825
+ "2020-05-01",
826
+ "2020-05-02",
827
+ "2020-05-03",
828
+ "2020-05-04",
829
+ "2020-05-05",
830
+ "2020-06-25",
831
+ "2020-06-26",
832
+ "2020-06-27",
833
+ "2020-10-01",
834
+ "2020-10-02",
835
+ "2020-10-03",
836
+ "2020-10-04",
837
+ "2020-10-05",
838
+ "2020-10-06",
839
+ "2020-10-07",
840
+ "2020-10-08"
841
+ ],
842
+ "2021": [
843
+ "2021-01-01",
844
+ "2021-01-02",
845
+ "2021-01-03",
846
+ "2021-02-11",
847
+ "2021-02-12",
848
+ "2021-02-13",
849
+ "2021-02-14",
850
+ "2021-02-15",
851
+ "2021-02-16",
852
+ "2021-02-17",
853
+ "2021-04-03",
854
+ "2021-04-04",
855
+ "2021-04-05",
856
+ "2021-05-01",
857
+ "2021-05-02",
858
+ "2021-05-03",
859
+ "2021-05-04",
860
+ "2021-05-05",
861
+ "2021-06-12",
862
+ "2021-06-13",
863
+ "2021-06-14",
864
+ "2021-09-19",
865
+ "2021-09-20",
866
+ "2021-09-21",
867
+ "2021-10-01",
868
+ "2021-10-02",
869
+ "2021-10-03",
870
+ "2021-10-04",
871
+ "2021-10-05",
872
+ "2021-10-06",
873
+ "2021-10-07"
874
+ ],
875
+ "2022": [
876
+ "2022-01-01",
877
+ "2022-01-02",
878
+ "2022-01-03",
879
+ "2022-01-31",
880
+ "2022-02-01",
881
+ "2022-02-02",
882
+ "2022-02-03",
883
+ "2022-02-04",
884
+ "2022-02-05",
885
+ "2022-02-06",
886
+ "2022-04-03",
887
+ "2022-04-04",
888
+ "2022-04-05",
889
+ "2022-04-30",
890
+ "2022-05-01",
891
+ "2022-05-02",
892
+ "2022-05-03",
893
+ "2022-05-04",
894
+ "2022-06-03",
895
+ "2022-06-04",
896
+ "2022-06-05",
897
+ "2022-09-10",
898
+ "2022-09-11",
899
+ "2022-09-12",
900
+ "2022-10-01",
901
+ "2022-10-02",
902
+ "2022-10-03",
903
+ "2022-10-04",
904
+ "2022-10-05",
905
+ "2022-10-06",
906
+ "2022-10-07"
907
+ ],
908
+ "2023": [
909
+ "2022-12-31",
910
+ "2023-01-01",
911
+ "2023-01-02",
912
+ "2023-01-21",
913
+ "2023-01-22",
914
+ "2023-01-23",
915
+ "2023-01-24",
916
+ "2023-01-25",
917
+ "2023-01-26",
918
+ "2023-01-27",
919
+ "2023-04-29",
920
+ "2023-04-30",
921
+ "2023-05-01",
922
+ "2023-05-02",
923
+ "2023-05-03",
924
+ "2023-06-22",
925
+ "2023-06-23",
926
+ "2023-06-24",
927
+ "2023-09-29",
928
+ "2023-09-30",
929
+ "2023-10-01",
930
+ "2023-10-02",
931
+ "2023-10-03",
932
+ "2023-10-04",
933
+ "2023-10-05",
934
+ "2023-10-06"
935
+ ],
936
+ "2024": [
937
+ "2023-12-30",
938
+ "2023-12-31",
939
+ "2024-01-01",
940
+ "2024-02-10",
941
+ "2024-02-11",
942
+ "2024-02-12",
943
+ "2024-02-13",
944
+ "2024-02-14",
945
+ "2024-02-15",
946
+ "2024-02-16",
947
+ "2024-02-17",
948
+ "2024-04-04",
949
+ "2024-04-05",
950
+ "2024-04-06",
951
+ "2024-05-01",
952
+ "2024-05-02",
953
+ "2024-05-03",
954
+ "2024-05-04",
955
+ "2024-05-05",
956
+ "2024-06-08",
957
+ "2024-06-09",
958
+ "2024-06-10",
959
+ "2024-09-15",
960
+ "2024-09-16",
961
+ "2024-09-17",
962
+ "2024-10-01",
963
+ "2024-10-02",
964
+ "2024-10-03",
965
+ "2024-10-04",
966
+ "2024-10-05",
967
+ "2024-10-06",
968
+ "2024-10-07"
969
+ ],
970
+ "2025": [
971
+ "2025-01-28",
972
+ "2025-01-29",
973
+ "2025-01-30",
974
+ "2025-01-31",
975
+ "2025-02-01",
976
+ "2025-02-02",
977
+ "2025-02-03",
978
+ "2025-02-04",
979
+ "2025-04-04",
980
+ "2025-04-05",
981
+ "2025-04-06",
982
+ "2025-05-01",
983
+ "2025-05-02",
984
+ "2025-05-03",
985
+ "2025-05-04",
986
+ "2025-05-05",
987
+ "2025-05-31",
988
+ "2025-06-01",
989
+ "2025-06-02",
990
+ "2025-10-01",
991
+ "2025-10-02",
992
+ "2025-10-03",
993
+ "2025-10-04",
994
+ "2025-10-05",
995
+ "2025-10-06",
996
+ "2025-10-07",
997
+ "2025-10-08"
998
+ ],
999
+ "2026": [
1000
+ "2026-01-01",
1001
+ "2026-01-02",
1002
+ "2026-01-03",
1003
+ "2026-02-15",
1004
+ "2026-02-16",
1005
+ "2026-02-17",
1006
+ "2026-02-18",
1007
+ "2026-02-19",
1008
+ "2026-02-20",
1009
+ "2026-02-21",
1010
+ "2026-02-22",
1011
+ "2026-02-23",
1012
+ "2026-04-04",
1013
+ "2026-04-05",
1014
+ "2026-04-06",
1015
+ "2026-05-01",
1016
+ "2026-05-02",
1017
+ "2026-05-03",
1018
+ "2026-05-04",
1019
+ "2026-05-05",
1020
+ "2026-06-19",
1021
+ "2026-06-20",
1022
+ "2026-06-21",
1023
+ "2026-09-25",
1024
+ "2026-09-26",
1025
+ "2026-09-27",
1026
+ "2026-10-01",
1027
+ "2026-10-02",
1028
+ "2026-10-03",
1029
+ "2026-10-04",
1030
+ "2026-10-05",
1031
+ "2026-10-06",
1032
+ "2026-10-07"
1033
+ ]
1034
+ };
1035
+ //#endregion
780
1036
  //#region src/core/pricing.ts
781
1037
  /** The published peak windows: weekday 09:00-12:00 and 14:00-18:00. */
782
1038
  const PEAK_WINDOWS = [{
@@ -828,23 +1084,52 @@ function withinWindow(minuteOfDay) {
828
1084
  return PEAK_WINDOWS.find((window) => minuteOfDay >= window.from && minuteOfDay < window.to);
829
1085
  }
830
1086
  /**
1087
+ * The default calendar: the published Chinese public-holiday table, so a holiday
1088
+ * weekday prices off-peak instead of over-reporting at the peak rate. A year the
1089
+ * table does not cover holds no dates and falls back to peak on weekdays, which
1090
+ * is the safe direction to be wrong in: the estimate reads high rather than low.
1091
+ */
1092
+ const CN_PUBLIC_HOLIDAYS = { isPublicHoliday: (date) => {
1093
+ const year = date.slice(0, 4);
1094
+ if (Number(year) > 2026) return false;
1095
+ return CHINESE_PUBLIC_HOLIDAYS[year]?.includes(date) ?? false;
1096
+ } };
1097
+ /** The Beijing-time `YYYY-MM-DD` calendar day of `ms` (UTC+8 has no DST, so the shift is exact). */
1098
+ function beijingDate(ms) {
1099
+ const shifted = new Date(ms + BEIJING_UTC_OFFSET_MS);
1100
+ const month = `${shifted.getUTCMonth() + 1}`.padStart(2, "0");
1101
+ const day = `${shifted.getUTCDate()}`.padStart(2, "0");
1102
+ return `${shifted.getUTCFullYear()}-${month}-${day}`;
1103
+ }
1104
+ /**
1105
+ * Whether `ms` falls on a peak-eligible Beijing day: Monday-Friday that is
1106
+ * not a public holiday. A Saturday or Sunday is never eligible, including on
1107
+ * an adjusted workday — the provider counts the calendar day, not the
1108
+ * working schedule.
1109
+ */
1110
+ function isPeakDay(ms, calendar) {
1111
+ const weekday = new Date(ms + BEIJING_UTC_OFFSET_MS).getUTCDay();
1112
+ if (weekday < 1 || weekday > 5) return false;
1113
+ return !calendar.isPublicHoliday(beijingDate(ms));
1114
+ }
1115
+ /**
831
1116
  * The DeepSeek billing period at `ms`, plus when it next flips. The clock is
832
1117
  * Beijing time regardless of the host timezone (UTC+8 has no DST, so a fixed
833
1118
  * shift is exact). `boundaryMs` is the instant the current period ends — the
834
1119
  * window's close while peaking, the next window's open otherwise.
835
1120
  */
836
- function deepseekPeriodAt(ms) {
1121
+ function deepseekPeriodAt(ms, calendar = CN_PUBLIC_HOLIDAYS) {
837
1122
  const shifted = new Date(ms + BEIJING_UTC_OFFSET_MS);
838
- const weekday = shifted.getUTCDay();
839
1123
  const minuteOfDay = shifted.getUTCHours() * 60 + shifted.getUTCMinutes();
840
- const current = weekday >= 1 && weekday <= 5 ? withinWindow(minuteOfDay) : void 0;
1124
+ const current = isPeakDay(ms, calendar) ? withinWindow(minuteOfDay) : void 0;
841
1125
  if (current !== void 0) return {
842
1126
  peak: true,
843
1127
  boundaryMs: ms + (current.to - minuteOfDay) * 6e4 - shifted.getUTCSeconds() * 1e3 - shifted.getUTCMilliseconds()
844
1128
  };
845
1129
  for (let dayOffset = 0; dayOffset < 8; dayOffset += 1) {
846
- const day = new Date(ms + BEIJING_UTC_OFFSET_MS + dayOffset * 864e5);
847
- if (day.getUTCDay() < 1 || day.getUTCDay() > 5) continue;
1130
+ const dayStartMs = ms + dayOffset * 864e5;
1131
+ if (!isPeakDay(dayStartMs, calendar)) continue;
1132
+ const day = new Date(dayStartMs + BEIJING_UTC_OFFSET_MS);
848
1133
  const realDayStart = Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate()) - BEIJING_UTC_OFFSET_MS;
849
1134
  for (const window of PEAK_WINDOWS) {
850
1135
  if (dayOffset === 0 && window.from <= minuteOfDay) continue;
@@ -869,9 +1154,9 @@ function priceFor(model, atMs) {
869
1154
  * row; ids from other providers never reach this function (the service gates
870
1155
  * by route family). Rounded to micro-CNY so the ledger stays readable.
871
1156
  */
872
- function deepseekModelSpend(model, totals, atMs) {
1157
+ function deepseekModelSpend(model, totals, atMs, calendar = CN_PUBLIC_HOLIDAYS) {
873
1158
  const price = priceFor(model, atMs);
874
- const column = deepseekPeriodAt(atMs).peak ? "peak" : "offPeak";
1159
+ const column = deepseekPeriodAt(atMs, calendar).peak ? "peak" : "offPeak";
875
1160
  const spend = (totals.cacheReadTokens * price.cacheHit[column] + (totals.inputTokens + totals.cacheWriteTokens) * price.inputMiss[column] + totals.outputTokens * price.output[column]) / 1e6;
876
1161
  return Math.round(spend * 1e6) / 1e6;
877
1162
  }
@@ -915,6 +1200,18 @@ function localDateKey(ms) {
915
1200
  const day = String(date.getDate()).padStart(2, "0");
916
1201
  return `${date.getFullYear()}-${month}-${day}`;
917
1202
  }
1203
+ /**
1204
+ * Whether `key` is a real local-date key (`YYYY-MM-DD`). The shape check is
1205
+ * round-tripped: an impossible date (9999-99-99) parses to NaN and a rollover
1206
+ * (2026-02-30) lands on another day, so both would otherwise fold into a bogus,
1207
+ * never-pruned bucket. Shared by the persisted-document reader and the day
1208
+ * route's query gate.
1209
+ */
1210
+ function isLedgerDateKey(key) {
1211
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) return false;
1212
+ const atMs = (/* @__PURE__ */ new Date(key + "T12:00:00")).getTime();
1213
+ return Number.isFinite(atMs) && localDateKey(atMs) === key;
1214
+ }
918
1215
  /** An empty ledger document. */
919
1216
  function createLedgerDocument() {
920
1217
  return {
@@ -1041,9 +1338,8 @@ function deserializeLedger(value) {
1041
1338
  const days = value.days;
1042
1339
  if (typeof days !== "object" || days === null) return doc;
1043
1340
  for (const [dateKey, providers] of Object.entries(days)) {
1044
- if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey) || typeof providers !== "object" || providers === null) continue;
1341
+ if (!isLedgerDateKey(dateKey) || typeof providers !== "object" || providers === null) continue;
1045
1342
  const atMs = (/* @__PURE__ */ new Date(dateKey + "T12:00:00")).getTime();
1046
- if (!Number.isFinite(atMs) || localDateKey(atMs) !== dateKey) continue;
1047
1343
  for (const [provider, models] of Object.entries(providers)) {
1048
1344
  if (typeof models !== "object" || models === null) continue;
1049
1345
  for (const [model, totals] of Object.entries(models)) {
@@ -1275,6 +1571,7 @@ var UsageService = class {
1275
1571
  totals: range.totals,
1276
1572
  providers: range.providers
1277
1573
  },
1574
+ availableDays: allKeys,
1278
1575
  all: {
1279
1576
  from: allKeys[0] ?? todayKey,
1280
1577
  to: allKeys[allKeys.length - 1] ?? todayKey,
@@ -1315,6 +1612,16 @@ var UsageService = class {
1315
1612
  ...today.calls > 0 && totalTokens(today) > 0 ? { today } : {}
1316
1613
  };
1317
1614
  }
1615
+ /**
1616
+ * One retained local day aggregated per provider and model, as the day
1617
+ * route serves it. A day the ledger never held — pruned by retention, or
1618
+ * simply without usage — reads as a zeroed day rather than an error, so a
1619
+ * picker selection that aged out degrades to "no usage" instead of a broken
1620
+ * panel. The route gates the key's shape before it gets here.
1621
+ */
1622
+ day(dateKey) {
1623
+ return this.daySummary(dateKey);
1624
+ }
1318
1625
  /** One local day aggregated per provider. */
1319
1626
  daySummary(dateKey) {
1320
1627
  const { totals, providers } = summarizeDays([this.ledger.days[dateKey] ?? {}]);
@@ -1798,6 +2105,43 @@ function makeUsageOverviewRoute(ctx, service) {
1798
2105
  };
1799
2106
  }
1800
2107
  /**
2108
+ * One retained day route: `GET /api/dsh-usage/day?date=YYYY-MM-DD` answers
2109
+ * that local day's per-provider and per-model totals, so the section's day
2110
+ * pickers can show a past day instead of only today. Same trust fence as the
2111
+ * overview route, and a key that is not a real local date is refused with 400
2112
+ * rather than folded into a bogus day.
2113
+ * @param ctx - host context; may expose remoteWebUiPairing.
2114
+ * @param service - the usage service.
2115
+ * @returns the route.
2116
+ */
2117
+ function makeUsageDayRoute(ctx, service) {
2118
+ return {
2119
+ kind: "exact",
2120
+ path: "/api/dsh-usage/day",
2121
+ handler: (req, res) => {
2122
+ if (!isUsageAllowed(ctx, req)) {
2123
+ writeJson(res, 403, {
2124
+ ok: false,
2125
+ error: "forbidden: loopback-only"
2126
+ });
2127
+ return;
2128
+ }
2129
+ const date = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("date") ?? "";
2130
+ if (!isLedgerDateKey(date)) {
2131
+ writeJson(res, 400, {
2132
+ ok: false,
2133
+ error: "date must be YYYY-MM-DD"
2134
+ });
2135
+ return;
2136
+ }
2137
+ writeJson(res, 200, {
2138
+ ok: true,
2139
+ day: service.day(date)
2140
+ }, { "cache-control": "no-store" });
2141
+ }
2142
+ };
2143
+ }
2144
+ /**
1801
2145
  * Manual refresh: forces one probe cycle now and answers with the fresh
1802
2146
  * overview. Same trust fence as the overview route.
1803
2147
  * @param ctx - host context; may expose remoteWebUiPairing.
@@ -1913,9 +2257,13 @@ const apply = mountOnce("@linxin666/dsh-usage", (ctx, config) => {
1913
2257
  let disposeRoutes;
1914
2258
  /** True while this activation has a start queued behind a predecessor's flush. */
1915
2259
  let queued = false;
1916
- /** Register the service's two routes; the returned disposer unregisters them. */
2260
+ /** Register the service's routes; the returned disposer unregisters them. */
1917
2261
  const mountRoutes = (next) => {
1918
- const disposers = [makeUsageOverviewRoute(ctx, next), makeUsageRefreshRoute(ctx, next)].map((route) => ctx.webServer.register(route));
2262
+ const disposers = [
2263
+ makeUsageOverviewRoute(ctx, next),
2264
+ makeUsageDayRoute(ctx, next),
2265
+ makeUsageRefreshRoute(ctx, next)
2266
+ ].map((route) => ctx.webServer.register(route));
1919
2267
  return () => {
1920
2268
  for (const dispose of disposers) try {
1921
2269
  dispose();
@@ -10,6 +10,7 @@
10
10
  import { type ReactNode } from 'react';
11
11
  import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
12
12
  import type { UsageStoreInstance } from './usage-store.ts';
13
+ import type { UsageDayView } from '../core/types.ts';
13
14
  /** The settings fields this section edits (immediate-apply semantics). */
14
15
  export interface UsageSettings {
15
16
  enabled?: boolean;
@@ -23,6 +24,12 @@ export interface UsageSectionFace {
23
24
  poll: () => void;
24
25
  /** Force a host probe cycle now (resolves with the fresh overview). */
25
26
  refresh: () => void;
27
+ /**
28
+ * Fetch one retained local day (YYYY-MM-DD) from the host. The overview
29
+ * only carries today plus the list of recorded days, so a day the user picks
30
+ * is one small on-demand request.
31
+ */
32
+ loadDay: (date: string) => Promise<UsageDayView>;
26
33
  /** The shared configuration form this section's settings row reads and writes. */
27
34
  settings: ConfigForm<UsageSettings>;
28
35
  }
@@ -20,6 +20,13 @@ export declare const zh: {
20
20
  'usage.current': string;
21
21
  'usage.today': string;
22
22
  'usage.today.cost': string;
23
+ 'usage.day.label': string;
24
+ 'usage.day.today': string;
25
+ 'usage.day.title': string;
26
+ 'usage.day.cost': string;
27
+ 'usage.day.empty': string;
28
+ 'usage.day.loading': string;
29
+ 'usage.day.error': string;
23
30
  'usage.peak.on': string;
24
31
  'usage.peak.off': string;
25
32
  'usage.calls': string;
@@ -54,6 +61,7 @@ export declare const zh: {
54
61
  'usage.bank.spend.observed': string;
55
62
  'usage.bank.spend.estimated': string;
56
63
  'usage.bank.window': string;
64
+ 'usage.bank.windowAll': string;
57
65
  'usage.bank.save': string;
58
66
  'usage.bank.share': string;
59
67
  'usage.bank.drawError': string;
@@ -42,6 +42,24 @@ export interface PlanParse {
42
42
  export interface ProviderAdapter {
43
43
  /** Provider route keys this adapter serves. */
44
44
  ids: readonly string[];
45
+ /**
46
+ * Route keys that belong to this family for accounting (token folding, the
47
+ * spend price book, the whale-yuan bank) but must never be probed with this
48
+ * adapter's endpoints.
49
+ *
50
+ * A route reaches the DeepSeek family without an API key when the user is
51
+ * signed in to a DeepSeek account: the llm-deepseek-account plugin registers
52
+ * the route id deepseek-account, which authenticates with the account token
53
+ * rather than a key. Its tokens bill the same account the official route
54
+ * bills, so the family must own it, but the balance endpoint reads an
55
+ * API-key account: probing it would either fail on the account token or,
56
+ * when the user also configured DEEPSEEK_API_KEY, print the API-key account's
57
+ * money under the account route's name (issue #1772, the same
58
+ * mis-attribution class as #1688). adapterFor() deliberately does not
59
+ * resolve these ids, so no probe, credential fallback, or alias fold can
60
+ * reach them; family membership is read through isDeepSeekProviderRoute().
61
+ */
62
+ familyOnlyIds?: readonly string[];
45
63
  /** Fallback display name when the LLM runtime has none. */
46
64
  displayName: string;
47
65
  balance?: {
@@ -101,8 +119,16 @@ export declare function balanceAppliesToRoute(adapter: ProviderAdapter, baseURL:
101
119
  /**
102
120
  * Whether a provider route belongs to the official DeepSeek family: the only
103
121
  * family with a spend price book and a settings-section-owned env credential
104
- * (llm-deepseek) rather than a pi-ai profile. Drives the env fallback in
105
- * credential resolution and the fold-time cost stamping.
122
+ * (llm-deepseek) rather than a pi-ai profile. Drives the fold-time cost
123
+ * stamping, the client fold-time spend estimate, and the whale-yuan bank.
124
+ *
125
+ * Family membership is wider than the probe directory. The account route
126
+ * (deepseek-account) bills the same account but authenticates with the account
127
+ * token, so it prices and mints while `adapterFor()` still returns undefined
128
+ * for it and no probe or env-credential fallback can reach it. Every caller of
129
+ * this predicate is an accounting decision; the credential and probing
130
+ * decisions read `adapterFor()` instead, so the two never widen together
131
+ * (issue #1772).
106
132
  */
107
133
  export declare function isDeepSeekProviderRoute(provider: string): boolean;
108
134
  /**
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Chinese public-holiday dates, Beijing calendar days, as `YYYY-MM-DD`.
3
+ *
4
+ * The DeepSeek peak rule bills a public holiday off-peak in full, and the
5
+ * State Council publishes these dates per year, so they cannot be derived
6
+ * from a rule. This module is that data; `pricing.ts` keeps the arithmetic.
7
+ *
8
+ * Adjusted workdays are deliberately absent: the provider counts the
9
+ * calendar day, so an adjusted Saturday or Sunday is off-peak exactly like
10
+ * any other weekend and listing it would price it as peak.
11
+ *
12
+ * Source: the yearly schedules in bastengao/chinese-holidays-node
13
+ * (`data/<year>.json`, MIT). Only the `holiday` entries are taken; that
14
+ * package's runtime code (which fetches over the network) and its
15
+ * dependencies are not vendored.
16
+ *
17
+ * Coverage ends at the newest year below. A year with no entry is priced as
18
+ * if it had no holidays, which is the documented default of
19
+ * {@link PublicHolidayCalendar} in `pricing.ts`.
20
+ * @module @linxin666/dsh-usage/core/holidays
21
+ */
22
+ /** Public-holiday dates keyed by Beijing calendar year. */
23
+ export declare const CHINESE_PUBLIC_HOLIDAYS: Readonly<Record<string, readonly string[]>>;
24
+ /** The newest year this table covers; later years carry no entry. */
25
+ export declare const HOLIDAY_TABLE_LAST_YEAR = 2026;
@@ -7,6 +7,14 @@
7
7
  import { type UsageLedgerDocument, type UsageProviderSummary, type UsageTokenTotals } from './types.ts';
8
8
  /** Local-date key (`YYYY-MM-DD`) for an epoch ms timestamp. */
9
9
  export declare function localDateKey(ms: number): string;
10
+ /**
11
+ * Whether `key` is a real local-date key (`YYYY-MM-DD`). The shape check is
12
+ * round-tripped: an impossible date (9999-99-99) parses to NaN and a rollover
13
+ * (2026-02-30) lands on another day, so both would otherwise fold into a bogus,
14
+ * never-pruned bucket. Shared by the persisted-document reader and the day
15
+ * route's query gate.
16
+ */
17
+ export declare function isLedgerDateKey(key: string): boolean;
10
18
  /** An empty ledger document. */
11
19
  export declare function createLedgerDocument(): UsageLedgerDocument;
12
20
  /**
@@ -4,10 +4,19 @@
4
4
  * at fold time (the provider bills each request in the period the request ran
5
5
  * in, so pricing at the fold is the honest estimate).
6
6
  *
7
- * Policy (api-docs.deepseek.com pricing page, effective 2026-09-10 12:00
8
- * Beijing): peak hours are Beijing time Monday-Friday 09:00-12:00 and
9
- * 14:00-18:00; every other hour (nights, weekends) is off-peak and billed at
10
- * half the peak price. All prices here are CNY per million tokens.
7
+ * Policy (api-docs.deepseek.com pricing page): peak hours are Beijing time
8
+ * Monday-Friday 09:00-12:00 and 14:00-18:00, excluding Chinese public
9
+ * holidays; every other hour is off-peak and billed at half the peak price.
10
+ * The published rule counts a calendar day, not the working schedule: a
11
+ * Saturday or Sunday is off-peak in full even on an adjusted workday, and a
12
+ * public holiday is off-peak in full even on a Monday-Friday. All prices
13
+ * here are CNY per million tokens.
14
+ *
15
+ * Public-holiday dates are not derivable from the rule, so the rule takes them as
16
+ * a `PublicHolidayCalendar`. The default is the published table in `./holidays.ts`,
17
+ * covering 2020 through ${HOLIDAY_TABLE_LAST_YEAR}; a caller with newer dates
18
+ * passes its own. A holiday weekday therefore prices off-peak, which is the half
19
+ * of the published rule that is easy to get wrong.
11
20
  *
12
21
  * The published rows are `deepseek-flash` (DeepSeek-V4.1-Flash) and
13
22
  * `deepseek-v4-pro` (DeepSeek-V4-Pro-0813). The retired flash ids
@@ -17,13 +26,32 @@
17
26
  * @module @linxin666/dsh-usage/core/pricing
18
27
  */
19
28
  import type { UsageTokenTotals } from './types.ts';
29
+ /**
30
+ * Supplies the Chinese public-holiday dates the peak rule excludes. The dates
31
+ * are not derivable from any rule (the State Council publishes them per year),
32
+ * so this repository ships no table of its own: the default calendar is empty,
33
+ * and a caller that has real dates passes them in. A date is compared as a
34
+ * Beijing-time `YYYY-MM-DD` string, which is the granularity the published
35
+ * rule needs (a holiday is off-peak for the whole day, so times never matter).
36
+ */
37
+ export interface PublicHolidayCalendar {
38
+ /** True when the Beijing-time calendar day `date` is a Chinese public holiday. */
39
+ isPublicHoliday(date: string): boolean;
40
+ }
41
+ /**
42
+ * The default calendar: the published Chinese public-holiday table, so a holiday
43
+ * weekday prices off-peak instead of over-reporting at the peak rate. A year the
44
+ * table does not cover holds no dates and falls back to peak on weekdays, which
45
+ * is the safe direction to be wrong in: the estimate reads high rather than low.
46
+ */
47
+ export declare const CN_PUBLIC_HOLIDAYS: PublicHolidayCalendar;
20
48
  /**
21
49
  * The DeepSeek billing period at `ms`, plus when it next flips. The clock is
22
50
  * Beijing time regardless of the host timezone (UTC+8 has no DST, so a fixed
23
51
  * shift is exact). `boundaryMs` is the instant the current period ends — the
24
52
  * window's close while peaking, the next window's open otherwise.
25
53
  */
26
- export declare function deepseekPeriodAt(ms: number): {
54
+ export declare function deepseekPeriodAt(ms: number, calendar?: PublicHolidayCalendar): {
27
55
  peak: boolean;
28
56
  boundaryMs: number;
29
57
  };
@@ -33,4 +61,4 @@ export declare function deepseekPeriodAt(ms: number): {
33
61
  * row; ids from other providers never reach this function (the service gates
34
62
  * by route family). Rounded to micro-CNY so the ledger stays readable.
35
63
  */
36
- export declare function deepseekModelSpend(model: string, totals: Readonly<UsageTokenTotals>, atMs: number): number;
64
+ export declare function deepseekModelSpend(model: string, totals: Readonly<UsageTokenTotals>, atMs: number, calendar?: PublicHolidayCalendar): number;