@kenz1117/dsh-ui-usage-billing 1.0.20 → 1.1.0
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 +13 -8
- package/README.md +24 -14
- package/lib/client.js +5 -5
- package/lib/index.js +22 -259
- package/lib/types/aggregate.d.ts +8 -13
- package/lib/types/balance.d.ts +0 -26
- package/lib/types/client/PerfPanel.d.ts +8 -10
- package/lib/types/client/TokenPanel.d.ts +3 -53
- package/lib/types/client/apply.d.ts +3 -10
- package/lib/types/client/budget-store.d.ts +1 -1
- package/lib/types/client/completion-notify.d.ts +2 -2
- package/lib/types/client/heatmap.d.ts +1 -1
- package/lib/types/client/live-cost.d.ts +9 -9
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +0 -54
- package/lib/types/client/round-chart.d.ts +1 -1
- package/lib/types/client/usage-billing-settings.d.ts +0 -40
- package/package.json +11 -11
package/lib/index.js
CHANGED
|
@@ -7,7 +7,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";
|
|
10
|
-
import { createHash, createHmac } from "node:crypto";
|
|
11
10
|
//#region lib/types/client/plan-knowledge.js
|
|
12
11
|
/**
|
|
13
12
|
* Plan-knowledge reference (adapted from dsh-spend's `knowledge.js`, MIT):
|
|
@@ -1635,31 +1634,24 @@ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Dat
|
|
|
1635
1634
|
return peak * peakShare + off * (1 - peakShare);
|
|
1636
1635
|
}
|
|
1637
1636
|
/**
|
|
1638
|
-
* v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
|
|
1639
|
-
* 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
|
|
1640
|
-
* 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
|
|
1641
|
-
*/
|
|
1642
|
-
function tariffV1At(timeMs) {
|
|
1643
|
-
return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
|
|
1644
|
-
}
|
|
1645
|
-
/**
|
|
1646
1637
|
* 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
|
|
1647
1638
|
* (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
|
|
1648
1639
|
* 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
|
|
1649
1640
|
* 同口径:按事件时刻判定该笔流量当时享受的单价。
|
|
1650
|
-
*
|
|
1651
|
-
* 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
|
|
1652
|
-
* - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
|
|
1653
|
-
* ({@link LEGACY_DEEPSEEK_BANDS})计费;
|
|
1654
|
-
* - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
|
|
1655
|
-
* 周六日 9-12 / 14-18 计峰);
|
|
1656
|
-
* - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
|
|
1657
1641
|
* @param entry - the catalog entry whose prices apply.
|
|
1658
1642
|
* @param buckets - token usage counts.
|
|
1659
1643
|
* @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
|
|
1660
1644
|
* @param peakShare - fallback mix used only when `timeMs` is missing.
|
|
1661
1645
|
* @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
|
|
1662
1646
|
*/
|
|
1647
|
+
/**
|
|
1648
|
+
* v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
|
|
1649
|
+
* 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
|
|
1650
|
+
* 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
|
|
1651
|
+
*/
|
|
1652
|
+
function tariffV1At(timeMs) {
|
|
1653
|
+
return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
|
|
1654
|
+
}
|
|
1663
1655
|
function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
|
|
1664
1656
|
if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
|
|
1665
1657
|
const priced = applyPromo(entry, timeMs);
|
|
@@ -2397,7 +2389,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2397
2389
|
id,
|
|
2398
2390
|
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
2399
2391
|
...stamp === null ? {} : { stamp },
|
|
2400
|
-
foldVersion:
|
|
2392
|
+
foldVersion: 5,
|
|
2401
2393
|
fold: serializeFold(fold)
|
|
2402
2394
|
};
|
|
2403
2395
|
const previous = ledger.get(id);
|
|
@@ -2421,7 +2413,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2421
2413
|
for (const entry of ledger.values()) {
|
|
2422
2414
|
if (included.has(entry.id)) continue;
|
|
2423
2415
|
try {
|
|
2424
|
-
const stale = (entry.foldVersion ?? 1) <
|
|
2416
|
+
const stale = (entry.foldVersion ?? 1) < 5;
|
|
2425
2417
|
folds.push({
|
|
2426
2418
|
id: entry.id,
|
|
2427
2419
|
...entry.cwd === void 0 ? {} : { cwd: entry.cwd },
|
|
@@ -2467,7 +2459,7 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2467
2459
|
outputCost: 0
|
|
2468
2460
|
};
|
|
2469
2461
|
const perfModel = /* @__PURE__ */ new Map();
|
|
2470
|
-
const
|
|
2462
|
+
const perfHour = /* @__PURE__ */ new Map();
|
|
2471
2463
|
for (const { id: sessionId, cwd, fold, staleLedger } of folds) {
|
|
2472
2464
|
mergeUsageInto(total, fold.total);
|
|
2473
2465
|
roles.userChars += fold.roles.userChars;
|
|
@@ -2498,18 +2490,13 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2498
2490
|
if (sample.tps !== void 0) modelAccum.tps.push(sample.tps);
|
|
2499
2491
|
if (sample.latencyMs !== void 0) modelAccum.latencies.push(sample.latencyMs);
|
|
2500
2492
|
if (sample.estimated) modelAccum.estimated += 1;
|
|
2501
|
-
let
|
|
2502
|
-
if (hourModels === void 0) {
|
|
2503
|
-
hourModels = /* @__PURE__ */ new Map();
|
|
2504
|
-
perfHourModel.set(sample.hour, hourModels);
|
|
2505
|
-
}
|
|
2506
|
-
let hourAccum = hourModels.get(sample.model);
|
|
2493
|
+
let hourAccum = perfHour.get(sample.hour);
|
|
2507
2494
|
if (hourAccum === void 0) {
|
|
2508
2495
|
hourAccum = {
|
|
2509
2496
|
ttfts: [],
|
|
2510
2497
|
tps: []
|
|
2511
2498
|
};
|
|
2512
|
-
|
|
2499
|
+
perfHour.set(sample.hour, hourAccum);
|
|
2513
2500
|
}
|
|
2514
2501
|
hourAccum.ttfts.push(sample.ttftMs);
|
|
2515
2502
|
if (sample.tps !== void 0) hourAccum.tps.push(sample.tps);
|
|
@@ -2569,14 +2556,14 @@ function createUsageAggregator(persistence, options = {}) {
|
|
|
2569
2556
|
latencyAvg: acc.latencies.length === 0 ? 0 : mean(acc.latencies),
|
|
2570
2557
|
estimatedSamples: acc.estimated
|
|
2571
2558
|
}])),
|
|
2572
|
-
|
|
2559
|
+
byHour: Object.fromEntries([...perfHour].map(([hour, acc]) => [hour, {
|
|
2573
2560
|
samples: acc.ttfts.length,
|
|
2574
2561
|
ttftAvg: mean(acc.ttfts),
|
|
2575
2562
|
...acc.tps.length === 0 ? {} : { tpsAvg: mean(acc.tps) }
|
|
2576
|
-
}]))
|
|
2563
|
+
}]))
|
|
2577
2564
|
};
|
|
2578
2565
|
lastDoc = {
|
|
2579
|
-
version:
|
|
2566
|
+
version: 3,
|
|
2580
2567
|
updatedAt: now,
|
|
2581
2568
|
source: "session-logs",
|
|
2582
2569
|
timezone: hostTimeZone(),
|
|
@@ -2975,26 +2962,6 @@ const QUERIERS = [
|
|
|
2975
2962
|
route: "zai-coding-cn",
|
|
2976
2963
|
displayName: "智谱 AI",
|
|
2977
2964
|
querier: queryZhipu
|
|
2978
|
-
},
|
|
2979
|
-
{
|
|
2980
|
-
route: "tencent-tokenhub",
|
|
2981
|
-
displayName: "腾讯云 TokenHub",
|
|
2982
|
-
querier: queryTencentTokenPlan
|
|
2983
|
-
},
|
|
2984
|
-
{
|
|
2985
|
-
route: "tokenhub",
|
|
2986
|
-
displayName: "腾讯云 TokenHub",
|
|
2987
|
-
querier: queryTencentTokenPlan
|
|
2988
|
-
},
|
|
2989
|
-
{
|
|
2990
|
-
route: "tencent",
|
|
2991
|
-
displayName: "腾讯云 TokenHub",
|
|
2992
|
-
querier: queryTencentTokenPlan
|
|
2993
|
-
},
|
|
2994
|
-
{
|
|
2995
|
-
route: "tencentcloud",
|
|
2996
|
-
displayName: "腾讯云 TokenHub",
|
|
2997
|
-
querier: queryTencentTokenPlan
|
|
2998
2965
|
}
|
|
2999
2966
|
];
|
|
3000
2967
|
/**
|
|
@@ -3027,200 +2994,6 @@ async function queryBalances(ctx, providers) {
|
|
|
3027
2994
|
return querier(ctx, env);
|
|
3028
2995
|
}));
|
|
3029
2996
|
}
|
|
3030
|
-
/** TokenHub 管控面 API 端点(cloud.tencent.cn/document/api/1823/132270)。 */
|
|
3031
|
-
const TOKENHUB_HOST = "tokenhub.tencentcloudapi.com";
|
|
3032
|
-
/** 云 API 3.0 产品名与版本(签名的 service 段与请求头都必须一致)。 */
|
|
3033
|
-
const TOKENHUB_SERVICE = "tokenhub";
|
|
3034
|
-
const TOKENHUB_VERSION = "2026-03-22";
|
|
3035
|
-
/** 请求地域:管控面对地域不敏感,取默认国内地域(文档地域列表含 ap-guangzhou)。 */
|
|
3036
|
-
const TOKENHUB_REGION = "ap-guangzhou";
|
|
3037
|
-
/** 腾讯云凭据引用值格式:`<SecretId>:<SecretKey>`(分隔符取首个冒号)。 */
|
|
3038
|
-
function parseTencentCredential(value) {
|
|
3039
|
-
const sep = value.indexOf(":");
|
|
3040
|
-
if (sep === -1) return void 0;
|
|
3041
|
-
const secretId = value.slice(0, sep).trim();
|
|
3042
|
-
const secretKey = value.slice(sep + 1).trim();
|
|
3043
|
-
if (secretId === "" || secretKey === "") return void 0;
|
|
3044
|
-
return {
|
|
3045
|
-
secretId,
|
|
3046
|
-
secretKey
|
|
3047
|
-
};
|
|
3048
|
-
}
|
|
3049
|
-
/**
|
|
3050
|
-
* 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
|
|
3051
|
-
* 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
|
|
3052
|
-
* @param secretId - 云 API SecretId。
|
|
3053
|
-
* @param secretKey - 云 API SecretKey。
|
|
3054
|
-
* @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
|
|
3055
|
-
* @param timestamp - 签名时间戳(秒)。
|
|
3056
|
-
* @returns Authorization 头的值。
|
|
3057
|
-
*/
|
|
3058
|
-
function tc3Authorization(secretId, secretKey, payload, timestamp) {
|
|
3059
|
-
const date = (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().slice(0, 10);
|
|
3060
|
-
const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
|
|
3061
|
-
const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
|
|
3062
|
-
const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
|
|
3063
|
-
return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", date).update(secretKey).digest()).update(TOKENHUB_SERVICE).digest()).update("tc3_request").digest()).update(stringToSign).digest("hex")}`;
|
|
3064
|
-
}
|
|
3065
|
-
/** 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。 */
|
|
3066
|
-
async function callTokenHub(secretId, secretKey, action, params) {
|
|
3067
|
-
const payload = JSON.stringify({
|
|
3068
|
-
Action: action,
|
|
3069
|
-
Version: TOKENHUB_VERSION,
|
|
3070
|
-
Region: TOKENHUB_REGION,
|
|
3071
|
-
...params
|
|
3072
|
-
});
|
|
3073
|
-
const timestamp = Math.floor(Date.now() / 1e3);
|
|
3074
|
-
const controller = new AbortController();
|
|
3075
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS$3);
|
|
3076
|
-
try {
|
|
3077
|
-
const response = await fetch(`https://${TOKENHUB_HOST}/`, {
|
|
3078
|
-
method: "POST",
|
|
3079
|
-
headers: {
|
|
3080
|
-
"content-type": "application/json; charset=utf-8",
|
|
3081
|
-
host: TOKENHUB_HOST,
|
|
3082
|
-
"x-tc-action": action.toLowerCase(),
|
|
3083
|
-
"x-tc-version": TOKENHUB_VERSION,
|
|
3084
|
-
"x-tc-region": TOKENHUB_REGION,
|
|
3085
|
-
"x-tc-timestamp": String(timestamp),
|
|
3086
|
-
authorization: tc3Authorization(secretId, secretKey, payload, timestamp)
|
|
3087
|
-
},
|
|
3088
|
-
body: payload,
|
|
3089
|
-
signal: controller.signal
|
|
3090
|
-
});
|
|
3091
|
-
if (response.status === 401 || response.status === 403) throw Object.assign(/* @__PURE__ */ new Error("unauthorized"), { code: "unauthorized" });
|
|
3092
|
-
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`HTTP ${String(response.status)}`), {
|
|
3093
|
-
httpStatus: response.status,
|
|
3094
|
-
code: "unreachable"
|
|
3095
|
-
});
|
|
3096
|
-
const inner = (await response.json()).Response;
|
|
3097
|
-
if (inner === void 0) throw Object.assign(/* @__PURE__ */ new Error("no Response envelope"), { code: "invalid" });
|
|
3098
|
-
if (inner.Error !== void 0 && inner.Error !== null) {
|
|
3099
|
-
const err = inner.Error;
|
|
3100
|
-
const code = err.Code === "AuthFailure.SignatureFailure" || err.Code === "AuthFailure.SecretIdNotFound" ? "unauthorized" : "unreachable";
|
|
3101
|
-
throw Object.assign(new Error(String(err.Code ?? "api-error")), { code });
|
|
3102
|
-
}
|
|
3103
|
-
return inner;
|
|
3104
|
-
} finally {
|
|
3105
|
-
clearTimeout(timer);
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
/**
|
|
3109
|
-
* 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
|
|
3110
|
-
* 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
|
|
3111
|
-
* balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
|
|
3112
|
-
* {@link toNumber} 归一化(上游可能给字符串)。
|
|
3113
|
-
* 导出供测试:纯函数。
|
|
3114
|
-
* @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
|
|
3115
|
-
* @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
|
|
3116
|
-
*/
|
|
3117
|
-
function pickRemainingQuota(source) {
|
|
3118
|
-
if (source === null || typeof source !== "object") return void 0;
|
|
3119
|
-
const numeric = Object.entries(source).filter(([, v]) => v !== null && toNumber$1(v) !== void 0);
|
|
3120
|
-
const byKey = (needles) => {
|
|
3121
|
-
for (const [key, value] of numeric) {
|
|
3122
|
-
const lower = key.toLowerCase();
|
|
3123
|
-
if (needles.some((n) => lower.includes(n))) {
|
|
3124
|
-
const num = toNumber$1(value);
|
|
3125
|
-
if (num !== void 0 && num >= 0) return num;
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
};
|
|
3129
|
-
const remaining = byKey([
|
|
3130
|
-
"remain",
|
|
3131
|
-
"balance",
|
|
3132
|
-
"left",
|
|
3133
|
-
"available"
|
|
3134
|
-
]);
|
|
3135
|
-
if (remaining !== void 0) return remaining;
|
|
3136
|
-
const total = byKey(["total"]);
|
|
3137
|
-
const used = byKey(["used", "consume"]);
|
|
3138
|
-
if (total !== void 0 && used !== void 0) return Math.max(0, total - used);
|
|
3139
|
-
}
|
|
3140
|
-
/** 套餐列表里提取第一个启用套餐的 TeamId:集合字段名做候选兼容。 */
|
|
3141
|
-
function firstEnabledTeamId(inner) {
|
|
3142
|
-
const candidates = inner.TeamSet ?? inner.TokenPlanSet ?? inner.PlanSet;
|
|
3143
|
-
if (!Array.isArray(candidates)) return void 0;
|
|
3144
|
-
for (const item of candidates) {
|
|
3145
|
-
if (item === null || typeof item !== "object") continue;
|
|
3146
|
-
const row = item;
|
|
3147
|
-
if (row.TeamId === void 0 && row.PlanId === void 0) continue;
|
|
3148
|
-
if (row.Status !== void 0 && row.Status !== "enable") continue;
|
|
3149
|
-
const id = row.TeamId ?? row.PlanId;
|
|
3150
|
-
if (typeof id === "string" && id !== "") return id;
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
/**
|
|
3154
|
-
* 查询腾讯云 TokenHub Token Plan 套餐余量。凭据值格式 `<SecretId>:<SecretKey>`
|
|
3155
|
-
* (云 API 密钥,非 TokenHub 推理 key)。链路:套餐列表取 TeamId → 套餐详情读
|
|
3156
|
-
* 主额度包余量。管控面字段名未完全稳定,解析按语义键防御提取。
|
|
3157
|
-
* @param ctx - host context carrying the credentials seam.
|
|
3158
|
-
* @param apiKeyEnv - credential reference resolving the `<SecretId>:<SecretKey>` pair.
|
|
3159
|
-
*/
|
|
3160
|
-
async function queryTencentTokenPlan(ctx, apiKeyEnv) {
|
|
3161
|
-
const provider = "腾讯云 TokenHub";
|
|
3162
|
-
const hit = await ctx.credentials.resolve(credentialRef(apiKeyEnv));
|
|
3163
|
-
if (hit === void 0) return {
|
|
3164
|
-
provider,
|
|
3165
|
-
displayName: provider,
|
|
3166
|
-
error: "unconfigured"
|
|
3167
|
-
};
|
|
3168
|
-
if (!balanceGate.check(provider)) return {
|
|
3169
|
-
provider,
|
|
3170
|
-
displayName: provider,
|
|
3171
|
-
error: "unreachable"
|
|
3172
|
-
};
|
|
3173
|
-
const credential = parseTencentCredential(hit.value);
|
|
3174
|
-
if (credential === void 0) return {
|
|
3175
|
-
provider,
|
|
3176
|
-
displayName: provider,
|
|
3177
|
-
error: "unauthorized"
|
|
3178
|
-
};
|
|
3179
|
-
const doRequest = async () => {
|
|
3180
|
-
const teamId = firstEnabledTeamId(await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlanList", {}));
|
|
3181
|
-
if (teamId === void 0) return {
|
|
3182
|
-
provider,
|
|
3183
|
-
displayName: provider,
|
|
3184
|
-
error: "invalid"
|
|
3185
|
-
};
|
|
3186
|
-
const detail = await callTokenHub(credential.secretId, credential.secretKey, "DescribeTokenPlan", { TeamId: teamId });
|
|
3187
|
-
const remaining = pickRemainingQuota(detail.PackageInfo) ?? pickRemainingQuota(detail);
|
|
3188
|
-
if (remaining === void 0) {
|
|
3189
|
-
console.warn(`[usage-billing] balance response drifted for ${provider}: no remaining-quota field parsed`);
|
|
3190
|
-
return {
|
|
3191
|
-
provider,
|
|
3192
|
-
displayName: provider,
|
|
3193
|
-
error: "invalid"
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
|
-
const plan = typeof detail.Name === "string" ? detail.Name : void 0;
|
|
3197
|
-
const exhausted = detail.StopReason === "EXHAUSTED";
|
|
3198
|
-
return {
|
|
3199
|
-
provider,
|
|
3200
|
-
displayName: provider,
|
|
3201
|
-
currency: "CNY",
|
|
3202
|
-
totalBalance: remaining,
|
|
3203
|
-
...plan !== void 0 ? { plan } : {},
|
|
3204
|
-
...exhausted ? { isAvailable: false } : {}
|
|
3205
|
-
};
|
|
3206
|
-
};
|
|
3207
|
-
try {
|
|
3208
|
-
const row = await withRetry(doRequest, {
|
|
3209
|
-
retries: 1,
|
|
3210
|
-
baseDelayMs: 250,
|
|
3211
|
-
maxDelayMs: 2e3
|
|
3212
|
-
});
|
|
3213
|
-
balanceGate.success(provider);
|
|
3214
|
-
return row;
|
|
3215
|
-
} catch (error) {
|
|
3216
|
-
balanceGate.fail(provider);
|
|
3217
|
-
return {
|
|
3218
|
-
provider,
|
|
3219
|
-
displayName: provider,
|
|
3220
|
-
error: error.code === "unauthorized" ? "unauthorized" : "unreachable"
|
|
3221
|
-
};
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
2997
|
/** 点路径取值:`data.total_available` → 逐层下钻;任一缺失返回 undefined。 */
|
|
3225
2998
|
function getPath(data, path) {
|
|
3226
2999
|
let cursor = data;
|
|
@@ -3258,28 +3031,18 @@ function evalExtract(rule, data) {
|
|
|
3258
3031
|
}
|
|
3259
3032
|
return base;
|
|
3260
3033
|
}
|
|
3261
|
-
/**
|
|
3262
|
-
* 请求头占位符解析:值中任意位置的 `{{ENV_NAME}}` 经凭据 seam 替换(如
|
|
3263
|
-
* `Bearer {{KEY}}`、`token={{KEY}}`、一处多占位符);被引用的任一凭据缺失
|
|
3264
|
-
* 或为空 → 返回 null(fail-closed,与完整占位符形态的历史语义一致)。
|
|
3265
|
-
*/
|
|
3034
|
+
/** 请求头占位符解析:`{{ENV_NAME}}` 经凭据 seam 替换;解析失败返回 null。 */
|
|
3266
3035
|
async function resolveHeaders(ctx, headers) {
|
|
3267
3036
|
const resolved = {};
|
|
3268
3037
|
for (const [key, value] of Object.entries(headers)) {
|
|
3269
|
-
const
|
|
3270
|
-
if (
|
|
3038
|
+
const match = /^\{\{([A-Z0-9_]+)\}\}$/i.exec(value.trim());
|
|
3039
|
+
if (match === null) {
|
|
3271
3040
|
resolved[key] = value;
|
|
3272
3041
|
continue;
|
|
3273
3042
|
}
|
|
3274
|
-
const
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
if (hits.has(name)) continue;
|
|
3278
|
-
const hit = await ctx.credentials.resolve(credentialRef(name));
|
|
3279
|
-
if (hit === void 0 || hit.value === "") return null;
|
|
3280
|
-
hits.set(name, hit.value);
|
|
3281
|
-
}
|
|
3282
|
-
resolved[key] = value.replace(/\{\{([A-Z0-9_]+)\}\}/gi, (raw, name) => hits.get(name) ?? raw);
|
|
3043
|
+
const hit = await ctx.credentials.resolve(credentialRef(match[1] ?? ""));
|
|
3044
|
+
if (hit === void 0 || hit.value === "") return null;
|
|
3045
|
+
resolved[key] = value.replace(match[0], hit.value);
|
|
3283
3046
|
}
|
|
3284
3047
|
return resolved;
|
|
3285
3048
|
}
|
package/lib/types/aggregate.d.ts
CHANGED
|
@@ -220,8 +220,6 @@ export interface UsageStatsDocument {
|
|
|
220
220
|
* 以 step/start 为起点估算并计 estimated。
|
|
221
221
|
*/
|
|
222
222
|
perf?: PerfStats;
|
|
223
|
-
/** 只存在于账本、且缺 foldVersion 的旧会话数;无旧行时省略。 */
|
|
224
|
-
staleLedgerSessions?: number;
|
|
225
223
|
}
|
|
226
224
|
/** 按角色费用归因:user / tool 为输入成本的启发式摊分,assistant 为输出成本实测。 */
|
|
227
225
|
export interface RoleCost {
|
|
@@ -236,8 +234,8 @@ export interface RoleCost {
|
|
|
236
234
|
export interface PerfStats {
|
|
237
235
|
/** 按模型聚合(键 = 计费目录键;未收录模型原样保留)。 */
|
|
238
236
|
byModel: Record<string, ModelPerf>;
|
|
239
|
-
/**
|
|
240
|
-
|
|
237
|
+
/** 按小时聚合(键 = {@link hourStamp},北京时间)。 */
|
|
238
|
+
byHour: Record<string, HourPerf>;
|
|
241
239
|
}
|
|
242
240
|
/** 一个模型的性能统计:首字延时均值 / P50 / P90、生成速度均值、总延迟均值。 */
|
|
243
241
|
export interface ModelPerf {
|
|
@@ -260,11 +258,11 @@ export interface ModelPerf {
|
|
|
260
258
|
/** 以 step/start 估算的样本数(工具续写步骤无独立 request/header)。 */
|
|
261
259
|
estimatedSamples: number;
|
|
262
260
|
}
|
|
263
|
-
/**
|
|
264
|
-
export interface
|
|
261
|
+
/** 一个小时的性能统计(键 = {@link hourStamp})。 */
|
|
262
|
+
export interface HourPerf {
|
|
265
263
|
samples: number;
|
|
266
264
|
ttftAvg: number;
|
|
267
|
-
/** 平均生成速度(tokens/s
|
|
265
|
+
/** 平均生成速度(tokens/s);该小时无可测生成窗口时缺失。 */
|
|
268
266
|
tpsAvg?: number;
|
|
269
267
|
}
|
|
270
268
|
/** 会话明细行:仪表盘「会话明细」面板的数据源。 */
|
|
@@ -420,13 +418,10 @@ export interface UsageLedgerDocument {
|
|
|
420
418
|
* 两次 header 之间的用量串到上一个模型,订阅模型首当其冲,issue #14);v2 =
|
|
421
419
|
* `assistant/message` 自带 source 归账(1.0.7 起);v3 = 联网搜索请求按次估算
|
|
422
420
|
* 计费(issue #15,1.0.9 起);v4 = 模型×日期×站点三维桶(issue #16,按 origin
|
|
423
|
-
* 绑定自定义价的显示层重估)——旧行缺该维度,按无 origin
|
|
424
|
-
* 时代之前(2026-08-16T16:00Z)的 DeepSeek 事件按当时基础价计费(v4 把全部
|
|
425
|
-
* 历史套现行峰/谷档价,高估约 50%);v6 = v1 峰谷窗口(至 2026-08-22T16:00Z)
|
|
426
|
-
* 的周末不豁免峰时(v5 错套现行周末全谷规则,低估该窗口周六日的峰时费用)。
|
|
421
|
+
* 绑定自定义价的显示层重估)——旧行缺该维度,按无 origin 价处理。
|
|
427
422
|
* 持久账本行据此区分新旧算法:日志已删/不可读而只能沿用旧行时,UI 标注置信度提示。
|
|
428
423
|
*/
|
|
429
|
-
export declare const FOLD_VERSION =
|
|
424
|
+
export declare const FOLD_VERSION = 5;
|
|
430
425
|
/**
|
|
431
426
|
* 一次性账本迁移:id 唯一,apply 在加载边界对原始文档执行,已应用过的跳过。
|
|
432
427
|
* 未来账本/schema 字段变更(重命名、拆桶、语义调整)时,在此追加一条迁移并
|
|
@@ -477,7 +472,7 @@ export declare function messageTextLength(message: unknown): number;
|
|
|
477
472
|
export declare function foldSession(events: readonly {
|
|
478
473
|
type: string;
|
|
479
474
|
time: number;
|
|
480
|
-
data:
|
|
475
|
+
data: never;
|
|
481
476
|
seq?: number;
|
|
482
477
|
}[], subscriptionProviders: ReadonlySet<string>, officialProviderIds?: ReadonlySet<string>, routes?: Readonly<Record<string, ProviderRouteView>>, searchCallEstimateCny?: number): SessionFold;
|
|
483
478
|
/**
|
package/lib/types/balance.d.ts
CHANGED
|
@@ -25,26 +25,6 @@ import type { CustomBalanceConfig, CustomBalanceExtract, ProviderBalance } from
|
|
|
25
25
|
export declare function queryBalances(ctx: Context, providers: Readonly<Record<string, {
|
|
26
26
|
apiKeyEnv?: string;
|
|
27
27
|
}>>): Promise<readonly ProviderBalance[]>;
|
|
28
|
-
/**
|
|
29
|
-
* 构造云 API 3.0 TC3-HMAC-SHA256 签名(官方签名方法 v3)。导出供测试:纯函数,
|
|
30
|
-
* 输入确定则签名确定。Action 不参与签名——它走 `X-TC-Action` 请求头。
|
|
31
|
-
* @param secretId - 云 API SecretId。
|
|
32
|
-
* @param secretKey - 云 API SecretKey。
|
|
33
|
-
* @param payload - 已序列化的请求体(含 Action/Version/Region 公共参数)。
|
|
34
|
-
* @param timestamp - 签名时间戳(秒)。
|
|
35
|
-
* @returns Authorization 头的值。
|
|
36
|
-
*/
|
|
37
|
-
export declare function tc3Authorization(secretId: string, secretKey: string, payload: string, timestamp: number): string;
|
|
38
|
-
/**
|
|
39
|
-
* 在套餐余量对象里防御性提取「剩余额度」:官方 SubPackageBalance/PackageInfo
|
|
40
|
-
* 的字段名未稳定公开(issue #18 调研期),按语义键名扫描——命中 remaining /
|
|
41
|
-
* balance / left 语义键直接用;命中 total 与 used 则相减推导。数字一律经
|
|
42
|
-
* {@link toNumber} 归一化(上游可能给字符串)。
|
|
43
|
-
* 导出供测试:纯函数。
|
|
44
|
-
* @param source - 套餐详情里的余量对象(PackageInfo / SubPackageBalance 等)。
|
|
45
|
-
* @returns 剩余额度(上游单位,通常为 token 数或元);提取不到返回 undefined。
|
|
46
|
-
*/
|
|
47
|
-
export declare function pickRemainingQuota(source: unknown): number | undefined;
|
|
48
28
|
/**
|
|
49
29
|
* 按 extract 规则从响应 JSON 求值。导出供测试:纯函数。
|
|
50
30
|
* @param rule - 提取规则(const / path / add / subtract / divide)。
|
|
@@ -52,12 +32,6 @@ export declare function pickRemainingQuota(source: unknown): number | undefined;
|
|
|
52
32
|
* @returns 数值;取不到或结果非有限数返回 undefined。
|
|
53
33
|
*/
|
|
54
34
|
export declare function evalExtract(rule: CustomBalanceExtract, data: unknown): number | undefined;
|
|
55
|
-
/**
|
|
56
|
-
* 请求头占位符解析:值中任意位置的 `{{ENV_NAME}}` 经凭据 seam 替换(如
|
|
57
|
-
* `Bearer {{KEY}}`、`token={{KEY}}`、一处多占位符);被引用的任一凭据缺失
|
|
58
|
-
* 或为空 → 返回 null(fail-closed,与完整占位符形态的历史语义一致)。
|
|
59
|
-
*/
|
|
60
|
-
export declare function resolveHeaders(ctx: Context, headers: Record<string, string>): Promise<Record<string, string> | null>;
|
|
61
35
|
/**
|
|
62
36
|
* 查询自定义 Provider 余额(插件 config 的 `customBalances`)。每个条目独立
|
|
63
37
|
* 成败:占位符凭据缺失 → unconfigured;401/403 → unauthorized;网络或提取
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PerfPanel: per-model latency/perf table + per-hour
|
|
2
|
+
* PerfPanel: per-model latency/perf table + per-hour TTFT/generation-speed curve.
|
|
3
3
|
*
|
|
4
4
|
* Reads the optional `perf` field of the usage-stats document (aggregated by
|
|
5
5
|
* the host from session logs). Renders a per-model table of TTFT mean/P50/P90,
|
|
6
6
|
* generation speed, total latency and estimated-step count, plus a small
|
|
7
|
-
* dependency-free SVG hourly curve
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Absent `perf` renders an empty state; the panel never fabricates samples.
|
|
7
|
+
* dependency-free SVG twin-series hourly curve (TTFT in ms on the left axis,
|
|
8
|
+
* tokens/s on the right). Absent `perf` (older snapshot or stream-less logs)
|
|
9
|
+
* renders an empty state; the panel never fabricates samples.
|
|
11
10
|
*/
|
|
12
11
|
import type { TrendSeriesModel } from './TrendChart.tsx';
|
|
13
12
|
import type { UsageBillingKey } from './locales.ts';
|
|
@@ -25,8 +24,8 @@ export interface PerfModelData {
|
|
|
25
24
|
latencyAvg: number;
|
|
26
25
|
estimatedSamples: number;
|
|
27
26
|
}
|
|
28
|
-
/**
|
|
29
|
-
export interface
|
|
27
|
+
/** 每小时性能统计(与服务端 `HourPerf` 同形)。 */
|
|
28
|
+
export interface PerfHourData {
|
|
30
29
|
samples: number;
|
|
31
30
|
ttftAvg: number;
|
|
32
31
|
tpsAvg?: number;
|
|
@@ -34,13 +33,12 @@ export interface PerfHourModelData {
|
|
|
34
33
|
/** 性能指标文档(服务端可选 `perf` 字段;旧快照缺失)。 */
|
|
35
34
|
export interface ClientPerf {
|
|
36
35
|
byModel: Record<string, PerfModelData>;
|
|
37
|
-
|
|
38
|
-
byHourModel: Record<string, Record<string, PerfHourModelData>>;
|
|
36
|
+
byHour: Record<string, PerfHourData>;
|
|
39
37
|
}
|
|
40
38
|
/**
|
|
41
39
|
* Render the performance panel.
|
|
42
40
|
* @param props.perf - the optional perf doc; `undefined`/empty renders an empty state.
|
|
43
|
-
* @param props.models - model legend (key/name/color) for the table swatches and curve
|
|
41
|
+
* @param props.models - model legend (key/name/color) for the table swatches and curve legend.
|
|
44
42
|
* @param props.t - locale function.
|
|
45
43
|
*/
|
|
46
44
|
export declare function PerfPanel({ perf, models, t, }: {
|
|
@@ -1,73 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TokenPanel: 「Token」分区——把 token 从费用里独立出来洞察。
|
|
3
3
|
* 四个板块 + 导出,全部由 `UsageStats` 派生,服务端零改动:
|
|
4
|
-
* 1. 每日 Token
|
|
5
|
-
*
|
|
6
|
-
* - 按模型:每天按模型堆叠,段 = 该模型当日「模型总 Token」(输入[含命中] + 输出),
|
|
7
|
-
* 分色复用趋势页 `chartModels`(同模型同色跨页一致);
|
|
8
|
-
* 旧快照缺 `byDayModels` 时隐藏切换钮,仅保留结构视角。
|
|
9
|
-
* 悬停显示当日精确明细;图例 / 「模型 Token」表格行可点击聚焦单个模型
|
|
10
|
-
* (目标段保持原色,其余段弱化,y 轴不变)。
|
|
11
|
-
* 归因边界:reasoning 无按日 × 模型明细(`byDayModels` 无该字段),图与导出不含此列。
|
|
12
|
-
* 2. 模型 Token 总量排行 + 占比(行点击 = 聚焦该模型并切到按模型视角);
|
|
4
|
+
* 1. 每日 Token 堆叠趋势(未命中输入 / 缓存命中 / 输出[含 reasoning]),7/30 天切换;
|
|
5
|
+
* 2. 模型 Token 总量排行 + 占比;
|
|
13
6
|
* 3. Token 结构 KPI(缓存命中率 / reasoning 占比 / 输入:输出比 / 峰值日)+ 显式缓存写入;
|
|
14
7
|
* 4. 工具调用排行(byTool 计次;token 无法按工具归因)。
|
|
15
8
|
*/
|
|
16
9
|
import type { UsageBillingKey } from './locales.ts';
|
|
17
|
-
import type { TrendSeriesModel } from './TrendChart.tsx';
|
|
18
10
|
import type { UsageStats } from './UsageBilling.tsx';
|
|
19
|
-
/** 每日 token 堆叠图元(按结构视角)。 */
|
|
20
|
-
interface DailyBucket {
|
|
21
|
-
date: string;
|
|
22
|
-
miss: number;
|
|
23
|
-
hit: number;
|
|
24
|
-
output: number;
|
|
25
|
-
reasoning: number;
|
|
26
|
-
}
|
|
27
|
-
/** 按模型视角下某模型某日的 token 明细(`byDayModels` 无 reasoning 列)。 */
|
|
28
|
-
interface ModelDayCell {
|
|
29
|
-
hit: number;
|
|
30
|
-
miss: number;
|
|
31
|
-
output: number;
|
|
32
|
-
/** 命中 + 未命中 + 输出(= 模型总 Token 口径)。 */
|
|
33
|
-
total: number;
|
|
34
|
-
}
|
|
35
|
-
/** 按模型视角的一天:各模型段 + 当日合计。 */
|
|
36
|
-
interface DayModelsRow {
|
|
37
|
-
date: string;
|
|
38
|
-
models: Record<string, ModelDayCell>;
|
|
39
|
-
total: number;
|
|
40
|
-
}
|
|
41
|
-
/** 模型 token 行。 */
|
|
42
|
-
interface ModelTokenRow {
|
|
43
|
-
key: string;
|
|
44
|
-
name: string;
|
|
45
|
-
input: number;
|
|
46
|
-
output: number;
|
|
47
|
-
reasoning: number;
|
|
48
|
-
calls: number;
|
|
49
|
-
cacheHit: number;
|
|
50
|
-
cacheMiss: number;
|
|
51
|
-
cacheHitRate: number;
|
|
52
|
-
total: number;
|
|
53
|
-
/** 占总 token 比例(0..1)。 */
|
|
54
|
-
share: number;
|
|
55
|
-
}
|
|
56
|
-
/** 导出全量 JSON 文档:按日结构 + 模型排行 + 按日×模型明细 + 总量(无 reasoning 列)。 */
|
|
57
|
-
export declare function tokenDailyJson(days: readonly DailyBucket[], models: readonly ModelTokenRow[], dayModels: readonly DayModelsRow[], total: UsageStats['total']): string;
|
|
58
11
|
/**
|
|
59
12
|
* Token 洞察面板。
|
|
60
|
-
* @param props.stats - usage-stats 文档(byDay/byModel/
|
|
13
|
+
* @param props.stats - usage-stats 文档(byDay/byModel/total)。
|
|
61
14
|
* @param props.trendDays - 每日 token 窗口(7/30 天)。
|
|
62
15
|
* @param props.onTrendDays - 切换趋势窗口。
|
|
63
|
-
* @param props.models - 趋势页同款模型图例(key/name/色):按模型视角的分色来源;缺省用兜底灰。
|
|
64
16
|
*/
|
|
65
17
|
export declare function TokenPanel(props: {
|
|
66
18
|
stats: UsageStats;
|
|
67
19
|
trendDays: 7 | 30;
|
|
68
20
|
onTrendDays: (d: 7 | 30) => void;
|
|
69
|
-
models?: readonly TrendSeriesModel[];
|
|
70
21
|
t: (key: UsageBillingKey) => string;
|
|
71
22
|
}): React.ReactNode;
|
|
72
|
-
export {};
|
|
73
23
|
//# sourceMappingURL=TokenPanel.d.ts.map
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*(billing.dashboard.decor)并注册计费指标服务(ctx.billingMetrics),主题
|
|
11
11
|
* 插件主动注入装饰视觉、消费费用数据——billing 不反向依赖任何主题包。
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
13
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
14
14
|
import { type UsageBillingKey } from './locales.ts';
|
|
15
15
|
import { type BillingMetricsService } from './billing-service.ts';
|
|
16
16
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
@@ -45,18 +45,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
45
45
|
usageBilling: UsageBillingKey;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
/** Required services for the usage billing surface.
|
|
49
|
-
*
|
|
50
|
-
* `remote.llm` is deliberately NOT injected: the LLM remote namespace only
|
|
51
|
-
* exists on 0.1.2-alpha.1 hosts, and a pending injection blocks the whole
|
|
52
|
-
* web boot on older hosts. `checkModels` probes `ctx.remote.llm` defensively
|
|
53
|
-
* instead — on hosts without it the model health check degrades to the
|
|
54
|
-
* static catalog result (the surrounding try/catch already swallows the
|
|
55
|
-
* undefined access). */
|
|
48
|
+
/** Required services for the usage billing surface. */
|
|
56
49
|
export declare const inject: string[];
|
|
57
50
|
/**
|
|
58
51
|
* Client plugin body: the UsageBilling entry in the sidebar footer.
|
|
59
52
|
* @param ctx - client root context.
|
|
60
53
|
*/
|
|
61
|
-
export declare function apply(ctx:
|
|
54
|
+
export declare function apply(ctx: ClientContext): void;
|
|
62
55
|
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* store 引擎持久化到 localStorage(persist key 即存储身份),重启后保留。
|
|
6
6
|
* 宿主 Config 的 monthlyBudget 仅作为金额未设置时的默认值,用户输入优先。
|
|
7
7
|
*/
|
|
8
|
-
import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-
|
|
8
|
+
import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
9
|
/** 预算偏好状态。 */
|
|
10
10
|
export interface BudgetPrefsState {
|
|
11
11
|
/** 预算条开关:关 = 只显示标题行与开关,不显示进度。 */
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 可选:跨 tab 只保留一个提醒 leader(Web Locks 优先,降级 localStorage 租约),
|
|
6
6
|
* 避免多窗口同时弹同一条。配置持久化在 localStorage(默认关闭,用户在面板设置开启)。
|
|
7
7
|
*/
|
|
8
|
-
import type { SessionListState } from '@deepseek-ai/dsh-
|
|
9
|
-
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-
|
|
8
|
+
import type { SessionListState } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
|
+
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
10
|
/** 配置持久化 key:开启/关闭 + 提醒持续模式(0=常驻,其余=秒后自动关)。 */
|
|
11
11
|
export declare const COMPLETION_NOTIFY_KEY = "dsh-billing-completion-notify-v1";
|
|
12
12
|
export interface CompletionNotifyConfig {
|
|
@@ -32,6 +32,6 @@ export declare function UsageHeatmap({ days, currency, now, t, range }: {
|
|
|
32
32
|
currency: CostCurrency;
|
|
33
33
|
now?: Date;
|
|
34
34
|
range?: 'month' | 'year';
|
|
35
|
-
t: (key: 'costAbbr' | 'noData' | 'heatmapLess' | 'heatmapMore') => string;
|
|
35
|
+
t: (key: 'billing.costAbbr' | 'billing.noData' | 'billing.heatmapLess' | 'billing.heatmapMore') => string;
|
|
36
36
|
}): React.ReactNode;
|
|
37
37
|
//# sourceMappingURL=heatmap.d.ts.map
|