@ponydev/dsh-model-usage 0.0.0-stage → 1.0.1
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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +119 -2
- package/assets/logos/anthropic.svg +1 -0
- package/assets/logos/deepseek-dark.svg +3 -0
- package/assets/logos/deepseek.svg +3 -0
- package/assets/logos/googlegemini.svg +1 -0
- package/assets/logos/openai.svg +1 -0
- package/cordis.patch.yml +6 -0
- package/docs/architecture.md +855 -0
- package/docs/screenshots/01-sidebar.png +0 -0
- package/docs/screenshots/02-modal.png +0 -0
- package/docs/screenshots/03-settings.png +0 -0
- package/lib/adapters.js +465 -0
- package/lib/client.js +1149 -0
- package/lib/index.js +308 -0
- package/lib/ledger.js +420 -0
- package/package.json +65 -4
package/lib/ledger.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// 请求账本:session 事件 → 按日聚合(按小时/供应商/模型)+ 逐条请求环形日志
|
|
2
|
+
// 口径:Tokens = input + output + cacheRead + cacheWrite(reasoning 已含在 output)
|
|
3
|
+
// 不做成本估算:订阅套餐不是按 API 计费,金额以服务商账单/余额为准。
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { mkdir, readFile, readdir, rename, stat as statFile, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
8
|
+
|
|
9
|
+
const RECENT_MAX = 300
|
|
10
|
+
const BACKFILL_DAYS = 30
|
|
11
|
+
|
|
12
|
+
export function dateKey(d = new Date()) {
|
|
13
|
+
const y = d.getFullYear()
|
|
14
|
+
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
15
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
16
|
+
return `${y}-${m}-${day}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dshHome() {
|
|
20
|
+
return process.env.DSH_HOME && process.env.DSH_HOME.trim() ? process.env.DSH_HOME.trim() : join(homedir(), '.dsh')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ledgerFilePath() {
|
|
24
|
+
return join(dshHome(), 'model-usage', 'ledger.json')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sessionsRoot() {
|
|
28
|
+
return join(dshHome(), 'sessions')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function emptyDay() {
|
|
32
|
+
return { hours: {}, providers: {} }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function usageOf(u) {
|
|
36
|
+
// 兼容两种写法:日志事件用 inputTokens/…,实时链路用 input/…
|
|
37
|
+
const out = {
|
|
38
|
+
input: Number(u.inputTokens != null ? u.inputTokens : u.input) || 0,
|
|
39
|
+
cacheRead: Number(u.cacheReadTokens != null ? u.cacheReadTokens : u.cacheRead) || 0,
|
|
40
|
+
cacheWrite: Number(u.cacheWriteTokens != null ? u.cacheWriteTokens : u.cacheWrite) || 0,
|
|
41
|
+
output: Number(u.outputTokens != null ? u.outputTokens : u.output) || 0,
|
|
42
|
+
}
|
|
43
|
+
out.total = out.input + out.cacheRead + out.cacheWrite + out.output
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function bumpBucket(bucket, usage) {
|
|
48
|
+
bucket.requests = (bucket.requests || 0) + 1
|
|
49
|
+
bucket.input = (bucket.input || 0) + (usage.input || 0)
|
|
50
|
+
bucket.cacheRead = (bucket.cacheRead || 0) + (usage.cacheRead || 0)
|
|
51
|
+
bucket.cacheWrite = (bucket.cacheWrite || 0) + (usage.cacheWrite || 0)
|
|
52
|
+
bucket.output = (bucket.output || 0) + (usage.output || 0)
|
|
53
|
+
bucket.tokens = (bucket.tokens || 0) + (usage.total || 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pushRecord(data, rec) {
|
|
57
|
+
if (!Array.isArray(data.recent)) data.recent = []
|
|
58
|
+
data.recent.push(rec)
|
|
59
|
+
if (data.recent.length > RECENT_MAX) data.recent.splice(0, data.recent.length - RECENT_MAX)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyRecord(data, { time, provider, model, usage, durationMs, ttftMs }) {
|
|
63
|
+
const u = usageOf(usage)
|
|
64
|
+
const d = new Date(time || Date.now())
|
|
65
|
+
const dayKey = dateKey(d)
|
|
66
|
+
const day = data.days[dayKey] || (data.days[dayKey] = emptyDay())
|
|
67
|
+
const hourBucket = day.hours[d.getHours()] || (day.hours[d.getHours()] = {})
|
|
68
|
+
const prov = day.providers[provider] || (day.providers[provider] = { models: {} })
|
|
69
|
+
const modelBucket = prov.models[model] || (prov.models[model] = {})
|
|
70
|
+
bumpBucket(day, u)
|
|
71
|
+
bumpBucket(hourBucket, u)
|
|
72
|
+
bumpBucket(prov, u)
|
|
73
|
+
bumpBucket(modelBucket, u)
|
|
74
|
+
if (!hourBucket.byProvider) hourBucket.byProvider = {}
|
|
75
|
+
const hp = hourBucket.byProvider[provider] || (hourBucket.byProvider[provider] = { tokens: 0, requests: 0 })
|
|
76
|
+
hp.tokens += u.total
|
|
77
|
+
hp.requests += 1
|
|
78
|
+
pushRecord(data, {
|
|
79
|
+
t: time || Date.now(),
|
|
80
|
+
p: provider,
|
|
81
|
+
m: model,
|
|
82
|
+
i: u.input,
|
|
83
|
+
o: u.output,
|
|
84
|
+
cr: u.cacheRead,
|
|
85
|
+
cw: u.cacheWrite,
|
|
86
|
+
dur: Number.isFinite(durationMs) ? durationMs : null,
|
|
87
|
+
ttft: Number.isFinite(ttftMs) ? ttftMs : null,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 从会话日志回填:完整重算 days/recent(日志为权威),完成后设 backfilled 标记 */
|
|
92
|
+
async function backfillFromLogs(data, root, days) {
|
|
93
|
+
const cutoff = Date.now() - days * 86400000
|
|
94
|
+
const files = []
|
|
95
|
+
async function walk(dir) {
|
|
96
|
+
let ents
|
|
97
|
+
try { ents = await readdir(dir, { withFileTypes: true }) } catch { return }
|
|
98
|
+
for (const e of ents) {
|
|
99
|
+
const p = join(dir, e.name)
|
|
100
|
+
if (e.isDirectory()) await walk(p)
|
|
101
|
+
else if (e.name.endsWith('.jsonl.zstd')) files.push(p)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await walk(root)
|
|
105
|
+
const tmp = { days: {}, recent: [] }
|
|
106
|
+
const recs = []
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
let text
|
|
109
|
+
try {
|
|
110
|
+
const st = await statFile(file)
|
|
111
|
+
if (st.mtimeMs < cutoff) continue
|
|
112
|
+
const buf = await readFile(file)
|
|
113
|
+
text = zstdDecompressSync(buf).toString('utf8')
|
|
114
|
+
} catch { continue }
|
|
115
|
+
let pendingAt = null
|
|
116
|
+
for (const line of text.split('\n')) {
|
|
117
|
+
if (!line.trim()) continue
|
|
118
|
+
let ev
|
|
119
|
+
try { ev = JSON.parse(line) } catch { continue }
|
|
120
|
+
const type = ev.type
|
|
121
|
+
const d = ev.data
|
|
122
|
+
if (!d) continue
|
|
123
|
+
if (type === 'step/start') { pendingAt = ev.time || null; continue }
|
|
124
|
+
if (type !== 'assistant/message') continue
|
|
125
|
+
if (!d.usage || !d.message || !d.message.source) continue
|
|
126
|
+
const t = ev.time || Date.now()
|
|
127
|
+
if (t < cutoff) continue
|
|
128
|
+
const stream = Array.isArray(d.stream) ? d.stream : []
|
|
129
|
+
const firstTime = stream.length && Number.isFinite(stream[0].time) ? stream[0].time : null
|
|
130
|
+
const ttft = pendingAt && firstTime && firstTime >= pendingAt ? firstTime - pendingAt : null
|
|
131
|
+
const dur = pendingAt ? Math.max(0, t - pendingAt) : null
|
|
132
|
+
applyRecord(tmp, {
|
|
133
|
+
time: t,
|
|
134
|
+
provider: d.message.source.provider,
|
|
135
|
+
model: d.message.source.model || '',
|
|
136
|
+
usage: d.usage,
|
|
137
|
+
durationMs: dur,
|
|
138
|
+
ttftMs: ttft,
|
|
139
|
+
})
|
|
140
|
+
pendingAt = null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
tmp.recent.sort((a, b) => a.t - b.t)
|
|
144
|
+
data.days = tmp.days
|
|
145
|
+
data.recent = tmp.recent.slice(-RECENT_MAX)
|
|
146
|
+
data.backfilled = true
|
|
147
|
+
data.backfillAt = Date.now()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function createLedger() {
|
|
151
|
+
const file = ledgerFilePath()
|
|
152
|
+
let data = { version: 1, days: {}, recent: [], obs: { last: {}, days: {} } }
|
|
153
|
+
let ready = null
|
|
154
|
+
let dirty = false
|
|
155
|
+
let flushTimer = null
|
|
156
|
+
let backfilling = false
|
|
157
|
+
const queue = []
|
|
158
|
+
|
|
159
|
+
async function load() {
|
|
160
|
+
try {
|
|
161
|
+
const text = await readFile(file, 'utf8')
|
|
162
|
+
const parsed = JSON.parse(text)
|
|
163
|
+
if (parsed && parsed.version === 1 && parsed.days) data = parsed
|
|
164
|
+
} catch {}
|
|
165
|
+
if (!Array.isArray(data.recent)) data.recent = []
|
|
166
|
+
if (!data.obs || typeof data.obs !== 'object') data.obs = { last: {}, days: {} }
|
|
167
|
+
if (!data.obs.last) data.obs.last = {}
|
|
168
|
+
if (!data.obs.days) data.obs.days = {}
|
|
169
|
+
prune()
|
|
170
|
+
}
|
|
171
|
+
function ensureLoaded() {
|
|
172
|
+
if (!ready) ready = load()
|
|
173
|
+
return ready
|
|
174
|
+
}
|
|
175
|
+
function prune() {
|
|
176
|
+
const cutoff = Date.now() - 120 * 86400000
|
|
177
|
+
for (const key of Object.keys(data.days)) {
|
|
178
|
+
const t = new Date(key + 'T00:00:00').getTime()
|
|
179
|
+
if (Number.isFinite(t) && t < cutoff) delete data.days[key]
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(data.recent)) data.recent = data.recent.filter((r) => r && r.t > cutoff).slice(-RECENT_MAX)
|
|
182
|
+
if (data.obs && data.obs.days) {
|
|
183
|
+
for (const key of Object.keys(data.obs.days)) {
|
|
184
|
+
const t = new Date(key + 'T00:00:00').getTime()
|
|
185
|
+
if (Number.isFinite(t) && t < cutoff) delete data.obs.days[key]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 首次运行从会话日志回填;回填期间的实时记录先入队,完成后合并 */
|
|
191
|
+
async function backfill() {
|
|
192
|
+
if (data.backfilled) return false
|
|
193
|
+
backfilling = true
|
|
194
|
+
try {
|
|
195
|
+
await backfillFromLogs(data, sessionsRoot(), BACKFILL_DAYS)
|
|
196
|
+
for (const rec of queue.splice(0)) applyRecord(data, rec)
|
|
197
|
+
dirty = true
|
|
198
|
+
await flush()
|
|
199
|
+
return true
|
|
200
|
+
} catch {
|
|
201
|
+
return false
|
|
202
|
+
} finally {
|
|
203
|
+
backfilling = false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function record(provider, model, usage, timing) {
|
|
208
|
+
const rec = {
|
|
209
|
+
time: (timing && timing.time) || Date.now(),
|
|
210
|
+
provider,
|
|
211
|
+
model,
|
|
212
|
+
usage,
|
|
213
|
+
durationMs: timing && timing.durationMs,
|
|
214
|
+
ttftMs: timing && timing.ttftMs,
|
|
215
|
+
}
|
|
216
|
+
if (backfilling) { queue.push(rec); return }
|
|
217
|
+
applyRecord(data, rec)
|
|
218
|
+
dirty = true
|
|
219
|
+
scheduleFlush()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function scheduleFlush() {
|
|
223
|
+
if (flushTimer) return
|
|
224
|
+
flushTimer = setTimeout(() => {
|
|
225
|
+
flushTimer = null
|
|
226
|
+
flush().catch(() => {})
|
|
227
|
+
}, 3000)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function flush() {
|
|
231
|
+
if (!dirty) return
|
|
232
|
+
dirty = false
|
|
233
|
+
try {
|
|
234
|
+
await mkdir(dirname(file), { recursive: true })
|
|
235
|
+
const tmp = file + '.tmp'
|
|
236
|
+
await writeFile(tmp, JSON.stringify(data))
|
|
237
|
+
await rename(tmp, file)
|
|
238
|
+
} catch {
|
|
239
|
+
dirty = true
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function statBucket(b) {
|
|
244
|
+
return {
|
|
245
|
+
requests: b.requests || 0,
|
|
246
|
+
tokens: b.tokens || 0,
|
|
247
|
+
input: b.input || 0,
|
|
248
|
+
output: b.output || 0,
|
|
249
|
+
cacheRead: b.cacheRead || 0,
|
|
250
|
+
cacheWrite: b.cacheWrite || 0,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function cacheHitRate(hit, totalIn) {
|
|
254
|
+
const d = hit + totalIn
|
|
255
|
+
return d > 0 ? Math.round((hit / d) * 1000) / 10 : null
|
|
256
|
+
}
|
|
257
|
+
function sumRange(fromKey, toKey) {
|
|
258
|
+
const agg = { requests: 0, tokens: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, byProvider: {}, byModel: [] }
|
|
259
|
+
for (const [key, day] of Object.entries(data.days)) {
|
|
260
|
+
if (key < fromKey || key > toKey) continue
|
|
261
|
+
for (const [route, prov] of Object.entries(day.providers || {})) {
|
|
262
|
+
const s = statBucket(prov)
|
|
263
|
+
agg.requests += s.requests
|
|
264
|
+
agg.tokens += s.tokens
|
|
265
|
+
agg.input += s.input
|
|
266
|
+
agg.output += s.output
|
|
267
|
+
agg.cacheRead += s.cacheRead
|
|
268
|
+
agg.cacheWrite += s.cacheWrite
|
|
269
|
+
const bp = agg.byProvider[route] || (agg.byProvider[route] = { requests: 0, tokens: 0, input: 0, cacheRead: 0 })
|
|
270
|
+
bp.requests += s.requests
|
|
271
|
+
bp.tokens += s.tokens
|
|
272
|
+
bp.input += s.input
|
|
273
|
+
bp.cacheRead += s.cacheRead
|
|
274
|
+
for (const [model, mb] of Object.entries(prov.models || {})) {
|
|
275
|
+
const ms = statBucket(mb)
|
|
276
|
+
let row = agg.byModel.find((x) => x.provider === route && x.model === model)
|
|
277
|
+
if (!row) { row = { provider: route, model, requests: 0, tokens: 0 }; agg.byModel.push(row) }
|
|
278
|
+
row.requests += ms.requests
|
|
279
|
+
row.tokens += ms.tokens
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
agg.cacheHitRate = cacheHitRate(agg.cacheRead, agg.input)
|
|
284
|
+
agg.byProvider = Object.entries(agg.byProvider)
|
|
285
|
+
.map(([route, v]) => ({
|
|
286
|
+
route, requests: v.requests, tokens: v.tokens, input: v.input, cacheRead: v.cacheRead,
|
|
287
|
+
cacheHitRate: cacheHitRate(v.cacheRead, v.input),
|
|
288
|
+
}))
|
|
289
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
290
|
+
agg.byModel.sort((a, b) => b.tokens - a.tokens)
|
|
291
|
+
return agg
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function obsDay(route) {
|
|
295
|
+
const key = dateKey()
|
|
296
|
+
const days = data.obs.days
|
|
297
|
+
const day = days[key] || (days[key] = {})
|
|
298
|
+
return day[route] || (day[route] = {})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 观测服务商真实额度变动(不估算):
|
|
303
|
+
* - 余额类:余额下降 = 当日支出;余额上升 = 充值/赠金(分开记,不冲抵)
|
|
304
|
+
* - 窗口类:同一重置周期内 usedPct 上升量 = 当日额度消耗
|
|
305
|
+
*/
|
|
306
|
+
function observe(route, usage) {
|
|
307
|
+
if (!usage) return
|
|
308
|
+
let changed = false
|
|
309
|
+
if (usage.balance && usage.balance.currency) {
|
|
310
|
+
const cur = usage.balance.currency
|
|
311
|
+
const val = Number(usage.balance.total)
|
|
312
|
+
if (Number.isFinite(val)) {
|
|
313
|
+
const last = data.obs.last[route]
|
|
314
|
+
if (last && last.kind === 'balance' && last.currency === cur && Number.isFinite(last.total)) {
|
|
315
|
+
const delta = last.total - val
|
|
316
|
+
if (delta > 0.000001) {
|
|
317
|
+
const d = obsDay(route)
|
|
318
|
+
d.kind = 'balance'
|
|
319
|
+
d.currency = cur
|
|
320
|
+
d.spent = Math.round(((d.spent || 0) + delta) * 10000) / 10000
|
|
321
|
+
changed = true
|
|
322
|
+
} else if (delta < -0.000001) {
|
|
323
|
+
const d = obsDay(route)
|
|
324
|
+
d.kind = 'balance'
|
|
325
|
+
d.currency = cur
|
|
326
|
+
d.recharged = Math.round(((d.recharged || 0) - delta) * 10000) / 10000
|
|
327
|
+
changed = true
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
data.obs.last[route] = { kind: 'balance', total: val, currency: cur, ts: Date.now() }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(usage.windows) && usage.windows.length) {
|
|
334
|
+
const last = data.obs.last[route]
|
|
335
|
+
const prevWindows = last && last.kind === 'windows' ? last.windows || {} : null
|
|
336
|
+
const nextWindows = {}
|
|
337
|
+
for (const w of usage.windows) {
|
|
338
|
+
if (!w || !w.key) continue
|
|
339
|
+
const used = Number(w.usedPct)
|
|
340
|
+
if (!Number.isFinite(used)) continue
|
|
341
|
+
const resetAt = w.resetAt || null
|
|
342
|
+
const prev = prevWindows && prevWindows[w.key]
|
|
343
|
+
if (prev && prev.resetAt === resetAt && used >= prev.used) {
|
|
344
|
+
const delta = used - prev.used
|
|
345
|
+
if (delta > 0.01) {
|
|
346
|
+
const d = obsDay(route)
|
|
347
|
+
d.kind = 'windows'
|
|
348
|
+
if (!d.windows) d.windows = {}
|
|
349
|
+
d.windows[w.key] = Math.round(((d.windows[w.key] || 0) + delta) * 10) / 10
|
|
350
|
+
changed = true
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
nextWindows[w.key] = { used, resetAt }
|
|
354
|
+
}
|
|
355
|
+
data.obs.last[route] = { kind: 'windows', windows: nextWindows, ts: Date.now() }
|
|
356
|
+
}
|
|
357
|
+
if (changed) { dirty = true; scheduleFlush() }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 今日观测汇总:{ [route]: {kind, currency?, spent?, recharged?, windows?} } */
|
|
361
|
+
function spendToday() {
|
|
362
|
+
const day = data.obs.days[dateKey()] || {}
|
|
363
|
+
return day
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** @param {{days?: number}} opts 范围天数:1=当天,7/30=近 N 天(含当天) */
|
|
367
|
+
function stats(opts = {}) {
|
|
368
|
+
const rangeDays = Math.min(90, Math.max(1, Number(opts.days) || 7))
|
|
369
|
+
const now = new Date()
|
|
370
|
+
const todayKey = dateKey(now)
|
|
371
|
+
const fromKey = dateKey(new Date(now.getTime() - (rangeDays - 1) * 86400000))
|
|
372
|
+
const today = sumRange(todayKey, todayKey)
|
|
373
|
+
const range = sumRange(fromKey, todayKey)
|
|
374
|
+
const day = data.days[todayKey] || emptyDay()
|
|
375
|
+
const hours = []
|
|
376
|
+
for (let h = 0; h < 24; h++) {
|
|
377
|
+
const b = day.hours[h]
|
|
378
|
+
hours.push({ h, tokens: (b && b.tokens) || 0, requests: (b && b.requests) || 0, byProvider: (b && b.byProvider) || {} })
|
|
379
|
+
}
|
|
380
|
+
const daysList = []
|
|
381
|
+
for (let i = rangeDays - 1; i >= 0; i--) {
|
|
382
|
+
const key = dateKey(new Date(now.getTime() - i * 86400000))
|
|
383
|
+
const d = data.days[key]
|
|
384
|
+
const byProvider = {}
|
|
385
|
+
for (const [route, prov] of Object.entries((d && d.providers) || {})) {
|
|
386
|
+
const sb = statBucket(prov)
|
|
387
|
+
byProvider[route] = { tokens: sb.tokens, requests: sb.requests, input: sb.input, cacheRead: sb.cacheRead }
|
|
388
|
+
}
|
|
389
|
+
daysList.push({ date: key, tokens: (d && d.tokens) || 0, requests: (d && d.requests) || 0, byProvider })
|
|
390
|
+
}
|
|
391
|
+
const kpi = (s) => ({ requests: s.requests, tokens: s.tokens, cacheHitRate: s.cacheHitRate })
|
|
392
|
+
return {
|
|
393
|
+
generatedAt: Date.now(),
|
|
394
|
+
rangeDays,
|
|
395
|
+
today: kpi(today),
|
|
396
|
+
range: kpi(range),
|
|
397
|
+
last7: kpi(sumRange(dateKey(new Date(now.getTime() - 6 * 86400000)), todayKey)),
|
|
398
|
+
hours,
|
|
399
|
+
days: daysList,
|
|
400
|
+
providers: range.byProvider,
|
|
401
|
+
todayByProvider: Object.entries((data.days[todayKey] && data.days[todayKey].providers) || {}).map(([route, prov]) => {
|
|
402
|
+
const sb = statBucket(prov)
|
|
403
|
+
return { route, requests: sb.requests, tokens: sb.tokens, cacheHitRate: cacheHitRate(sb.cacheRead, sb.input) }
|
|
404
|
+
}),
|
|
405
|
+
models: range.byModel,
|
|
406
|
+
recent: (data.recent || []).slice(-100).reverse(),
|
|
407
|
+
spendToday: spendToday(),
|
|
408
|
+
backfilled: !!data.backfilled,
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
ready: ensureLoaded,
|
|
414
|
+
backfill,
|
|
415
|
+
observe,
|
|
416
|
+
record,
|
|
417
|
+
stats,
|
|
418
|
+
flush,
|
|
419
|
+
}
|
|
420
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ponydev/dsh-model-usage",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "DSH Web 插件:实时显示模型供应商额度与用量(DeepSeek / GLM Coding Plan / OpenRouter / Kimi Coding Plan + 自定义适配器)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"deepseek",
|
|
10
|
+
"glm",
|
|
11
|
+
"openrouter",
|
|
12
|
+
"kimi",
|
|
13
|
+
"quota",
|
|
14
|
+
"usage",
|
|
15
|
+
"dashboard"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Pony <205503242+jmaubay7@users.noreply.github.com>",
|
|
19
|
+
"homepage": "https://github.com/PonyDevAI/dsh-model-usage#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/PonyDevAI/dsh-model-usage.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/PonyDevAI/dsh-model-usage/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"assets",
|
|
31
|
+
"cordis.patch.yml",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": "./lib/index.js",
|
|
39
|
+
"./client": "./lib/client.js"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dsh": {
|
|
48
|
+
"manifestVersion": 1,
|
|
49
|
+
"bundle": {
|
|
50
|
+
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"platform": "web",
|
|
54
|
+
"immediately": true,
|
|
55
|
+
"inject": [
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
57
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
60
|
+
"@deepseek-ai/dsh-client-locale"
|
|
61
|
+
],
|
|
62
|
+
"external": [
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|