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.
@@ -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 or balance reconciliation. */
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.billingMode === 'deepseek-peak' && config?.prices?.currency === 'CNY' ? 'CNY' : 'USD', config?.exchangeRate)
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: '对账提示:本地账本今日官方渠道费用 {cost} 与官方余额当日变动 {delta} 偏差较大,请核对价格表或近期账单',
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 official-channel cost ({cost}) deviates significantly from the official balance change ({delta}); please check the price table or recent bills',
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 { ref, event } = reconcileBalanceDelta(ledger.balanceRef, balanceCache.value, ledger.todayOfficialCost(), localDayKey(nowMs), nowMs, { exchangeRate: ledger.config.exchangeRate })
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
  }
@@ -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
- const canonical = models[`${planId}:${modelId}`]
132
- if (canonical === 'plan' || canonical === 'api') return canonical
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
- 'mimo-v2.5': { input: 0.14, cachedInput: 0.0028, output: 0.28, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
241
- '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' },
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 的 19 个(不含免费档):订阅制下请求
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: { 'mimo-v2.5': 'MiMo V2.5', 'mimo-v2.5-pro': 'MiMo V2.5' },
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
- // 前缀匹配:modelId(去饰后)以候选(去饰后)开头且紧接分隔符,取最长候选。
787
- // 分隔符后的整段若为 1-2 位纯数字(gpt-5.9 的 '.9'、kimi-k2.6 的 '.6')同样
788
- // 视为版本分叉拒绝;'-128k'/''-v3.1'/日期后缀等不受影响。
789
- let prefixHit = null
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(去后缀/前缀/家族相似) | exact(仅精确)
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, 'USD', ledger.config.exchangeRate)
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, 'USD', ledger.config.exchangeRate)
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
@@ -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
@@ -0,0 +1,3 @@
1
+ {
2
+ "meta": {}
3
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "费用计量 (dsh-cost-meter)",
4
+ "description": "费用计量:按会话统计 token 与成本,含各厂商余额与峰谷计价提醒"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.7.39",
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": { "dsh-cost-meter-repair-sessions": "./lib/repair-sessions-cli.js" },
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": {"patch": "./cordis.patch.yml"},
22
- "client": {"platform": "web"},
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": ["darwin", "linux", "win32"],
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"