dsh-cost-meter 1.6.12 → 1.6.13

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.
@@ -89,6 +89,14 @@ export const CODING_PLAN_PROVIDERS = {
89
89
  credentialEnvsSecret: ['VOLC_SECRETKEY', 'VOLCENGINE_SECRET_ACCESS_KEY', 'VOLCENGINE_SECRET_KEY', 'ARK_SECRET_ACCESS_KEY'],
90
90
  keyHint: '火山引擎访问密钥 AccessKeyID + SecretAccessKey(控制台 IAM→用户→密钥,需 ArkReadOnlyAccess + BillingCenterReadOnlyAccess;或导出 VOLC_ACCESSKEY / VOLC_SECRETKEY)',
91
91
  },
92
+ qwen: {
93
+ label: '千问 Qwen Token Plan',
94
+ credentialEnvs: [],
95
+ // 千问 Token Plan(个人版,platform.qianwenai.com)未提供 API-Key 化的额度查询端点:
96
+ // 额度仅控制台可见(网关 cookie + sec_token 会话),不走网络。与 SCNet 同模式:
97
+ // 按官方 Credits 抵扣率由本地账本估算,无需任何凭据(issue #78)。
98
+ keyHint: '无需凭据:按官方 Credits 抵扣率由本地账本估算月度用量(抵扣率可在设置中修改)',
99
+ },
92
100
  }
93
101
 
94
102
  export const CODING_PLAN_PROVIDER_IDS = Object.keys(CODING_PLAN_PROVIDERS)
@@ -859,6 +867,77 @@ export function scnetTokenPlanWindows(days, entry, nowMs) {
859
867
  }
860
868
  }
861
869
 
870
+ /**
871
+ * 千问 Token Plan(个人版)Credits 抵扣率(issue #78)。
872
+ *
873
+ * 平台未提供 API-Key 化的额度查询端点(额度仅控制台可见,需 cookie+sec_token
874
+ * 网关会话),与 SCNet 同模式走本地账本估算。抵扣率取自平台定价页
875
+ * (platform.qianwenai.com,单位:每百万 token 抵扣的 Credits;个人版为 Credits
876
+ * 计费,5 小时 + 7 天滚动窗口,本表按官方页 2026-08 数值内置,可在设置中覆盖)。
877
+ * cachedInput 为缓存读抵扣率;cacheWrite 计入未命中输入。
878
+ */
879
+ export const QWEN_CREDIT_RATES = {
880
+ 'qwen3.8-max-preview': { input: 60, cachedInput: 6, output: 60 },
881
+ 'qwen3.7-max': { input: 60, cachedInput: 6, output: 60 },
882
+ 'qwen3.7-plus': { input: 15, cachedInput: 1.5, output: 60 },
883
+ 'qwen3.6-flash': { input: 3, cachedInput: 0.3, output: 12 },
884
+ 'glm-5.2': { input: 45, cachedInput: 4.5, output: 120 },
885
+ 'deepseek-v4-pro': { input: 45, cachedInput: 4.5, output: 90 },
886
+ 'deepseek-v4-flash': { input: 3, cachedInput: 0.3, output: 12 },
887
+ 'kimi-k2.7-code': { input: 30, cachedInput: 3, output: 60 },
888
+ 'kimi-k2.6': { input: 30, cachedInput: 3, output: 60 },
889
+ 'minimax-m2.5': { input: 30, cachedInput: 3, output: 60 },
890
+ }
891
+
892
+ const QWEN_CREDIT_RATES_BY_CANON = Object.fromEntries(
893
+ Object.entries(QWEN_CREDIT_RATES).map(([id, rate]) => [scnetCanonModelId(id), rate]),
894
+ )
895
+
896
+ /**
897
+ * 汇总本地账本当前计费周期(自然月,与 SCNet 同款 scnetPlanPeriod)的千问
898
+ * Credits 用量。entry 为 codingPlans.qwen 配置:{ planCredits(必填,月度总额),
899
+ * planStart(可选,计费周期锚日), rates(可选,模型名 → 抵扣率覆盖,键为归一模型名) }。
900
+ * 返回 null(planCredits 非法)或 { used, total, percent, resetsAt, byModel, windows }。
901
+ */
902
+ export function qwenTokenPlanWindows(days, entry, nowMs) {
903
+ const total = Number(entry?.planCredits)
904
+ if (!Number.isFinite(total) || total <= 0) return null
905
+ // 用户覆盖的抵扣率优先,内置表兜底;键与模型名均做字母数字归一比较。
906
+ const overrides = entry?.rates !== null && typeof entry?.rates === 'object' && !Array.isArray(entry?.rates) ? entry.rates : {}
907
+ const rateOf = canon => {
908
+ if (overrides[canon] !== undefined && overrides[canon] !== null && typeof overrides[canon] === 'object') return overrides[canon]
909
+ return QWEN_CREDIT_RATES_BY_CANON[canon]
910
+ }
911
+ const period = scnetPlanPeriod(nowMs, entry?.planStart)
912
+ const byModel = {}
913
+ let used = 0
914
+ for (const [date, day] of Object.entries(days ?? {})) {
915
+ if (typeof date !== 'string' || date < period.fromKey || date > period.toKeyInclusive) continue
916
+ for (const [pmKey, buckets] of Object.entries(day?.byProviderModel ?? {})) {
917
+ const model = pmKey.includes(':') ? pmKey.slice(pmKey.indexOf(':') + 1) : pmKey
918
+ const canon = scnetCanonModelId(model)
919
+ const rate = rateOf(canon)
920
+ if (rate === undefined || buckets === null || typeof buckets !== 'object') continue
921
+ const credits = scnetModelCredits(buckets, rate)
922
+ used += credits
923
+ byModel[canon] = (byModel[canon] ?? 0) + credits
924
+ }
925
+ }
926
+ const percent = Math.min(100, Math.round((used / total) * 1000) / 10)
927
+ const fmt = n => Math.round(n).toLocaleString('en-US')
928
+ return {
929
+ used,
930
+ total,
931
+ percent,
932
+ resetsAt: period.resetsAt,
933
+ byModel,
934
+ windows: {
935
+ monthly: { percent, resetsAt: period.resetsAt },
936
+ credits: { resetsAt: '', text: `${fmt(used)} / ${fmt(total)} Credits (est.)` },
937
+ },
938
+ }
939
+ }
940
+
862
941
  /** 各家固定官方端点(硬编码白名单;region 变体按序尝试)。 */
863
942
  export const CODING_PLAN_ENDPOINTS = {
864
943
  anthropic: ['https://api.anthropic.com/api/oauth/usage'],
@@ -891,6 +970,8 @@ export const CODING_PLAN_ENDPOINTS = {
891
970
  commandcode: ['https://api.commandcode.ai/alpha/billing/credits'],
892
971
  // SCNet 无 API-Key 化额度端点:本地 Credits 计量(见 scnetTokenPlanWindows),不走网络。
893
972
  scnet: [],
973
+ // 千问 Token Plan 无 API-Key 化额度端点:本地 Credits 计量(见 qwenTokenPlanWindows),不走网络。
974
+ qwen: [],
894
975
  // 火山方舟:管控面 OpenAPI(需 AK/SK+HMAC,非 Bearer),按 Action 变体尝试(见 VOLCENGINE_ACTIONS)
895
976
  volcengine: VOLCENGINE_ACTIONS.map(action => `https://${VOLCENGINE_HOST}/?Action=${action}&Version=${VOLCENGINE_VERSION}`),
896
977
  }
package/lib/index.js CHANGED
@@ -23,7 +23,7 @@ import { backfillLegacyLedger, importLegacyHistory, repairForkSeed, repairProvid
23
23
  import { createLlmStreamBilling } from './billing-stream.js'
24
24
  import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, LEGACY_BASE_BOUNDARY, normalizePrice, parsePricingHtml, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost, isWrapperProviderId, wrapperUpstreamProvider } from './pricing.js'
25
25
  import { createUsageDeduper, USAGE_DEDUP_WINDOW_MS, usageFingerprint } from './usage-dedup.js'
26
- import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance, normalizeVolcengineKey } from './coding-plans.js'
26
+ import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, qwenTokenPlanWindows, emptyCustomBalance, queryCustomBalance, normalizeVolcengineKey } from './coding-plans.js'
27
27
  import { recordSamples, buildPlanStats, canonicalWindowKey, periodStartOf, aggregateUsageSince, enabledPlanSetOf, pruneHourBuckets, suggestPlanAutoClasses } from './plan-billing.js'
28
28
  import { fetchWithRetry } from './net.js'
29
29
  import { stateSchema } from './typert.host.js'
@@ -86,6 +86,7 @@ const SERVER_MESSAGES = {
86
86
  codingPlanRefreshed: '{provider} 额度已刷新',
87
87
  codingPlanQueryFailed: '{provider} 额度查询失败:{message}',
88
88
  scnetPlanCreditsInvalid: 'SCNet 月度 Credits 额度无效,请填写大于 0 的数值。',
89
+ qwenPlanCreditsInvalid: '千问月度 Credits 额度无效,请填写大于 0 的数值。',
89
90
  legacyImportDone: '导入完成:更新 {days} 天、新增 {sessions} 个会话(扫描 {scanned} 份会话日志)。',
90
91
  legacyImportNone: '没有可导入的安装前历史(扫描 {scanned} 份会话日志,缺失日期为空或已导入)。',
91
92
  legacyImportFailed: '导入安装前历史失败:{message}',
@@ -142,6 +143,7 @@ const SERVER_MESSAGES = {
142
143
  codingPlanRefreshed: '{provider} quota refreshed',
143
144
  codingPlanQueryFailed: '{provider} quota query failed: {message}',
144
145
  scnetPlanCreditsInvalid: 'Invalid SCNet monthly credits quota; enter a value greater than 0.',
146
+ qwenPlanCreditsInvalid: 'Invalid Qwen monthly credits quota; enter a value greater than 0.',
145
147
  legacyImportDone: 'Import finished: {days} day(s) updated, {sessions} session(s) added (scanned {scanned} session logs).',
146
148
  legacyImportNone: 'No pre-install history to import (scanned {scanned} session logs; missing dates are empty or already imported).',
147
149
  legacyImportFailed: 'Failed to import pre-install history: {message}',
@@ -1362,6 +1364,12 @@ function createService(ctx, ledger) {
1362
1364
  ...await describePlanKey(credentials, id, ledger.config),
1363
1365
  ...cached,
1364
1366
  windows: cached.windows !== null && typeof cached.windows === 'object' ? cached.windows : {},
1367
+ // 本地计量配置透传(scnet/qwen,issue #26/#78):设置页编辑入口需看到当前值。
1368
+ ...(id === 'scnet' || id === 'qwen' ? {
1369
+ ...(Number.isFinite(Number(cfg.planCredits)) && Number(cfg.planCredits) > 0 ? { planCredits: Number(cfg.planCredits) } : {}),
1370
+ ...(typeof cfg.planStart === 'string' ? { planStart: cfg.planStart } : {}),
1371
+ } : {}),
1372
+ ...(id === 'qwen' && cfg.rates !== null && typeof cfg.rates === 'object' && !Array.isArray(cfg.rates) ? { rates: cfg.rates } : {}),
1365
1373
  }
1366
1374
  }
1367
1375
  return out
@@ -1386,6 +1394,18 @@ function createService(ctx, ledger) {
1386
1394
  }
1387
1395
  return
1388
1396
  }
1397
+ // 千问 Token Plan(issue #78)与 SCNet 同型:平台无 API-Key 化额度端点(额度仅
1398
+ // 控制台可见,网关需 cookie+sec_token),按官方 Credits 抵扣率本地估算,跳过缓存间隔。
1399
+ if (id === 'qwen') {
1400
+ const result = qwenTokenPlanWindows(ledger.days ?? {}, config, Date.now())
1401
+ codingPlanCaches[id] = {
1402
+ fetchedAt: Date.now(),
1403
+ value: result === null
1404
+ ? { ...emptyCodingPlan(), status: 'off', fetchedAt: Date.now(), message: tmsg(localeOf(ledger.config), 'qwenPlanCreditsInvalid') }
1405
+ : { status: 'ok', message: '', fetchedAt: Date.now(), windows: result.windows },
1406
+ }
1407
+ return
1408
+ }
1389
1409
  const interval = Math.max(1, Number(config.refreshMinutes) || 15) * 60_000
1390
1410
  let cache = codingPlanCaches[id]
1391
1411
  if (!force && cache !== undefined && Date.now() - cache.fetchedAt < interval) return
@@ -42,6 +42,7 @@ export const DEFAULT_PLAN_PROVIDER_CLASS = {
42
42
  commandcode: 'auto',
43
43
  scnet: 'auto',
44
44
  volcengine: 'auto',
45
+ qwen: 'auto',
45
46
  go: 'auto',
46
47
  }
47
48
 
package/lib/store.js CHANGED
@@ -91,6 +91,7 @@ export function defaultConfig() {
91
91
  // SCNet Token Plan 无 API 额度端点:按官方 Credits 抵扣表本地估算(issue #26)。
92
92
  // planCredits=月度 Credits 额度;planStart=订阅起始日(空 = 自然月)。
93
93
  scnet: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '', planCredits: 240000, planStart: '' },
94
+ qwen: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '', planCredits: 500000, planStart: '', rates: {} },
94
95
  // 火山方舟 Volcano Ark Coding Plan(issue #60):管控面 AK/SK+HMAC,三窗口(5h/weekly/monthly)
95
96
  volcengine: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '', accessKeyId: '', secretAccessKey: '' },
96
97
  },
@@ -879,6 +880,25 @@ export function applyConfigPatch(current, rawPatch) {
879
880
  entry.planCredits = Number.isFinite(credits) && credits > 0 ? credits : 240000
880
881
  entry.planStart = typeof entry.planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.planStart) ? entry.planStart : ''
881
882
  }
883
+ // 千问 Token Plan 本地计量专用字段(issue #78):月度 Credits 额度、订阅起始日
884
+ // 与抵扣率覆盖(归一模型名 → { input, cachedInput, output },仅正有限数保留)。
885
+ if (id === 'qwen') {
886
+ const credits = Number(entry.planCredits)
887
+ entry.planCredits = Number.isFinite(credits) && credits > 0 ? credits : 500000
888
+ entry.planStart = typeof entry.planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.planStart) ? entry.planStart : ''
889
+ const rates = entry.rates !== null && typeof entry.rates === 'object' && !Array.isArray(entry.rates) ? entry.rates : {}
890
+ const cleaned = {}
891
+ for (const [model, rate] of Object.entries(rates)) {
892
+ if (typeof model !== 'string' || model.length === 0 || rate === null || typeof rate !== 'object') continue
893
+ const num = v => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : null }
894
+ const input = num(rate.input)
895
+ const cachedInput = num(rate.cachedInput)
896
+ const output = num(rate.output)
897
+ if (input === null || cachedInput === null || output === null) continue
898
+ cleaned[model] = { input, cachedInput, output }
899
+ }
900
+ entry.rates = cleaned
901
+ }
882
902
  // 火山方舟专用:AccessKeyID/SecretAccessKey(双凭据,空 = 未配置)。
883
903
  if (id === 'volcengine') {
884
904
  const rawId = typeof entry.accessKeyId === 'string' ? entry.accessKeyId.trim() : ''
@@ -1282,6 +1302,23 @@ export function sanitizeConfig(raw) {
1282
1302
  plans[id].planCredits = Number.isFinite(credits) && credits > 0 ? credits : 240000
1283
1303
  plans[id].planStart = typeof entry.planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.planStart) ? entry.planStart : ''
1284
1304
  }
1305
+ if (id === 'qwen') {
1306
+ const credits = Number(entry.planCredits)
1307
+ plans[id].planCredits = Number.isFinite(credits) && credits > 0 ? credits : 500000
1308
+ plans[id].planStart = typeof entry.planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.planStart) ? entry.planStart : ''
1309
+ const rates = entry.rates !== null && typeof entry.rates === 'object' && !Array.isArray(entry.rates) ? entry.rates : {}
1310
+ const cleaned = {}
1311
+ for (const [model, rate] of Object.entries(rates)) {
1312
+ if (typeof model !== 'string' || model.length === 0 || rate === null || typeof rate !== 'object') continue
1313
+ const num = v => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : null }
1314
+ const input = num(rate.input)
1315
+ const cachedInput = num(rate.cachedInput)
1316
+ const output = num(rate.output)
1317
+ if (input === null || cachedInput === null || output === null) continue
1318
+ cleaned[model] = { input, cachedInput, output }
1319
+ }
1320
+ plans[id].rates = cleaned
1321
+ }
1285
1322
  if (id === 'volcengine') {
1286
1323
  const rawId = typeof entry.accessKeyId === 'string' ? entry.accessKeyId.trim() : ''
1287
1324
  const rawApi = typeof entry.apiKey === 'string' ? entry.apiKey.trim() : ''
@@ -148,6 +148,12 @@ const configSchema = z.object({
148
148
  // SCNet 本地计量专用(issue #26):月度 Credits 额度与订阅起始日;其余厂商无此二键。
149
149
  planCredits: num.optional(),
150
150
  planStart: z.string().optional(),
151
+ // 千问 Token Plan 抵扣率覆盖(issue #78):归一模型名 → 三费率;仅千问有此键。
152
+ rates: z.record(z.string(), z.object({
153
+ input: num,
154
+ cachedInput: num,
155
+ output: num,
156
+ })).optional(),
151
157
  // 火山方舟双凭据(issue #60):AccessKeyID + SecretAccessKey
152
158
  accessKeyId: z.string().optional(),
153
159
  secretAccessKey: z.string().optional(),
@@ -275,6 +281,14 @@ const codingPlanSchema = z.object({
275
281
  message: z.string(),
276
282
  fetchedAt: num,
277
283
  windows: z.record(z.string(), z.object({ percent: num.optional(), resetsAt: z.string(), text: z.string().optional() })),
284
+ // 本地计量配置透传(仅 scnet / qwen 有;settings 页编辑入口需要看到当前值)。
285
+ planCredits: num.optional(),
286
+ planStart: z.string().optional(),
287
+ rates: z.record(z.string(), z.object({
288
+ input: num,
289
+ cachedInput: num,
290
+ output: num,
291
+ })).optional(),
278
292
  })
279
293
 
280
294
  // Token Plan 统计(issue #64):每 provider×window 的本地聚合、每 1%/满窗估算与采样区间序列。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.6.12",
3
+ "version": "1.6.13",
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",
@@ -63,7 +63,9 @@
63
63
  "https://openrouter.ai",
64
64
  "https://api.siliconflow.cn",
65
65
  "https://api.commandcode.ai",
66
- "https://ax.ac.sugon.com"
66
+ "https://ax.ac.sugon.com",
67
+ "https://platform.qianwenai.com",
68
+ "https://platform-home.qianwenai.com"
67
69
  ]
68
70
  }
69
71
  },