dsh-cost-meter 1.6.2 → 1.6.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.en.md +22 -6
- package/README.md +22 -6
- package/lib/backfill.js +6 -1
- package/lib/client.js +3 -6324
- package/lib/coding-plans.js +13 -7
- package/lib/index.js +27 -2
- package/lib/pricing.js +31 -0
- package/lib/store.js +123 -0
- package/lib/typert.host.js +7 -0
- package/package.json +11 -5
package/lib/coding-plans.js
CHANGED
|
@@ -513,9 +513,12 @@ export const VOLCENGINE_SERVICE = 'ark'
|
|
|
513
513
|
export const VOLCENGINE_REGION = 'cn-beijing'
|
|
514
514
|
export const VOLCENGINE_VERSION = '2024-01-01'
|
|
515
515
|
// 管控面 Action 白名单(按序尝试;需 AK/SK+HMAC 签名,非 Bearer)。
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
516
|
+
// GetCodingPlanUsage 为 CodingPlan 官方用量接口(issue #71 实测 by @suyukun):
|
|
517
|
+
// 无参即可返回 Result.QuotaUsage[] 三窗(session/weekly/monthly,只含 Percent),
|
|
518
|
+
// 置于首位;GetAFPUsage 实为 AgentPlan 接口(issue #60 评论 by @sanqiPanax,
|
|
519
|
+
// v1.5.46),CodingPlan 用户拿到全 0/空,GetUsageDetails 裸调 400(缺
|
|
520
|
+
// Filter.StartTime),GetPersonalPlan 亦需额外参数,保留作兜底变体。
|
|
521
|
+
export const VOLCENGINE_ACTIONS = ['GetCodingPlanUsage', 'GetAFPUsage', 'GetUsageDetails', 'GetPersonalPlan']
|
|
519
522
|
|
|
520
523
|
function hmacSha256(key, data) {
|
|
521
524
|
return createHmac('sha256', key).update(data, 'utf8').digest()
|
|
@@ -588,8 +591,9 @@ function normalizeVolcWindowName(raw) {
|
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
/**
|
|
591
|
-
* 解析火山方舟用量响应(管控面 GetUsageDetails / GetAFPUsage / GetPersonalPlan 等)。
|
|
592
|
-
*
|
|
594
|
+
* 解析火山方舟用量响应(管控面 GetCodingPlanUsage / GetUsageDetails / GetAFPUsage / GetPersonalPlan 等)。
|
|
595
|
+
* CodingPlan 官方形态 { Result: { QuotaUsage:[{Level:'session'|'weekly'|'monthly', Percent, ResetTimestamp, Cap}], ... } }(issue #71,
|
|
596
|
+
* 只返 Percent,无 used/total);或 { ResponseMetadata:..., Result: { UsageDetails:[{QuotaType,Total,Used,Remaining,ResetTime}], ... } }
|
|
593
597
|
* 或 arkcli usage plan 的 { items:[{product:"coding-plan",periods:[{label:"session"/"weekly"/"monthly",percent,reset_at}]}] }
|
|
594
598
|
* 或扁平窗口对象 { fiveHour:{used,limit,reset}, weekly:{...}, monthly:{...} }。
|
|
595
599
|
* 容忍多种字段命名与大小写,百分比由 used/total 推导(或直接取 percent),非法窗口忽略。
|
|
@@ -640,7 +644,7 @@ export function parseVolcengineUsage(data) {
|
|
|
640
644
|
// 尝试定位配额数组字段
|
|
641
645
|
let quotaList = null
|
|
642
646
|
if (root !== null && typeof root === 'object') {
|
|
643
|
-
quotaList = root.UsageDetails ?? root.usageDetails ?? root.usages ?? root.limits ?? root.quotas ?? root.windows ?? root.periods ?? root.details ?? root.items ?? null
|
|
647
|
+
quotaList = root.QuotaUsage ?? root.quotaUsage ?? root.UsageDetails ?? root.usageDetails ?? root.usages ?? root.limits ?? root.quotas ?? root.windows ?? root.periods ?? root.details ?? root.items ?? null
|
|
644
648
|
if (!Array.isArray(quotaList) && Array.isArray(root)) quotaList = root
|
|
645
649
|
// 单个Result包装对象里可能直接含窗口字段
|
|
646
650
|
if (quotaList === null && typeof root === 'object' && !Array.isArray(root)) {
|
|
@@ -660,7 +664,9 @@ export function parseVolcengineUsage(data) {
|
|
|
660
664
|
const windows = {}
|
|
661
665
|
for (const entry of quotaList) {
|
|
662
666
|
if (entry === null || typeof entry !== 'object') continue
|
|
663
|
-
|
|
667
|
+
// GetCodingPlanUsage 的窗口名字段为 Level(session/weekly/monthly,issue #71),
|
|
668
|
+
// 置于候选最前;其余 Action 沿用 QuotaType 等既有字段。
|
|
669
|
+
const nameRaw = entry.Level ?? entry.level ?? entry.QuotaType ?? entry.quotaType ?? entry.Type ?? entry.type ?? entry.Label ?? entry.label ?? entry.Period ?? entry.period ?? entry.Name ?? entry.name ?? entry.Window ?? entry.window ?? entry.QuotaName ?? entry.quotaName
|
|
664
670
|
let name = normalizeVolcWindowName(nameRaw)
|
|
665
671
|
if (name === null) continue
|
|
666
672
|
let pct = null
|
package/lib/index.js
CHANGED
|
@@ -18,10 +18,10 @@ import fs from 'node:fs'
|
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
20
20
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
21
|
-
import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay, splitLedgerApiCost, repairLedgerPricing } from './store.js'
|
|
21
|
+
import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay, splitLedgerApiCost, repairLedgerPricing, dedupeWrapperProviderDays } from './store.js'
|
|
22
22
|
import { backfillLegacyLedger, importLegacyHistory, repairForkSeed, repairProviderDupes } from './backfill.js'
|
|
23
23
|
import { createLlmStreamBilling } from './billing-stream.js'
|
|
24
|
-
import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, normalizePrice, parsePricingHtml, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost } from './pricing.js'
|
|
24
|
+
import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, normalizePrice, parsePricingHtml, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost, isWrapperProviderId } from './pricing.js'
|
|
25
25
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance, normalizeVolcengineKey } from './coding-plans.js'
|
|
26
26
|
import { recordSamples, buildPlanStats, canonicalWindowKey, periodStartOf, aggregateUsageSince, enabledPlanSetOf, pruneHourBuckets, suggestPlanAutoClasses } from './plan-billing.js'
|
|
27
27
|
import { fetchWithRetry } from './net.js'
|
|
@@ -414,6 +414,11 @@ function makeCostUsageProjection(ledger) {
|
|
|
414
414
|
} else {
|
|
415
415
|
return state
|
|
416
416
|
}
|
|
417
|
+
// modlens 视觉包装层(modlens-<upstream> / deepseek-modlens)把同一份 usage
|
|
418
|
+
// 经上游 llm.stream 再转发一次:日志里同一调用出现两套 header+usage 事件,
|
|
419
|
+
// 且因 provider 不同绕过 (turn, step) 键去重,投影会翻倍(issue #70)。与账本
|
|
420
|
+
// 钩子一致,包装层 provider 状态下的 usage 事件跳过,只统计上游真实流。
|
|
421
|
+
if (isWrapperProviderId(state.provider)) return state
|
|
417
422
|
const buckets = {
|
|
418
423
|
input: usage.inputTokens ?? 0,
|
|
419
424
|
output: usage.outputTokens ?? 0,
|
|
@@ -1487,6 +1492,21 @@ export async function runStartupImports(ledger, sessionsRoot) {
|
|
|
1487
1492
|
console.log(`[dsh-cost-meter] 包装路由重复计费清洗完成:合并 ${repaired.groups} 组重复条目,扣除 ${repaired.removedCost.toFixed(4)} USD`)
|
|
1488
1493
|
}
|
|
1489
1494
|
}
|
|
1495
|
+
// modlens 视觉包装层镜像一次性清洗(issue #70):现行 modlens 在监听器体内
|
|
1496
|
+
// 急切发起上游调用,逃逸 ALS 嵌套标记,上游与包装层(modlens-<upstream> /
|
|
1497
|
+
// deepseek-modlens)各入账一次,token/费用整体翻倍。实时钩子/投影/回放的
|
|
1498
|
+
// 包装层跳过已堵住新污染,此处修正历史存量:镜像对扣除包装层份,仅包装层
|
|
1499
|
+
// 入账的残留(provider-dedup-v1 合并后只剩字母序靠前的包装层键)改挂上游键。
|
|
1500
|
+
if (!ledger.migrations.includes('modlens-wrapper-dedup-v1')) {
|
|
1501
|
+
const repaired = dedupeWrapperProviderDays(ledger.days ?? {})
|
|
1502
|
+
ledger.migrations.push('modlens-wrapper-dedup-v1')
|
|
1503
|
+
if (repaired.removed + repaired.renamed > 0) {
|
|
1504
|
+
// 改挂上游键的条目 provider 变了,plan/api 分类需按新 provider 重算。
|
|
1505
|
+
splitLedgerApiCost(ledger)
|
|
1506
|
+
ledger.scheduleWrite()
|
|
1507
|
+
console.log(`[dsh-cost-meter] modlens 包装层重复计费清洗完成:扣除 ${repaired.removed} 条镜像,${repaired.renamed} 条改挂上游 provider`)
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1490
1510
|
// Plan/API 双轨拆分一次性迁移(issue #64):历史账本按当前分类回溯重算
|
|
1491
1511
|
// apiCost(真金白银部分),plan 类条目金额只记等值;幂等,标记防重跑。
|
|
1492
1512
|
if (!ledger.migrations.includes('plan-billing-split-v1')) {
|
|
@@ -1577,8 +1597,13 @@ export function apply(ctx) {
|
|
|
1577
1597
|
// 体内再发起 ctx.llm.stream(),旧实现在瀑布每层都记账(同请求 ×2~3);
|
|
1578
1598
|
// createLlmStreamBilling 用 AsyncLocalStorage 深度标记识别嵌套调用,
|
|
1579
1599
|
// 只由最外层记一次。历史污染由启动期 provider-dedup-v1 清洗兜底。
|
|
1600
|
+
// 包装层跳过(issue #70):现行 modlens(modlens-<upstream> id 方案)在监听器
|
|
1601
|
+
// 体内(瀑布派发期)急切发起上游调用,逃逸 ALS 深度标记,上游与包装层两份
|
|
1602
|
+
// usage 都到达这里——账本对 provider 为包装层 id 的事件直接跳过,只记上游
|
|
1603
|
+
// 真实流;历史翻倍由启动期 modlens-wrapper-dedup-v1 清洗兜底。
|
|
1580
1604
|
ctx.on('llm/stream', createLlmStreamBilling({
|
|
1581
1605
|
account: (usage, model, sessionId, atMs, provider) => {
|
|
1606
|
+
if (isWrapperProviderId(provider)) return
|
|
1582
1607
|
ledger.account({
|
|
1583
1608
|
input: usage.inputTokens ?? 0,
|
|
1584
1609
|
output: usage.outputTokens ?? 0,
|
package/lib/pricing.js
CHANGED
|
@@ -720,6 +720,37 @@ function deepseekExplicitEntry(modelName, prices, mode) {
|
|
|
720
720
|
return { entry, billingMode: 'deepseek-peak', priced: true }
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
+
/**
|
|
724
|
+
* 是否 modlens 视觉包装层 provider id(`modlens-<upstream>` / `deepseek-modlens`)。
|
|
725
|
+
*
|
|
726
|
+
* modlens 插件(@liustack/modlens)为每条承载纯文本 DeepSeek/GLM 模型的 provider
|
|
727
|
+
* 路由自动注册「(modlens vision)」包装模型,provider id 形如 `modlens-opencode-go`
|
|
728
|
+
* (deepseek-official 路由固定为 `deepseek-modlens`)。包装层会在自身监听器体内
|
|
729
|
+
* 再发起一次上游 llm.stream 转发同一份 usage——该内层分发发生在瀑布派发期,
|
|
730
|
+
* 逃逸 billing-stream 的 ALS 深度标记,实时钩子与历史回放照单全收会把每次调用
|
|
731
|
+
* 记两遍(上游 + 包装层,issue #70 实测账本 token/费用整体翻倍)。统一判定函数
|
|
732
|
+
* 供各计费入口(实时钩子 / 会话投影 / 历史回放 / 账本清洗)跳过包装层、只记上游真实流。
|
|
733
|
+
*/
|
|
734
|
+
export function isWrapperProviderId(provider) {
|
|
735
|
+
if (typeof provider !== 'string' || provider.length === 0) return false
|
|
736
|
+
return provider.startsWith('modlens-') || provider === 'deepseek-modlens'
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 包装层 provider 对应的上游 provider id:`modlens-opencode-go` → `opencode-go`,
|
|
741
|
+
* `deepseek-modlens` → `deepseek-official`(modlens 对官方路由的固定包装名);
|
|
742
|
+
* 非包装层返回 null。供账本清洗把「仅包装层入账」的存量条目改挂回上游键。
|
|
743
|
+
*/
|
|
744
|
+
export function wrapperUpstreamProvider(provider) {
|
|
745
|
+
if (typeof provider !== 'string' || provider.length === 0) return null
|
|
746
|
+
if (provider === 'deepseek-modlens') return 'deepseek-official'
|
|
747
|
+
if (provider.startsWith('modlens-')) {
|
|
748
|
+
const upstream = provider.slice('modlens-'.length)
|
|
749
|
+
return upstream.length > 0 ? upstream : null
|
|
750
|
+
}
|
|
751
|
+
return null
|
|
752
|
+
}
|
|
753
|
+
|
|
723
754
|
/**
|
|
724
755
|
* provider-aware 价格查找。provider 缺失时保持旧版 DeepSeek 行为;
|
|
725
756
|
* 已知非 DeepSeek provider 未配置模型时返回 null,避免误套 DeepSeek default。
|
package/lib/store.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
priceEntryFor,
|
|
19
19
|
providerPriceEntryFor,
|
|
20
20
|
usdFromCost,
|
|
21
|
+
isWrapperProviderId,
|
|
22
|
+
wrapperUpstreamProvider,
|
|
21
23
|
} from './pricing.js'
|
|
22
24
|
import { CODING_PLAN_PROVIDER_IDS } from './coding-plans.js'
|
|
23
25
|
import { DEFAULT_PLAN_PROVIDER_CLASS, PLAN_PROVIDER_IDS, billingClassOf, enabledPlanSetOf, planProviderIdOf, appendHourBucket, pruneHourBuckets, convertRecentCallsToBuckets, isRoutedThirdPartyCall } from './plan-billing.js'
|
|
@@ -140,6 +142,16 @@ export function defaultConfig() {
|
|
|
140
142
|
plans: true, // 横条含已启用的 Coding Plan 窗口
|
|
141
143
|
promptSeen: false, // 首次更新后的功能引导是否已处理(用户自主决定开关)
|
|
142
144
|
},
|
|
145
|
+
// 进度条方向(issue #67):各条独立选择填充语义。
|
|
146
|
+
// balance = 余额条(官方/自定义):默认 remaining(满条起步随消耗递减);
|
|
147
|
+
// budget/go/plan = 预算与额度条:默认 used(空条起步随消耗填满,#57 统一口径)。
|
|
148
|
+
// 值只能是 'remaining' | 'used',语义为「填充代表什么」。
|
|
149
|
+
barDirections: {
|
|
150
|
+
balance: 'remaining',
|
|
151
|
+
budget: 'used',
|
|
152
|
+
go: 'used',
|
|
153
|
+
plan: 'used',
|
|
154
|
+
},
|
|
143
155
|
usage: {
|
|
144
156
|
position: 'cost', // Token 用量统计显示位置:cost(费用设置) | general(通用设置) | section(独立分节)
|
|
145
157
|
},
|
|
@@ -361,6 +373,92 @@ export function repairLedgerPricing(ledger) {
|
|
|
361
373
|
return { touchedDays, touchedSessions, recostedBuckets }
|
|
362
374
|
}
|
|
363
375
|
|
|
376
|
+
/**
|
|
377
|
+
* modlens 视觉包装层镜像一次性清洗(issue #70)。
|
|
378
|
+
*
|
|
379
|
+
* 现行 modlens(modlens-<upstream> / deepseek-modlens)在监听器体内急切发起
|
|
380
|
+
* 上游 llm.stream,逃逸 billing-stream 的 ALS 嵌套标记,同一次调用在上游与
|
|
381
|
+
* 包装层两个 provider 键下各入账一次(六值桶逐位相同),token/费用整体翻倍。
|
|
382
|
+
*
|
|
383
|
+
* 与 provider-dedup-v1(issue #48 指纹合并,保留字母序第一个键)的遗留交互
|
|
384
|
+
* 需要区分三种形态,逐容器(day 与其下每个 session)处理:
|
|
385
|
+
* 1. 镜像对:上游键与包装层键六值桶全等 → 从容器合计扣除包装层份并删除
|
|
386
|
+
* (真实上游键本就存在,扣减即恢复真实值);
|
|
387
|
+
* 2. 包装层 ⊃ 上游(#48 合并过旧镜像、之后又积累新镜像的混存形态:
|
|
388
|
+
* 包装层键 = 合并残留 + 新镜像,上游键 = 新镜像):上游键是包装层键的
|
|
389
|
+
* 子集 → 扣除并删除上游键,包装层键改挂上游 provider 名;
|
|
390
|
+
* 3. 无上游键(仅有包装层入账,如 #48 合并后只剩字母序靠前的包装层键):
|
|
391
|
+
* 包装层键改挂上游 provider 名,合计不动;
|
|
392
|
+
* 4. 包装层 ⊂ 上游(同日直连 + 包装调用混存:上游键 = 直连量 + 全部包装
|
|
393
|
+
* 调用量,包装层键是纯镜像子集)→ 从容器合计扣除包装层份并删除。
|
|
394
|
+
* 六值桶互不为子集(无法安全判定)的条目不动(保守);幂等,二次运行无
|
|
395
|
+
* 包装层键时返回 0。调用方以账本 migrations 标记保证只跑一次。
|
|
396
|
+
* @param days - 账本每日记录对象(原地修改)。
|
|
397
|
+
* @returns { removed, renamed } 扣除删除的镜像条目数 / 改挂上游键的条目数。
|
|
398
|
+
*/
|
|
399
|
+
export function dedupeWrapperProviderDays(days) {
|
|
400
|
+
const result = { removed: 0, renamed: 0 }
|
|
401
|
+
const FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning', 'calls']
|
|
402
|
+
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0)
|
|
403
|
+
const fingerprint = (b) => FIELDS.map(f => num(b?.[f])).join('|')
|
|
404
|
+
const isSubset = (small, big) => FIELDS.every(f => num(small?.[f]) <= num(big?.[f]))
|
|
405
|
+
const subtractInto = (container, bucket) => {
|
|
406
|
+
for (const f of [...FIELDS, 'cost', 'apiCost']) {
|
|
407
|
+
container[f] = Math.max(0, num(container[f]) - num(bucket?.[f]))
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const repairContainer = (container) => {
|
|
411
|
+
const pm = container?.byProviderModel
|
|
412
|
+
if (pm === null || typeof pm !== 'object' || Array.isArray(pm)) return
|
|
413
|
+
for (const key of Object.keys(pm)) {
|
|
414
|
+
const sep = key.indexOf(':')
|
|
415
|
+
if (sep <= 0) continue
|
|
416
|
+
const provider = key.slice(0, sep)
|
|
417
|
+
if (!isWrapperProviderId(provider)) continue
|
|
418
|
+
const model = key.slice(sep + 1)
|
|
419
|
+
const upstream = wrapperUpstreamProvider(provider)
|
|
420
|
+
if (upstream === null) continue
|
|
421
|
+
const bucket = pm[key]
|
|
422
|
+
const twinKey = `${upstream}:${model}`
|
|
423
|
+
const twin = pm[twinKey]
|
|
424
|
+
if (twin === undefined) {
|
|
425
|
+
// 形态 3:仅包装层入账——改挂上游键,容器合计不动。
|
|
426
|
+
delete pm[key]
|
|
427
|
+
pm[twinKey] = bucket
|
|
428
|
+
result.renamed += 1
|
|
429
|
+
} else if (fingerprint(twin) === fingerprint(bucket)) {
|
|
430
|
+
// 形态 1:经典镜像对——扣除包装层份并删除,上游键即真实值。
|
|
431
|
+
subtractInto(container, bucket)
|
|
432
|
+
delete pm[key]
|
|
433
|
+
result.removed += 1
|
|
434
|
+
} else if (isSubset(twin, bucket)) {
|
|
435
|
+
// 形态 2:上游键 ⊆ 包装层键(混存残留)——扣除并删除上游键,包装层改挂上游。
|
|
436
|
+
subtractInto(container, twin)
|
|
437
|
+
delete pm[key]
|
|
438
|
+
pm[twinKey] = bucket
|
|
439
|
+
result.renamed += 1
|
|
440
|
+
} else if (isSubset(bucket, twin)) {
|
|
441
|
+
// 形态 4:包装层键 ⊆ 上游键(同日直连 + 包装调用混存:上游键含直连量 +
|
|
442
|
+
// 全部包装调用量,包装层键是纯镜像子集)——扣除包装层份并删除。
|
|
443
|
+
subtractInto(container, bucket)
|
|
444
|
+
delete pm[key]
|
|
445
|
+
result.removed += 1
|
|
446
|
+
}
|
|
447
|
+
// 其余:互不为子集,无法安全判定,保守不动。
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const day of Object.values(days ?? {})) {
|
|
451
|
+
if (day === null || typeof day !== 'object') continue
|
|
452
|
+
repairContainer(day)
|
|
453
|
+
if (Array.isArray(day.sessions)) {
|
|
454
|
+
for (const session of day.sessions) {
|
|
455
|
+
if (session !== null && typeof session === 'object') repairContainer(session)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return result
|
|
460
|
+
}
|
|
461
|
+
|
|
364
462
|
function zeroSession(id) {
|
|
365
463
|
return { id, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0, apiCost: 0, byProviderModel: {} }
|
|
366
464
|
}
|
|
@@ -506,6 +604,8 @@ const VALIDATION_MESSAGES = {
|
|
|
506
604
|
cornerFlag: 'corner.{field} 必须是布尔值',
|
|
507
605
|
quotaStrip: 'quotaStrip 非法',
|
|
508
606
|
quotaStripFlag: 'quotaStrip.{field} 必须是布尔值',
|
|
607
|
+
barDirections: 'barDirections 非法',
|
|
608
|
+
barDirectionValue: 'barDirections.{field} 必须是 remaining / used',
|
|
509
609
|
usage: 'usage 非法',
|
|
510
610
|
usagePosition: 'usage.position 必须是 cost / general / section',
|
|
511
611
|
planBilling: 'planBilling 非法',
|
|
@@ -583,6 +683,8 @@ const VALIDATION_MESSAGES = {
|
|
|
583
683
|
cornerFlag: 'corner.{field} must be a boolean',
|
|
584
684
|
quotaStrip: 'Invalid quotaStrip',
|
|
585
685
|
quotaStripFlag: 'quotaStrip.{field} must be a boolean',
|
|
686
|
+
barDirections: 'Invalid barDirections',
|
|
687
|
+
barDirectionValue: 'barDirections.{field} must be remaining / used',
|
|
586
688
|
usage: 'Invalid usage',
|
|
587
689
|
usagePosition: 'usage.position must be cost / general / section',
|
|
588
690
|
planBilling: 'Invalid planBilling',
|
|
@@ -831,6 +933,18 @@ export function applyConfigPatch(current, patch) {
|
|
|
831
933
|
if (typeof quotaStrip[field] !== 'boolean') errors.push(vmsg(locale, 'quotaStripFlag', { field }))
|
|
832
934
|
}
|
|
833
935
|
}
|
|
936
|
+
// 进度条方向(issue #67):四组条各自 remaining / used,非法值整体报错
|
|
937
|
+
// (客户端提交完整对象,与 corner/quotaStrip 同策略)。
|
|
938
|
+
const barDirections = candidate.barDirections
|
|
939
|
+
if (barDirections === null || typeof barDirections !== 'object' || Array.isArray(barDirections)) {
|
|
940
|
+
errors.push(vmsg(locale, 'barDirections'))
|
|
941
|
+
} else {
|
|
942
|
+
for (const field of ['balance', 'budget', 'go', 'plan']) {
|
|
943
|
+
if (barDirections[field] !== 'remaining' && barDirections[field] !== 'used') {
|
|
944
|
+
errors.push(vmsg(locale, 'barDirectionValue', { field }))
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
834
948
|
// Token 用量统计显示位置校验。
|
|
835
949
|
const usage = candidate.usage
|
|
836
950
|
if (usage === null || typeof usage !== 'object' || Array.isArray(usage)) {
|
|
@@ -1036,6 +1150,15 @@ export function sanitizeConfig(raw) {
|
|
|
1036
1150
|
strip.enabled = strip.enabled === true
|
|
1037
1151
|
strip.promptSeen = strip.promptSeen === true
|
|
1038
1152
|
for (const key of ['budget', 'go', 'plans']) strip[key] = strip[key] !== false
|
|
1153
|
+
// 进度条方向(issue #67):非法值逐键回落默认(balance=remaining,其余=used)。
|
|
1154
|
+
const dirs = out.barDirections !== null && typeof out.barDirections === 'object' && !Array.isArray(out.barDirections)
|
|
1155
|
+
? out.barDirections : {}
|
|
1156
|
+
out.barDirections = {
|
|
1157
|
+
balance: dirs.balance === 'used' ? 'used' : 'remaining',
|
|
1158
|
+
budget: dirs.budget === 'remaining' ? 'remaining' : 'used',
|
|
1159
|
+
go: dirs.go === 'remaining' ? 'remaining' : 'used',
|
|
1160
|
+
plan: dirs.plan === 'remaining' ? 'remaining' : 'used',
|
|
1161
|
+
}
|
|
1039
1162
|
// codingPlans:逐家收敛(非法条目整家回落默认;只保留已知提供商)。
|
|
1040
1163
|
const plans = {}
|
|
1041
1164
|
if (out.codingPlans !== null && typeof out.codingPlans === 'object') {
|
package/lib/typert.host.js
CHANGED
|
@@ -199,6 +199,13 @@ const configSchema = z.object({
|
|
|
199
199
|
plans: z.boolean(),
|
|
200
200
|
promptSeen: z.boolean(),
|
|
201
201
|
}),
|
|
202
|
+
// 进度条方向(issue #67):balance/budget/go/plan 各自 remaining | used。
|
|
203
|
+
barDirections: z.object({
|
|
204
|
+
balance: z.enum(['remaining', 'used']),
|
|
205
|
+
budget: z.enum(['remaining', 'used']),
|
|
206
|
+
go: z.enum(['remaining', 'used']),
|
|
207
|
+
plan: z.enum(['remaining', 'used']),
|
|
208
|
+
}).optional(),
|
|
202
209
|
// Token 用量统计显示位置(当前固定 cost;键保留供读侧白名单)。
|
|
203
210
|
usage: z.object({ position: z.enum(['cost', 'general', 'section']) }).optional(),
|
|
204
211
|
// Plan/API 双轨计费分类(issue #64):providers 值 auto/plan/api;models 为 provider:model 显式覆盖。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(9 家,含 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 9 vendors (incl. 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",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"docs/provider-pricing.json"
|
|
62
62
|
],
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@deepseek-ai/dsh-credentials": "
|
|
65
|
-
"@deepseek-ai/dsh-home-paths": "
|
|
66
|
-
"zod": "
|
|
64
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6",
|
|
65
|
+
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.6",
|
|
66
|
+
"zod": "4.4.3"
|
|
67
67
|
},
|
|
68
68
|
"repository": {
|
|
69
69
|
"type": "git",
|
|
@@ -86,5 +86,11 @@
|
|
|
86
86
|
"en",
|
|
87
87
|
"language"
|
|
88
88
|
],
|
|
89
|
-
"license": "MIT"
|
|
89
|
+
"license": "MIT",
|
|
90
|
+
"scripts": {
|
|
91
|
+
"build": "node scripts/build.mjs"
|
|
92
|
+
},
|
|
93
|
+
"devDependencies": {
|
|
94
|
+
"esbuild": "^0.28.1"
|
|
95
|
+
}
|
|
90
96
|
}
|