@linxin666/dsh-usage 0.4.3 → 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/README.i18n.yaml +2 -2
- package/README.md +21 -5
- package/README.zh.md +21 -5
- package/lib/client.js +466 -32
- package/lib/client.js.map +1 -1
- package/lib/index.js +434 -23
- package/lib/types/client/UsageSectionCard.d.ts +7 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/core/adapters.d.ts +28 -2
- package/lib/types/core/holidays.d.ts +25 -0
- package/lib/types/core/ledger.d.ts +8 -0
- package/lib/types/core/pricing.d.ts +34 -6
- package/lib/types/core/types.d.ts +26 -5
- package/lib/types/host/routes.d.ts +11 -0
- package/lib/types/host/usage-service.d.ts +13 -1
- package/package.json +14 -14
- package/src/client/UsageSectionCard.tsx +174 -26
- package/src/client/index.ts +10 -2
- package/src/client/locales.ts +16 -0
- package/src/client/usage.module.css +47 -0
- package/src/core/adapters.ts +34 -3
- package/src/core/holidays.ts +248 -0
- package/src/core/ledger.ts +14 -5
- package/src/core/pricing.ts +75 -15
- package/src/core/types.ts +28 -1
- package/src/host/routes.ts +31 -0
- package/src/host/usage-service.ts +145 -10
- package/src/index.ts +3 -3
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
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.
|
|
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.
|
|
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
|
|
733
|
-
*
|
|
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
|
-
|
|
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 =
|
|
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
|
|
847
|
-
if (
|
|
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 (
|
|
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)) {
|
|
@@ -1098,16 +1394,71 @@ async function writeJsonAtomic(path, value) {
|
|
|
1098
1394
|
throw error;
|
|
1099
1395
|
}
|
|
1100
1396
|
}
|
|
1397
|
+
function unwrapVolatiles(value) {
|
|
1398
|
+
if (value === null || value === void 0) return value;
|
|
1399
|
+
if (typeof value === "object") {
|
|
1400
|
+
if (typeof value.get === "function") return unwrapVolatiles(value.get());
|
|
1401
|
+
if (Array.isArray(value)) return value.map(unwrapVolatiles);
|
|
1402
|
+
const result = {};
|
|
1403
|
+
for (const [k, v] of Object.entries(value)) result[k] = unwrapVolatiles(v);
|
|
1404
|
+
return result;
|
|
1405
|
+
}
|
|
1406
|
+
return value;
|
|
1407
|
+
}
|
|
1408
|
+
function isPlainObject(value) {
|
|
1409
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1410
|
+
}
|
|
1411
|
+
function deepMerge(base, patch) {
|
|
1412
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch !== void 0 ? patch : base;
|
|
1413
|
+
const result = { ...base };
|
|
1414
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1415
|
+
if (value === void 0) continue;
|
|
1416
|
+
result[key] = isPlainObject(value) && isPlainObject(result[key]) ? deepMerge(result[key], value) : value;
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
1101
1420
|
/**
|
|
1102
1421
|
* Read a foreign settings namespace's resolved value (the llm adapter
|
|
1103
|
-
* profiles, the agent default model).
|
|
1104
|
-
*
|
|
1422
|
+
* profiles, the agent default model). Resolves across pre-0.1.7 settings.get(),
|
|
1423
|
+
* 0.1.7 SettingsForms.describe(), live loader entries, and configEditor.
|
|
1424
|
+
* Unregistered namespaces read as undefined; nothing here throws into the poll loop.
|
|
1105
1425
|
*/
|
|
1106
1426
|
function readNamespace(ctx, ns) {
|
|
1107
1427
|
try {
|
|
1108
1428
|
const settings = service(ctx, "settings");
|
|
1109
|
-
if (settings
|
|
1110
|
-
|
|
1429
|
+
if (settings !== void 0) {
|
|
1430
|
+
if (typeof settings.get === "function") {
|
|
1431
|
+
const value = settings.get(ns);
|
|
1432
|
+
if (value !== void 0) return unwrapVolatiles(value);
|
|
1433
|
+
}
|
|
1434
|
+
if (typeof settings.describe === "function") {
|
|
1435
|
+
const descriptors = settings.describe({ redactSecrets: false });
|
|
1436
|
+
if (Array.isArray(descriptors)) {
|
|
1437
|
+
const match = descriptors.find((d) => d.ns === ns || ns === "llm-pi-ai" && (d.ns === "llm" || isPlainObject(d.value?.providers)) || ns === "llm-deepseek" && (d.ns === "llm-deepseek" || d.ns === "llm-deepseek-api-key"));
|
|
1438
|
+
if (match !== void 0 && match.value !== void 0) return unwrapVolatiles(match.value);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const loader = ctx.root?.loader ?? ctx.loader;
|
|
1443
|
+
if (loader !== void 0 && typeof loader.entries === "function") for (const entry of loader.entries()) {
|
|
1444
|
+
const id = entry.options?.id;
|
|
1445
|
+
const name = entry.options?.name;
|
|
1446
|
+
if (id === ns || name === `@deepseek-ai/dsh-${ns}` || name?.endsWith(`/${ns}`) || ns === "llm-pi-ai" && (id === "llm" || name === "@deepseek-ai/dsh-llm-pi-ai") || ns === "llm-deepseek" && (id === "llm-deepseek-api-key" || name === "@deepseek-ai/dsh-llm-deepseek-api-key")) {
|
|
1447
|
+
if (entry.fiber?.config !== void 0) return unwrapVolatiles(entry.fiber.config);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
const editor = service(ctx, "configEditor");
|
|
1451
|
+
if (editor !== void 0 && typeof editor.configuration === "function") {
|
|
1452
|
+
const match = editor.configuration().find((r) => {
|
|
1453
|
+
const id = r.entry.options.id;
|
|
1454
|
+
const name = r.entry.options.name;
|
|
1455
|
+
return id === ns || name === `@deepseek-ai/dsh-${ns}` || name?.endsWith(`/${ns}`) || ns === "llm-pi-ai" && (id === "llm" || name === "@deepseek-ai/dsh-llm-pi-ai") || ns === "llm-deepseek" && (id === "llm-deepseek-api-key" || name === "@deepseek-ai/dsh-llm-deepseek-api-key");
|
|
1456
|
+
});
|
|
1457
|
+
if (match !== void 0) {
|
|
1458
|
+
if (match.entry.fiber?.config !== void 0) return unwrapVolatiles(match.entry.fiber.config);
|
|
1459
|
+
return unwrapVolatiles(deepMerge(match.inherited, match.override));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1111
1462
|
} catch {
|
|
1112
1463
|
return;
|
|
1113
1464
|
}
|
|
@@ -1188,8 +1539,8 @@ var UsageService = class {
|
|
|
1188
1539
|
displayName: route.displayName || adapter?.displayName || route.id,
|
|
1189
1540
|
credential: snapshot?.credential ?? "none",
|
|
1190
1541
|
supported: adapter !== void 0 && (adapter.balance !== void 0 || adapter.plan !== void 0),
|
|
1191
|
-
...adapter?.balance !== void 0 && balanceAppliesToRoute(adapter, this.
|
|
1192
|
-
...adapter?.balance !== void 0 && !balanceAppliesToRoute(adapter, this.
|
|
1542
|
+
...adapter?.balance !== void 0 && balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id)) ? { balanceSupported: true } : {},
|
|
1543
|
+
...adapter?.balance !== void 0 && !balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id)) ? { balanceSupported: false } : {},
|
|
1193
1544
|
...adapter?.plan !== void 0 ? { planSupported: true } : {},
|
|
1194
1545
|
...snapshot?.balance !== void 0 ? { balance: snapshot.balance } : {},
|
|
1195
1546
|
...snapshot?.plan !== void 0 ? { plan: snapshot.plan } : {},
|
|
@@ -1220,6 +1571,7 @@ var UsageService = class {
|
|
|
1220
1571
|
totals: range.totals,
|
|
1221
1572
|
providers: range.providers
|
|
1222
1573
|
},
|
|
1574
|
+
availableDays: allKeys,
|
|
1223
1575
|
all: {
|
|
1224
1576
|
from: allKeys[0] ?? todayKey,
|
|
1225
1577
|
to: allKeys[allKeys.length - 1] ?? todayKey,
|
|
@@ -1260,6 +1612,16 @@ var UsageService = class {
|
|
|
1260
1612
|
...today.calls > 0 && totalTokens(today) > 0 ? { today } : {}
|
|
1261
1613
|
};
|
|
1262
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
|
+
}
|
|
1263
1625
|
/** One local day aggregated per provider. */
|
|
1264
1626
|
daySummary(dateKey) {
|
|
1265
1627
|
const { totals, providers } = summarizeDays([this.ledger.days[dateKey] ?? {}]);
|
|
@@ -1518,7 +1880,7 @@ var UsageService = class {
|
|
|
1518
1880
|
async probeRoute(route, adapter) {
|
|
1519
1881
|
const credential = await this.resolveCredential(route.id);
|
|
1520
1882
|
const previous = this.snapshots.get(route.id);
|
|
1521
|
-
const balanceApplies = balanceAppliesToRoute(adapter, this.
|
|
1883
|
+
const balanceApplies = balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id));
|
|
1522
1884
|
if (credential.key === void 0) {
|
|
1523
1885
|
this.snapshots.set(route.id, {
|
|
1524
1886
|
provider: route.id,
|
|
@@ -1633,6 +1995,14 @@ var UsageService = class {
|
|
|
1633
1995
|
deepseekApiKeyEnv() {
|
|
1634
1996
|
return readNamespace(this.ctx, "llm-deepseek")?.apiKeyEnv ?? "DEEPSEEK_API_KEY";
|
|
1635
1997
|
}
|
|
1998
|
+
/** The DeepSeek official adapter's configured baseURL. */
|
|
1999
|
+
deepseekBaseUrl() {
|
|
2000
|
+
return readNamespace(this.ctx, "llm-deepseek")?.baseURL;
|
|
2001
|
+
}
|
|
2002
|
+
/** The effective base URL for a provider route (pi-ai or deepseek). */
|
|
2003
|
+
routeBaseUrl(provider) {
|
|
2004
|
+
return this.piAiProfile(provider)?.baseURL ?? (isDeepSeekProviderRoute(provider) ? this.deepseekBaseUrl() : void 0);
|
|
2005
|
+
}
|
|
1636
2006
|
};
|
|
1637
2007
|
//#endregion
|
|
1638
2008
|
//#region src/host/loopback.ts
|
|
@@ -1735,6 +2105,43 @@ function makeUsageOverviewRoute(ctx, service) {
|
|
|
1735
2105
|
};
|
|
1736
2106
|
}
|
|
1737
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
|
+
/**
|
|
1738
2145
|
* Manual refresh: forces one probe cycle now and answers with the fresh
|
|
1739
2146
|
* overview. Same trust fence as the overview route.
|
|
1740
2147
|
* @param ctx - host context; may expose remoteWebUiPairing.
|
|
@@ -1850,9 +2257,13 @@ const apply = mountOnce("@linxin666/dsh-usage", (ctx, config) => {
|
|
|
1850
2257
|
let disposeRoutes;
|
|
1851
2258
|
/** True while this activation has a start queued behind a predecessor's flush. */
|
|
1852
2259
|
let queued = false;
|
|
1853
|
-
/** Register the service's
|
|
2260
|
+
/** Register the service's routes; the returned disposer unregisters them. */
|
|
1854
2261
|
const mountRoutes = (next) => {
|
|
1855
|
-
const disposers = [
|
|
2262
|
+
const disposers = [
|
|
2263
|
+
makeUsageOverviewRoute(ctx, next),
|
|
2264
|
+
makeUsageDayRoute(ctx, next),
|
|
2265
|
+
makeUsageRefreshRoute(ctx, next)
|
|
2266
|
+
].map((route) => ctx.webServer.register(route));
|
|
1856
2267
|
return () => {
|
|
1857
2268
|
for (const dispose of disposers) try {
|
|
1858
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
|
|
105
|
-
*
|
|
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
|
/**
|