dsh-cost-meter 1.7.39 → 1.7.41
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 +7 -7
- package/README.zh-CN.md +7 -7
- package/docs/external-usage.md +2 -2
- package/docs/provider-pricing.json +101 -3
- package/lib/backfill.js +1 -3
- package/lib/client.js +5 -5
- package/lib/external-usage.js +9 -2
- package/lib/index.js +12 -10
- package/lib/plan-billing.js +5 -3
- package/lib/pricing.js +46 -69
- package/lib/store.js +7 -6
- package/lib/typert.host.js +2 -0
- package/locale/en.json +3 -0
- package/locale/zh.json +6 -0
- package/package.json +18 -6
package/lib/external-usage.js
CHANGED
|
@@ -30,7 +30,7 @@ function summary(days, dayKey) {
|
|
|
30
30
|
return { today, month, total, history }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** A snapshot replaces its predecessor; it never mutates the DSH ledger
|
|
33
|
+
/** A snapshot replaces its predecessor; it never mutates the DSH ledger. */
|
|
34
34
|
export function parseExternalUsageSnapshot(value, config, nowMs = Date.now()) {
|
|
35
35
|
if (!object(value)) return null
|
|
36
36
|
if (Object.hasOwn(value, 'sources') && !Array.isArray(value.sources)) return null
|
|
@@ -75,7 +75,7 @@ export function parseExternalUsageSnapshot(value, config, nowMs = Date.now()) {
|
|
|
75
75
|
effectiveAtMs: Date.parse(config?.peakEffectiveAt ?? ''), windows: config?.peakWindows, holidays: config?.peakHolidays }
|
|
76
76
|
const tokens = { input: raw.input, output: raw.output, cacheRead: raw.cached, cacheWrite: raw.cacheWrite ?? 0, reasoning: raw.reasoning ?? 0 }
|
|
77
77
|
const priced = costOf(tokens, resolved.entry, at, peak)
|
|
78
|
-
const usd = usdFromCost(priced, resolved.
|
|
78
|
+
const usd = usdFromCost(priced, resolved.currency, config?.exchangeRate)
|
|
79
79
|
const date = localDayKey(at)
|
|
80
80
|
const day = days[date] ?? (days[date] = zeroDay(date))
|
|
81
81
|
for (const key of ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning']) day[key] += tokens[key]
|
|
@@ -109,3 +109,10 @@ export async function readExternalUsageSnapshot(path, config, nowMs = Date.now()
|
|
|
109
109
|
return parseExternalUsageSnapshot(JSON.parse(buffer.subarray(0, length).toString('utf8')), config, nowMs)
|
|
110
110
|
} catch { return null } finally { await file?.close().catch(() => {}) }
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
/** Fresh external usage participates in same-account balance reconciliation. */
|
|
114
|
+
export function externalTodayCostForReconcile(snapshot) {
|
|
115
|
+
if (snapshot?.stale === true) return null
|
|
116
|
+
const cost = snapshot?.combined?.today?.cost
|
|
117
|
+
return Number.isFinite(cost) && cost >= 0 ? cost : 0
|
|
118
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { queryGatewayQuota, emptyGatewayQuota, managementKeyVarOf, gatewaySource
|
|
|
33
33
|
import { stateSchema, TYPERT } from './typert.host.js'
|
|
34
34
|
import { customBalanceCredentialVars } from './custom-balance.js'
|
|
35
35
|
import { readScnetSnapshot } from './scnet-snapshot.js'
|
|
36
|
-
import { readExternalUsageSnapshot } from './external-usage.js'
|
|
36
|
+
import { readExternalUsageSnapshot, externalTodayCostForReconcile } from './external-usage.js'
|
|
37
37
|
import { queryQwenCli } from './qwen-cli.js'
|
|
38
38
|
import { queryBailianCli } from './bailian-cli.js'
|
|
39
39
|
import { getSessionCost as readSessionCost } from './session-tree.js'
|
|
@@ -55,7 +55,7 @@ const SERVER_MESSAGES = {
|
|
|
55
55
|
balanceDisplayOff: '余额显示已关闭,请先在 显示设置 中开启',
|
|
56
56
|
balanceRefreshed: '余额已刷新',
|
|
57
57
|
balanceQueryFailed: '余额查询失败:{message}',
|
|
58
|
-
reconcileWarn: '
|
|
58
|
+
reconcileWarn: '对账提示:本地账本及外部来源今日费用 {cost} 与官方余额当日变动 {delta} 偏差较大,请核对价格表或近期账单',
|
|
59
59
|
goQuotaKeyMissing: '未找到可用的 OpenCode Go API Key。请检查模型设置中 https://opencode.ai/zen/go/v1 路由的凭据,或在费用显示设置中填写 Go Key、导出 OPENCODE_GO_API_KEY、运行 opencode login;没有订阅可关闭「启用」。',
|
|
60
60
|
goQuotaHttp: 'OpenCode Go 额度接口 HTTP {code}',
|
|
61
61
|
goQuotaNoSub: '没有检测到生效的 OpenCode Go 订阅(接口返回 {code}),或 API Key 无效。没有订阅可关闭上方「启用」开关。',
|
|
@@ -130,7 +130,7 @@ const SERVER_MESSAGES = {
|
|
|
130
130
|
balanceDisplayOff: 'Balance display is off; enable it in Display settings first',
|
|
131
131
|
balanceRefreshed: 'Balance refreshed',
|
|
132
132
|
balanceQueryFailed: 'Balance query failed: {message}',
|
|
133
|
-
reconcileWarn: 'Reconciliation notice: today\'s local
|
|
133
|
+
reconcileWarn: 'Reconciliation notice: today\'s local and external usage cost ({cost}) deviates significantly from the official balance change ({delta}); please check the price table or recent bills',
|
|
134
134
|
goQuotaKeyMissing: 'No usable OpenCode Go API key found. Check the credential for your https://opencode.ai/zen/go/v1 route in Models, set a Go key in Cost display settings, export OPENCODE_GO_API_KEY, or run opencode login; otherwise turn off Enable.',
|
|
135
135
|
goQuotaHttp: 'OpenCode Go quota API returned HTTP {code}',
|
|
136
136
|
goQuotaNoSub: 'No active OpenCode Go subscription detected (API returned {code}), or the API key is invalid. Turn off the Enable switch above if you have no subscription.',
|
|
@@ -591,9 +591,7 @@ function makeCostUsageProjection(ledger) {
|
|
|
591
591
|
const peak = peakConfig()
|
|
592
592
|
peak.enabled = resolved.billingMode === 'deepseek-peak' && peak.enabled
|
|
593
593
|
const priced = resolved.priced ? costOf(buckets, resolved.entry, atMs, peak) : 0
|
|
594
|
-
const billed = usdFromCost(priced,
|
|
595
|
-
resolved.billingMode === 'deepseek-peak' && ledger.config.prices?.currency === 'CNY' ? 'CNY' : 'USD',
|
|
596
|
-
ledger.config.exchangeRate)
|
|
594
|
+
const billed = usdFromCost(priced, resolved.currency, ledger.config.exchangeRate)
|
|
597
595
|
// 同一 (turn, step) 的最终样本替换流式样本,先减后加,避免重复计数。
|
|
598
596
|
const totals = { ...state.totals, reasoning: state.totals.reasoning ?? 0 }
|
|
599
597
|
const byModel = { ...state.byModel }
|
|
@@ -1440,9 +1438,9 @@ function createService(ctx, ledger, openRouterPrices) {
|
|
|
1440
1438
|
if (balanceCache.inFlight === prev) break // 上一个任务结束后由本调用继续走新建流程
|
|
1441
1439
|
}
|
|
1442
1440
|
const controller = new AbortController()
|
|
1443
|
-
const task = queryBalance(ctx, localeOf(ledger.config), controller.signal).then(result => {
|
|
1441
|
+
const task = queryBalance(ctx, localeOf(ledger.config), controller.signal).then(async result => {
|
|
1444
1442
|
if (!active || balanceCache.inFlight !== task) return
|
|
1445
|
-
balanceCache = { fetchedAt: Date.now(), value: { status: 'ok', message: '', fetchedAt: Date.now(), ...result } }
|
|
1443
|
+
balanceCache = { ...balanceCache, fetchedAt: Date.now(), value: { status: 'ok', message: '', fetchedAt: Date.now(), ...result } }
|
|
1446
1444
|
// 余额差交叉校验(issue #18):官方余额当日变动 vs 本地账本今日官方渠道费用,偏差超阈提示。
|
|
1447
1445
|
// issue #36:Coding Plan / 自定义 Provider 的费用不动官方余额,对账只统计 deepseek 渠道,否则订阅用户会恒报 drift。
|
|
1448
1446
|
if ((ledger.config?.balance?.reconcile ?? true) === true && balanceCache.value.status === 'ok') {
|
|
@@ -1468,12 +1466,16 @@ function createService(ctx, ledger, openRouterPrices) {
|
|
|
1468
1466
|
: ''
|
|
1469
1467
|
return symbol + Number(event.spent).toFixed(4) + converted
|
|
1470
1468
|
}
|
|
1471
|
-
const
|
|
1469
|
+
const external = await readExternalUsageSnapshot(join(dirname(ledger.path), 'external_usage.json'), ledger.config, nowMs)
|
|
1470
|
+
if (!active || balanceCache.inFlight !== task) return
|
|
1471
|
+
const externalCost = externalTodayCostForReconcile(external)
|
|
1472
|
+
const todayCost = ledger.todayOfficialCost() + (externalCost ?? 0)
|
|
1473
|
+
const { ref, event } = reconcileBalanceDelta(ledger.balanceRef, balanceCache.value, todayCost, localDayKey(nowMs), nowMs, { exchangeRate: ledger.config.exchangeRate })
|
|
1472
1474
|
if (ref !== ledger.balanceRef) {
|
|
1473
1475
|
ledger.balanceRef = ref
|
|
1474
1476
|
ledger.scheduleWrite()
|
|
1475
1477
|
}
|
|
1476
|
-
reconcileNotice = event !== null && event.kind === 'drift'
|
|
1478
|
+
reconcileNotice = externalCost !== null && event !== null && event.kind === 'drift'
|
|
1477
1479
|
? { ok: false, message: tmsg(localeOf(ledger.config), 'reconcileWarn', { cost: nativeCost(event), delta: nativeDelta(event) }) }
|
|
1478
1480
|
: { ok: true, message: '' }
|
|
1479
1481
|
}
|
package/lib/plan-billing.js
CHANGED
|
@@ -123,14 +123,16 @@ export function isRoutedThirdPartyCall(provider, modelId, prices) {
|
|
|
123
123
|
export function billingClassOf(provider, modelId, planBilling, enabledPlans, prices) {
|
|
124
124
|
let planId = planProviderIdOf(provider)
|
|
125
125
|
if (planId === null && isRoutedThirdPartyCall(provider, modelId, prices)) planId = 'go'
|
|
126
|
-
if (planId === null) return 'api'
|
|
127
126
|
const models = planBilling?.models
|
|
128
127
|
if (models !== null && typeof models === 'object') {
|
|
129
128
|
const direct = models[`${provider}:${modelId}`]
|
|
130
129
|
if (direct === 'plan' || direct === 'api') return direct
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
if (planId !== null) {
|
|
131
|
+
const canonical = models[`${planId}:${modelId}`]
|
|
132
|
+
if (canonical === 'plan' || canonical === 'api') return canonical
|
|
133
|
+
}
|
|
133
134
|
}
|
|
135
|
+
if (planId === null) return 'api'
|
|
134
136
|
const providers = planBilling?.providers
|
|
135
137
|
const configured = providers !== null && typeof providers === 'object' ? providers[planId] : undefined
|
|
136
138
|
if (configured === 'plan' || configured === 'api') return configured
|
package/lib/pricing.js
CHANGED
|
@@ -237,8 +237,18 @@ export const DEFAULT_PROVIDER_PRICE_TABLE = {
|
|
|
237
237
|
},
|
|
238
238
|
xiaomi: {
|
|
239
239
|
models: {
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
// MiMo V2.6(2026-09-28 官方价目页核对):官方把 2.6 与 2.5 两代并列同价并
|
|
241
|
+
// 标注 2.5 系即将下线;2.6-pro-ultraspeed 为速度优先档,单价是 pro 的 10 倍。
|
|
242
|
+
// 三者按官方 USD 价收录(2.6-pro 命中价官方页 $0.0036、Go 目录 $0.003625,
|
|
243
|
+
// opencode-go 目录随 Go 值);批量推理为实时价一半,官方人民币价一并记入
|
|
244
|
+
// notes。第三方 flat 条目恒按美元入账(store.account / 实时钩子 / 回放同
|
|
245
|
+
// 口径),而官方 CNY 与 USD 两套数字各自舍入(pro 系折算率 6.90、flash 系
|
|
246
|
+
// 7.14),人民币账单精度只能靠展示汇率对齐。
|
|
247
|
+
'mimo-v2.6-pro': { input: 0.435, cachedInput: 0.0036, output: 0.87, cny: { cacheHit: 0.025, cacheMiss: 3, output: 6 }, billingMode: 'flat', sourceUrl: 'https://mimo.mi.com/docs/zh-CN/price/pay-as-you-go', checkedAt: '2026-09-28', notes: '批量推理按半价' },
|
|
248
|
+
'mimo-v2.6-flash': { input: 0.14, cachedInput: 0.0028, output: 0.28, cny: { cacheHit: 0.02, cacheMiss: 1, output: 2 }, billingMode: 'flat', sourceUrl: 'https://mimo.mi.com/docs/zh-CN/price/pay-as-you-go', checkedAt: '2026-09-28', notes: '批量推理按半价' },
|
|
249
|
+
'mimo-v2.6-pro-ultraspeed': { input: 4.35, cachedInput: 0.036, output: 8.7, cny: { cacheHit: 0.25, cacheMiss: 30, output: 60 }, billingMode: 'flat', sourceUrl: 'https://mimo.mi.com/docs/zh-CN/price/pay-as-you-go', checkedAt: '2026-09-28', notes: '速度优先档;官方未提供批量推理档' },
|
|
250
|
+
'mimo-v2.5': { input: 0.14, cachedInput: 0.0028, output: 0.28, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: '官方标注即将下线,由 mimo-v2.6-flash 接替' },
|
|
251
|
+
'mimo-v2.5-pro': { input: 0.435, cachedInput: 0.003625, output: 0.87, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: '官方标注即将下线,由 mimo-v2.6-pro 接替' },
|
|
242
252
|
},
|
|
243
253
|
},
|
|
244
254
|
upstage: {
|
|
@@ -267,7 +277,7 @@ export const DEFAULT_PROVIDER_PRICE_TABLE = {
|
|
|
267
277
|
'mistral-small-4.0': { input: 0.15, cachedInput: 0.015, output: 0.6, billingMode: 'flat' },
|
|
268
278
|
},
|
|
269
279
|
},
|
|
270
|
-
// OpenCode Go 订阅($10/月)包含的模型中非 DeepSeek
|
|
280
|
+
// OpenCode Go 订阅($10/月)包含的模型中非 DeepSeek 的部分(不含免费档):订阅制下请求
|
|
271
281
|
// 不按 token 扣费,此处为官方公布的参考单价(用于成本估算/对比),来源 opencode.ai/docs/go。
|
|
272
282
|
// DeepSeek V4 Flash/Pro 与官方主表重复,以官方为准(含峰谷两档),Go 目录不重复收录(v1.5.2 移除)。
|
|
273
283
|
'opencode-go': {
|
|
@@ -283,6 +293,10 @@ export const DEFAULT_PROVIDER_PRICE_TABLE = {
|
|
|
283
293
|
'kimi-k3': { input: 3, cachedInput: 0.3, output: 15, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
284
294
|
'kimi-k2.7-code': { input: 0.95, cachedInput: 0.19, output: 4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
285
295
|
'kimi-k2.6': { input: 0.95, cachedInput: 0.16, output: 4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
296
|
+
// MiMo V2.6-Flash / V2.6-Pro 已在 Go 目录在册(2026-09-28 抓取),与 2.5 系同价;
|
|
297
|
+
// 速度优先档 Pro UltraSpeed 不在 Go 目录(见 xiaomi 目录,官方价)。
|
|
298
|
+
'mimo-v2.6-flash': { input: 0.14, cachedInput: 0.0028, output: 0.28, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-09-28' },
|
|
299
|
+
'mimo-v2.6-pro': { input: 0.435, cachedInput: 0.003625, output: 0.87, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-09-28' },
|
|
286
300
|
'mimo-v2.5': { input: 0.14, cachedInput: 0.0028, output: 0.28, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
287
301
|
'mimo-v2.5-pro': { input: 0.435, cachedInput: 0.003625, output: 0.87, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
288
302
|
'minimax-m3': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
@@ -404,7 +418,10 @@ export const PROVIDER_MODEL_FAMILIES = {
|
|
|
404
418
|
alibaba: { 'qwen3.8-max': 'Qwen3.8', 'qwen3.7-max': 'Qwen3.7', 'qwen3.7-plus': 'Qwen3.7', 'qwen3.6-plus': 'Qwen3.6', 'qwen3.5-plus': 'Qwen3.5', 'qwen3-plus': 'Qwen3' },
|
|
405
419
|
minimax: { 'minimax-m3': 'MiniMax M3', 'minimax-m2.7': 'MiniMax M2', 'minimax-m2.5': 'MiniMax M2' },
|
|
406
420
|
tencent: { 'hunyuan-a13b': '混元', 'hy3': 'Hy3' },
|
|
407
|
-
xiaomi: {
|
|
421
|
+
xiaomi: {
|
|
422
|
+
'mimo-v2.6-pro': 'MiMo V2.6', 'mimo-v2.6-flash': 'MiMo V2.6', 'mimo-v2.6-pro-ultraspeed': 'MiMo V2.6',
|
|
423
|
+
'mimo-v2.5': 'MiMo V2.5', 'mimo-v2.5-pro': 'MiMo V2.5',
|
|
424
|
+
},
|
|
408
425
|
upstage: { 'solar-pro4': 'Solar', 'solar-pro3': 'Solar' },
|
|
409
426
|
nvidia: { 'nvidia/nemotron-3-ultra-550b-a55b': 'Nemotron' },
|
|
410
427
|
mistral: { 'mistral-large-2512': 'Mistral Large', 'mistral-medium-3.5': 'Mistral Medium', 'mistral-small-4.0': 'Mistral Small' },
|
|
@@ -417,6 +434,7 @@ export const PROVIDER_MODEL_FAMILIES = {
|
|
|
417
434
|
'opencode-go': {
|
|
418
435
|
'gpt-5.6-luna': 'GPT', 'grok-4.5': 'Grok', 'glm-5.3': 'GLM', 'glm-5.3-flash': 'GLM Flash', 'glm-5.2': 'GLM', 'glm-5.1': 'GLM',
|
|
419
436
|
'kimi-k3': 'Kimi', 'kimi-k2.7-code': 'Kimi', 'kimi-k2.6': 'Kimi',
|
|
437
|
+
'mimo-v2.6-pro': 'MiMo', 'mimo-v2.6-flash': 'MiMo',
|
|
420
438
|
'mimo-v2.5': 'MiMo', 'mimo-v2.5-pro': 'MiMo', 'minimax-m3': 'MiniMax', 'minimax-m2.7': 'MiniMax',
|
|
421
439
|
'qwen3.8-max': 'Qwen', 'qwen3.7-max': 'Qwen', 'qwen3.7-plus': 'Qwen', 'qwen3.6-plus': 'Qwen',
|
|
422
440
|
'hy3': 'Hy3', 'longcat-2.0': 'LongCat', 'muse-spark-1.2-contributor': 'Muse Spark',
|
|
@@ -667,6 +685,11 @@ export function normalizePrice(value) {
|
|
|
667
685
|
if (value.legacy === true) entry.legacy = true
|
|
668
686
|
if (value.billingMode === 'flat' || value.billingMode === 'deepseek-peak' || value.billingMode === 'batch') entry.billingMode = value.billingMode
|
|
669
687
|
for (const key of ['sourceUrl', 'checkedAt', 'notes']) if (typeof value[key] === 'string') entry[key] = value[key]
|
|
688
|
+
if (value.cny !== undefined) {
|
|
689
|
+
if (!value.cny || typeof value.cny !== 'object' || Array.isArray(value.cny)
|
|
690
|
+
|| !['cacheHit', 'cacheMiss', 'output'].every(key => validRate(value.cny[key]))) return null
|
|
691
|
+
entry.cny = completeTier(value.cny)
|
|
692
|
+
}
|
|
670
693
|
const offPeak = completeTier(value.offPeak)
|
|
671
694
|
if (offPeak !== undefined) entry.offPeak = offPeak
|
|
672
695
|
const peak = completeTier(value.peak)
|
|
@@ -719,7 +742,7 @@ export function priceEntryFor(modelId, table) {
|
|
|
719
742
|
return table?.default ?? { cacheHit: 0, cacheMiss: 0, output: 0 }
|
|
720
743
|
}
|
|
721
744
|
|
|
722
|
-
// ── 模型名自动匹配(精确 →
|
|
745
|
+
// ── 模型名自动匹配(精确 → 归一化 → 安全后缀) ───────────
|
|
723
746
|
|
|
724
747
|
/**
|
|
725
748
|
* 模型名归一化:小写,去掉括号括起的附注(如 (go)),再只保留字母与数字。
|
|
@@ -742,13 +765,9 @@ function stripIdDecor(id) {
|
|
|
742
765
|
return out
|
|
743
766
|
}
|
|
744
767
|
|
|
745
|
-
const tokensOf = id => stripIdDecor(id).split(/[-_./:]+/).filter(Boolean)
|
|
746
|
-
|
|
747
768
|
/**
|
|
748
769
|
* 把请求模型 id 匹配到候选价格表 id。
|
|
749
|
-
* 顺序:精确 → 归一化等价
|
|
750
|
-
* (请求名归一化后包含候选名即算命中,取最长候选) → 去日期/版本后缀精确
|
|
751
|
-
* → 候选前缀(取最长) → 家族 token 相似(≥2 个前缀 token 且最长者胜)。
|
|
770
|
+
* 顺序:精确 → 归一化等价 → 去日期/版本后缀 → 只接受日期、版本、上下文长度等安全后缀。
|
|
752
771
|
* @param modelId - 请求模型 id。
|
|
753
772
|
* @param candidates - 候选 id 数组。
|
|
754
773
|
* @returns 命中的候选 id,或 null。
|
|
@@ -764,64 +783,13 @@ export function matchModelId(modelId, candidates) {
|
|
|
764
783
|
// 归一化后等价:'GPT-5.6 Luna' ≡ 'gpt-5.6-luna'。
|
|
765
784
|
const byCanon = list.find(c => canonModelId(c) === canon)
|
|
766
785
|
if (byCanon !== undefined) return byCanon
|
|
767
|
-
// 宽泛包含:'gpt5.6 luna(go)' 归一化后包含 'gpt56luna' 即命中;取最长候选,过短候选(≤3)防误配。
|
|
768
|
-
// 数字分叉守卫(issue #18 同源):候选是请求 canon 的真前缀且剩余段为 1-2 位
|
|
769
|
-
// 纯数字(glm-5 vs glm-5.3 → 'glm53' 含 'glm5' 余 '3')时视为版本分叉拒绝;
|
|
770
|
-
// '-20260821' 等日期快照(≥3 位)与 '-128k' 容量后缀不受影响。
|
|
771
|
-
let containHit = null
|
|
772
|
-
let containLen = 0
|
|
773
|
-
for (const c of list) {
|
|
774
|
-
const cc = canonModelId(c)
|
|
775
|
-
if (cc.length < 4 || cc === canon) continue
|
|
776
|
-
if (canon.includes(cc) && cc.length > containLen) {
|
|
777
|
-
const idx = canon.indexOf(cc)
|
|
778
|
-
if (/^\d{1,2}$/.test(canon.slice(idx + cc.length))) continue
|
|
779
|
-
containHit = c; containLen = cc.length
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (containHit !== null) return containHit
|
|
783
786
|
const stripped = stripIdDecor(modelId)
|
|
784
787
|
const byStripped = list.find(c => stripIdDecor(c) === stripped)
|
|
785
788
|
if (byStripped !== undefined) return byStripped
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
for (const c of list) {
|
|
791
|
-
const cs = stripIdDecor(c)
|
|
792
|
-
if (cs.length === 0 || cs === stripped) continue
|
|
793
|
-
const rest = stripped.slice(cs.length)
|
|
794
|
-
if (stripped.startsWith(cs) && /^[\-_./:]/.test(rest)) {
|
|
795
|
-
if (/^\d{1,2}$/.test(rest.replace(/^[\-_./:]+/, ''))) continue
|
|
796
|
-
if (prefixHit === null || stripIdDecor(prefixHit).length < cs.length) prefixHit = c
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
if (prefixHit !== null) return prefixHit
|
|
800
|
-
// 家族 token 相似:前缀公共 token ≥2,取公共最长者;同长取候选最短(更泛化的家族)。
|
|
801
|
-
const mt = tokensOf(modelId)
|
|
802
|
-
if (mt.length < 2) return null
|
|
803
|
-
let best = null
|
|
804
|
-
let bestLen = 0
|
|
805
|
-
for (const c of list) {
|
|
806
|
-
const ct = tokensOf(c)
|
|
807
|
-
let n = 0
|
|
808
|
-
while (n < mt.length && n < ct.length && mt[n] === ct[n]) n += 1
|
|
809
|
-
// 防跨版本误配(issue #18):分歧位置两侧都有数字/版本号 token(如 glm-5.3 vs glm-5.2)
|
|
810
|
-
// 视为不同模型拒绝匹配——订阅制/新版本模型不应落到同家族其它版本的付费单价。
|
|
811
|
-
if (n < mt.length && n < ct.length && /^\d+$/.test(mt[n]) && /^\d+$/.test(ct[n])) continue
|
|
812
|
-
// 候选 token 耗尽而请求多出的部分全是 1-2 位纯数字 token(glm-5 vs glm-5.3)
|
|
813
|
-
// 同为版本分叉,拒绝;多出日期(≥3 位)或带字母的容量/变体 token 时放行。
|
|
814
|
-
if (n >= 2 && n === ct.length && n < mt.length && mt.slice(n).every(t => /^\d{1,2}$/.test(t))) continue
|
|
815
|
-
// 分歧位一侧为 1-2 位版本号、另一侧为变体名(gpt-5.9 的 '9' vs gpt-5-nano
|
|
816
|
-
// 的 'nano'):containment/prefix 守卫生效后不再掉进变体互配,一并拒绝。
|
|
817
|
-
if (n >= 2 && n < mt.length && n < ct.length
|
|
818
|
-
&& ((/^\d{1,2}$/.test(mt[n]) && /^[a-z]/.test(ct[n])) || (/^\d{1,2}$/.test(ct[n]) && /^[a-z]/.test(mt[n])))) continue
|
|
819
|
-
if (n >= 2 && (n > bestLen || (n === bestLen && best !== null && c.length < best.length))) {
|
|
820
|
-
best = c
|
|
821
|
-
bestLen = n
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return best
|
|
789
|
+
// 仅允许日期、显式版本及上下文长度等装饰后缀;新变体保持未定价。
|
|
790
|
+
const suffix = /^(?:[-_./:@](?:\d{4}-?\d{2}-?\d{2}|v\d+(?:\.\d+)*|\d+k|latest))+$/
|
|
791
|
+
return list.filter(c => stripped.startsWith(stripIdDecor(c)) && suffix.test(stripped.slice(stripIdDecor(c).length)))
|
|
792
|
+
.sort((a, b) => b.length - a.length)[0] ?? null
|
|
825
793
|
}
|
|
826
794
|
|
|
827
795
|
/**
|
|
@@ -924,6 +892,7 @@ function providerPriceEntryForNormalized(normalized, modelId, prices, options) {
|
|
|
924
892
|
const catalog = providerTable?.models ?? {}
|
|
925
893
|
let hit = Object.hasOwn(catalog, targetModel) ? targetModel : null
|
|
926
894
|
if (hit === null && mode === 'auto') hit = matchModelId(targetModel, Object.keys(catalog))
|
|
895
|
+
if (hit === null && targetProvider === 'opencode-go') return { entry: null, billingMode: 'flat', priced: false }
|
|
927
896
|
if (hit === null && mode === 'auto') {
|
|
928
897
|
// 跨厂商兑底:请求携带的 provider 未在价格表登记(opencode / zen 等路由入口)时,
|
|
929
898
|
// 按模型名全库查找——先查 DeepSeek 主表(保留峰谷两档),再取其余厂商中归一化最长命中。
|
|
@@ -1046,17 +1015,25 @@ export function providerPriceEntryFor(provider, modelId, prices, options) {
|
|
|
1046
1015
|
const rawProvider = typeof provider === 'string' ? provider.trim().toLowerCase() : ''
|
|
1047
1016
|
const normalized = rawProvider.startsWith('llm-') ? rawProvider.slice(4) : rawProvider
|
|
1048
1017
|
const primary = providerPriceEntryForNormalized(normalized, modelId, prices, options)
|
|
1049
|
-
if (primary.priced) return primary
|
|
1018
|
+
if (primary.priced) return selectPriceCurrency(primary, prices)
|
|
1050
1019
|
// issue #56:v1.5.42 及之前设置页下拉框把 DeepSeek 目标模型存成裸名(缺
|
|
1051
1020
|
// 'deepseek:' 前缀),被按「同渠道换名」解析——映射键的渠道与 DeepSeek 不同时
|
|
1052
1021
|
// 查无此价,金额归零。此处对「裸值覆盖 + 非 DeepSeek 渠道解析失败」回退按
|
|
1053
1022
|
// DeepSeek 渠道再查一次,存量错误配置无需手工修正即自愈。
|
|
1054
|
-
if (normalized === '' || normalized === 'deepseek' || normalized.includes('deepseek')) return primary
|
|
1023
|
+
if (normalized === '' || normalized === 'deepseek' || normalized.includes('deepseek')) return selectPriceCurrency(primary, prices)
|
|
1055
1024
|
const overrides = options?.overrides !== null && typeof options?.overrides === 'object' ? options.overrides : {}
|
|
1056
1025
|
const override = overrides[normalized + ':' + modelId]
|
|
1057
|
-
if (typeof override !== 'string' || override.length === 0 || override.includes(':')) return primary
|
|
1026
|
+
if (typeof override !== 'string' || override.length === 0 || override.includes(':')) return selectPriceCurrency(primary, prices)
|
|
1058
1027
|
const mode = options?.mode === 'exact' ? 'exact' : 'auto'
|
|
1059
|
-
return deepseekExplicitEntry(override, prices, mode) ?? primary
|
|
1028
|
+
return selectPriceCurrency(deepseekExplicitEntry(override, prices, mode) ?? primary, prices)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function selectPriceCurrency(result, prices) {
|
|
1032
|
+
if (!result.priced) return { ...result, currency: 'USD' }
|
|
1033
|
+
if (prices?.currency === 'CNY' && result.entry?.cny) {
|
|
1034
|
+
return { ...result, entry: { ...result.entry.cny }, currency: 'CNY' }
|
|
1035
|
+
}
|
|
1036
|
+
return { ...result, currency: result.billingMode === 'deepseek-peak' && prices?.currency === 'CNY' ? 'CNY' : 'USD' }
|
|
1060
1037
|
}
|
|
1061
1038
|
|
|
1062
1039
|
/**
|
package/lib/store.js
CHANGED
|
@@ -139,6 +139,7 @@ export function defaultConfig() {
|
|
|
139
139
|
showSessionId: false, // 会话列表中附显会话 ID(默认只显示标题,需要时开启)
|
|
140
140
|
hideOfficialBalance: false, // 隐藏官方账户余额(issue #45):开启后侧边栏/会话页/设置页的官方余额 UI 整体不渲染
|
|
141
141
|
hideTodayCost: false, // 隐藏今日消耗金额(issue #46):开启后侧栏今日费用行/预算明细今日行/概览今日卡片不渲染
|
|
142
|
+
sidebarTodayMetric: 'cost',
|
|
142
143
|
showTotalWithPlan: false, // 「含 Plan 总额」全局开关(v1.6.0):开启后全部金额展示按总等值(cost)计;默认按真金白银(apiCost)
|
|
143
144
|
includeSubagentCost: false, // 本会话展示可合并连续子代理后代;不重复写入每日账本。
|
|
144
145
|
codexQuotaEnabled: false, // Codex 额度按需开启,避免未使用 Codex 时显示卡片或查询。
|
|
@@ -148,7 +149,7 @@ export function defaultConfig() {
|
|
|
148
149
|
sidebarStyle: 'standard', // 侧边栏进度条样式:standard(标准,纵向单列) | compact(紧凑,额度卡两列网格)
|
|
149
150
|
legacyAutoImportedAt: 0, // 安装前历史自动导入标记(issue #27):完成时刻 ms;0 = 尚未跑过
|
|
150
151
|
peakStyle: 'compact', // 峰谷时段条样式:compact(简洁单行/竖向同构) | classic(经典分段/胶囊芯片)
|
|
151
|
-
priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(
|
|
152
|
+
priceMatch: 'auto', // 未知模型名自动匹配价格表:auto(归一化/安全后缀) | exact(仅精确)
|
|
152
153
|
priceMatchDismissed: [], // 从匹配设置中移除的 provider:model 行;不删除用量或改变计价规则。
|
|
153
154
|
priceOverrides: {}, // 手动匹配覆盖:{ 'provider:modelId': 'provider:模型 | deepseek:__default__' };裸模型名 = 同渠道换名(旧版遗留,跨渠道裸 DeepSeek 名由查价兜底自愈,issue #56)
|
|
154
155
|
priceTableDisplay: {}, // 费用设置直接显示(按模型):键 'provider:modelId' → 布尔;缺省 = DeepSeek 模型直接显示、第三方收入拓展价格表(含 DeepSeek 模型也可逐模型收入)
|
|
@@ -420,7 +421,7 @@ export function repairLedgerPricing(ledger, accepts = () => true) {
|
|
|
420
421
|
// 只允许完整请求日志回放重算,否则多次短请求合计会误套长上下文价格。
|
|
421
422
|
if (resolved.billingMode === 'deepseek-peak' || resolved.entry.longContext) continue
|
|
422
423
|
const priced = costOf(tokens, resolved.entry, atMsSafe, { enabled: false })
|
|
423
|
-
const newCost = usdFromCost(priced,
|
|
424
|
+
const newCost = usdFromCost(priced, resolved.currency, ledger.config.exchangeRate)
|
|
424
425
|
const oldCost = Number(bucket.cost) || 0
|
|
425
426
|
if (!Number.isFinite(newCost) || Math.abs(newCost - oldCost) < 1e-9) continue
|
|
426
427
|
const absDiff = Math.abs(newCost - oldCost)
|
|
@@ -461,7 +462,7 @@ export function repairLedgerPricing(ledger, accepts = () => true) {
|
|
|
461
462
|
if (!resolved.priced || resolved.entry === null) continue
|
|
462
463
|
if (resolved.billingMode === 'deepseek-peak' || resolved.entry.longContext) continue
|
|
463
464
|
const priced = costOf(tokens, resolved.entry, sessAtMs, { enabled: false })
|
|
464
|
-
const newCost = usdFromCost(priced,
|
|
465
|
+
const newCost = usdFromCost(priced, resolved.currency, ledger.config.exchangeRate)
|
|
465
466
|
const oldCost = Number(bucket.cost) || 0
|
|
466
467
|
if (!Number.isFinite(newCost) || Math.abs(newCost - oldCost) < 1e-9) continue
|
|
467
468
|
const absDiff = Math.abs(newCost - oldCost)
|
|
@@ -1138,6 +1139,7 @@ export function applyConfigPatch(current, rawPatch) {
|
|
|
1138
1139
|
// UI 隐藏开关(issues #45/#46):布尔,可缺省(默认关);开启后对应区块整体不渲染。
|
|
1139
1140
|
if (candidate.hideOfficialBalance !== undefined && typeof candidate.hideOfficialBalance !== 'boolean') errors.push(vmsg(locale, 'hideOfficialBalance'))
|
|
1140
1141
|
if (candidate.hideTodayCost !== undefined && typeof candidate.hideTodayCost !== 'boolean') errors.push(vmsg(locale, 'hideTodayCost'))
|
|
1142
|
+
if (candidate.sidebarTodayMetric !== undefined && !['cost', 'tokens'].includes(candidate.sidebarTodayMetric)) errors.push('sidebarTodayMetric must be cost or tokens')
|
|
1141
1143
|
// 「含 Plan 总额」全局开关(v1.6.0):布尔,可缺省(默认关)。
|
|
1142
1144
|
if (candidate.showTotalWithPlan !== undefined && typeof candidate.showTotalWithPlan !== 'boolean') errors.push(vmsg(locale, 'showTotalWithPlan'))
|
|
1143
1145
|
for (const field of ['includeSubagentCost', 'codexQuotaEnabled']) {
|
|
@@ -1474,6 +1476,7 @@ export function sanitizeConfig(raw) {
|
|
|
1474
1476
|
out.showSessionId = out.showSessionId === true
|
|
1475
1477
|
out.hideOfficialBalance = out.hideOfficialBalance === true
|
|
1476
1478
|
out.hideTodayCost = out.hideTodayCost === true
|
|
1479
|
+
out.sidebarTodayMetric = oneOf(out.sidebarTodayMetric, ['cost', 'tokens'], 'cost')
|
|
1477
1480
|
out.showTotalWithPlan = out.showTotalWithPlan === true
|
|
1478
1481
|
out.includeSubagentCost = out.includeSubagentCost === true
|
|
1479
1482
|
out.codexQuotaEnabled = out.codexQuotaEnabled === true
|
|
@@ -2053,9 +2056,7 @@ export class Ledger {
|
|
|
2053
2056
|
// 兜底与跨厂商兑底命中)计出的成本为人民币,按展示汇率折算为美元入账;
|
|
2054
2057
|
// 第三方 flat 恒为美元价,直接入账。
|
|
2055
2058
|
const priced = resolved.priced ? costOf(tokens, entry, atMs, peak) : 0
|
|
2056
|
-
const cost = usdFromCost(priced,
|
|
2057
|
-
resolved.billingMode === 'deepseek-peak' && this.config.prices?.currency === 'CNY' ? 'CNY' : 'USD',
|
|
2058
|
-
this.config.exchangeRate)
|
|
2059
|
+
const cost = usdFromCost(priced, resolved.currency, this.config.exchangeRate)
|
|
2059
2060
|
// Plan/API 双轨分类(issue #64):plan 类调用金额只记等值(cost),apiCost 记真金白银部分;
|
|
2060
2061
|
// Plan 类调用同时进入 provider×小时聚合桶(供 5 小时滚动窗本地量聚合)。
|
|
2061
2062
|
const planBilling = this.config.planBilling
|
package/lib/typert.host.js
CHANGED
|
@@ -88,6 +88,7 @@ const providerPriceSchema = z.object({
|
|
|
88
88
|
cachedInput: num.optional(),
|
|
89
89
|
cacheRead: num.optional(),
|
|
90
90
|
cacheWrite: z.number().nonnegative().optional(),
|
|
91
|
+
cny: priceTierSchema.optional(),
|
|
91
92
|
longContext: longContextSchema.optional(),
|
|
92
93
|
cacheCreation5m: num.optional(),
|
|
93
94
|
cacheCreation1h: num.optional(),
|
|
@@ -154,6 +155,7 @@ const configSchema = z.object({
|
|
|
154
155
|
// UI 隐藏开关(issues #45/#46):开启后官方余额/今日消耗金额的对应 UI 区块整体不渲染。
|
|
155
156
|
hideOfficialBalance: z.boolean().optional(),
|
|
156
157
|
hideTodayCost: z.boolean().optional(),
|
|
158
|
+
sidebarTodayMetric: z.enum(['cost', 'tokens']).optional(),
|
|
157
159
|
// 「含 Plan 总额」全局开关(v1.6.0):开启后全部金额展示按总等值(cost)计。
|
|
158
160
|
showTotalWithPlan: z.boolean().optional(),
|
|
159
161
|
includeSubagentCost: z.boolean().optional(),
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.41",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(11 家,含小米 MiMo Token Plan 控制台 Cookie 查询、SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条、峰谷计价时段显示与切换前弹窗/系统通知提醒(位置/提前量/类型可配),界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 11 vendors (incl. Xiaomi MiMo Token Plan via console cookie, local credits metering for the SCNet Token Plan and Volcano Ark Coding Plan via AK/SK signature), plan/API dual-track billing with per-1% & full-window Token Plan usage estimates, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, plus peak/off-peak pricing display with pre-switch popup & system-notification alerts (position / lead time / type configurable), in a bilingual (Chinese/English) UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
|
-
"bin": {
|
|
7
|
+
"bin": {
|
|
8
|
+
"dsh-cost-meter-repair-sessions": "./lib/repair-sessions-cli.js"
|
|
9
|
+
},
|
|
8
10
|
"exports": {
|
|
9
11
|
".": {
|
|
10
12
|
"default": "./lib/index.js"
|
|
@@ -15,11 +17,16 @@
|
|
|
15
17
|
"./typert": {
|
|
16
18
|
"default": "./lib/typert.host.js"
|
|
17
19
|
},
|
|
18
|
-
"./package.json": "./package.json"
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./locale/*.json": "./locale/*.json"
|
|
19
22
|
},
|
|
20
23
|
"dsh": {
|
|
21
|
-
"bundle": {
|
|
22
|
-
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
},
|
|
27
|
+
"client": {
|
|
28
|
+
"platform": "web"
|
|
29
|
+
},
|
|
23
30
|
"compatibility": {
|
|
24
31
|
"dsh": ">=0.1.0-rc.5",
|
|
25
32
|
"dshReleases": {
|
|
@@ -82,6 +89,7 @@
|
|
|
82
89
|
}
|
|
83
90
|
},
|
|
84
91
|
"files": [
|
|
92
|
+
"locale",
|
|
85
93
|
"lib",
|
|
86
94
|
"cordis.patch.yml",
|
|
87
95
|
"docs/provider-pricing.json",
|
|
@@ -119,7 +127,11 @@
|
|
|
119
127
|
"engines": {
|
|
120
128
|
"node": ">=20"
|
|
121
129
|
},
|
|
122
|
-
"os": [
|
|
130
|
+
"os": [
|
|
131
|
+
"darwin",
|
|
132
|
+
"linux",
|
|
133
|
+
"win32"
|
|
134
|
+
],
|
|
123
135
|
"scripts": {
|
|
124
136
|
"build": "node scripts/build.mjs",
|
|
125
137
|
"test": "node test/verify.mjs"
|