@kenz1117/dsh-ui-usage-billing 1.4.16 → 1.4.17

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
@@ -431,6 +431,37 @@ function canonIndex() {
431
431
  return map;
432
432
  }
433
433
  /**
434
+ * models.dev 补充条目的索引(归一化键 / 精确键各一份):补充列表可达数千条,
435
+ * 聚合折叠每条事件都查一次(此前是逐元素跑正则归一化的线性扫描),索引把
436
+ * 单次查询降到 O(1)。惰性构建,`applyLivePricing` 替换数组后按引用失效重建。
437
+ */
438
+ let extraIndexSource;
439
+ let extraCanonCache;
440
+ let extraKeyCache;
441
+ function buildExtraIndex() {
442
+ if (extraIndexSource === liveExtraModels) return;
443
+ const byCanon = /* @__PURE__ */ new Map();
444
+ const byKey = /* @__PURE__ */ new Map();
445
+ for (const item of liveExtraModels ?? []) {
446
+ if (!byKey.has(item.key)) byKey.set(item.key, item);
447
+ const canon = canonModelId(item.key);
448
+ if (canon !== "" && !byCanon.has(canon)) byCanon.set(canon, item);
449
+ }
450
+ extraIndexSource = liveExtraModels;
451
+ extraCanonCache = byCanon;
452
+ extraKeyCache = byKey;
453
+ }
454
+ /** 补充条目的归一化键索引(宽松匹配日志模型 id)。 */
455
+ function extraModelByCanon() {
456
+ buildExtraIndex();
457
+ return extraCanonCache ?? /* @__PURE__ */ new Map();
458
+ }
459
+ /** 补充条目的精确键索引(目录键直查)。 */
460
+ function extraModelByKey() {
461
+ buildExtraIndex();
462
+ return extraKeyCache ?? /* @__PURE__ */ new Map();
463
+ }
464
+ /**
434
465
  * 解析真实日志模型 id → 计费目录键。先精确别名映射(既有行为);未命中时做
435
466
  * 归一化匹配(忽略大小写/分隔符/括号附注),命中内置目录 / 别名目标 / 兜底键 /
436
467
  * models.dev 补充键即返回其真实键;完全未知时保持原样(回退 other,不计费)。
@@ -477,7 +508,7 @@ function lookupCandidate(candidate) {
477
508
  if (canon === "") return void 0;
478
509
  const hit = canonIndex().get(canon);
479
510
  if (hit !== void 0) return hit;
480
- return (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon)?.key;
511
+ return extraModelByCanon().get(canon)?.key;
481
512
  }
482
513
  function resolveCatalogKey(id) {
483
514
  const exact = userModelAliases?.[id] ?? modelKeyAliases()[id] ?? id;
@@ -486,7 +517,7 @@ function resolveCatalogKey(id) {
486
517
  if (canon !== "") {
487
518
  const hit = canonIndex().get(canon);
488
519
  if (hit !== void 0) return hit;
489
- const extraHit = (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon);
520
+ const extraHit = extraModelByCanon().get(canon);
490
521
  if (extraHit !== void 0) return extraHit.key;
491
522
  }
492
523
  for (const candidate of derivedKeyCandidates(id)) {
@@ -513,10 +544,11 @@ function livePriceOf(key) {
513
544
  function modelOf(key) {
514
545
  const resolved = resolveCatalogKey(key);
515
546
  const found = modelCatalog().find((entry) => entry.key === resolved);
516
- const extra = liveExtraModels?.find((item) => item.key === resolved);
517
- const base = found ?? (extra !== void 0 ? extraEntryOf(extra) : modelCatalog().at(-1) ?? UNSEEDED_OTHER);
547
+ const extra = extraModelByKey().get(resolved);
548
+ const base = found ?? (extra !== void 0 ? extraEntryOf(extra) : modelCatalog().find((entry) => entry.key === "other") ?? UNSEEDED_OTHER);
518
549
  const live = livePriceOf(resolved);
519
550
  if (live === void 0) return base;
551
+ if (base.price.offPeak !== void 0) return base;
520
552
  return {
521
553
  ...base,
522
554
  price: {
@@ -549,7 +581,7 @@ function extraEntryOf(extra) {
549
581
  function isPriced(key) {
550
582
  const resolved = resolveCatalogKey(key);
551
583
  if (modelCatalog().some((entry) => entry.key === resolved)) return true;
552
- if ((liveExtraModels ?? []).some((item) => item.key === resolved)) return true;
584
+ if (extraModelByKey().has(resolved)) return true;
553
585
  return FALLBACK_RATES.some((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
554
586
  }
555
587
  /**
@@ -663,6 +695,11 @@ function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
663
695
  output: repriced.output * 2
664
696
  } : repriced, buckets, "CNY");
665
697
  if (priced.price.offPeak === void 0) return priceBandCost(priced.price, buckets, priced.price.currency);
698
+ if (entry.tierSemantics === "latency") {
699
+ const peak = priceBandCost(priced.price, buckets, priced.price.currency);
700
+ const off = priceBandCost(priced.price.offPeak, buckets, priced.price.currency);
701
+ return peak * peakShare + off * (1 - peakShare);
702
+ }
666
703
  return priceBandCost((timeMs < WEEKEND_OFFPEAK_START_MS ? tariffV1At(timeMs) : tierAt(timeMs)) === "peak" ? priced.price : priced.price.offPeak, buckets, priced.price.currency);
667
704
  }
668
705
  /**
@@ -2334,6 +2371,30 @@ function siteBucketKey(ref) {
2334
2371
  if (ref.kind === "direct") return `direct:${ref.provider}`;
2335
2372
  return "unknown";
2336
2373
  }
2374
+ /**
2375
+ * 站点桶按范围求和(usage_stats 工具的 bySite/relay 口径):bySite 全量累计;
2376
+ * relay 只计中转站桶(`site:<origin>` 键)——`direct:*` 与 `unknown` 不计入,
2377
+ * 否则 relay 总额与 all 相同,中转站费用被高估。
2378
+ */
2379
+ function sumSiteBuckets(bySite, range) {
2380
+ let cost = 0;
2381
+ let calls = 0;
2382
+ let input = 0;
2383
+ let output = 0;
2384
+ for (const [bucket, usage] of Object.entries(bySite)) {
2385
+ if (range === "relay" && !bucket.startsWith("site:")) continue;
2386
+ cost += usage.cost;
2387
+ calls += usage.calls;
2388
+ input += usage.input;
2389
+ output += usage.output;
2390
+ }
2391
+ return {
2392
+ cost,
2393
+ calls,
2394
+ input,
2395
+ output
2396
+ };
2397
+ }
2337
2398
  /** Zeroed usage accumulator. */
2338
2399
  function emptyUsage() {
2339
2400
  return {
@@ -2358,8 +2419,11 @@ function emptyUsage() {
2358
2419
  * @param subscription - whether the call went through a subscription plan; such calls never cost money.
2359
2420
  * @param timeMs - the call's wall-clock time (epoch ms); drives peak/off-peak pricing.
2360
2421
  * @param official - whether the call went through the official DeepSeek channel (vs a third-party relay).
2422
+ * @param pricedCost - 调用方预算好的本次费用(同一条消息折叠进多个桶时计价结果相同,
2423
+ * 逐桶重算会让最热路径付出 N 倍常数);提供时跳过内部的计价闸门与计算,
2424
+ * 调用方保证其已按 `!subscription && isPriced(key)` 判定。
2361
2425
  */
2362
- function foldUsage(acc, usage, key, subscription, timeMs, official = false) {
2426
+ function foldUsage(acc, usage, key, subscription, timeMs, official = false, pricedCost) {
2363
2427
  const cacheHit = usage.cacheReadTokens ?? 0;
2364
2428
  const cacheMiss = usage.inputTokens + (usage.cacheWriteTokens ?? 0);
2365
2429
  acc.calls += 1;
@@ -2370,6 +2434,11 @@ function foldUsage(acc, usage, key, subscription, timeMs, official = false) {
2370
2434
  acc.cacheMiss += cacheMiss;
2371
2435
  if ((usage.cacheWriteTokens ?? 0) > 0) acc.cacheWrite = (acc.cacheWrite ?? 0) + (usage.cacheWriteTokens ?? 0);
2372
2436
  if (official) acc.officialCalls += 1;
2437
+ if (pricedCost !== void 0) {
2438
+ acc.cost += pricedCost;
2439
+ if (official) acc.officialCost += pricedCost;
2440
+ return;
2441
+ }
2373
2442
  if (!subscription && isPriced(key)) {
2374
2443
  const thisCost = computeCostAt(modelOf(key), {
2375
2444
  input: cacheHit + cacheMiss,
@@ -2925,14 +2994,22 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
2925
2994
  }
2926
2995
  const modelKey = key;
2927
2996
  const day = dayStamp(event.time);
2928
- if (!subscription && !isPriced(modelKey)) fold.unpricedModels.add(modelKey);
2929
- foldUsage(fold.total, usage, modelKey, subscription, event.time, official);
2930
- foldUsage(usageCell(fold.byModel, modelKey), usage, modelKey, subscription, event.time, official);
2931
- foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official);
2932
- foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official);
2933
- foldUsage(modelDaySiteCell(fold.byDayModelsSite, day, modelKey, siteBucket), usage, modelKey, subscription, event.time, official);
2934
- foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official);
2935
- foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official);
2997
+ const priced = !subscription && isPriced(modelKey);
2998
+ if (!subscription && !priced) fold.unpricedModels.add(modelKey);
2999
+ const entry = priced ? modelOf(modelKey) : void 0;
3000
+ const thisCost = entry === void 0 ? void 0 : computeCostAt(entry, {
3001
+ input: (usage.cacheReadTokens ?? 0) + usage.inputTokens + (usage.cacheWriteTokens ?? 0),
3002
+ cacheHit: usage.cacheReadTokens ?? 0,
3003
+ cacheMiss: usage.inputTokens + (usage.cacheWriteTokens ?? 0),
3004
+ output: usage.outputTokens
3005
+ }, event.time);
3006
+ foldUsage(fold.total, usage, modelKey, subscription, event.time, official, thisCost);
3007
+ foldUsage(usageCell(fold.byModel, modelKey), usage, modelKey, subscription, event.time, official, thisCost);
3008
+ foldUsage(usageCell(fold.byDay, day), usage, modelKey, subscription, event.time, official, thisCost);
3009
+ foldUsage(modelDayCell(fold.byDayModels, day, modelKey), usage, modelKey, subscription, event.time, official, thisCost);
3010
+ foldUsage(modelDaySiteCell(fold.byDayModelsSite, day, modelKey, siteBucket), usage, modelKey, subscription, event.time, official, thisCost);
3011
+ foldUsage(usageCell(fold.bySite, siteBucket), usage, modelKey, subscription, event.time, official, thisCost);
3012
+ foldUsage(usageCell(fold.byTier, tierAt(event.time)), usage, modelKey, subscription, event.time, official, thisCost);
2936
3013
  if (subscription) fold.planCalls.set(modelKey, (fold.planCalls.get(modelKey) ?? 0) + 1);
2937
3014
  const turn = event.data.turn ?? -1;
2938
3015
  const state = turnState(turns, turn);
@@ -2941,23 +3018,16 @@ function foldInto(fold, machine, events, subscriptionProviders, officialProvider
2941
3018
  state.output += usage.outputTokens;
2942
3019
  state.cacheHit += usage.cacheReadTokens ?? 0;
2943
3020
  state.cacheMiss += usage.inputTokens + (usage.cacheWriteTokens ?? 0);
2944
- if (!subscription && isPriced(modelKey)) {
2945
- const buckets = {
2946
- input: (usage.cacheReadTokens ?? 0) + usage.inputTokens + (usage.cacheWriteTokens ?? 0),
2947
- cacheHit: usage.cacheReadTokens ?? 0,
2948
- cacheMiss: usage.inputTokens + (usage.cacheWriteTokens ?? 0),
2949
- output: usage.outputTokens
2950
- };
2951
- const fullCost = computeCostAt(modelOf(modelKey), buckets, event.time);
2952
- state.cost += fullCost;
2953
- const outputCost = computeCostAt(modelOf(modelKey), {
3021
+ if (thisCost !== void 0 && entry !== void 0) {
3022
+ state.cost += thisCost;
3023
+ const outputCost = computeCostAt(entry, {
2954
3024
  input: 0,
2955
3025
  cacheHit: 0,
2956
3026
  cacheMiss: 0,
2957
3027
  output: usage.outputTokens
2958
3028
  }, event.time);
2959
3029
  fold.roles.outputCost += outputCost;
2960
- fold.roles.inputCost += fullCost - outputCost;
3030
+ fold.roles.inputCost += thisCost - outputCost;
2961
3031
  }
2962
3032
  if (state.startedAt === Number.MAX_SAFE_INTEGER) state.startedAt = event.time;
2963
3033
  const stepNum = event.data.step;
@@ -4206,7 +4276,7 @@ async function queryTencentTokenPlan(ctx, apiKeyEnv) {
4206
4276
  if (credential === void 0) return {
4207
4277
  provider,
4208
4278
  displayName: provider,
4209
- error: "unauthorized"
4279
+ error: "unconfigured"
4210
4280
  };
4211
4281
  const doRequest = async () => {
4212
4282
  const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
@@ -4954,12 +5024,7 @@ async function fetchRateFrom(sources) {
4954
5024
  }
4955
5025
  }
4956
5026
  async function fetchRate() {
4957
- for (const source of RATE_SOURCES) {
4958
- const text = await fetchText(source.url);
4959
- if (text === null) continue;
4960
- const value = source.parse(text);
4961
- if (value !== void 0 && Number.isFinite(value) && value > 0) return value;
4962
- }
5027
+ return fetchRateFrom(RATE_SOURCES);
4963
5028
  }
4964
5029
  /** OpenRouter model rows with usable USD unit prices, or undefined on failure. */
4965
5030
  async function fetchRouterModels() {
@@ -4986,13 +5051,15 @@ async function fetchRouterModels() {
4986
5051
  }
4987
5052
  return models;
4988
5053
  }
4989
- /** Match one catalog key's candidates: exact id first, then a single strong substring hit. */
5054
+ /** Match one catalog key's candidates: exact id first, then per-hint unique strong substring. */
4990
5055
  function matchRouterModel(hints, models) {
4991
5056
  const exact = models.find((model) => hints.some((hint) => model.id === hint));
4992
5057
  if (exact !== void 0) return exact;
4993
- const strong = models.filter((model) => hints.some((hint) => hint.length >= 8 && model.id.includes(hint)));
4994
- if (strong.length !== 1) return void 0;
4995
- return strong[0];
5058
+ for (const hint of hints) {
5059
+ if (hint.length < 8) continue;
5060
+ const strong = models.filter((model) => model.id.includes(hint));
5061
+ if (strong.length === 1) return strong[0];
5062
+ }
4996
5063
  }
4997
5064
  /** Map router matches onto catalog keys; undefined when nothing matched. */
4998
5065
  function buildPrices(models) {
@@ -6378,22 +6445,35 @@ function isLoopbackPeer(req) {
6378
6445
  * 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
6379
6446
  * 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
6380
6447
  /**
6381
- * 信任主机名归一化:去空白、去端口、转小写,丢弃空项。与请求侧
6382
- * `host.split(':')[0].toLowerCase()` 同口径,因此匹配忽略大小写与端口。
6448
+ * Host 头/名单条目的主机名部分:IPv6 字面量带方括号(`[::1]:3080`),整体去括号;
6449
+ * 裸 IPv6(多个冒号、无方括号)整体是字面量;其余按首个冒号去端口。统一小写。
6450
+ * (此前一律 `split(':')[0]`:`[::1]:3080` 截成 `[`,IPv6 回环请求被误拒,
6451
+ * trustedHosts 也配不进去——fail-closed 无安全暴露,但本机 IPv6 场景功能失效。)
6452
+ */
6453
+ function hostNameOf(host) {
6454
+ const trimmed = host.trim();
6455
+ const bracket = /^\[([0-9a-fA-F:]+)\](?::\d+)?$/.exec(trimmed);
6456
+ if (bracket !== null) return (bracket[1] ?? "").toLowerCase();
6457
+ if ((trimmed.match(/:/g) ?? []).length > 1) return trimmed.toLowerCase();
6458
+ return trimmed.split(":")[0]?.toLowerCase() ?? "";
6459
+ }
6460
+ /**
6461
+ * 信任主机名归一化:去空白、去端口、转小写,丢弃空项(IPv6 经 {@link hostNameOf}
6462
+ * 去方括号)。与请求侧 Host 头解析同口径,因此匹配忽略大小写与端口。
6383
6463
  * @param hosts - 配置里的原始名单。
6384
6464
  * @returns 归一化后的主机名集合(空集 = 与历史版本行为一致)。
6385
6465
  */
6386
6466
  function normalizeTrustedHosts(hosts) {
6387
6467
  if (hosts === void 0) return /* @__PURE__ */ new Set();
6388
- const names = hosts.filter((host) => typeof host === "string").map((host) => host.trim().split(":")[0]?.toLowerCase() ?? "").filter((host) => host !== "");
6468
+ const names = hosts.filter((host) => typeof host === "string").map((host) => hostNameOf(host)).filter((host) => host !== "");
6389
6469
  return new Set(names);
6390
6470
  }
6391
6471
  function isLoopbackHost(req, trustedHosts = /* @__PURE__ */ new Set()) {
6392
6472
  const host = req.headers.host;
6393
6473
  if (host === void 0 || host === "") return true;
6394
- const name = host.split(":")[0];
6395
- if (name === "localhost" || name === "::1" || name !== void 0 && /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
6396
- return name !== void 0 && trustedHosts.has(name.toLowerCase());
6474
+ const name = hostNameOf(host);
6475
+ if (name === "localhost" || name === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
6476
+ return name !== "" && trustedHosts.has(name);
6397
6477
  }
6398
6478
  /** 校验 Origin 头是否回环(写操作用,防止跨站表单/fetch 改写设置)。Origin 缺失
6399
6479
  * (curl 或同源 fetch 不带)视为放行,交给下方的 Content-Type 校验兜底;Origin 存在
@@ -6401,7 +6481,8 @@ function isLoopbackHost(req, trustedHosts = /* @__PURE__ */ new Set()) {
6401
6481
  function isLoopbackOrigin(origin) {
6402
6482
  if (origin === void 0 || origin === "") return true;
6403
6483
  try {
6404
- const host = new URL(origin).hostname;
6484
+ const raw = new URL(origin).hostname;
6485
+ const host = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
6405
6486
  return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
6406
6487
  } catch {
6407
6488
  return false;
@@ -7015,26 +7096,10 @@ function apply(ctx, config = {}) {
7015
7096
  input: stats.total.input,
7016
7097
  output: stats.total.output
7017
7098
  };
7018
- if (args.range === "bySite" || args.range === "relay") {
7019
- const bySite = stats.bySite ?? {};
7020
- let cost = 0;
7021
- let calls = 0;
7022
- let input = 0;
7023
- let output = 0;
7024
- for (const usage of Object.values(bySite)) {
7025
- cost += usage.cost;
7026
- calls += usage.calls;
7027
- input += usage.input;
7028
- output += usage.output;
7029
- }
7030
- return {
7031
- range: args.range,
7032
- cost,
7033
- calls,
7034
- input,
7035
- output
7036
- };
7037
- }
7099
+ if (args.range === "bySite" || args.range === "relay") return {
7100
+ range: args.range,
7101
+ ...sumSiteBuckets(stats.bySite ?? {}, args.range)
7102
+ };
7038
7103
  if (args.range === "today") {
7039
7104
  const day = stats.byDay[dayStamp(Date.now())];
7040
7105
  return day === void 0 ? zero : {
@@ -7188,7 +7253,8 @@ function apply(ctx, config = {}) {
7188
7253
  ]) if (row.error === "unauthorized") warnAuthOnce("balance", row.provider, row.displayName);
7189
7254
  const official = balances.find((row) => row.provider === "deepseek");
7190
7255
  let reconcile;
7191
- if (official !== void 0 && official.totalBalance !== void 0) {
7256
+ const reconcilable = official !== void 0 && official.totalBalance !== void 0 && (official.currency === void 0 || official.currency.toUpperCase() === "CNY");
7257
+ if (official !== void 0 && reconcilable) {
7192
7258
  const now = Date.now();
7193
7259
  const today = dayStamp(now);
7194
7260
  let todayOfficialCost = 0;
@@ -7196,12 +7262,14 @@ function apply(ctx, config = {}) {
7196
7262
  todayOfficialCost = (await aggregator.aggregate()).byDay[today]?.officialCost ?? 0;
7197
7263
  } catch {}
7198
7264
  const result = reconcileBalanceDelta(reconcileRef, official, todayOfficialCost, today, now);
7265
+ const refChanged = result.ref !== null && result.ref !== reconcileRef;
7199
7266
  reconcileRef = result.ref;
7200
7267
  if (result.event !== null && result.event.kind !== "flat") reconcile = {
7201
7268
  ...result.event,
7202
- provider: official.displayName
7269
+ provider: official.displayName,
7270
+ ...official.currency === void 0 ? {} : { currency: official.currency }
7203
7271
  };
7204
- if (result.ref !== null) persistReconcileRef();
7272
+ if (refChanged) persistReconcileRef();
7205
7273
  }
7206
7274
  const doc = {
7207
7275
  balances: [
@@ -7227,17 +7295,17 @@ function apply(ctx, config = {}) {
7227
7295
  path: "/api/billing/usage-tool",
7228
7296
  handler: async (req, res) => {
7229
7297
  if (!guardLoopback(req, res, trustedHosts)) return;
7298
+ if (req.method !== "GET" && req.method !== "POST") {
7299
+ res.writeHead(405, { "content-type": "application/json; charset=utf-8" });
7300
+ res.end(JSON.stringify({ error: "method not allowed" }));
7301
+ return;
7302
+ }
7230
7303
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
7231
7304
  const enabled = usageSettingsScope?.get().enableUsageStatsTool ?? false;
7232
7305
  if (req.method === "GET") {
7233
7306
  res.end(JSON.stringify({ enabled }));
7234
7307
  return;
7235
7308
  }
7236
- if (req.method !== "POST") {
7237
- res.writeHead(405, { "content-type": "application/json; charset=utf-8" });
7238
- res.end(JSON.stringify({ error: "method not allowed" }));
7239
- return;
7240
- }
7241
7309
  if (!isLoopbackOrigin(req.headers.origin)) {
7242
7310
  res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
7243
7311
  res.end(JSON.stringify({ error: "forbidden: loopback only" }));
@@ -85,6 +85,22 @@ export declare function siteRefOf(provider: string, routes: Readonly<Record<stri
85
85
  }): SiteRef;
86
86
  /** 站点桶的稳定 key:`site:<origin>` 与 `direct:<provider>` 分开,`unknown` 单一桶。 */
87
87
  export declare function siteBucketKey(ref: SiteRef): string;
88
+ /**
89
+ * 站点桶按范围求和(usage_stats 工具的 bySite/relay 口径):bySite 全量累计;
90
+ * relay 只计中转站桶(`site:<origin>` 键)——`direct:*` 与 `unknown` 不计入,
91
+ * 否则 relay 总额与 all 相同,中转站费用被高估。
92
+ */
93
+ export declare function sumSiteBuckets(bySite: Readonly<Record<string, {
94
+ cost: number;
95
+ calls: number;
96
+ input: number;
97
+ output: number;
98
+ }>>, range: 'bySite' | 'relay'): {
99
+ cost: number;
100
+ calls: number;
101
+ input: number;
102
+ output: number;
103
+ };
88
104
  /** Aggregation tuning options. */
89
105
  export interface AggregateOptions {
90
106
  /** 订阅制 provider id 列表;缺省按订阅卡同款 id 判定(`isSubscriptionProviderId`)。 */
@@ -155,8 +171,11 @@ export declare function emptyUsage(): ModelUsage;
155
171
  * @param subscription - whether the call went through a subscription plan; such calls never cost money.
156
172
  * @param timeMs - the call's wall-clock time (epoch ms); drives peak/off-peak pricing.
157
173
  * @param official - whether the call went through the official DeepSeek channel (vs a third-party relay).
174
+ * @param pricedCost - 调用方预算好的本次费用(同一条消息折叠进多个桶时计价结果相同,
175
+ * 逐桶重算会让最热路径付出 N 倍常数);提供时跳过内部的计价闸门与计算,
176
+ * 调用方保证其已按 `!subscription && isPriced(key)` 判定。
158
177
  */
159
- export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number, official?: boolean): void;
178
+ export declare function foldUsage(acc: ModelUsage, usage: TokenUsage, key: string, subscription: boolean, timeMs: number, official?: boolean, pricedCost?: number): void;
160
179
  /**
161
180
  * 联网搜索辅助请求的单次费用估算默认值(人民币元)。DeepSeek 官方对搜索请求
162
181
  * (web_search 服务端工具注入上下文)照常计费,实测每次约 0.01~0.03 元,取中值;
@@ -478,6 +478,9 @@ export declare function formatUnitPrice(price: number, currency?: 'CNY' | 'USD'
478
478
  * @returns 换算到目标币种的单价;同币种或汇率不可用时原值。
479
479
  */
480
480
  export declare function convertUnitPrice(price: number, native: 'CNY' | 'USD', target: CostCurrency, rate: number): number;
481
+ /** 本地时区日期戳 `YYYY-MM-DD`(与服务端 aggregate.ts 的 dayStamp 同口径);
482
+ * 触发卡/热力图/Token 面板共用一个实现(此前三处各抄一份)。 */
483
+ export declare function localDayStamp(time?: number): string;
481
484
  /** Format a large token count with B/M/K suffix. */
482
485
  export declare function formatTokens(value: number): string;
483
486
  /** Format a percentage. */
@@ -27,12 +27,9 @@ interface SettingsReader {
27
27
  value: unknown;
28
28
  }[];
29
29
  }
30
- /** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
31
- * 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
32
- * 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
33
30
  /**
34
- * 信任主机名归一化:去空白、去端口、转小写,丢弃空项。与请求侧
35
- * `host.split(':')[0].toLowerCase()` 同口径,因此匹配忽略大小写与端口。
31
+ * 信任主机名归一化:去空白、去端口、转小写,丢弃空项(IPv6 经 {@link hostNameOf}
32
+ * 去方括号)。与请求侧 Host 头解析同口径,因此匹配忽略大小写与端口。
36
33
  * @param hosts - 配置里的原始名单。
37
34
  * @returns 归一化后的主机名集合(空集 = 与历史版本行为一致)。
38
35
  */
@@ -31,6 +31,8 @@ export interface ReconcileEvent {
31
31
  kind: ReconcileKind;
32
32
  /** 余额提供商显示名(如 DeepSeek);仅 `ok`/`drift`。 */
33
33
  provider?: string;
34
+ /** 余额币种(CNY / USD);仅 `ok`/`drift` 且上游标注币种时携带,供界面标注金额单位。 */
35
+ currency?: string;
34
36
  /** 按余额差反推的当日消费(CNY/USD 按余额币种);仅 `ok`/`drift`。 */
35
37
  spent?: number;
36
38
  /** 本地账本当日的官方渠道费用;仅 `ok`/`drift`。 */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kenz1117/dsh-ui-usage-billing",
3
3
  "description": "Usage billing dashboard for DeepSeek Harness: sidebar cost metrics plus a full dashboard modal, priced from a current multi-provider catalog with real usage aggregated from session logs.",
4
- "version": "1.4.16",
4
+ "version": "1.4.17",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "deepseek",