dsh-key-usage 0.1.2 → 0.1.4
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.md +115 -24
- package/client/client.js +2793 -419
- package/client/client.js.map +1 -1
- package/docs/images/usage-daily.png +0 -0
- package/docs/images/usage-overview.png +0 -0
- package/docs/images/usage-quota.png +0 -0
- package/docs/images//346/214/211/346/250/241/345/236/213.png +0 -0
- package/docs/images//351/241/271/347/233/256/346/200/273/351/207/217.png +0 -0
- package/lib/index.js +73 -4
- package/lib/live-quota.js +96 -30
- package/lib/merge.js +8 -1
- package/lib/pricing-catalog.js +349 -0
- package/lib/pricing.js +500 -0
- package/lib/quota-snapshots.js +68 -0
- package/lib/routes.js +275 -28
- package/lib/session-usage.js +67 -46
- package/lib/sessions.js +98 -8
- package/package.json +18 -9
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -1,18 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-key-usage
|
|
3
|
-
*
|
|
2
|
+
* dsh-key-usage 服务端入口:声明计费规则配置项、挂载只读 HTTP 路由,
|
|
3
|
+
* 向客户端提供用量台账、通道配置、余额快照与会话用量的聚合数据。
|
|
4
4
|
*
|
|
5
5
|
* 数据来源:
|
|
6
6
|
* - 台账 / 快照 / 通道配置:本地文件只读;
|
|
7
7
|
* - 项目、会话、按小时:宿主 sessionPersistence 的只读 handle;
|
|
8
8
|
* - 余额与订阅窗口:优先查各通道官方接口(凭据经宿主 credentials seam 解析,
|
|
9
|
-
*
|
|
9
|
+
* 不落盘、不进日志、不返回给客户端),失败时回落到本地快照;
|
|
10
|
+
* - 计费规则:宿主设置服务(宿主负责落盘,本插件不写任何文件)。
|
|
10
11
|
*/
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
13
|
import { mountKeyUsageRoutes } from './routes.js';
|
|
12
14
|
import { createLiveQuotaService } from './live-quota.js';
|
|
13
15
|
import { createSessionSource } from './sessions.js';
|
|
16
|
+
import { EMPTY_PRICING, normalizePricing } from './pricing.js';
|
|
14
17
|
export const name = 'dsh-key-usage';
|
|
15
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 计费规则存在宿主设置服务的这个命名空间里。
|
|
20
|
+
*
|
|
21
|
+
* 宿主的「设置」服务并不维护独立存储:命名空间就是 profile 里**本条目的 id**
|
|
22
|
+
* (`cordis.patch.yml` 的 `insert.id`,与本包同名),字段必须用 `.volatile()` 标记才会被
|
|
23
|
+
* 列出;写入由宿主的 config-editor 落到当前 profile 的 cordis patch 上并原地生效,
|
|
24
|
+
* 因此本插件不写文件、也不需要重启宿主。
|
|
25
|
+
*/
|
|
26
|
+
const SETTINGS_NAMESPACE = 'dsh-key-usage';
|
|
27
|
+
/** 规则的 Config 形状;数值全部可改,默认空规则 = 沿用台账反推均价(与升级前行为一致)。 */
|
|
28
|
+
const RulesSchema = z.array(z.object({
|
|
29
|
+
provider: z.string().default('*'),
|
|
30
|
+
model: z.string().default('*'),
|
|
31
|
+
unit: z.union(['perMillionTokens', 'perThousandTokens', 'perCall']).default('perMillionTokens'),
|
|
32
|
+
currency: z.string().default('CNY'),
|
|
33
|
+
input: z.number().default(0),
|
|
34
|
+
cacheRead: z.number().default(0),
|
|
35
|
+
cacheWrite: z.number().default(0),
|
|
36
|
+
output: z.number().default(0),
|
|
37
|
+
offPeak: z.object({
|
|
38
|
+
// `days`(ISO 1=周一 … 7=周日)缺省 = 每天适用;加它之前保存的规则没有这个字段,语义必须不变
|
|
39
|
+
windows: z.array(z.object({
|
|
40
|
+
from: z.string(),
|
|
41
|
+
to: z.string(),
|
|
42
|
+
days: z.array(z.number()).default([]),
|
|
43
|
+
})).default([]),
|
|
44
|
+
input: z.number().default(0),
|
|
45
|
+
cacheRead: z.number().default(0),
|
|
46
|
+
cacheWrite: z.number().default(0),
|
|
47
|
+
output: z.number().default(0),
|
|
48
|
+
}),
|
|
49
|
+
note: z.string().default(''),
|
|
50
|
+
}));
|
|
51
|
+
export const Config = z.object({
|
|
52
|
+
pricing: z.object({
|
|
53
|
+
version: z.number().default(1),
|
|
54
|
+
currency: z.string().default('CNY'),
|
|
55
|
+
rules: RulesSchema.default([]),
|
|
56
|
+
}).default(EMPTY_PRICING).volatile(),
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* 读当前规则:优先问宿主设置服务的实时投影(用户改完立即生效,无需重启宿主),
|
|
60
|
+
* 服务缺失(非桌面 / 未组配 dsh-settings)时退回插件自己的 Config 引用。
|
|
61
|
+
*/
|
|
62
|
+
function readPricing(ctx, configRef) {
|
|
63
|
+
const settings = (typeof ctx.get === 'function' ? ctx.get('settings') : undefined);
|
|
64
|
+
if (settings !== undefined && typeof settings.describe === 'function') {
|
|
65
|
+
try {
|
|
66
|
+
const rows = settings.describe({ redactSecrets: false });
|
|
67
|
+
const row = Array.isArray(rows) ? rows.find((candidate) => candidate?.ns === SETTINGS_NAMESPACE) : undefined;
|
|
68
|
+
if (row !== undefined) {
|
|
69
|
+
return {
|
|
70
|
+
config: normalizePricing(row.value),
|
|
71
|
+
writable: true,
|
|
72
|
+
revision: typeof row.revision === 'number' ? row.revision : null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// 设置服务尚未就绪(组合中或已停用):静默退回本地 Config
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { config: normalizePricing(configRef), writable: false, revision: null };
|
|
81
|
+
}
|
|
82
|
+
export function apply(ctx, config) {
|
|
16
83
|
// 会话服务是可选依赖:用 get 惰性取,缺了也只是会话相关页面显示空态
|
|
17
84
|
const sessionSource = createSessionSource({
|
|
18
85
|
get: (serviceName) => (typeof ctx.get === 'function' ? ctx.get(serviceName) : undefined),
|
|
@@ -40,6 +107,8 @@ export function apply(ctx) {
|
|
|
40
107
|
host.effect(() => mountKeyUsageRoutes(host.webServer, sessionSource, liveQuota, {
|
|
41
108
|
// workspaceRegistry 用来读官方归档集合(拿不到就回退本地 storages/workspace.json)
|
|
42
109
|
get: (serviceName) => (typeof ctx.get === 'function' ? ctx.get(serviceName) : undefined),
|
|
110
|
+
}, {
|
|
111
|
+
current: () => readPricing(ctx, config?.pricing),
|
|
43
112
|
}), 'dsh-key-usage: http routes');
|
|
44
113
|
});
|
|
45
114
|
}
|
package/lib/live-quota.js
CHANGED
|
@@ -11,7 +11,62 @@ function textOf(value) {
|
|
|
11
11
|
return String(value);
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/** 金额字段:官方既有字符串(DeepSeek)也有数字(Moonshot),两种都接受,统一输出原样字符串。 */
|
|
15
|
+
function amountTextOf(value) {
|
|
16
|
+
if (typeof value === 'number')
|
|
17
|
+
return Number.isFinite(value) ? String(value) : null;
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed !== '' && Number.isFinite(Number(trimmed)) ? trimmed : null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** 金额 / 百分比的数值形态:接受 number 与数字字符串,其它一律 null。 */
|
|
25
|
+
function amountOf(value) {
|
|
26
|
+
const text = amountTextOf(value);
|
|
27
|
+
if (text === null)
|
|
28
|
+
return null;
|
|
29
|
+
const parsed = Number(text);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
/** epoch → ISO:13 位按毫秒、10 位按秒;越界返回 null。 */
|
|
33
|
+
function isoFromEpoch(value) {
|
|
34
|
+
const milliseconds = value >= 1e11 || value < 1e9 ? value : value * 1000;
|
|
35
|
+
const date = new Date(milliseconds);
|
|
36
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
37
|
+
}
|
|
38
|
+
/** 重置时间:官方为 ISO 字符串,兼容 epoch 毫秒 / 秒(数字或纯数字字符串);识别不出时原样保留。 */
|
|
39
|
+
function resetAtOf(value) {
|
|
40
|
+
if (typeof value === 'number')
|
|
41
|
+
return Number.isFinite(value) ? isoFromEpoch(value) : null;
|
|
42
|
+
if (typeof value !== 'string' || value === '')
|
|
43
|
+
return null;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
if (/^\d{9,14}$/.test(trimmed))
|
|
46
|
+
return isoFromEpoch(Number(trimmed));
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/** 接口没返回币种时的兜底:按站点判定(Z.AI 海外站按美元计价,BigModel / Moonshot 国内站按人民币)。 */
|
|
50
|
+
function currencyFromHost(requestUrl, fallback) {
|
|
51
|
+
let host = '';
|
|
52
|
+
try {
|
|
53
|
+
host = new URL(requestUrl).hostname.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
if (host === 'z.ai' || host.endsWith('.z.ai'))
|
|
59
|
+
return 'USD';
|
|
60
|
+
if (host === 'bigmodel.cn' || host.endsWith('.bigmodel.cn'))
|
|
61
|
+
return 'CNY';
|
|
62
|
+
if (host === 'moonshot.cn' || host.endsWith('.moonshot.cn'))
|
|
63
|
+
return 'CNY';
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* OpenCode Go:GET https://opencode.ai/zen/go/v1/usage → usage.rolling / weekly / monthly。
|
|
68
|
+
* 该端点未收录于官方文档(opencode.ai/docs/go 只指向 console),字段结构取自社区实现,见 test/fixtures/providers/opencode-go。
|
|
69
|
+
*/
|
|
15
70
|
const OPENCODE_GO_SCHEME = {
|
|
16
71
|
url: () => 'https://opencode.ai/zen/go/v1/usage',
|
|
17
72
|
parse: (body) => {
|
|
@@ -24,13 +79,14 @@ const OPENCODE_GO_SCHEME = {
|
|
|
24
79
|
const raw = usage[sourceKey];
|
|
25
80
|
if (!isObject(raw))
|
|
26
81
|
continue;
|
|
27
|
-
|
|
28
|
-
|
|
82
|
+
// 社区实现里 percent 可能是字符串、resetsAt 可能是 epoch,两者都按漂移兼容处理
|
|
83
|
+
const percent = amountOf(raw.percent);
|
|
84
|
+
if (percent === null)
|
|
29
85
|
continue;
|
|
30
86
|
windows.push({
|
|
31
87
|
key: windowKey,
|
|
32
88
|
percent,
|
|
33
|
-
resetsAt:
|
|
89
|
+
resetsAt: resetAtOf(raw.resetsAt),
|
|
34
90
|
status: typeof raw.status === 'string' ? raw.status : null,
|
|
35
91
|
});
|
|
36
92
|
}
|
|
@@ -41,22 +97,26 @@ const OPENCODE_GO_SCHEME = {
|
|
|
41
97
|
function balanceScheme(path, parse, fallbackOrigin) {
|
|
42
98
|
return {
|
|
43
99
|
url: (baseURL) => new URL(path, baseURL ?? fallbackOrigin).href,
|
|
44
|
-
parse: (body) => ({ balance: isObject(body) ? parse(body) : null, windows: null }),
|
|
100
|
+
parse: (body, requestUrl) => ({ balance: isObject(body) ? parse(body, requestUrl) : null, windows: null }),
|
|
45
101
|
};
|
|
46
102
|
}
|
|
47
103
|
const SCHEMES = {
|
|
48
104
|
'opencode-go': OPENCODE_GO_SCHEME,
|
|
49
105
|
// DeepSeek: GET {origin}/user/balance → balance_infos[{currency,total_balance,granted_balance,topped_up_balance}]
|
|
50
106
|
'deepseek-official': balanceScheme('/user/balance', (body) => {
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
107
|
+
// 文档 balance_infos[].currency 只可能是 CNY / USD:没有 CNY 项时退回第一项,并如实保留它的币种
|
|
108
|
+
const infos = (Array.isArray(body.balance_infos) ? body.balance_infos : []).filter(isObject);
|
|
109
|
+
const info = infos.find((entry) => (textOf(entry.currency) ?? '').toUpperCase() === 'CNY') ?? infos[0];
|
|
110
|
+
if (info === undefined)
|
|
111
|
+
return null;
|
|
112
|
+
const total = amountTextOf(info.total_balance);
|
|
113
|
+
const granted = amountTextOf(info.granted_balance);
|
|
114
|
+
const toppedUp = amountTextOf(info.topped_up_balance);
|
|
115
|
+
// 三个金额全缺说明结构已经漂移,判 bad-json,不返回「只有币种」的空壳成功
|
|
116
|
+
if (total === null && granted === null && toppedUp === null)
|
|
54
117
|
return null;
|
|
55
118
|
const balance = {};
|
|
56
119
|
const currency = textOf(info.currency);
|
|
57
|
-
const total = textOf(info.total_balance);
|
|
58
|
-
const granted = textOf(info.granted_balance);
|
|
59
|
-
const toppedUp = textOf(info.topped_up_balance);
|
|
60
120
|
if (currency !== null)
|
|
61
121
|
balance.currency = currency;
|
|
62
122
|
if (total !== null)
|
|
@@ -65,29 +125,31 @@ const SCHEMES = {
|
|
|
65
125
|
balance.giftBalance = granted;
|
|
66
126
|
if (toppedUp !== null)
|
|
67
127
|
balance.topUpBalance = toppedUp;
|
|
68
|
-
return
|
|
128
|
+
return balance;
|
|
69
129
|
}, 'https://api.deepseek.com'),
|
|
70
130
|
// OpenRouter: GET {origin}/api/v1/credits(需要 Management Key)
|
|
71
131
|
openrouter: balanceScheme('/api/v1/credits', (body) => {
|
|
72
132
|
const data = isObject(body.data) ? body.data : null;
|
|
73
133
|
if (data === null)
|
|
74
134
|
return null;
|
|
75
|
-
const totalCredits =
|
|
76
|
-
const totalUsage =
|
|
135
|
+
const totalCredits = amountOf(data.total_credits);
|
|
136
|
+
const totalUsage = amountOf(data.total_usage);
|
|
77
137
|
if (totalCredits === null || totalUsage === null)
|
|
78
138
|
return null;
|
|
79
139
|
return { currency: 'USD', totalBalance: (totalCredits - totalUsage).toFixed(2) };
|
|
80
140
|
}, 'https://openrouter.ai'),
|
|
81
|
-
// Moonshot / Kimi: GET {origin}/v1/users/me/balance
|
|
82
|
-
moonshotai: balanceScheme('/v1/users/me/balance', (body) => {
|
|
141
|
+
// Moonshot / Kimi: GET {origin}/v1/users/me/balance(文档未定义 currency 字段,国内站按人民币计)
|
|
142
|
+
moonshotai: balanceScheme('/v1/users/me/balance', (body, requestUrl) => {
|
|
83
143
|
const data = isObject(body.data) ? body.data : null;
|
|
84
144
|
if (data === null)
|
|
85
145
|
return null;
|
|
146
|
+
const available = amountTextOf(data.available_balance);
|
|
147
|
+
const cash = amountTextOf(data.cash_balance);
|
|
148
|
+
const voucher = amountTextOf(data.voucher_balance);
|
|
149
|
+
if (available === null && cash === null && voucher === null)
|
|
150
|
+
return null;
|
|
86
151
|
const balance = {};
|
|
87
|
-
const currency = textOf(data.currency);
|
|
88
|
-
const available = textOf(data.available_balance);
|
|
89
|
-
const cash = textOf(data.cash_balance);
|
|
90
|
-
const voucher = textOf(data.voucher_balance);
|
|
152
|
+
const currency = textOf(data.currency) ?? currencyFromHost(requestUrl, null);
|
|
91
153
|
if (currency !== null)
|
|
92
154
|
balance.currency = currency;
|
|
93
155
|
if (available !== null)
|
|
@@ -96,21 +158,23 @@ const SCHEMES = {
|
|
|
96
158
|
balance.giftBalance = voucher;
|
|
97
159
|
if (cash !== null)
|
|
98
160
|
balance.topUpBalance = cash;
|
|
99
|
-
return
|
|
161
|
+
return balance;
|
|
100
162
|
}, 'https://api.moonshot.cn'),
|
|
101
|
-
// Z.AI / GLM: GET {origin}/api/paas/v4/balance
|
|
102
|
-
zai: balanceScheme('/api/paas/v4/balance', (body) => {
|
|
163
|
+
// Z.AI / GLM: GET {origin}/api/paas/v4/balance(官方文档未收录,接口自带 currency 时才采信)
|
|
164
|
+
zai: balanceScheme('/api/paas/v4/balance', (body, requestUrl) => {
|
|
103
165
|
const data = isObject(body.data) ? body.data : null;
|
|
104
166
|
if (data === null)
|
|
105
167
|
return null;
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
168
|
+
const total = amountTextOf(data.total_balance);
|
|
169
|
+
const available = amountTextOf(data.available_balance);
|
|
170
|
+
if (total === null && available === null)
|
|
171
|
+
return null;
|
|
172
|
+
const balance = { currency: textOf(data.currency) ?? currencyFromHost(requestUrl, 'CNY') };
|
|
109
173
|
if (total !== null)
|
|
110
174
|
balance.totalBalance = total;
|
|
111
175
|
if (available !== null)
|
|
112
176
|
balance.availableBalance = available;
|
|
113
|
-
return
|
|
177
|
+
return balance;
|
|
114
178
|
}, 'https://api.z.ai'),
|
|
115
179
|
};
|
|
116
180
|
/** provider id → 方案 id(事件里的 provider 可能是路由名,先取末段)。 */
|
|
@@ -133,7 +197,8 @@ async function queryOne(schemeId, apiKey, baseURL, fetchImpl) {
|
|
|
133
197
|
const controller = new AbortController();
|
|
134
198
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
135
199
|
try {
|
|
136
|
-
const
|
|
200
|
+
const requestUrl = scheme.url(baseURL);
|
|
201
|
+
const response = await fetchImpl(requestUrl, {
|
|
137
202
|
headers: { Authorization: 'Bearer ' + apiKey, Accept: 'application/json' },
|
|
138
203
|
signal: controller.signal,
|
|
139
204
|
});
|
|
@@ -142,7 +207,8 @@ async function queryOne(schemeId, apiKey, baseURL, fetchImpl) {
|
|
|
142
207
|
if (!response.ok)
|
|
143
208
|
return { balance: null, windows: null, error: 'http-' + String(response.status) };
|
|
144
209
|
const raw = await response.text();
|
|
145
|
-
|
|
210
|
+
// 上限按**字节**算:raw.length 是 UTF-16 码元数,中文响应会因此超限仍被接受
|
|
211
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_RESPONSE_BYTES)
|
|
146
212
|
return { balance: null, windows: null, error: 'too-large' };
|
|
147
213
|
let body;
|
|
148
214
|
try {
|
|
@@ -151,7 +217,7 @@ async function queryOne(schemeId, apiKey, baseURL, fetchImpl) {
|
|
|
151
217
|
catch {
|
|
152
218
|
return { balance: null, windows: null, error: 'bad-json' };
|
|
153
219
|
}
|
|
154
|
-
const parsed = scheme.parse(body);
|
|
220
|
+
const parsed = scheme.parse(body, requestUrl);
|
|
155
221
|
if (parsed.balance === null && parsed.windows === null)
|
|
156
222
|
return { balance: null, windows: null, error: 'bad-json' };
|
|
157
223
|
return { ...parsed, error: null };
|
package/lib/merge.js
CHANGED
|
@@ -14,6 +14,7 @@ export function sessionDayRows(records, knownProviders) {
|
|
|
14
14
|
inputTokens: model.buckets.inputTokens,
|
|
15
15
|
outputTokens: model.buckets.outputTokens,
|
|
16
16
|
cacheReadTokens: model.buckets.cacheReadTokens,
|
|
17
|
+
cacheWriteTokens: model.buckets.cacheWriteTokens,
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -44,11 +45,13 @@ function synthesizeDay(date, rows) {
|
|
|
44
45
|
let inputTokens = 0;
|
|
45
46
|
let outputTokens = 0;
|
|
46
47
|
let cacheReadTokens = 0;
|
|
48
|
+
let cacheWriteTokens = 0;
|
|
47
49
|
for (const row of rows) {
|
|
48
50
|
calls += row.calls;
|
|
49
51
|
inputTokens += row.inputTokens;
|
|
50
52
|
outputTokens += row.outputTokens;
|
|
51
53
|
cacheReadTokens += row.cacheReadTokens;
|
|
54
|
+
cacheWriteTokens += row.cacheWriteTokens;
|
|
52
55
|
const key = row.provider + '|' + row.model;
|
|
53
56
|
const entry = byModel.get(key);
|
|
54
57
|
if (entry === undefined) {
|
|
@@ -60,6 +63,7 @@ function synthesizeDay(date, rows) {
|
|
|
60
63
|
inputTokens: row.inputTokens,
|
|
61
64
|
outputTokens: row.outputTokens,
|
|
62
65
|
cacheReadTokens: row.cacheReadTokens,
|
|
66
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
63
67
|
reasoningTokens: 0,
|
|
64
68
|
// 会话事件没有官方成本,成本列对这些天留 0,由界面按计费模式展示
|
|
65
69
|
cost: 0,
|
|
@@ -70,6 +74,7 @@ function synthesizeDay(date, rows) {
|
|
|
70
74
|
entry.inputTokens += row.inputTokens;
|
|
71
75
|
entry.outputTokens += row.outputTokens;
|
|
72
76
|
entry.cacheReadTokens += row.cacheReadTokens;
|
|
77
|
+
entry.cacheWriteTokens += row.cacheWriteTokens;
|
|
73
78
|
}
|
|
74
79
|
const models = [...byModel.values()];
|
|
75
80
|
return {
|
|
@@ -79,6 +84,7 @@ function synthesizeDay(date, rows) {
|
|
|
79
84
|
inputTokens,
|
|
80
85
|
outputTokens,
|
|
81
86
|
cacheReadTokens,
|
|
87
|
+
cacheWriteTokens,
|
|
82
88
|
reasoningTokens: 0,
|
|
83
89
|
cost: 0,
|
|
84
90
|
models: models.sort((first, second) => (second.inputTokens + second.outputTokens + second.cacheReadTokens)
|
|
@@ -120,12 +126,13 @@ export function mergeDailySeries(ledgerDays, sessionRows, from, to) {
|
|
|
120
126
|
}
|
|
121
127
|
/** 合并后的范围总计(展示口径 tokens 不含 cacheWrite)。 */
|
|
122
128
|
export function totalsOfMerged(days) {
|
|
123
|
-
const totals = { date: '', calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, reasoningTokens: 0, cost: 0 };
|
|
129
|
+
const totals = { date: '', calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, reasoningTokens: 0, cost: 0 };
|
|
124
130
|
for (const day of days) {
|
|
125
131
|
totals.calls += day.calls;
|
|
126
132
|
totals.inputTokens += day.inputTokens;
|
|
127
133
|
totals.outputTokens += day.outputTokens;
|
|
128
134
|
totals.cacheReadTokens += day.cacheReadTokens;
|
|
135
|
+
totals.cacheWriteTokens += day.cacheWriteTokens;
|
|
129
136
|
totals.reasoningTokens += day.reasoningTokens;
|
|
130
137
|
totals.cost += day.cost;
|
|
131
138
|
}
|