@kenz1117/dsh-ui-usage-billing 0.9.17 → 0.9.19

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
@@ -3,7 +3,7 @@ import { readFile, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
6
- import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
6
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
7
7
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
8
8
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
9
9
  import z from "@deepseek-ai/schemastery";
@@ -68,6 +68,7 @@ const PLAN_KNOWLEDGE = {
68
68
  "ark-token-plan": { type: "code" },
69
69
  "doubao-token-plan": { type: "code" },
70
70
  "minimax": { type: "code" },
71
+ "minimax-cn": { type: "code" },
71
72
  "minimax-token-plan": { type: "code" },
72
73
  "minimax-token-plan-cn": { type: "code" }
73
74
  };
@@ -1057,11 +1058,58 @@ function hourStamp(time) {
1057
1058
  const pad = (n) => String(n).padStart(2, "0");
1058
1059
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}`;
1059
1060
  }
1061
+ /**
1062
+ * 宿主进程的 IANA 时区名与 UTC 偏移,供面板标注「天按哪个时区切分」。
1063
+ * `getTimezoneOffset` 是 UTC 以西的分钟数,符号与日常写法相反,故取反。
1064
+ * @returns `{ name, offset }`,如 `{ name: "Asia/Shanghai", offset: "UTC+08:00" }`。
1065
+ */
1066
+ function hostTimeZone(now = /* @__PURE__ */ new Date()) {
1067
+ const minutes = -now.getTimezoneOffset();
1068
+ const sign = minutes < 0 ? "-" : "+";
1069
+ const abs = Math.abs(minutes);
1070
+ const hh = String(Math.floor(abs / 60)).padStart(2, "0");
1071
+ const mm = String(abs % 60).padStart(2, "0");
1072
+ let name;
1073
+ try {
1074
+ name = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
1075
+ } catch {
1076
+ name = "UTC";
1077
+ }
1078
+ return {
1079
+ name,
1080
+ offset: `UTC${sign}${hh}:${mm}`
1081
+ };
1082
+ }
1060
1083
  /** 工作区名:取 cwd 的末级目录名;无 cwd 时返回 {@link UNKNOWN_WORKSPACE_NAME}。 */
1061
1084
  function workspaceNameOf(cwd) {
1062
1085
  if (cwd === void 0 || cwd === "") return "—";
1063
1086
  return cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? "—";
1064
1087
  }
1088
+ /**
1089
+ * 账本迁移注册表。当前账本 schema(version 1)尚无字段变更需求,故为空表;
1090
+ * 机制已就绪,schema 变更时在此登记幂等迁移,见 {@link LedgerMigration}。
1091
+ */
1092
+ const LEDGER_MIGRATIONS = [];
1093
+ /**
1094
+ * 在加载边界对账本文档应用未执行的迁移,并记录已应用 id 供写回。
1095
+ * @param document - 从持久化读出的原始账本文档。
1096
+ * @param migrations - 待执行的迁移注册表;缺省用模块级 {@link LEDGER_MIGRATIONS}。
1097
+ * @returns 是否发生了需要重新落盘的修改。
1098
+ */
1099
+ function runLedgerMigrations(document, migrations = LEDGER_MIGRATIONS) {
1100
+ const applied = new Set(document.appliedMigrations ?? []);
1101
+ let changed = false;
1102
+ for (const migration of migrations) {
1103
+ if (applied.has(migration.id)) continue;
1104
+ if (migration.apply(document)) changed = true;
1105
+ applied.add(migration.id);
1106
+ }
1107
+ if (applied.size > 0 && (document.appliedMigrations === void 0 || document.appliedMigrations.length !== applied.size || document.appliedMigrations.some((id) => !applied.has(id)))) {
1108
+ document.appliedMigrations = [...applied];
1109
+ changed = true;
1110
+ }
1111
+ return changed;
1112
+ }
1065
1113
  /** Serialize Map/Set-heavy fold state into a JSON-safe ledger entry. */
1066
1114
  function serializeFold(fold) {
1067
1115
  return {
@@ -1168,6 +1216,11 @@ function turnState(turns, turn) {
1168
1216
  * @returns the per-session fold (cached by the incremental aggregator).
1169
1217
  */
1170
1218
  function foldSession(events, subscriptionProviders, officialProviderIds, routes = {}) {
1219
+ let seedBoundary = -1;
1220
+ for (const event of events) if (event.type === "session/end-seed" && typeof event.seq === "number" && Number.isFinite(event.seq)) seedBoundary = Math.max(seedBoundary, event.seq);
1221
+ if (seedBoundary >= 0) {
1222
+ if (!events.some((event) => typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq > seedBoundary)) seedBoundary = -1;
1223
+ }
1171
1224
  const fold = {
1172
1225
  total: emptyUsage(),
1173
1226
  byModel: /* @__PURE__ */ new Map(),
@@ -1194,6 +1247,7 @@ function foldSession(events, subscriptionProviders, officialProviderIds, routes
1194
1247
  const steps = /* @__PURE__ */ new Map();
1195
1248
  let lastOpenStepKey;
1196
1249
  for (const event of events) {
1250
+ if (seedBoundary >= 0 && typeof event.seq === "number" && Number.isFinite(event.seq) && event.seq < seedBoundary) continue;
1197
1251
  fold.lastActive = Math.max(fold.lastActive, event.time);
1198
1252
  if (event.type === "session/title") {
1199
1253
  const title = event.data.title;
@@ -1395,6 +1449,9 @@ function createUsageAggregator(persistence, options = {}) {
1395
1449
  ledgerLoaded = true;
1396
1450
  try {
1397
1451
  const stored = await options.ledger.load();
1452
+ if (stored !== null && typeof stored === "object" && stored.sessions !== void 0) {
1453
+ if (runLedgerMigrations(stored)) ledgerNeedsSave = true;
1454
+ }
1398
1455
  for (const entry of ledgerSessionsOf(stored)) ledger.set(entry.id, entry);
1399
1456
  } catch (error) {
1400
1457
  console.warn("[usage-billing] failed to load durable usage ledger; rebuilding from current sessions:", error);
@@ -1625,6 +1682,7 @@ function createUsageAggregator(persistence, options = {}) {
1625
1682
  version: 3,
1626
1683
  updatedAt: now,
1627
1684
  source: "session-logs",
1685
+ timezone: hostTimeZone(),
1628
1686
  total,
1629
1687
  byModel: toRecord(byModel),
1630
1688
  byDay: toRecord(byDay),
@@ -1739,7 +1797,7 @@ function createCooldownGate(options = {}) {
1739
1797
  * provider's key once and every surface reuses it.
1740
1798
  */
1741
1799
  /** Abort a balance fetch when the upstream hangs beyond this budget. */
1742
- const FETCH_TIMEOUT_MS$2 = 8e3;
1800
+ const FETCH_TIMEOUT_MS$3 = 8e3;
1743
1801
  /**
1744
1802
  * 每平台熔断门:单个 provider 连续可重试失败(网络波动 / 5xx / 429)达阈值后
1745
1803
  * 短路一段真实时间,避免 30 秒轮询在已不可用的上游上反复打满超时。
@@ -1767,7 +1825,7 @@ const XAI_CREDITS_URL = "https://api.x.ai/v1/billing/credits";
1767
1825
  /** 智谱 GLM(大模型国内域)官方余额接口(open.bigmodel.cn/api/paas/v4/balance)。 */
1768
1826
  const ZHIPU_BALANCE_URL = "https://open.bigmodel.cn/api/paas/v4/balance";
1769
1827
  /** 数字归一化:接口返回的余额是字符串(如 `"110.00"`),统一转 number。 */
1770
- function toNumber(value) {
1828
+ function toNumber$1(value) {
1771
1829
  if (typeof value === "number" && Number.isFinite(value)) return value;
1772
1830
  if (typeof value === "string") {
1773
1831
  const parsed = Number(value);
@@ -1800,7 +1858,7 @@ async function queryBearerBalance(ctx, url, apiKeyEnv, provider, displayName, pa
1800
1858
  };
1801
1859
  const doRequest = async () => {
1802
1860
  const controller = new AbortController();
1803
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$2);
1861
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
1804
1862
  try {
1805
1863
  const response = await fetch(url, {
1806
1864
  headers: {
@@ -1860,9 +1918,9 @@ function queryDeepSeek(ctx, apiKeyEnv) {
1860
1918
  const doc = data;
1861
1919
  const info = (Array.isArray(doc.balance_infos) ? doc.balance_infos : [])[0];
1862
1920
  const currency = typeof info?.currency === "string" ? info.currency : void 0;
1863
- const totalBalance = toNumber(info?.total_balance);
1864
- const grantedBalance = toNumber(info?.granted_balance);
1865
- const toppedUpBalance = toNumber(info?.topped_up_balance);
1921
+ const totalBalance = toNumber$1(info?.total_balance);
1922
+ const grantedBalance = toNumber$1(info?.granted_balance);
1923
+ const toppedUpBalance = toNumber$1(info?.topped_up_balance);
1866
1924
  const isAvailable = typeof doc.is_available === "boolean" ? doc.is_available : void 0;
1867
1925
  return {
1868
1926
  provider: "deepseek",
@@ -1884,9 +1942,9 @@ function queryDeepSeek(ctx, apiKeyEnv) {
1884
1942
  function queryMoonshot(ctx, apiKeyEnv) {
1885
1943
  return queryBearerBalance(ctx, MOONSHOT_BALANCE_URL, apiKeyEnv, "月之暗面", "月之暗面", (data) => {
1886
1944
  const doc = data;
1887
- const totalBalance = toNumber(doc.data?.available_balance);
1888
- const grantedBalance = toNumber(doc.data?.voucher_balance);
1889
- const toppedUpBalance = toNumber(doc.data?.cash_balance);
1945
+ const totalBalance = toNumber$1(doc.data?.available_balance);
1946
+ const grantedBalance = toNumber$1(doc.data?.voucher_balance);
1947
+ const toppedUpBalance = toNumber$1(doc.data?.cash_balance);
1890
1948
  return {
1891
1949
  provider: "月之暗面",
1892
1950
  displayName: "月之暗面",
@@ -1906,9 +1964,9 @@ function queryMoonshot(ctx, apiKeyEnv) {
1906
1964
  function queryStepFun(ctx, apiKeyEnv) {
1907
1965
  return queryBearerBalance(ctx, STEPFUN_BALANCE_URL, apiKeyEnv, "阶跃星辰", "阶跃星辰", (data) => {
1908
1966
  const doc = data;
1909
- const totalBalance = toNumber(doc.balance);
1910
- const toppedUpBalance = toNumber(doc.total_cash_balance);
1911
- const grantedBalance = toNumber(doc.total_voucher_balance);
1967
+ const totalBalance = toNumber$1(doc.balance);
1968
+ const toppedUpBalance = toNumber$1(doc.total_cash_balance);
1969
+ const grantedBalance = toNumber$1(doc.total_voucher_balance);
1912
1970
  return {
1913
1971
  provider: "阶跃星辰",
1914
1972
  displayName: "阶跃星辰",
@@ -1929,7 +1987,7 @@ function querySiliconFlow(ctx, apiKeyEnv) {
1929
1987
  return queryBearerBalance(ctx, SILICONFLOW_BALANCE_URL, apiKeyEnv, "硅基流动", "硅基流动", (data) => {
1930
1988
  const doc = data;
1931
1989
  const inner = doc.data ?? doc;
1932
- const totalBalance = toNumber(inner.balance ?? inner.balance_cny);
1990
+ const totalBalance = toNumber$1(inner.balance ?? inner.balance_cny);
1933
1991
  return {
1934
1992
  provider: "硅基流动",
1935
1993
  displayName: "硅基流动",
@@ -1946,7 +2004,7 @@ function querySiliconFlow(ctx, apiKeyEnv) {
1946
2004
  */
1947
2005
  function queryXai(ctx, apiKeyEnv) {
1948
2006
  return queryBearerBalance(ctx, XAI_CREDITS_URL, apiKeyEnv, "xAI", "xAI", (data) => {
1949
- const cents = toNumber(data.total?.val);
2007
+ const cents = toNumber$1(data.total?.val);
1950
2008
  const totalBalance = cents === void 0 ? void 0 : Math.abs(cents) / 100;
1951
2009
  return {
1952
2010
  provider: "xAI",
@@ -1968,7 +2026,7 @@ function queryXai(ctx, apiKeyEnv) {
1968
2026
  function queryZhipu(ctx, apiKeyEnv) {
1969
2027
  return queryBearerBalance(ctx, ZHIPU_BALANCE_URL, apiKeyEnv, "智谱 AI", "智谱 AI", (data) => {
1970
2028
  const doc = data;
1971
- const totalBalance = toNumber(doc.balance?.available) ?? toNumber(doc.balance?.total);
2029
+ const totalBalance = toNumber$1(doc.balance?.available) ?? toNumber$1(doc.balance?.total);
1972
2030
  return {
1973
2031
  provider: "智谱 AI",
1974
2032
  displayName: "智谱 AI",
@@ -2055,13 +2113,13 @@ function evalExtract(rule, data) {
2055
2113
  if (paths.length === 0) return void 0;
2056
2114
  let total;
2057
2115
  for (const path of paths) {
2058
- const value = toNumber(getPath(data, path));
2116
+ const value = toNumber$1(getPath(data, path));
2059
2117
  if (value === void 0) return void 0;
2060
2118
  total = total === void 0 ? value : rule.op === "add" ? total + value : total - value;
2061
2119
  }
2062
2120
  return total;
2063
2121
  }
2064
- const base = typeof rule.path === "string" ? toNumber(getPath(data, rule.path)) : void 0;
2122
+ const base = typeof rule.path === "string" ? toNumber$1(getPath(data, rule.path)) : void 0;
2065
2123
  if (base === void 0) return void 0;
2066
2124
  if (rule.op === "divide") {
2067
2125
  const by = rule.by;
@@ -2115,7 +2173,7 @@ async function queryCustomBalances(ctx, configs) {
2115
2173
  };
2116
2174
  const doRequest = async () => {
2117
2175
  const controller = new AbortController();
2118
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$2);
2176
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
2119
2177
  try {
2120
2178
  const response = await fetch(config.url, {
2121
2179
  method: config.method ?? "GET",
@@ -2170,6 +2228,383 @@ async function queryCustomBalances(ctx, configs) {
2170
2228
  }));
2171
2229
  }
2172
2230
  //#endregion
2231
+ //#region lib/types/declarative.js
2232
+ /**
2233
+ * 声明端点(declarative endpoints):让用户为内置表没有的供应商自声明余额/额度接口,
2234
+ * 不用等插件发版。用户提供「数字在哪儿」的取值路径,而非「怎么取」——没有表达式、
2235
+ * 没有任何东西被求值,`fields` / `windows` 里写的只是响应 JSON 的点路径,唯一操作是
2236
+ * 逐层下钻。
2237
+ *
2238
+ * ## 为什么把安全边界写在代码里而不是建议里
2239
+ *
2240
+ * 这个功能让**配置文件**决定一个携带用户 API key 的请求发往哪里。风险全在此,靠
2241
+ * 「提醒用户小心」一点都没用:
2242
+ *
2243
+ * 1. `origin` 只是查找键:请求 URL 由**匹配到的 provider 的 origin** 构造,绝不由
2244
+ * 声明的 origin 自己决定。匹配不到任何已配置的 provider 就不发请求。
2245
+ * 2. `path` 必须单斜杠绝对路径:`//evil.example/x` 是协议相对 URL,`new URL()` 会
2246
+ * 把它解析到别的主机;构造后还会再校验一次 origin。
2247
+ * 3. 只发 GET,无请求体,无自定义 method / headers。
2248
+ * 4. 凭据仍从匹配 provider 自己的 `apiKeyEnv` 取,经同一凭据 seam 解析;声明不能
2249
+ * 指定任何凭据。
2250
+ * 5. 跨源重定向直接失败,不跟随——那是绕过第 1 条最省事的办法。
2251
+ * 6. 响应体有大小上限与共享超时,坏 / 恶意端点拖不住面板。
2252
+ * 7. 声明不能覆盖内置读法:只在内置表答不上来时它才轮到。
2253
+ *
2254
+ * 这类行会标 `declared`,因为数字来自用户自己写的路径——取错是配置问题,界面要让
2255
+ * 这一点看得出来;全部字段都没取到时给 `reason`,而不是留一张和「上游没返回」无从
2256
+ * 区分的空卡。
2257
+ */
2258
+ /** Abort a declared fetch when the upstream hangs beyond this budget. */
2259
+ const FETCH_TIMEOUT_MS$2 = 8e3;
2260
+ /** 响应体上限(字节):坏 / 恶意端点不能拖住面板或耗尽内存。 */
2261
+ const MAX_BODY_BYTES = 1e6;
2262
+ /** 声明端点查询的熔断门:按(匹配 provider origin + path)独立熔断,各端点互不干扰。 */
2263
+ const declaredGate = createCooldownGate({
2264
+ failures: 3,
2265
+ cooldownMs: 6e4
2266
+ });
2267
+ /** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
2268
+ const FORBIDDEN_SEGMENTS = new Set([
2269
+ "__proto__",
2270
+ "constructor",
2271
+ "prototype"
2272
+ ]);
2273
+ /**
2274
+ * 归一化 baseURL 为可比的 origin:`scheme://host[:port]`,剥掉 scheme 的默认端口,
2275
+ * host 转小写,忽略路径。无法解析返回 undefined。
2276
+ * @param baseURL - provider 的端点地址,或声明里的 origin。
2277
+ */
2278
+ function normalizeDeclaredOrigin(baseURL) {
2279
+ if (typeof baseURL !== "string" || baseURL.trim() === "") return void 0;
2280
+ let url;
2281
+ try {
2282
+ url = new URL(baseURL.trim());
2283
+ } catch {
2284
+ return;
2285
+ }
2286
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
2287
+ const defaultPort = url.protocol === "https:" ? "443" : "80";
2288
+ const port = url.port === "" || url.port === defaultPort ? "" : `:${url.port}`;
2289
+ return `${url.protocol}//${url.hostname.toLowerCase()}${port}`;
2290
+ }
2291
+ /**
2292
+ * 沿点路径走进已解析的响应体。
2293
+ * 任何失败都是同一个答案 `undefined`——路径不匹配是这份响应没有那个字段,是卡片
2294
+ * 本来就渲染得了的事实,不是错误,而且不该让已解析成功的字段被牵连。
2295
+ * @param body - 已解析的响应 JSON。
2296
+ * @param path - 点路径(如 `data.balance`)。
2297
+ * @returns 路径处的值,或 undefined(路径缺失 / 中途不是对象 / 命中原型链)。
2298
+ */
2299
+ function readDeclaredPath(body, path) {
2300
+ if (typeof path !== "string" || path === "") return void 0;
2301
+ let cursor = body;
2302
+ for (const segment of path.split(".")) {
2303
+ if (cursor === null || typeof cursor !== "object") return void 0;
2304
+ if (FORBIDDEN_SEGMENTS.has(segment)) return void 0;
2305
+ cursor = cursor[segment];
2306
+ }
2307
+ return cursor;
2308
+ }
2309
+ /** 数字常以字符串抵达(如 `"110.00"`),统一转 number;非有限数返回 undefined。 */
2310
+ function toNumber(value) {
2311
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2312
+ if (typeof value === "string" && value.trim() !== "") {
2313
+ const parsed = Number(value);
2314
+ if (Number.isFinite(parsed)) return parsed;
2315
+ }
2316
+ }
2317
+ /** 百分比收敛到 0–100。 */
2318
+ function clampPercent$2(value) {
2319
+ return Math.round(Math.max(0, Math.min(100, value)) * 10) / 10;
2320
+ }
2321
+ /** 解出一个窗口的已用百分比:usedPercent 路径 > usedRatio 路径 > used/limit 组合。 */
2322
+ function windowUsedPercent(body, input) {
2323
+ const ratio = typeof input.usedPercent === "string" ? toNumber(readDeclaredPath(body, input.usedPercent)) : void 0;
2324
+ if (ratio !== void 0) return clampPercent$2(ratio);
2325
+ const usageRatio = typeof input.usedRatio === "string" ? toNumber(readDeclaredPath(body, input.usedRatio)) : void 0;
2326
+ if (usageRatio !== void 0) return clampPercent$2(usageRatio * 100);
2327
+ const used = typeof input.used === "string" ? toNumber(readDeclaredPath(body, input.used)) : void 0;
2328
+ const total = typeof input.limit === "string" ? toNumber(readDeclaredPath(body, input.limit)) : void 0;
2329
+ if (used !== void 0 && total !== void 0 && total > 0) return clampPercent$2(used / total * 100);
2330
+ return null;
2331
+ }
2332
+ /**
2333
+ * 解出一个窗口的重置时刻:resetsAt 路径 > resetInSeconds 路径(now + 秒)推算。
2334
+ * @param body - 已解析的响应 JSON。
2335
+ * @param input - 窗口配置。
2336
+ * @param nowMs - 当前时刻(epoch ms)。
2337
+ * @returns ISO 时刻字符串,或 undefined。
2338
+ */
2339
+ function windowResetsAt(body, input, nowMs) {
2340
+ if (typeof input.resetsAt === "string") {
2341
+ const value = readDeclaredPath(body, input.resetsAt);
2342
+ if (typeof value === "string" && value !== "") return value;
2343
+ }
2344
+ if (typeof input.resetInSeconds === "string") {
2345
+ const seconds = toNumber(readDeclaredPath(body, input.resetInSeconds));
2346
+ if (seconds !== void 0 && Number.isFinite(seconds) && seconds >= 0) return new Date(nowMs + seconds * 1e3).toISOString();
2347
+ }
2348
+ }
2349
+ /** 把一条声明窗口配置解析成面板可渲染的滚动窗口;取不到已用百分比则不产出。 */
2350
+ function resolveDeclaredWindow(body, input, nowMs) {
2351
+ const used = windowUsedPercent(body, input);
2352
+ if (used === null) return null;
2353
+ const remaining = clampPercent$2(Math.max(0, 100 - used));
2354
+ const resetsAt = windowResetsAt(body, input, nowMs);
2355
+ const remainingAmount = typeof input.used === "string" && typeof input.limit === "string" ? (() => {
2356
+ const u = toNumber(readDeclaredPath(body, input.used));
2357
+ const t = toNumber(readDeclaredPath(body, input.limit));
2358
+ return u !== void 0 && t !== void 0 ? t - u : void 0;
2359
+ })() : void 0;
2360
+ return {
2361
+ kind: input.kind ?? "weekly",
2362
+ usedPercent: used,
2363
+ remainingPercent: remaining,
2364
+ ...resetsAt === void 0 ? {} : { resetsAt },
2365
+ ...remainingAmount === void 0 ? {} : { remaining: remainingAmount }
2366
+ };
2367
+ }
2368
+ /**
2369
+ * 校验并编译一条声明端点:能否安全地给出一条查询方案。
2370
+ * 返回方案的 `read` 用匹配到 provider 的 origin 构造 URL;路径不合规(非单斜杠绝对)
2371
+ * 返回 undefined,该账户保持「unsupported」,这是真话,也是它没声明之前的样子。
2372
+ */
2373
+ function compileDeclaredEndpoint(decl) {
2374
+ const path = decl?.path;
2375
+ if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//")) return void 0;
2376
+ return path;
2377
+ }
2378
+ /**
2379
+ * 查询一组声明端点的余额/额度。每个声明独立成败,互不影响。
2380
+ * @param ctx - host context carrying the credentials seam.
2381
+ * @param providers - llm-pi-ai providers dict(`<route> → { baseURL?, apiKeyEnv? }`)。
2382
+ * @param declarations - 声明端点配置列表。
2383
+ * @returns 每个匹配到 provider 的声明一行结果;无匹配的声明不上报(不产生请求)。
2384
+ */
2385
+ async function queryDeclaredEndpoints(ctx, providers, declarations) {
2386
+ const byOrigin = /* @__PURE__ */ new Map();
2387
+ for (const entry of Object.values(providers)) {
2388
+ const origin = entry.baseURL === void 0 ? void 0 : normalizeDeclaredOrigin(entry.baseURL);
2389
+ if (origin !== void 0 && !byOrigin.has(origin)) byOrigin.set(origin, entry);
2390
+ }
2391
+ return await Promise.all(declarations.map(async (decl) => {
2392
+ const displayName = typeof decl.displayName === "string" && decl.displayName !== "" ? decl.displayName : "已声明";
2393
+ const provider = `declared:${displayName}`;
2394
+ const requestPath = compileDeclaredEndpoint(decl);
2395
+ if (requestPath === void 0) return {
2396
+ provider,
2397
+ displayName,
2398
+ declared: true,
2399
+ error: "invalid",
2400
+ reason: "path 必须单斜杠绝对路径"
2401
+ };
2402
+ const declOrigin = normalizeDeclaredOrigin(decl.origin);
2403
+ const matched = declOrigin === void 0 ? void 0 : byOrigin.get(declOrigin);
2404
+ if (matched === void 0) return {
2405
+ provider,
2406
+ displayName,
2407
+ declared: true,
2408
+ error: "unconfigured",
2409
+ reason: "未匹配到同源 provider(需先在 llm-pi-ai 配好 baseURL)"
2410
+ };
2411
+ const gateKey = `${declOrigin}${requestPath}`;
2412
+ if (!declaredGate.check(gateKey)) return {
2413
+ provider,
2414
+ displayName,
2415
+ declared: true,
2416
+ error: "unreachable"
2417
+ };
2418
+ const hit = await ctx.credentials.resolve(credentialRef(matched.apiKeyEnv ?? ""));
2419
+ if (hit === void 0 || hit.value === "") return {
2420
+ provider,
2421
+ displayName,
2422
+ declared: true,
2423
+ error: "unconfigured"
2424
+ };
2425
+ const url = new URL(requestPath, declOrigin);
2426
+ if (url.origin !== declOrigin) return {
2427
+ provider,
2428
+ displayName,
2429
+ declared: true,
2430
+ error: "invalid",
2431
+ reason: "跨源路径被拒绝"
2432
+ };
2433
+ try {
2434
+ const body = await fetchDeclaredBody(url, decl.raw === true, hit.value);
2435
+ const fields = decl.fields ?? {};
2436
+ const total = typeof fields.total === "string" ? toNumber(readDeclaredPath(body, fields.total)) : void 0;
2437
+ const granted = typeof fields.granted === "string" ? toNumber(readDeclaredPath(body, fields.granted)) : void 0;
2438
+ const used = typeof fields.used === "string" ? toNumber(readDeclaredPath(body, fields.used)) : void 0;
2439
+ const currency = typeof fields.currency === "string" ? readDeclaredPath(body, fields.currency) : void 0;
2440
+ const plan = typeof fields.plan === "string" ? readDeclaredPath(body, fields.plan) : void 0;
2441
+ const windows = (decl.windows ?? []).map((w) => resolveDeclaredWindow(body, w, Date.now())).filter((w) => w !== null);
2442
+ declaredGate.success(gateKey);
2443
+ const nothingResolved = total === void 0 && granted === void 0 && used === void 0 && windows.length === 0;
2444
+ return {
2445
+ provider,
2446
+ displayName,
2447
+ declared: true,
2448
+ ...total !== void 0 ? { totalBalance: total } : {},
2449
+ ...granted !== void 0 ? { grantedBalance: granted } : {},
2450
+ ...typeof currency === "string" && currency !== "" ? { currency } : {},
2451
+ ...typeof plan === "string" && plan !== "" ? { plan } : {},
2452
+ ...windows.length === 0 ? {} : { windows },
2453
+ ...nothingResolved ? {
2454
+ error: "invalid",
2455
+ reason: "声明路径未命中任何字段"
2456
+ } : {}
2457
+ };
2458
+ } catch {
2459
+ declaredGate.fail(gateKey);
2460
+ return {
2461
+ provider,
2462
+ displayName,
2463
+ declared: true,
2464
+ error: "unreachable"
2465
+ };
2466
+ }
2467
+ }));
2468
+ }
2469
+ /**
2470
+ * 带边界约束的 GET 请求:只 GET、跨源重定向直接失败、响应体上限、共享超时。
2471
+ * 401/403 → unauthorized(调用方判定);其余非 2xx / 超限抛错(走 unreachable)。
2472
+ */
2473
+ async function fetchDeclaredBody(url, raw, apiKey) {
2474
+ const doRequest = async () => {
2475
+ const controller = new AbortController();
2476
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$2);
2477
+ try {
2478
+ const response = await fetch(url.toString(), {
2479
+ method: "GET",
2480
+ headers: {
2481
+ accept: "application/json",
2482
+ ...raw ? {} : { authorization: `Bearer ${apiKey}` }
2483
+ },
2484
+ signal: controller.signal,
2485
+ redirect: "manual"
2486
+ });
2487
+ if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) throw new HttpStatusError("redirect", 302);
2488
+ if (response.status === 401 || response.status === 403) throw new HttpStatusError("unauthorized", response.status);
2489
+ if (!response.ok) throw new HttpStatusError("http-error", response.status);
2490
+ const text = await response.text();
2491
+ if (text.length > MAX_BODY_BYTES) throw new HttpStatusError("body-too-large", 413);
2492
+ return JSON.parse(text);
2493
+ } finally {
2494
+ clearTimeout(timer);
2495
+ }
2496
+ };
2497
+ return await withRetry(doRequest, {
2498
+ retries: 1,
2499
+ baseDelayMs: 250,
2500
+ maxDelayMs: 2e3
2501
+ });
2502
+ }
2503
+ /** HTTP 状态错误:携带 statusCode,供调用方区分 unauthorized / 其它。 */
2504
+ var HttpStatusError = class extends Error {
2505
+ statusCode;
2506
+ kind;
2507
+ constructor(kind, statusCode) {
2508
+ super(`${kind}:${String(statusCode)}`);
2509
+ this.kind = kind;
2510
+ this.statusCode = statusCode;
2511
+ }
2512
+ };
2513
+ //#endregion
2514
+ //#region lib/types/reconcile.js
2515
+ /**
2516
+ * 余额差交叉校验(reconcile):用官方账户余额当日变动反推消费,与本地账本
2517
+ * 的当日官方费用比对。若二者偏差超阈值,提示用户核对价格表或近期账单——
2518
+ * 这是让计费数据可信的兜底机制,也是对账的稳定锚点。
2519
+ *
2520
+ * 只对官方直连渠道(DeepSeek 官方)对账:订阅 / Coding Plan / 第三方中转的
2521
+ * 消费不动官方余额,强行用余额差替代今日费用会把订阅用户全天的消费归零。
2522
+ * 充值、授信、币种变化都会让旧基准失去可比性,此时重置基准而非告警。
2523
+ *
2524
+ * 纯函数:输入上一基准与本次余额查询结果,输出新基准与对账事件,可独立单测。
2525
+ */
2526
+ /** 余额差对账阈值下限(元):消费低于该值时不值得提示,避免小金额波动刷屏。 */
2527
+ const MIN_DRIFT_ABS = .3;
2528
+ /** 余额差相对容忍度:偏差超过较大一方的该比例才判漂移。 */
2529
+ const DRIFT_RELATIVE = .15;
2530
+ /**
2531
+ * 归一化一个余额快照中的数值;非有限/缺失返回 0(对账只用其差值,0 安全)。
2532
+ * @param value - 待归一化的数值。
2533
+ */
2534
+ function num(value) {
2535
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2536
+ }
2537
+ /**
2538
+ * 由本次余额查询与上一基准推出一份新的基准快照(用于下一次对账)。
2539
+ * @param ref - 上一基准片段(date / currency / at)。
2540
+ * @param balance - 本次余额查询结果。
2541
+ * @param nowMs - 当前时刻。
2542
+ */
2543
+ function snapOf(ref, balance, nowMs) {
2544
+ return {
2545
+ date: ref.date,
2546
+ total: num(balance.totalBalance),
2547
+ granted: num(balance.grantedBalance),
2548
+ topped: num(balance.toppedUpBalance),
2549
+ currency: typeof balance.currency === "string" ? balance.currency : ref.currency,
2550
+ at: nowMs
2551
+ };
2552
+ }
2553
+ /**
2554
+ * 对账:用官方余额当日变动反推消费,与本地账本当日官方费用比对。
2555
+ * @param prevRef - 上一基准(可为 null,表示首次/基准缺失)。
2556
+ * @param balance - 本次余额查询结果。
2557
+ * @param todayOfficialCost - 本地账本当日的官方渠道费用(CNY)。
2558
+ * @param dayKey - 本地日期键(YYYY-MM-DD)。
2559
+ * @param nowMs - 当前时刻(epoch ms)。
2560
+ * @returns 新基准与对账事件;余额不可用(无 totalBalance)时返回 `{ ref: prevRef, event: null }`。
2561
+ */
2562
+ function reconcileBalanceDelta(prevRef, balance, todayOfficialCost, dayKey, nowMs) {
2563
+ const total = balance.totalBalance;
2564
+ if (typeof total !== "number" || !Number.isFinite(total)) return {
2565
+ ref: prevRef,
2566
+ event: null
2567
+ };
2568
+ const snap = snapOf({
2569
+ date: dayKey,
2570
+ currency: prevRef?.currency ?? ""
2571
+ }, balance, nowMs);
2572
+ if (prevRef === null || prevRef.date !== dayKey) return {
2573
+ ref: snap,
2574
+ event: { kind: "baseline" }
2575
+ };
2576
+ if (prevRef.currency !== snap.currency) return {
2577
+ ref: snap,
2578
+ event: { kind: "structure-reset" }
2579
+ };
2580
+ if (snap.granted > prevRef.granted + .009 || snap.topped > prevRef.topped + .009) return {
2581
+ ref: snap,
2582
+ event: { kind: "structure-reset" }
2583
+ };
2584
+ const spent = prevRef.total - snap.total;
2585
+ if (spent <= .009) return {
2586
+ ref: prevRef,
2587
+ event: { kind: "flat" }
2588
+ };
2589
+ const cost = num(todayOfficialCost);
2590
+ if (Math.abs(spent - cost) > Math.max(MIN_DRIFT_ABS, DRIFT_RELATIVE * Math.max(spent, cost))) return {
2591
+ ref: prevRef,
2592
+ event: {
2593
+ kind: "drift",
2594
+ spent,
2595
+ todayOfficialCost: cost
2596
+ }
2597
+ };
2598
+ return {
2599
+ ref: prevRef,
2600
+ event: {
2601
+ kind: "ok",
2602
+ spent,
2603
+ todayOfficialCost: cost
2604
+ }
2605
+ };
2606
+ }
2607
+ //#endregion
2173
2608
  //#region lib/types/pricing-fetch.js
2174
2609
  /**
2175
2610
  * One-shot live pricing refresh for the billing dashboard.
@@ -2485,10 +2920,11 @@ const SUBSCRIPTION_DISPLAY_NAMES = {
2485
2920
  "minimax": "MiniMax Coding Plan",
2486
2921
  "minimax-token-plan": "MiniMax Token Plan",
2487
2922
  "minimax-token-plan-cn": "MiniMax Token Plan(国内)",
2923
+ "minimax-cn": "MiniMax Token Plan(国内)",
2488
2924
  "openrouter": "OpenRouter"
2489
2925
  };
2490
2926
  /** 订阅类 provider id 判定:带 coding / agent-plan / token-plan 后缀,或已知订阅通道。 */
2491
- const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-token-plan|minimax-token-plan-cn|openrouter)", "i");
2927
+ const SUBSCRIPTION_ID_RE = /* @__PURE__ */ new RegExp("(?:^|-)(?:coding|agent[-_]?plan|token[-_]?plan)(?:$|-|_)|^(?:opencode|opencode-go|kimi-coding|zai-coding|minimax|minimax-cn|minimax-token-plan|minimax-token-plan-cn|openrouter)", "i");
2492
2928
  /** 是否是订阅类 provider id(如 kimi-coding、xiaomi-token-plan-cn)。 */
2493
2929
  function isSubscriptionProviderId(providerId) {
2494
2930
  if (SUBSCRIPTION_ID_RE.test(providerId)) return true;
@@ -2501,6 +2937,7 @@ const SUBSCRIPTION_ADAPTERS = {
2501
2937
  "opencode": { collect: collectOpenCodeGo },
2502
2938
  "opencode-go": { collect: collectOpenCodeGo },
2503
2939
  "minimax": { collect: collectMiniMax },
2940
+ "minimax-cn": { collect: collectMiniMax },
2504
2941
  "minimax-token-plan": { collect: collectMiniMax },
2505
2942
  "minimax-token-plan-cn": { collect: collectMiniMax },
2506
2943
  "openrouter": { collect: collectOpenRouter }
@@ -2813,7 +3250,8 @@ async function collectOpenCodeGo(keys, config, timeoutMs) {
2813
3250
  provider: config.provider,
2814
3251
  displayName: "OpenCode Go",
2815
3252
  status: "not-configured",
2816
- windows: []
3253
+ windows: [],
3254
+ hint: "未配置 key;可在 llm-pi-ai 里给 opencode(opencode-go) 配 apiKeyEnv,或让本机 OpenCode 凭据(~/.local/share/opencode/auth.json)可用"
2817
3255
  };
2818
3256
  try {
2819
3257
  const windows = parseOpenCodeGoApi(await requestJson(`${base}/zen/go/v1/usage`, { headers: {
@@ -2885,7 +3323,7 @@ function parseMiniMaxRemains(body) {
2885
3323
  */
2886
3324
  function resolveMiniMaxBaseUrl(config) {
2887
3325
  if (typeof config.baseUrl === "string" && config.baseUrl.trim() !== "") return config.baseUrl;
2888
- return config.provider === "minimax-token-plan-cn" ? "https://api.minimaxi.com" : "https://www.minimaxi.com";
3326
+ return config.provider === "minimax-cn" || config.provider === "minimax-token-plan-cn" ? "https://api.minimaxi.com" : "https://www.minimaxi.com";
2889
3327
  }
2890
3328
  /** Display name for a MiniMax quota row, aligned with the display-name map. */
2891
3329
  function minmaxDisplayName(provider) {
@@ -2951,7 +3389,8 @@ async function collectOpenRouter(keys, config, timeoutMs) {
2951
3389
  provider: config.provider,
2952
3390
  displayName: "OpenRouter",
2953
3391
  status: "not-configured",
2954
- windows: []
3392
+ windows: [],
3393
+ hint: "未配置 key;OpenRouter 的额度接口只认 Management Key,用推理 key 会 401"
2955
3394
  };
2956
3395
  try {
2957
3396
  const windows = parseOpenRouterCredits(await requestJson(`${base}/api/v1/credits`, { headers: {
@@ -2965,11 +3404,13 @@ async function collectOpenRouter(keys, config, timeoutMs) {
2965
3404
  windows
2966
3405
  };
2967
3406
  } catch (error) {
3407
+ const status = statusOf$1(error);
2968
3408
  return {
2969
3409
  provider: config.provider,
2970
3410
  displayName: "OpenRouter",
2971
- status: statusOf$1(error),
2972
- windows: []
3411
+ status,
3412
+ windows: [],
3413
+ ...status === "unauthorized" ? { hint: "OpenRouter 额度接口只认 Management Key;你配的 key 返回了 401,请换成管理密钥(在 openrouter.ai/settings/keys 创建)" } : {}
2973
3414
  };
2974
3415
  }
2975
3416
  }
@@ -3156,6 +3597,32 @@ function originOf(baseURL) {
3156
3597
  return baseURL;
3157
3598
  }
3158
3599
  }
3600
+ /**
3601
+ * 已知官方(非中转站)端点 host 判定:这些域是厂商自己的官方 API,卖的是
3602
+ * 官方按量余额而非中转站按 key 的额度,探测其 `/v1/usage`、`/api/status`
3603
+ * 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
3604
+ * 「未识别」。中转站面板只列真正的第三方中转程序。
3605
+ */
3606
+ const OFFICIAL_HOSTS = new Set([
3607
+ "api.deepseek.com",
3608
+ "api.openai.com",
3609
+ "open.bigmodel.cn",
3610
+ "api.moonshot.cn",
3611
+ "api.siliconflow.cn",
3612
+ "api.stepfun.com",
3613
+ "api.x.ai",
3614
+ "api.anthropic.com",
3615
+ "generativelanguage.googleapis.com"
3616
+ ]);
3617
+ /**
3618
+ * 判断一个 baseURL 是否指向已知官方端点(而非第三方中转站)。
3619
+ * @param baseURL - llm-pi-ai provider 路由的端点地址。
3620
+ * @returns 官方端点返回 true(中转站面板应排除)。
3621
+ */
3622
+ function isOfficialBaseUrl(baseURL) {
3623
+ const host = originOf(baseURL).replace(/^https?:\/\//, "");
3624
+ return host !== baseURL && OFFICIAL_HOSTS.has(host);
3625
+ }
3159
3626
  /** 构造端点 URL:`/v1/usage` 与 `/api/status` 都以 baseURL 为宿主解析。 */
3160
3627
  function endpointOf(baseURL, path) {
3161
3628
  return new URL(path, baseURL).toString();
@@ -3382,17 +3849,19 @@ function createFileUsageLedgerStore(ledgerPath) {
3382
3849
  } catch {}
3383
3850
  },
3384
3851
  async save(document) {
3385
- try {
3386
- const existing = await readFile(ledgerPath, "utf8");
3387
- JSON.parse(existing);
3388
- await writeFileAtomic(`${ledgerPath}.bak`, existing, {
3852
+ await withFileLock(ledgerPath, async () => {
3853
+ try {
3854
+ const existing = await readFile(ledgerPath, "utf8");
3855
+ JSON.parse(existing);
3856
+ await writeFileAtomic(`${ledgerPath}.bak`, existing, {
3857
+ mode: 384,
3858
+ dirMode: 448
3859
+ });
3860
+ } catch {}
3861
+ await writeFileAtomic(ledgerPath, JSON.stringify(document), {
3389
3862
  mode: 384,
3390
3863
  dirMode: 448
3391
3864
  });
3392
- } catch {}
3393
- await writeFileAtomic(ledgerPath, JSON.stringify(document), {
3394
- mode: 384,
3395
- dirMode: 448
3396
3865
  });
3397
3866
  }
3398
3867
  };
@@ -3434,6 +3903,14 @@ const SUBSCRIPTION_KEY_SOURCES = [
3434
3903
  provider: "minimax-token-plan",
3435
3904
  key: "minmaxApiKey"
3436
3905
  },
3906
+ {
3907
+ provider: "minimax-token-plan-cn",
3908
+ key: "minmaxApiKey"
3909
+ },
3910
+ {
3911
+ provider: "minimax-cn",
3912
+ key: "minmaxApiKey"
3913
+ },
3437
3914
  {
3438
3915
  provider: "openrouter",
3439
3916
  key: "openrouterApiKey"
@@ -3540,11 +4017,25 @@ async function resolveSubscriptionKeys(settings, credentials) {
3540
4017
  } catch {}
3541
4018
  }
3542
4019
  if (providers?.["zai-coding-cn"]?.apiKeyEnv !== void 0 && keys.zaiApiKey !== "") keys.zaiRegion = "bigmodel-cn";
4020
+ if (keys.opencodeApiKey === "") keys.opencodeApiKey = await readOpenCodeToken();
3543
4021
  return {
3544
4022
  keys,
3545
4023
  identified: identifySubscriptionPlans(providers)
3546
4024
  };
3547
4025
  }
4026
+ /** 读本机 OpenCode 客户端的凭据 token;取不到返回空串(安静退回)。 */
4027
+ async function readOpenCodeToken() {
4028
+ try {
4029
+ const auth = JSON.parse(await readFile(join(homedir(), ".local", "share", "opencode", "auth.json"), "utf8"));
4030
+ if (typeof auth === "string" && auth !== "") return auth;
4031
+ if (auth !== null && typeof auth === "object") {
4032
+ const record = auth;
4033
+ const token = record.token ?? record.key ?? record.apiKey;
4034
+ if (typeof token === "string" && token !== "") return token;
4035
+ }
4036
+ } catch {}
4037
+ return "";
4038
+ }
3548
4039
  /**
3549
4040
  * Host plugin body: serve real aggregated usage to the browser dashboard.
3550
4041
  * @param ctx - host context carrying webServer and sessionPersistence.
@@ -3553,8 +4044,26 @@ async function resolveSubscriptionKeys(settings, credentials) {
3553
4044
  function apply(ctx, config = {}) {
3554
4045
  let usageSettingsScope;
3555
4046
  const cwd = process.cwd();
3556
- const snapshotPath = join(homedir(), ".dsh/.dsh-usage-stats.json");
4047
+ const snapshotPath = config.snapshotPath ?? join(homedir(), ".dsh/.dsh-usage-stats.json");
3557
4048
  const ledgerStore = createFileUsageLedgerStore(config.ledgerPath ?? join(homedir(), ".dsh/.dsh-usage-ledger.json"));
4049
+ const reconcilePath = config.reconcilePath ?? join(homedir(), ".dsh/.dsh-usage-reconcile.json");
4050
+ let reconcileRef = null;
4051
+ (async () => {
4052
+ try {
4053
+ const ref = JSON.parse(await readFile(reconcilePath, "utf8")).ref;
4054
+ if (ref !== null && typeof ref === "object") {
4055
+ const candidate = ref;
4056
+ if (typeof candidate.date === "string" && typeof candidate.total === "number" && typeof candidate.granted === "number" && typeof candidate.topped === "number" && typeof candidate.currency === "string") reconcileRef = candidate;
4057
+ }
4058
+ } catch {}
4059
+ })();
4060
+ const persistReconcileRef = () => {
4061
+ if (reconcileRef === null) return;
4062
+ writeFileAtomic(reconcilePath, JSON.stringify({ ref: reconcileRef }), {
4063
+ mode: 384,
4064
+ dirMode: 448
4065
+ }).catch(() => {});
4066
+ };
3558
4067
  const workspaceTitleResolver = buildWorkspaceTitleResolver(ctx);
3559
4068
  const aggregator = createUsageAggregator(ctx.sessionPersistence, {
3560
4069
  ...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
@@ -3785,12 +4294,42 @@ function apply(ctx, config = {}) {
3785
4294
  handler: async (req, res) => {
3786
4295
  if (!guardLoopback(req, res)) return;
3787
4296
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
3788
- const providers = { ...await readPiAiProviders(ctx.settings) };
4297
+ const piProviders = await readPiAiProviders(ctx.settings);
4298
+ const providers = { ...piProviders };
3789
4299
  if (providers["deepseek"] === void 0) providers["deepseek"] = { apiKeyEnv: config.balanceApiKeyEnv ?? DEFAULT_BALANCE_API_KEY_ENV };
3790
4300
  const balances = await queryBalances(ctx, providers);
3791
4301
  const custom = await queryCustomBalances(ctx, config.customBalances ?? []);
3792
- for (const row of [...balances, ...custom]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
3793
- res.end(JSON.stringify({ balances: [...balances, ...custom] }));
4302
+ const declared = await queryDeclaredEndpoints(ctx, piProviders, config.declaredEndpoints ?? []);
4303
+ for (const row of [
4304
+ ...balances,
4305
+ ...custom,
4306
+ ...declared
4307
+ ]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
4308
+ const official = balances.find((row) => row.provider === "deepseek");
4309
+ let reconcile;
4310
+ if (official !== void 0 && official.totalBalance !== void 0) {
4311
+ const now = Date.now();
4312
+ const today = dayStamp(now);
4313
+ let todayOfficialCost = 0;
4314
+ try {
4315
+ todayOfficialCost = (await aggregator.aggregate()).byDay[today]?.officialCost ?? 0;
4316
+ } catch {}
4317
+ const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
4318
+ reconcileRef = result.ref;
4319
+ if (result.event !== null && result.event.kind !== "flat") reconcile = {
4320
+ ...result.event,
4321
+ provider: official.displayName
4322
+ };
4323
+ if (result.ref !== null) persistReconcileRef();
4324
+ }
4325
+ res.end(JSON.stringify({
4326
+ balances: [
4327
+ ...balances,
4328
+ ...custom,
4329
+ ...declared
4330
+ ],
4331
+ ...reconcile === void 0 ? {} : { reconcile }
4332
+ }));
3794
4333
  }
3795
4334
  }), "usage-billing: balance route");
3796
4335
  ctx.effect(() => ctx.webServer.register({
@@ -3878,6 +4417,7 @@ function apply(ctx, config = {}) {
3878
4417
  const routes = [];
3879
4418
  for (const [route, entry] of Object.entries(providers)) {
3880
4419
  if (entry.baseURL === void 0 || entry.apiKeyEnv === void 0) continue;
4420
+ if (isOfficialBaseUrl(entry.baseURL)) continue;
3881
4421
  routes.push({
3882
4422
  route,
3883
4423
  baseURL: entry.baseURL,