@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/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +5 -5
- package/lib/index.js +139 -71
- package/lib/types/aggregate.d.ts +20 -1
- package/lib/types/client/pricing.d.ts +3 -0
- package/lib/types/index.d.ts +2 -5
- package/lib/types/reconcile.d.ts +2 -0
- package/package.json +1 -1
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 (
|
|
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 = (
|
|
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 =
|
|
517
|
-
const base = found ?? (extra !== void 0 ? extraEntryOf(extra) : modelCatalog().
|
|
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 ((
|
|
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
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
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 (
|
|
2945
|
-
|
|
2946
|
-
|
|
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 +=
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
|
4994
|
-
|
|
4995
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
6395
|
-
if (name === "localhost" || name === "::1" ||
|
|
6396
|
-
return name !==
|
|
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
|
|
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
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
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
|
-
|
|
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 (
|
|
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" }));
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -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. */
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
31
|
+
* 信任主机名归一化:去空白、去端口、转小写,丢弃空项(IPv6 经 {@link hostNameOf}
|
|
32
|
+
* 去方括号)。与请求侧 Host 头解析同口径,因此匹配忽略大小写与端口。
|
|
36
33
|
* @param hosts - 配置里的原始名单。
|
|
37
34
|
* @returns 归一化后的主机名集合(空集 = 与历史版本行为一致)。
|
|
38
35
|
*/
|
package/lib/types/reconcile.d.ts
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "1.4.17",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"deepseek",
|