@kenz1117/dsh-ui-usage-billing 1.4.2 → 1.4.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/lib/index.js CHANGED
@@ -198,7 +198,7 @@ let liveExtraModels;
198
198
  /**
199
199
  * 用户自定义模型别名(插件配置 `modelKeyAliases`,聚合启动时注入):真实日志
200
200
  * model id → 计费目录键。优先级高于内置别名表——目录外的新模型无需等发版,
201
- * 配置一条别名即完成识别与计价(键必须是 MODEL_CATALOG 的既有 key)。
201
+ * 配置一条别名即完成识别与计价(键必须是内置目录的既有 key)。
202
202
  */
203
203
  let userModelAliases;
204
204
  /**
@@ -317,7 +317,7 @@ function tierAtWithBounds(timeMs, bounds) {
317
317
  }
318
318
  /** 时刻是否落在北京时间周末(周六/周日)。 */
319
319
  function isBeijingWeekend(timeMs) {
320
- const day = new Date(timeMs + 288e5).getUTCDay();
320
+ const day = new Date(timeMs + 8 * 36e5).getUTCDay();
321
321
  return day === 0 || day === 6;
322
322
  }
323
323
  /** 峰谷切换边界(北京时间的当日分钟数):09:00 / 12:00 / 14:00 / 18:00。 */
@@ -329,211 +329,540 @@ const TIER_BOUNDARY_MINUTES = [
329
329
  ];
330
330
  /** 北京时间的当日毫秒数(0–86,400,000)。 */
331
331
  function beijingMillisOfDay(timeMs) {
332
- return ((timeMs + 288e5) % 864e5 + 864e5) % 864e5;
332
+ return ((timeMs + 8 * 36e5) % 864e5 + 864e5) % 864e5;
333
333
  }
334
334
  /**
335
- * Built-in catalog of current mainstream models as of 2026-08-16, priced from
336
- * each provider's official price page. Domestic providers are OpenAI-API
337
- * compatible and publish RMB prices directly; overseas providers publish USD
338
- * and convert through the exchange rate at estimate time. Retired models
339
- * (GPT-4o family, Gemini 2.x, GLM-4.x-lite, older Qwen) are deliberately
340
- * absent, as are Anthropic Claude models (their native API is not
341
- * OpenAI-compatible, so the harness cannot drive them directly). DeepSeek
342
- * keys match the harness stats file so real usage prices from the catalog;
343
- * unknown keys fall back to `other`.
344
- *
345
- * Time-of-day billing (peak/off-peak) is now real: DeepSeek V4 officially
346
- * splits peak (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate
347
- * from 2026-08-17, and Gemini's Flex tier discounts spare-capacity traffic.
335
+ * 内置目录与别名表的运行时注入态:数据本体在 src/builtin-catalog.ts(仅随 node
336
+ * 半构建发布)。宿主半区在插件 activate 时同步注入;客户端在 /api/billing/pricing
337
+ * 首次响应时注入。未注入时目录为空——计价消费方走 UNSEEDED_OTHER 兜底(不产生
338
+ * 费用),费率表渲染加载态,不视为错误。
348
339
  */
349
- /** DeepSeek 官方高峰时段说明(峰谷分时计费目录条目共用)。 */
350
- const DEEPSEEK_PEAK_HOURS = "09:00-12:00 / 14:00-18:00";
351
- const GEMINI_PEAK_HOURS = "Standard / Flex";
352
- const MODEL_CATALOG = [
353
- {
354
- key: "flash",
355
- name: "DeepSeek V4.1 Flash",
356
- provider: "DeepSeek",
357
- colorVar: "ds-blue",
358
- price: {
359
- currency: "CNY",
360
- input: 2,
361
- cacheHit: .04,
362
- output: 8,
363
- offPeak: {
364
- input: 1,
365
- cacheHit: .02,
366
- output: 4
367
- }
368
- },
369
- peakHours: DEEPSEEK_PEAK_HOURS
370
- },
371
- {
372
- key: "flash-vision-exp",
373
- name: "DeepSeek V4 Flash Vision (Exp)",
374
- provider: "DeepSeek",
375
- colorVar: "ds-blue",
376
- retired: true,
377
- price: {
378
- currency: "CNY",
379
- input: 2,
380
- cacheHit: .04,
381
- output: 8,
382
- offPeak: {
383
- input: 1,
384
- cacheHit: .02,
385
- output: 4
386
- }
387
- },
388
- peakHours: DEEPSEEK_PEAK_HOURS
389
- },
390
- {
391
- key: "pro",
392
- name: "DeepSeek V4 Pro",
393
- provider: "DeepSeek",
394
- colorVar: "dsw-static-deepseek-500",
395
- price: {
396
- currency: "CNY",
397
- input: 9,
398
- cacheHit: .3,
399
- output: 27,
400
- offPeak: {
401
- input: 4.5,
402
- cacheHit: .15,
403
- output: 13.5
404
- }
405
- },
406
- peakHours: DEEPSEEK_PEAK_HOURS
407
- },
408
- {
409
- key: "glm",
410
- name: "GLM-5.2",
411
- provider: "智谱 AI",
412
- colorVar: "dsw-static-blue-600",
413
- price: {
414
- currency: "CNY",
415
- input: 8,
416
- cacheHit: 2,
417
- output: 28
340
+ let builtinCatalog = [];
341
+ let builtinAliases = {};
342
+ /**
343
+ * 注入内置目录与别名表(两侧各调用一次:宿主 activate、客户端首个 pricing 响应)。
344
+ * 重复注入整体替换并重建归一化索引。
345
+ * @param catalog - 内置目录条目(BUILTIN_MODEL_CATALOG 全量)。
346
+ * @param aliases - 内置别名表(BUILTIN_MODEL_KEY_ALIASES 全量)。
347
+ */
348
+ function applyBuiltinCatalog(catalog, aliases) {
349
+ builtinCatalog = catalog;
350
+ builtinAliases = aliases;
351
+ canonIndexCache = void 0;
352
+ }
353
+ /** 内置目录当前快照:未注入为空数组(client 首帧前渲染加载态)。 */
354
+ function modelCatalog() {
355
+ return builtinCatalog;
356
+ }
357
+ /** 内置别名表当前快照:未注入为空对象。 */
358
+ function modelKeyAliases() {
359
+ return builtinAliases;
360
+ }
361
+ /** 未注入时的目录兜底条目:与内置 other 条目同价(零价,不计费)。 */
362
+ const UNSEEDED_OTHER = {
363
+ key: "other",
364
+ name: "其他模型",
365
+ provider: "Custom",
366
+ colorVar: "dsw-static-neutral-bluish-500",
367
+ price: {
368
+ currency: "CNY",
369
+ input: 0,
370
+ cacheHit: 0,
371
+ cacheMiss: 0,
372
+ output: 0
373
+ }
374
+ };
375
+ /**
376
+ * 模型 id 归一化:小写、去括号附注(如 `gpt5.6 luna(go)` 只看主体)、再去所有
377
+ * 非字母数字分隔符(空格 / 横杠 / 点 / 下划线)。用于日志里的模型 id 与计费
378
+ * 目录键做宽松匹配,提升「大小写/分隔符差异导致未收录」的识别率。
379
+ * @param id - 原始模型 id(日志或目录键)。
380
+ * @returns 归一化键(字母数字小写串)。
381
+ */
382
+ function canonModelId(id) {
383
+ return String(id).toLowerCase().replace(/\([^)]*\)/g, "").replace(/[^a-z0-9]+/g, "");
384
+ }
385
+ /**
386
+ * 目录常量键的归一化索引:归一化键 → 真实计费键。只索引静态来源(内置目录、
387
+ * 别名表、dsh-spend 兜底键);models.dev 补充条目是运行时注入,单独实时匹配。
388
+ * 惰性构建:注入前目录为空,构建无意义;每次 applyBuiltinCatalog 后重建。
389
+ */
390
+ let canonIndexCache;
391
+ function canonIndex() {
392
+ if (canonIndexCache !== void 0) return canonIndexCache;
393
+ const map = /* @__PURE__ */ new Map();
394
+ const add = (candidate, target) => {
395
+ const canon = canonModelId(candidate);
396
+ if (canon !== "" && !map.has(canon)) map.set(canon, target);
397
+ };
398
+ for (const entry of builtinCatalog) add(entry.key, entry.key);
399
+ for (const [alias, key] of Object.entries(builtinAliases)) add(alias, key);
400
+ for (const rate of FALLBACK_RATES) add(rate.key, rate.key);
401
+ canonIndexCache = map;
402
+ return map;
403
+ }
404
+ /**
405
+ * 解析真实日志模型 id → 计费目录键。先精确别名映射(既有行为);未命中时做
406
+ * 归一化匹配(忽略大小写/分隔符/括号附注),命中内置目录 / 别名目标 / 兜底键 /
407
+ * models.dev 补充键即返回其真实键;完全未知时保持原样(回退 other,不计费)。
408
+ * 供聚合层折叠与客户端渲染共用,两侧一致。
409
+ * @param id - 真实模型 id(日志里出现的形式)。
410
+ * @returns 计费目录键。
411
+ */
412
+ /**
413
+ * 由一个未知模型 id 派生候选 id(仅当直接查全部未命中时才尝试):
414
+ * - 剥离组织前缀(`deepseek/deepseek-v4-flash` → `deepseek-v4-flash`);
415
+ * - 剥离尾部纯数字段(`deepseek-v4-flash-202605` / `-0731` → `deepseek-v4-flash`,
416
+ * 覆盖 TokenHub / 官方按日期滚动的快照 id);
417
+ * - 两者组合派生。目录键本身(如 `mistral-large-2512`、`command-a-03-2025`)
418
+ * 在直接查就已命中,永不进入派生分支,不受剥段影响。
419
+ */
420
+ function derivedKeyCandidates(id) {
421
+ const out = [];
422
+ const push = (value) => {
423
+ if (value !== "" && !out.includes(value)) out.push(value);
424
+ };
425
+ const stripTrailingDigits = (value) => {
426
+ let base = value;
427
+ for (;;) {
428
+ const next = base.replace(/[-_]\d{3,}$/u, "");
429
+ if (next === base || next === "") return;
430
+ base = next;
431
+ push(base);
418
432
  }
419
- },
420
- {
421
- key: "glm-5.3",
422
- name: "GLM-5.3",
423
- provider: "智谱 AI",
424
- colorVar: "ds-blue",
425
- price: {
426
- currency: "CNY",
427
- input: 8,
428
- cacheHit: 2,
429
- output: 28
433
+ };
434
+ const slash = id.lastIndexOf("/");
435
+ if (slash > 0 && slash < id.length - 1) {
436
+ const bare = id.slice(slash + 1);
437
+ push(bare);
438
+ stripTrailingDigits(bare);
439
+ }
440
+ stripTrailingDigits(id);
441
+ return out;
442
+ }
443
+ /** 查一个候选 id(用户别名 → 内置别名 → 目录归一化 → models.dev 补充);未命中返回 undefined。 */
444
+ function lookupCandidate(candidate) {
445
+ const alias = userModelAliases?.[candidate] ?? modelKeyAliases()[candidate];
446
+ if (alias !== void 0) return alias;
447
+ const canon = canonModelId(candidate);
448
+ if (canon === "") return void 0;
449
+ const hit = canonIndex().get(canon);
450
+ if (hit !== void 0) return hit;
451
+ return (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon)?.key;
452
+ }
453
+ function resolveCatalogKey(id) {
454
+ const exact = userModelAliases?.[id] ?? modelKeyAliases()[id] ?? id;
455
+ if (exact === id) {
456
+ const canon = canonModelId(id);
457
+ if (canon !== "") {
458
+ const hit = canonIndex().get(canon);
459
+ if (hit !== void 0) return hit;
460
+ const extraHit = (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon);
461
+ if (extraHit !== void 0) return extraHit.key;
430
462
  }
431
- },
432
- {
433
- key: "glm-5.3-flash",
434
- name: "GLM-5.3-Flash",
435
- provider: "智谱 AI",
436
- colorVar: "dsw-static-blue-300",
437
- price: {
438
- currency: "CNY",
439
- input: .8,
440
- cacheHit: .23,
441
- output: 2.8
442
- },
443
- promo: {
444
- factor: .5,
445
- endsAtMs: Date.UTC(2026, 8, 8, 16, 0, 0),
446
- note: "限时 5 折"
463
+ for (const candidate of derivedKeyCandidates(id)) {
464
+ const hit = lookupCandidate(candidate);
465
+ if (hit !== void 0) return hit;
447
466
  }
448
- },
449
- {
450
- key: "glm-5.3-flashx",
451
- name: "GLM-5.3-FlashX",
452
- provider: "智谱 AI",
453
- colorVar: "dsw-static-blue-300",
467
+ }
468
+ return exact;
469
+ }
470
+ /** 取一个计费键的实时单价(实时覆盖 > dsh-spend 官方价兜底)。 */
471
+ function livePriceOf(key) {
472
+ const resolved = resolveCatalogKey(key);
473
+ const live = livePrices?.[resolved];
474
+ if (live !== void 0) return live;
475
+ const fallback = FALLBACK_RATES.find((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
476
+ if (fallback === void 0) return void 0;
477
+ return {
478
+ input: fallback.input,
479
+ cacheHit: fallback.cacheHit,
480
+ output: fallback.output
481
+ };
482
+ }
483
+ /** Lookup a model by its stats key; falls back to the generic `other` entry. */
484
+ function modelOf(key) {
485
+ const resolved = resolveCatalogKey(key);
486
+ const found = modelCatalog().find((entry) => entry.key === resolved);
487
+ const extra = liveExtraModels?.find((item) => item.key === resolved);
488
+ const base = found ?? (extra !== void 0 ? extraEntryOf(extra) : modelCatalog().at(-1) ?? UNSEEDED_OTHER);
489
+ const live = livePriceOf(resolved);
490
+ if (live === void 0) return base;
491
+ return {
492
+ ...base,
454
493
  price: {
455
- currency: "CNY",
456
- input: 2,
457
- cacheHit: .57,
458
- output: 7
494
+ currency: "USD",
495
+ input: live.input,
496
+ cacheHit: live.cacheHit,
497
+ output: live.output
459
498
  }
460
- },
461
- {
462
- key: "glm-4.6",
463
- name: "GLM-4.6",
464
- provider: "智谱 AI",
465
- colorVar: "dsw-static-blue-400",
499
+ };
500
+ }
501
+ /** models.dev 补充条目转为目录条目:USD 直价(走汇率换算),无峰谷分档。 */
502
+ function extraEntryOf(extra) {
503
+ return {
504
+ key: extra.key,
505
+ name: extra.name,
506
+ provider: extra.provider,
507
+ colorVar: "dsw-static-neutral-400",
466
508
  price: {
467
- currency: "CNY",
468
- input: 4,
469
- cacheHit: .8,
470
- output: 16
509
+ currency: "USD",
510
+ input: extra.price.input,
511
+ cacheHit: extra.price.cacheHit,
512
+ output: extra.price.output
471
513
  }
472
- },
473
- {
474
- key: "glm-4.5-air",
475
- name: "GLM-4.5-Air",
476
- provider: "智谱 AI",
477
- colorVar: "dsw-static-blue-300",
514
+ };
515
+ }
516
+ /**
517
+ * 模型是否可计价:内置目录、models.dev 补充、或 dsh-spend 官方价兜底命中。
518
+ * 聚合层的计价闸门(目录外模型不产生费用,避免兜底档误估)。
519
+ */
520
+ function isPriced(key) {
521
+ const resolved = resolveCatalogKey(key);
522
+ if (modelCatalog().some((entry) => entry.key === resolved)) return true;
523
+ if ((liveExtraModels ?? []).some((item) => item.key === resolved)) return true;
524
+ return FALLBACK_RATES.some((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
525
+ }
526
+ /**
527
+ * 促销在 nowMs 是否生效:factor 必须落在 (0,1) 区间,截止时刻及之后视为过期;
528
+ * endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
529
+ * 导出供测试:纯函数。
530
+ * @param promo - 待判定的促销窗口。
531
+ * @param nowMs - 判定时刻(epoch ms)。
532
+ */
533
+ function isPromoActive(promo, nowMs) {
534
+ const expired = promo.endsAtMs !== void 0 && nowMs >= promo.endsAtMs;
535
+ return Number.isFinite(nowMs) && !expired && promo.factor > 0 && promo.factor < 1;
536
+ }
537
+ /**
538
+ * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 逐档乘折扣系数的
539
+ * 副本(某档在 promo.factors 有合法覆盖时用覆盖值,否则用 promo.factor),
540
+ * 其余字段原样保留;不在促销期(过期/未开始/factor 非法)原样返回。
541
+ * 幂等由调用方保证——计价与费率表显示各自只折一次,勿对已折价副本重复应用。
542
+ * @param entry - 目录条目(price 保持刊例价口径)。
543
+ * @param nowMs - 判定时刻(epoch ms)。
544
+ */
545
+ function applyPromo(entry, nowMs) {
546
+ const { promo } = entry;
547
+ if (promo === void 0 || !isPromoActive(promo, nowMs)) return entry;
548
+ const factorOf = (field) => promo.factors?.[field] ?? promo.factor;
549
+ const scaled = (band) => ({
550
+ input: band.input * factorOf("input"),
551
+ cacheHit: band.cacheHit * factorOf("cacheHit"),
552
+ ...band.cacheMiss !== void 0 ? { cacheMiss: band.cacheMiss * factorOf("cacheMiss") } : {},
553
+ output: band.output * factorOf("output")
554
+ });
555
+ return {
556
+ ...entry,
478
557
  price: {
479
- currency: "CNY",
480
- input: .8,
481
- cacheHit: .16,
482
- output: 2
558
+ ...scaled(entry.price),
559
+ currency: entry.price.currency,
560
+ ...entry.price.offPeak !== void 0 ? { offPeak: scaled(entry.price.offPeak) } : {}
483
561
  }
484
- },
562
+ };
563
+ }
564
+ /**
565
+ * Price one band's token usage in CNY. The stats `input` field is the TOTAL
566
+ * prompt tokens (cacheHit + cacheMiss), so billing splits it: the cache-hit
567
+ * share prices at the hit rate and the remaining share at the miss rate.
568
+ * Providers that report only disjoint buckets carry `cacheMiss` explicitly;
569
+ * otherwise the miss share is derived as `input - cacheHit`. Only USD-priced
570
+ * bands go through the exchange rate.
571
+ */
572
+ function priceBandCost(band, buckets, currency) {
573
+ const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
574
+ const hit = Math.min(buckets.cacheHit, buckets.input);
575
+ const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
576
+ return currency === "USD" ? raw * currentRate() : raw;
577
+ }
578
+ /**
579
+ * Estimate the CNY cost of one model's token usage, mixing the peak and
580
+ * off-peak bands by the given peak share (flat-priced models cost the same in
581
+ * both bands).
582
+ *
583
+ * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
584
+ * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
585
+ * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
586
+ * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
587
+ * 时段只能按比例估算,而非逐请求判定。
588
+ * @param entry - the catalog entry whose prices apply.
589
+ * @param buckets - token usage counts.
590
+ * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
591
+ * @returns the estimated cost in CNY.
592
+ */
593
+ function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Date.now()) {
594
+ const priced = applyPromo(entry, nowMs);
595
+ const peak = priceBandCost(priced.price, buckets, priced.price.currency);
596
+ const off = priced.price.offPeak === void 0 ? peak : priceBandCost(priced.price.offPeak, buckets, priced.price.currency);
597
+ return peak * peakShare + off * (1 - peakShare);
598
+ }
599
+ /**
600
+ * v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
601
+ * 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
602
+ * 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
603
+ */
604
+ function tariffV1At(timeMs) {
605
+ return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
606
+ }
607
+ /**
608
+ * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
609
+ * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
610
+ * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
611
+ * 同口径:按事件时刻判定该笔流量当时享受的单价。
612
+ *
613
+ * 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
614
+ * - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
615
+ * ({@link LEGACY_DEEPSEEK_BANDS})计费;
616
+ * - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
617
+ * 周六日 9-12 / 14-18 计峰);
618
+ * - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
619
+ * @param entry - the catalog entry whose prices apply.
620
+ * @param buckets - token usage counts.
621
+ * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
622
+ * @param peakShare - fallback mix used only when `timeMs` is missing.
623
+ * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
624
+ */
625
+ function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
626
+ if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
627
+ const priced = applyPromo(entry, timeMs);
628
+ const legacy = timeMs < PEAK_ERA_START_MS && entry.userPriced !== true ? LEGACY_DEEPSEEK_BANDS[entry.key] : void 0;
629
+ if (legacy !== void 0) return priceBandCost(legacy, buckets, "CNY");
630
+ const repriced = timeMs < FLASH_REPRICE_MS && entry.userPriced !== true ? FLASH_REPRICED_OFFPEAK[entry.key] : void 0;
631
+ if (repriced !== void 0) return priceBandCost((timeMs < WEEKEND_OFFPEAK_START_MS ? tariffV1At(timeMs) : tierAt(timeMs)) === "peak" ? {
632
+ input: repriced.input * 2,
633
+ cacheHit: repriced.cacheHit * 2,
634
+ output: repriced.output * 2
635
+ } : repriced, buckets, "CNY");
636
+ if (priced.price.offPeak === void 0) return priceBandCost(priced.price, buckets, priced.price.currency);
637
+ return priceBandCost((timeMs < WEEKEND_OFFPEAK_START_MS ? tariffV1At(timeMs) : tierAt(timeMs)) === "peak" ? priced.price : priced.price.offPeak, buckets, priced.price.currency);
638
+ }
639
+ /**
640
+ * Format an amount with adaptive precision and the given currency symbol.
641
+ * @param amount - the amount (CNY by default; pass `usd` for dollar display).
642
+ * @param currency - display currency; default `cny`.
643
+ */
644
+ function formatMoney(amount, currency = "cny") {
645
+ const value = Number(amount);
646
+ if (!Number.isFinite(value)) return currency === "cny" ? "¥0" : "$0";
647
+ const symbol = currency === "cny" ? "¥" : "$";
648
+ if (value <= 0) return `${symbol}0`;
649
+ if (value >= 1e3) return `${symbol}${value.toFixed(0)}`;
650
+ if (value >= 10) return `${symbol}${value.toFixed(1)}`;
651
+ if (value >= .1) return `${symbol}${value.toFixed(2)}`;
652
+ return `${symbol}${value.toFixed(3)}`;
653
+ }
654
+ /** Format a large token count with B/M/K suffix. */
655
+ function formatTokens(value) {
656
+ if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
657
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
658
+ if (value >= 1e3) return `${(value / 1e3).toFixed(0)}K`;
659
+ return String(value);
660
+ }
661
+ //#endregion
662
+ //#region lib/types/builtin-catalog.js
663
+ /**
664
+ * Built-in catalog of current mainstream models as of 2026-08-16, priced from
665
+ * each provider's official price page. Domestic providers are OpenAI-API
666
+ * compatible and publish RMB prices directly; overseas providers publish USD
667
+ * and convert through the exchange rate at estimate time. Retired models
668
+ * (GPT-4o family, Gemini 2.x, GLM-4.x-lite, older Qwen) are deliberately
669
+ * absent, as are Anthropic Claude models (their native API is not
670
+ * OpenAI-compatible, so the harness cannot drive them directly). DeepSeek
671
+ * keys match the harness stats file so real usage prices from the catalog;
672
+ * unknown keys fall back to `other`.
673
+ *
674
+ * Time-of-day billing (peak/off-peak) is now real: DeepSeek V4 officially
675
+ * splits peak (09:00-12:00 / 14:00-18:00 Beijing) at 2x the off-peak rate
676
+ * from 2026-08-17, and Gemini's Flex tier discounts spare-capacity traffic.
677
+ */
678
+ /** DeepSeek 官方高峰时段说明(峰谷分时计费目录条目共用)。 */
679
+ const DEEPSEEK_PEAK_HOURS = "09:00-12:00 / 14:00-18:00";
680
+ const GEMINI_PEAK_HOURS = "Standard / Flex";
681
+ const BUILTIN_MODEL_CATALOG = [
485
682
  {
486
- key: "glm-4.7",
487
- name: "GLM-4.7",
488
- provider: "智谱 AI",
489
- colorVar: "dsw-static-blue-400",
683
+ key: "flash",
684
+ name: "DeepSeek V4.1 Flash",
685
+ provider: "DeepSeek",
686
+ colorVar: "ds-blue",
490
687
  price: {
491
688
  currency: "CNY",
492
- input: 4,
493
- cacheHit: 1,
494
- output: 16
689
+ input: 2,
690
+ cacheHit: .04,
691
+ output: 8,
692
+ offPeak: {
693
+ input: 1,
694
+ cacheHit: .02,
695
+ output: 4
696
+ }
495
697
  },
496
- estimated: true
698
+ peakHours: DEEPSEEK_PEAK_HOURS
497
699
  },
498
700
  {
499
- key: "glm-5-turbo",
500
- name: "GLM-5-Turbo",
501
- provider: "智谱 AI",
701
+ key: "flash-vision-exp",
702
+ name: "DeepSeek V4 Flash Vision (Exp)",
703
+ provider: "DeepSeek",
502
704
  colorVar: "ds-blue",
705
+ retired: true,
503
706
  price: {
504
707
  currency: "CNY",
505
- input: 5,
506
- cacheHit: 1.2,
507
- output: 22
508
- }
708
+ input: 2,
709
+ cacheHit: .04,
710
+ output: 8,
711
+ offPeak: {
712
+ input: 1,
713
+ cacheHit: .02,
714
+ output: 4
715
+ }
716
+ },
717
+ peakHours: DEEPSEEK_PEAK_HOURS
509
718
  },
510
719
  {
511
- key: "glm-5.1",
512
- name: "GLM-5.1",
720
+ key: "pro",
721
+ name: "DeepSeek V4 Pro",
722
+ provider: "DeepSeek",
723
+ colorVar: "dsw-static-deepseek-500",
724
+ price: {
725
+ currency: "CNY",
726
+ input: 9,
727
+ cacheHit: .3,
728
+ output: 27,
729
+ offPeak: {
730
+ input: 4.5,
731
+ cacheHit: .15,
732
+ output: 13.5
733
+ }
734
+ },
735
+ peakHours: DEEPSEEK_PEAK_HOURS
736
+ },
737
+ {
738
+ key: "glm",
739
+ name: "GLM-5.2",
513
740
  provider: "智谱 AI",
514
741
  colorVar: "dsw-static-blue-600",
515
742
  price: {
516
743
  currency: "CNY",
517
- input: 6,
518
- cacheHit: 1.2,
519
- output: 24
744
+ input: 8,
745
+ cacheHit: 2,
746
+ output: 28
520
747
  }
521
748
  },
522
749
  {
523
- key: "glm-5v-turbo",
524
- name: "GLM-5V-Turbo",
750
+ key: "glm-5.3",
751
+ name: "GLM-5.3",
525
752
  provider: "智谱 AI",
526
- colorVar: "dsw-static-blue-300",
753
+ colorVar: "ds-blue",
527
754
  price: {
528
755
  currency: "CNY",
529
- input: 5,
530
- cacheHit: 1.2,
531
- output: 22
756
+ input: 8,
757
+ cacheHit: 2,
758
+ output: 28
532
759
  }
533
760
  },
534
761
  {
535
- key: "qwen-3.8-max",
536
- name: "Qwen3.8 Max",
762
+ key: "glm-5.3-flash",
763
+ name: "GLM-5.3-Flash",
764
+ provider: "智谱 AI",
765
+ colorVar: "dsw-static-blue-300",
766
+ price: {
767
+ currency: "CNY",
768
+ input: .8,
769
+ cacheHit: .23,
770
+ output: 2.8
771
+ },
772
+ promo: {
773
+ factor: .5,
774
+ endsAtMs: Date.UTC(2026, 8, 8, 16, 0, 0),
775
+ note: "限时 5 折"
776
+ }
777
+ },
778
+ {
779
+ key: "glm-5.3-flashx",
780
+ name: "GLM-5.3-FlashX",
781
+ provider: "智谱 AI",
782
+ colorVar: "dsw-static-blue-300",
783
+ price: {
784
+ currency: "CNY",
785
+ input: 2,
786
+ cacheHit: .57,
787
+ output: 7
788
+ }
789
+ },
790
+ {
791
+ key: "glm-4.6",
792
+ name: "GLM-4.6",
793
+ provider: "智谱 AI",
794
+ colorVar: "dsw-static-blue-400",
795
+ price: {
796
+ currency: "CNY",
797
+ input: 4,
798
+ cacheHit: .8,
799
+ output: 16
800
+ }
801
+ },
802
+ {
803
+ key: "glm-4.5-air",
804
+ name: "GLM-4.5-Air",
805
+ provider: "智谱 AI",
806
+ colorVar: "dsw-static-blue-300",
807
+ price: {
808
+ currency: "CNY",
809
+ input: .8,
810
+ cacheHit: .16,
811
+ output: 2
812
+ }
813
+ },
814
+ {
815
+ key: "glm-4.7",
816
+ name: "GLM-4.7",
817
+ provider: "智谱 AI",
818
+ colorVar: "dsw-static-blue-400",
819
+ price: {
820
+ currency: "CNY",
821
+ input: 4,
822
+ cacheHit: 1,
823
+ output: 16
824
+ },
825
+ estimated: true
826
+ },
827
+ {
828
+ key: "glm-5-turbo",
829
+ name: "GLM-5-Turbo",
830
+ provider: "智谱 AI",
831
+ colorVar: "ds-blue",
832
+ price: {
833
+ currency: "CNY",
834
+ input: 5,
835
+ cacheHit: 1.2,
836
+ output: 22
837
+ }
838
+ },
839
+ {
840
+ key: "glm-5.1",
841
+ name: "GLM-5.1",
842
+ provider: "智谱 AI",
843
+ colorVar: "dsw-static-blue-600",
844
+ price: {
845
+ currency: "CNY",
846
+ input: 6,
847
+ cacheHit: 1.2,
848
+ output: 24
849
+ }
850
+ },
851
+ {
852
+ key: "glm-5v-turbo",
853
+ name: "GLM-5V-Turbo",
854
+ provider: "智谱 AI",
855
+ colorVar: "dsw-static-blue-300",
856
+ price: {
857
+ currency: "CNY",
858
+ input: 5,
859
+ cacheHit: 1.2,
860
+ output: 22
861
+ }
862
+ },
863
+ {
864
+ key: "qwen-3.8-max",
865
+ name: "Qwen3.8 Max",
537
866
  provider: "阿里通义",
538
867
  colorVar: "dsw-static-blue-600",
539
868
  price: {
@@ -1457,430 +1786,144 @@ const MODEL_CATALOG = [
1457
1786
  cacheHit: .64,
1458
1787
  output: 16
1459
1788
  }
1460
- },
1461
- {
1462
- key: "doubao-seed-2.0-lite",
1463
- name: "Doubao Seed-2.0 Lite",
1464
- provider: "字节豆包",
1465
- colorVar: "dsw-static-red-300",
1466
- price: {
1467
- currency: "CNY",
1468
- input: .6,
1469
- cacheHit: .12,
1470
- output: 3.6
1471
- }
1472
- },
1473
- {
1474
- key: "other",
1475
- name: "其他模型",
1476
- provider: "Custom",
1477
- colorVar: "dsw-static-neutral-bluish-500",
1478
- price: {
1479
- currency: "CNY",
1480
- input: 0,
1481
- cacheHit: 0,
1482
- cacheMiss: 0,
1483
- output: 0
1484
- }
1485
- }
1486
- ];
1487
- /**
1488
- * 真实 provider model id → 计费目录键(`MODEL_CATALOG[].key`)的映射。未知 id
1489
- * 原样保留并落回 `other`(未知模型不估算费用)。聚合层(aggregate.ts)在折叠时
1490
- * 用同一张表把日志里的 model id 归并为目录键,客户端渲染(`modelOf`)也按它
1491
- * 解析,两侧共用一份映射,避免同一模型两侧不一致导致「未收录」。
1492
- */
1493
- const MODEL_KEY_ALIASES = {
1494
- "deepseek-v4-flash": "flash",
1495
- "deepseek-v4-flash-vision-exp": "flash-vision-exp",
1496
- "deepseek-v4.1-flash": "flash",
1497
- "deepseek-v4.1-flash-expires-on-0910": "flash",
1498
- "deepseek-flash": "flash",
1499
- "deepseek-v4-pro": "pro",
1500
- "glm-5.2": "glm",
1501
- "glm-4.5-air": "glm-4.5-air",
1502
- "glm-4.5air": "glm-4.5-air",
1503
- "glm-4.7": "glm-4.7",
1504
- "glm-5-turbo": "glm-5-turbo",
1505
- "glm-5.1": "glm-5.1",
1506
- "glm-5.3-flash": "glm-5.3-flash",
1507
- "glm-5.3-flashx": "glm-5.3-flashx",
1508
- "glm-5.3-flash-x": "glm-5.3-flashx",
1509
- "glm-5v-turbo": "glm-5v-turbo",
1510
- "glm-5v.1": "glm-5v-turbo",
1511
- "claude-opus-4-6": "claude-opus-4-6",
1512
- "claude-opus-4.6": "claude-opus-4-6",
1513
- "claude-sonnet-4-6": "claude-sonnet-4-6",
1514
- "claude-sonnet-4.6": "claude-sonnet-4-6",
1515
- "claude-haiku-4-5": "claude-haiku-4-5",
1516
- "claude-haiku-4.5": "claude-haiku-4-5",
1517
- "claude-opus-5": "claude-opus-5",
1518
- "claude-sonnet-5": "claude-sonnet-5",
1519
- "mistral-large-2512": "mistral-large-2512",
1520
- "mistral-large-3": "mistral-large-2512",
1521
- "mistral-small-2603": "mistral-small-2603",
1522
- "mistral-small-4": "mistral-small-2603",
1523
- "ministral-8b-latest": "ministral-8b-latest",
1524
- "ministral-8b": "ministral-8b-latest",
1525
- "command-a-03-2025": "command-a-03-2025",
1526
- "command-a": "command-a-03-2025",
1527
- "command-r-08-2024": "command-r-08-2024",
1528
- "command-r": "command-r-08-2024",
1529
- "hy3": "hunyuan",
1530
- "hy4-preview": "hunyuan-hy4-preview",
1531
- "hy4": "hunyuan-hy4-preview",
1532
- "hunyuan-hy4-preview": "hunyuan-hy4-preview",
1533
- "hunyuan-hy4": "hunyuan-hy4-preview",
1534
- "longcat-2.0": "longcat-2.0",
1535
- "longcat-2": "longcat-2.0",
1536
- "minicpm-v-4.5": "minicpm-v-4.5",
1537
- "minicpm-v-4.5-thinking": "minicpm-v-4.5",
1538
- "ernie-4.5": "ernie-4.5",
1539
- "ernie-4.5-300b": "ernie-4.5",
1540
- "dots-3-note-preview": "dots-3-note-preview",
1541
- "dots-3-note": "dots-3-note-preview",
1542
- "dots3-note-preview": "dots-3-note-preview",
1543
- "rednote-dots3": "dots-3-note-preview",
1544
- "doubao-seed-evolving": "doubao-seed-evolving",
1545
- "doubao-seed-evolve": "doubao-seed-evolving",
1546
- "doubao-seed-2.1-pro": "doubao-seed-2.1-pro",
1547
- "doubao-seed-2.1-pro-290000": "doubao-seed-2.1-pro",
1548
- "doubao-seed-2.1-turbo": "doubao-seed-2.1-turbo",
1549
- "doubao-seed-2-1-turbo": "doubao-seed-2.1-turbo",
1550
- "qwen3.8-max": "qwen-3.8-max",
1551
- "qwen3.8-flash": "qwen-3.8-flash",
1552
- "qwen3.7-max": "qwen-max",
1553
- "qwen3.6-max": "qwen3.6-max",
1554
- "qwen3.6-max-preview": "qwen3.6-max",
1555
- "qwen3-coder-plus": "qwen3-coder-plus",
1556
- "qwen3-coder": "qwen3-coder",
1557
- "qwen3-coder-480b": "qwen3-coder",
1558
- "glm-4.5-x": "glm-4.5-x",
1559
- "glm-4.5x": "glm-4.5-x",
1560
- "glm-5.2-fast": "glm-5.2-fast",
1561
- "glm-5.2f": "glm-5.2-fast",
1562
- "kimi-k3-fast": "kimi-k3-fast",
1563
- "kimi-k3f": "kimi-k3-fast",
1564
- "kimi-for-coding": "kimi-k2.8-preview",
1565
- "kimi-k2.7-code-fast": "kimi-k2.7-code-fast",
1566
- "kimi-k2.7-code-f": "kimi-k2.7-code-fast",
1567
- "kimi-k2.6-fast": "kimi-k2.6-fast",
1568
- "kimi-k2.6-turbo": "kimi-k2.6-turbo",
1569
- "kimi-k2-thinking-turbo": "kimi-k2-thinking-turbo",
1570
- "doubao-seed-2.0-code": "doubao-seed-2.0-code",
1571
- "doubao-seed-2-0-code": "doubao-seed-2.0-code",
1572
- "doubao-seed-2.0-lite": "doubao-seed-2.0-lite",
1573
- "doubao-seed-2-0-lite": "doubao-seed-2.0-lite",
1574
- "qwen-max": "qwen-max",
1575
- "hunyuan-t1": "hunyuan-t1",
1576
- "step-3.7-flash": "step",
1577
- "seed-2.0-mini": "doubao-mini",
1578
- "k3": "kimi-k3",
1579
- "kimi-k3": "kimi-k3",
1580
- "kimi-k2.8-preview": "kimi-k2.8-preview",
1581
- "kimi-k2.8": "kimi-k2.8-preview",
1582
- "kimi-k2-8-preview": "kimi-k2.8-preview",
1583
- "kimi-k2-8": "kimi-k2.8-preview",
1584
- "k2.8-preview": "kimi-k2.8-preview",
1585
- "k2.8": "kimi-k2.8-preview",
1586
- "minimax-m1": "minimax",
1587
- "minimax-m2": "minimax",
1588
- "minimax-m3": "minimax",
1589
- "minimax-m2.7": "minimax-m2.7",
1590
- "minimax-m2.7-highspeed": "minimax-m2.7-highspeed",
1591
- "minimax-m2.7-high-speed": "minimax-m2.7-highspeed",
1592
- "minimax-m2-7": "minimax-m2.7",
1593
- "minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
1594
- "minimax-m2-7-high-speed": "minimax-m2.7-highspeed",
1595
- "gpt-6": "gpt-6-astra",
1596
- "gpt-6-astra": "gpt-6-astra"
1597
- };
1598
- /**
1599
- * 模型 id 归一化:小写、去括号附注(如 `gpt5.6 luna(go)` 只看主体)、再去所有
1600
- * 非字母数字分隔符(空格 / 横杠 / 点 / 下划线)。用于日志里的模型 id 与计费
1601
- * 目录键做宽松匹配,提升「大小写/分隔符差异导致未收录」的识别率。
1602
- * @param id - 原始模型 id(日志或目录键)。
1603
- * @returns 归一化键(字母数字小写串)。
1604
- */
1605
- function canonModelId(id) {
1606
- return String(id).toLowerCase().replace(/\([^)]*\)/g, "").replace(/[^a-z0-9]+/g, "");
1607
- }
1608
- /**
1609
- * 目录常量键的归一化索引:归一化键 → 真实计费键。只索引静态来源(内置目录、
1610
- * 别名表、dsh-spend 兜底键);models.dev 补充条目是运行时注入,单独实时匹配。
1611
- */
1612
- const CATALOG_CANON_INDEX = (() => {
1613
- const map = /* @__PURE__ */ new Map();
1614
- const add = (candidate, target) => {
1615
- const canon = canonModelId(candidate);
1616
- if (canon !== "" && !map.has(canon)) map.set(canon, target);
1617
- };
1618
- for (const entry of MODEL_CATALOG) add(entry.key, entry.key);
1619
- for (const [alias, key] of Object.entries(MODEL_KEY_ALIASES)) add(alias, key);
1620
- for (const rate of FALLBACK_RATES) add(rate.key, rate.key);
1621
- return map;
1622
- })();
1623
- /**
1624
- * 解析真实日志模型 id → 计费目录键。先精确别名映射(既有行为);未命中时做
1625
- * 归一化匹配(忽略大小写/分隔符/括号附注),命中内置目录 / 别名目标 / 兜底键 /
1626
- * models.dev 补充键即返回其真实键;完全未知时保持原样(回退 other,不计费)。
1627
- * 供聚合层折叠与客户端渲染共用,两侧一致。
1628
- * @param id - 真实模型 id(日志里出现的形式)。
1629
- * @returns 计费目录键。
1630
- */
1631
- /**
1632
- * 由一个未知模型 id 派生候选 id(仅当直接查全部未命中时才尝试):
1633
- * - 剥离组织前缀(`deepseek/deepseek-v4-flash` → `deepseek-v4-flash`);
1634
- * - 剥离尾部纯数字段(`deepseek-v4-flash-202605` / `-0731` → `deepseek-v4-flash`,
1635
- * 覆盖 TokenHub / 官方按日期滚动的快照 id);
1636
- * - 两者组合派生。目录键本身(如 `mistral-large-2512`、`command-a-03-2025`)
1637
- * 在直接查就已命中,永不进入派生分支,不受剥段影响。
1638
- */
1639
- function derivedKeyCandidates(id) {
1640
- const out = [];
1641
- const push = (value) => {
1642
- if (value !== "" && !out.includes(value)) out.push(value);
1643
- };
1644
- const stripTrailingDigits = (value) => {
1645
- let base = value;
1646
- for (;;) {
1647
- const next = base.replace(/[-_]\d{3,}$/u, "");
1648
- if (next === base || next === "") return;
1649
- base = next;
1650
- push(base);
1651
- }
1652
- };
1653
- const slash = id.lastIndexOf("/");
1654
- if (slash > 0 && slash < id.length - 1) {
1655
- const bare = id.slice(slash + 1);
1656
- push(bare);
1657
- stripTrailingDigits(bare);
1658
- }
1659
- stripTrailingDigits(id);
1660
- return out;
1661
- }
1662
- /** 查一个候选 id(用户别名 → 内置别名 → 目录归一化 → models.dev 补充);未命中返回 undefined。 */
1663
- function lookupCandidate(candidate) {
1664
- const alias = userModelAliases?.[candidate] ?? MODEL_KEY_ALIASES[candidate];
1665
- if (alias !== void 0) return alias;
1666
- const canon = canonModelId(candidate);
1667
- if (canon === "") return void 0;
1668
- const hit = CATALOG_CANON_INDEX.get(canon);
1669
- if (hit !== void 0) return hit;
1670
- return (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon)?.key;
1671
- }
1672
- function resolveCatalogKey(id) {
1673
- const exact = userModelAliases?.[id] ?? MODEL_KEY_ALIASES[id] ?? id;
1674
- if (exact === id) {
1675
- const canon = canonModelId(id);
1676
- if (canon !== "") {
1677
- const hit = CATALOG_CANON_INDEX.get(canon);
1678
- if (hit !== void 0) return hit;
1679
- const extraHit = (liveExtraModels ?? []).find((item) => canonModelId(item.key) === canon);
1680
- if (extraHit !== void 0) return extraHit.key;
1681
- }
1682
- for (const candidate of derivedKeyCandidates(id)) {
1683
- const hit = lookupCandidate(candidate);
1684
- if (hit !== void 0) return hit;
1685
- }
1686
- }
1687
- return exact;
1688
- }
1689
- /** 取一个计费键的实时单价(实时覆盖 > dsh-spend 官方价兜底)。 */
1690
- function livePriceOf(key) {
1691
- const resolved = resolveCatalogKey(key);
1692
- const live = livePrices?.[resolved];
1693
- if (live !== void 0) return live;
1694
- const fallback = FALLBACK_RATES.find((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
1695
- if (fallback === void 0) return void 0;
1696
- return {
1697
- input: fallback.input,
1698
- cacheHit: fallback.cacheHit,
1699
- output: fallback.output
1700
- };
1701
- }
1702
- /** Lookup a model by its stats key; falls back to the generic `other` entry. */
1703
- function modelOf(key) {
1704
- const resolved = resolveCatalogKey(key);
1705
- const found = MODEL_CATALOG.find((entry) => entry.key === resolved);
1706
- const extra = liveExtraModels?.find((item) => item.key === resolved);
1707
- const base = found ?? (extra !== void 0 ? extraEntryOf(extra) : (() => {
1708
- const fallback = MODEL_CATALOG.at(-1);
1709
- if (fallback !== void 0) return fallback;
1710
- throw new Error("MODEL_CATALOG must not be empty");
1711
- })());
1712
- const live = livePriceOf(resolved);
1713
- if (live === void 0) return base;
1714
- return {
1715
- ...base,
1716
- price: {
1717
- currency: "USD",
1718
- input: live.input,
1719
- cacheHit: live.cacheHit,
1720
- output: live.output
1721
- }
1722
- };
1723
- }
1724
- /** models.dev 补充条目转为目录条目:USD 直价(走汇率换算),无峰谷分档。 */
1725
- function extraEntryOf(extra) {
1726
- return {
1727
- key: extra.key,
1728
- name: extra.name,
1729
- provider: extra.provider,
1730
- colorVar: "dsw-static-neutral-400",
1731
- price: {
1732
- currency: "USD",
1733
- input: extra.price.input,
1734
- cacheHit: extra.price.cacheHit,
1735
- output: extra.price.output
1736
- }
1737
- };
1738
- }
1739
- /**
1740
- * 模型是否可计价:内置目录、models.dev 补充、或 dsh-spend 官方价兜底命中。
1741
- * 聚合层的计价闸门(目录外模型不产生费用,避免兜底档误估)。
1742
- */
1743
- function isPriced(key) {
1744
- const resolved = resolveCatalogKey(key);
1745
- if (MODEL_CATALOG.some((entry) => entry.key === resolved)) return true;
1746
- if ((liveExtraModels ?? []).some((item) => item.key === resolved)) return true;
1747
- return FALLBACK_RATES.some((rate) => rate.key.toLowerCase() === resolved.toLowerCase());
1748
- }
1749
- /**
1750
- * 促销在 nowMs 是否生效:factor 必须落在 (0,1) 区间,截止时刻及之后视为过期;
1751
- * endsAtMs 缺省表示长期活动,在 factor 合法期间持续生效。
1752
- * 导出供测试:纯函数。
1753
- * @param promo - 待判定的促销窗口。
1754
- * @param nowMs - 判定时刻(epoch ms)。
1755
- */
1756
- function isPromoActive(promo, nowMs) {
1757
- const expired = promo.endsAtMs !== void 0 && nowMs >= promo.endsAtMs;
1758
- return Number.isFinite(nowMs) && !expired && promo.factor > 0 && promo.factor < 1;
1759
- }
1760
- /**
1761
- * 把限时促销折入条目单价:生效期内返回 price 主档与 offPeak 逐档乘折扣系数的
1762
- * 副本(某档在 promo.factors 有合法覆盖时用覆盖值,否则用 promo.factor),
1763
- * 其余字段原样保留;不在促销期(过期/未开始/factor 非法)原样返回。
1764
- * 幂等由调用方保证——计价与费率表显示各自只折一次,勿对已折价副本重复应用。
1765
- * @param entry - 目录条目(price 保持刊例价口径)。
1766
- * @param nowMs - 判定时刻(epoch ms)。
1767
- */
1768
- function applyPromo(entry, nowMs) {
1769
- const { promo } = entry;
1770
- if (promo === void 0 || !isPromoActive(promo, nowMs)) return entry;
1771
- const factorOf = (field) => promo.factors?.[field] ?? promo.factor;
1772
- const scaled = (band) => ({
1773
- input: band.input * factorOf("input"),
1774
- cacheHit: band.cacheHit * factorOf("cacheHit"),
1775
- ...band.cacheMiss !== void 0 ? { cacheMiss: band.cacheMiss * factorOf("cacheMiss") } : {},
1776
- output: band.output * factorOf("output")
1777
- });
1778
- return {
1779
- ...entry,
1789
+ },
1790
+ {
1791
+ key: "doubao-seed-2.0-lite",
1792
+ name: "Doubao Seed-2.0 Lite",
1793
+ provider: "字节豆包",
1794
+ colorVar: "dsw-static-red-300",
1780
1795
  price: {
1781
- ...scaled(entry.price),
1782
- currency: entry.price.currency,
1783
- ...entry.price.offPeak !== void 0 ? { offPeak: scaled(entry.price.offPeak) } : {}
1796
+ currency: "CNY",
1797
+ input: .6,
1798
+ cacheHit: .12,
1799
+ output: 3.6
1784
1800
  }
1785
- };
1786
- }
1787
- /**
1788
- * Price one band's token usage in CNY. The stats `input` field is the TOTAL
1789
- * prompt tokens (cacheHit + cacheMiss), so billing splits it: the cache-hit
1790
- * share prices at the hit rate and the remaining share at the miss rate.
1791
- * Providers that report only disjoint buckets carry `cacheMiss` explicitly;
1792
- * otherwise the miss share is derived as `input - cacheHit`. Only USD-priced
1793
- * bands go through the exchange rate.
1794
- */
1795
- function priceBandCost(band, buckets, currency) {
1796
- const miss = buckets.cacheMiss > 0 ? buckets.cacheMiss : Math.max(0, buckets.input - buckets.cacheHit);
1797
- const hit = Math.min(buckets.cacheHit, buckets.input);
1798
- const raw = (miss * (band.cacheMiss ?? band.input) + hit * band.cacheHit + buckets.output * band.output) / 1e6;
1799
- return currency === "USD" ? raw * currentRate() : raw;
1800
- }
1801
- /**
1802
- * Estimate the CNY cost of one model's token usage, mixing the peak and
1803
- * off-peak bands by the given peak share (flat-priced models cost the same in
1804
- * both bands).
1805
- *
1806
- * 计费维度是「缓存命中价 × 时段价」的交叉:每个时段档内部分别按缓存命中
1807
- * 价(cacheHit)与未命中价(input/cacheMiss)计价,两个时段档再按
1808
- * peakShare 混合。时段定义以北京时间为准(如 DeepSeek V4 高峰
1809
- * 09:00-12:00 / 14:00-18:00)。因聚合只有按日 token 量、没有请求级时间戳,
1810
- * 时段只能按比例估算,而非逐请求判定。
1811
- * @param entry - the catalog entry whose prices apply.
1812
- * @param buckets - token usage counts.
1813
- * @param peakShare - share of traffic in the peak band (0..1); defaults to {@link DEFAULT_PEAK_SHARE}.
1814
- * @returns the estimated cost in CNY.
1815
- */
1816
- function computeCost(entry, buckets, peakShare = DEFAULT_PEAK_SHARE, nowMs = Date.now()) {
1817
- const priced = applyPromo(entry, nowMs);
1818
- const peak = priceBandCost(priced.price, buckets, priced.price.currency);
1819
- const off = priced.price.offPeak === void 0 ? peak : priceBandCost(priced.price.offPeak, buckets, priced.price.currency);
1820
- return peak * peakShare + off * (1 - peakShare);
1821
- }
1822
- /**
1823
- * v1 峰谷档判定(峰谷开闸起、周末全谷分界止):不豁免周末——该时段官方
1824
- * 高峰时段为每天 9-12 / 14-18(周六日同样计峰)。仅用于历史事件计费;
1825
- * 「当前时刻」的档位(提醒/时段条/费率展示)一律走 {@link tierAt} 现行规则。
1826
- */
1827
- function tariffV1At(timeMs) {
1828
- return isPeakHour((new Date(timeMs).getUTCHours() + 8) % 24) ? "peak" : "offPeak";
1829
- }
1830
- /**
1831
- * 按调用时刻精确判定高峰/空闲档并计价(P0-1:替代固定比例混合)。时刻未知
1832
- * (null/NaN,理论不发生在真实事件流)时回退 {@link DEFAULT_PEAK_SHARE} 混合,
1833
- * 保持旧语义不低估。平档模型(无 offPeak)两个时段同价。限时促销与峰谷档
1834
- * 同口径:按事件时刻判定该笔流量当时享受的单价。
1835
- *
1836
- * 历史正确性(按变更节点分段适用规则,不统一套现行价重算历史):
1837
- * - 早于 {@link PEAK_ERA_START_MS} 的事件按当时官方基础价
1838
- * ({@link LEGACY_DEEPSEEK_BANDS})计费;
1839
- * - 峰谷开闸至 {@link WEEKEND_OFFPEAK_START_MS} 之间按 v1 规则(周末不豁免,
1840
- * 周六日 9-12 / 14-18 计峰);
1841
- * - 周末全谷分界起按现行规则({@link tierAt},周六日全天低谷)。
1842
- * @param entry - the catalog entry whose prices apply.
1843
- * @param buckets - token usage counts.
1844
- * @param timeMs - the call's wall-clock time (epoch ms); null falls back to the peak-share mix.
1845
- * @param peakShare - fallback mix used only when `timeMs` is missing.
1846
- * @returns the estimated cost in CNY(USD 计价模型已按当前汇率折算)。
1847
- */
1848
- function computeCostAt(entry, buckets, timeMs, peakShare = DEFAULT_PEAK_SHARE) {
1849
- if (timeMs === null || timeMs === void 0 || !Number.isFinite(timeMs)) return computeCost(entry, buckets, peakShare);
1850
- const priced = applyPromo(entry, timeMs);
1851
- const legacy = timeMs < PEAK_ERA_START_MS && entry.userPriced !== true ? LEGACY_DEEPSEEK_BANDS[entry.key] : void 0;
1852
- if (legacy !== void 0) return priceBandCost(legacy, buckets, "CNY");
1853
- const repriced = timeMs < FLASH_REPRICE_MS && entry.userPriced !== true ? FLASH_REPRICED_OFFPEAK[entry.key] : void 0;
1854
- if (repriced !== void 0) return priceBandCost((timeMs < WEEKEND_OFFPEAK_START_MS ? tariffV1At(timeMs) : tierAt(timeMs)) === "peak" ? {
1855
- input: repriced.input * 2,
1856
- cacheHit: repriced.cacheHit * 2,
1857
- output: repriced.output * 2
1858
- } : repriced, buckets, "CNY");
1859
- if (priced.price.offPeak === void 0) return priceBandCost(priced.price, buckets, priced.price.currency);
1860
- return priceBandCost((timeMs < WEEKEND_OFFPEAK_START_MS ? tariffV1At(timeMs) : tierAt(timeMs)) === "peak" ? priced.price : priced.price.offPeak, buckets, priced.price.currency);
1861
- }
1801
+ },
1802
+ {
1803
+ key: "other",
1804
+ name: "其他模型",
1805
+ provider: "Custom",
1806
+ colorVar: "dsw-static-neutral-bluish-500",
1807
+ price: {
1808
+ currency: "CNY",
1809
+ input: 0,
1810
+ cacheHit: 0,
1811
+ cacheMiss: 0,
1812
+ output: 0
1813
+ }
1814
+ }
1815
+ ];
1862
1816
  /**
1863
- * Format an amount with adaptive precision and the given currency symbol.
1864
- * @param amount - the amount (CNY by default; pass `usd` for dollar display).
1865
- * @param currency - display currency; default `cny`.
1817
+ * 真实 provider model id → 计费目录键(`MODEL_CATALOG[].key`)的映射。未知 id
1818
+ * 原样保留并落回 `other`(未知模型不估算费用)。聚合层(aggregate.ts)在折叠时
1819
+ * 用同一张表把日志里的 model id 归并为目录键,客户端渲染(`modelOf`)也按它
1820
+ * 解析,两侧共用一份映射,避免同一模型两侧不一致导致「未收录」。
1866
1821
  */
1867
- function formatMoney(amount, currency = "cny") {
1868
- const value = Number(amount);
1869
- if (!Number.isFinite(value)) return currency === "cny" ? "¥0" : "$0";
1870
- const symbol = currency === "cny" ? "¥" : "$";
1871
- if (value <= 0) return `${symbol}0`;
1872
- if (value >= 1e3) return `${symbol}${value.toFixed(0)}`;
1873
- if (value >= 10) return `${symbol}${value.toFixed(1)}`;
1874
- if (value >= .1) return `${symbol}${value.toFixed(2)}`;
1875
- return `${symbol}${value.toFixed(3)}`;
1876
- }
1877
- /** Format a large token count with B/M/K suffix. */
1878
- function formatTokens(value) {
1879
- if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
1880
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
1881
- if (value >= 1e3) return `${(value / 1e3).toFixed(0)}K`;
1882
- return String(value);
1883
- }
1822
+ const BUILTIN_MODEL_KEY_ALIASES = {
1823
+ "deepseek-v4-flash": "flash",
1824
+ "deepseek-v4-flash-vision-exp": "flash-vision-exp",
1825
+ "deepseek-v4.1-flash": "flash",
1826
+ "deepseek-v4.1-flash-expires-on-0910": "flash",
1827
+ "deepseek-flash": "flash",
1828
+ "deepseek-v4-pro": "pro",
1829
+ "glm-5.2": "glm",
1830
+ "glm-4.5-air": "glm-4.5-air",
1831
+ "glm-4.5air": "glm-4.5-air",
1832
+ "glm-4.7": "glm-4.7",
1833
+ "glm-5-turbo": "glm-5-turbo",
1834
+ "glm-5.1": "glm-5.1",
1835
+ "glm-5.3-flash": "glm-5.3-flash",
1836
+ "glm-5.3-flashx": "glm-5.3-flashx",
1837
+ "glm-5.3-flash-x": "glm-5.3-flashx",
1838
+ "glm-5v-turbo": "glm-5v-turbo",
1839
+ "glm-5v.1": "glm-5v-turbo",
1840
+ "claude-opus-4-6": "claude-opus-4-6",
1841
+ "claude-opus-4.6": "claude-opus-4-6",
1842
+ "claude-sonnet-4-6": "claude-sonnet-4-6",
1843
+ "claude-sonnet-4.6": "claude-sonnet-4-6",
1844
+ "claude-haiku-4-5": "claude-haiku-4-5",
1845
+ "claude-haiku-4.5": "claude-haiku-4-5",
1846
+ "claude-opus-5": "claude-opus-5",
1847
+ "claude-sonnet-5": "claude-sonnet-5",
1848
+ "mistral-large-2512": "mistral-large-2512",
1849
+ "mistral-large-3": "mistral-large-2512",
1850
+ "mistral-small-2603": "mistral-small-2603",
1851
+ "mistral-small-4": "mistral-small-2603",
1852
+ "ministral-8b-latest": "ministral-8b-latest",
1853
+ "ministral-8b": "ministral-8b-latest",
1854
+ "command-a-03-2025": "command-a-03-2025",
1855
+ "command-a": "command-a-03-2025",
1856
+ "command-r-08-2024": "command-r-08-2024",
1857
+ "command-r": "command-r-08-2024",
1858
+ "hy3": "hunyuan",
1859
+ "hy4-preview": "hunyuan-hy4-preview",
1860
+ "hy4": "hunyuan-hy4-preview",
1861
+ "hunyuan-hy4-preview": "hunyuan-hy4-preview",
1862
+ "hunyuan-hy4": "hunyuan-hy4-preview",
1863
+ "longcat-2.0": "longcat-2.0",
1864
+ "longcat-2": "longcat-2.0",
1865
+ "minicpm-v-4.5": "minicpm-v-4.5",
1866
+ "minicpm-v-4.5-thinking": "minicpm-v-4.5",
1867
+ "ernie-4.5": "ernie-4.5",
1868
+ "ernie-4.5-300b": "ernie-4.5",
1869
+ "dots-3-note-preview": "dots-3-note-preview",
1870
+ "dots-3-note": "dots-3-note-preview",
1871
+ "dots3-note-preview": "dots-3-note-preview",
1872
+ "rednote-dots3": "dots-3-note-preview",
1873
+ "doubao-seed-evolving": "doubao-seed-evolving",
1874
+ "doubao-seed-evolve": "doubao-seed-evolving",
1875
+ "doubao-seed-2.1-pro": "doubao-seed-2.1-pro",
1876
+ "doubao-seed-2.1-pro-290000": "doubao-seed-2.1-pro",
1877
+ "doubao-seed-2.1-turbo": "doubao-seed-2.1-turbo",
1878
+ "doubao-seed-2-1-turbo": "doubao-seed-2.1-turbo",
1879
+ "qwen3.8-max": "qwen-3.8-max",
1880
+ "qwen3.8-flash": "qwen-3.8-flash",
1881
+ "qwen3.7-max": "qwen-max",
1882
+ "qwen3.6-max": "qwen3.6-max",
1883
+ "qwen3.6-max-preview": "qwen3.6-max",
1884
+ "qwen3-coder-plus": "qwen3-coder-plus",
1885
+ "qwen3-coder": "qwen3-coder",
1886
+ "qwen3-coder-480b": "qwen3-coder",
1887
+ "glm-4.5-x": "glm-4.5-x",
1888
+ "glm-4.5x": "glm-4.5-x",
1889
+ "glm-5.2-fast": "glm-5.2-fast",
1890
+ "glm-5.2f": "glm-5.2-fast",
1891
+ "kimi-k3-fast": "kimi-k3-fast",
1892
+ "kimi-k3f": "kimi-k3-fast",
1893
+ "kimi-for-coding": "kimi-k2.8-preview",
1894
+ "kimi-k2.7-code-fast": "kimi-k2.7-code-fast",
1895
+ "kimi-k2.7-code-f": "kimi-k2.7-code-fast",
1896
+ "kimi-k2.6-fast": "kimi-k2.6-fast",
1897
+ "kimi-k2.6-turbo": "kimi-k2.6-turbo",
1898
+ "kimi-k2-thinking-turbo": "kimi-k2-thinking-turbo",
1899
+ "doubao-seed-2.0-code": "doubao-seed-2.0-code",
1900
+ "doubao-seed-2-0-code": "doubao-seed-2.0-code",
1901
+ "doubao-seed-2.0-lite": "doubao-seed-2.0-lite",
1902
+ "doubao-seed-2-0-lite": "doubao-seed-2.0-lite",
1903
+ "qwen-max": "qwen-max",
1904
+ "hunyuan-t1": "hunyuan-t1",
1905
+ "step-3.7-flash": "step",
1906
+ "seed-2.0-mini": "doubao-mini",
1907
+ "k3": "kimi-k3",
1908
+ "kimi-k3": "kimi-k3",
1909
+ "kimi-k2.8-preview": "kimi-k2.8-preview",
1910
+ "kimi-k2.8": "kimi-k2.8-preview",
1911
+ "kimi-k2-8-preview": "kimi-k2.8-preview",
1912
+ "kimi-k2-8": "kimi-k2.8-preview",
1913
+ "k2.8-preview": "kimi-k2.8-preview",
1914
+ "k2.8": "kimi-k2.8-preview",
1915
+ "minimax-m1": "minimax",
1916
+ "minimax-m2": "minimax",
1917
+ "minimax-m3": "minimax",
1918
+ "minimax-m2.7": "minimax-m2.7",
1919
+ "minimax-m2.7-highspeed": "minimax-m2.7-highspeed",
1920
+ "minimax-m2.7-high-speed": "minimax-m2.7-highspeed",
1921
+ "minimax-m2-7": "minimax-m2.7",
1922
+ "minimax-m2-7-highspeed": "minimax-m2.7-highspeed",
1923
+ "minimax-m2-7-high-speed": "minimax-m2.7-highspeed",
1924
+ "gpt-6": "gpt-6-astra",
1925
+ "gpt-6-astra": "gpt-6-astra"
1926
+ };
1884
1927
  //#endregion
1885
1928
  //#region lib/types/aggregate.js
1886
1929
  /**
@@ -3116,8 +3159,8 @@ function createUsageAggregator(persistence, options = {}) {
3116
3159
  };
3117
3160
  const toModelDayRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries(models)]));
3118
3161
  const toModelDaySiteRecord = (map) => Object.fromEntries([...map].map(([day, models]) => [day, Object.fromEntries([...models].map(([model, sites]) => [model, Object.fromEntries(sites)]))]));
3119
- const modelKeys = /* @__PURE__ */ new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3120
- const hourKeys = /* @__PURE__ */ new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
3162
+ const modelKeys = new Set([...perfModel.keys(), ...perfModelDigest.keys()]);
3163
+ const hourKeys = new Set([...perfHourModel.keys(), ...perfHourDigest.keys()]);
3121
3164
  const perf = modelKeys.size === 0 && hourKeys.size === 0 ? void 0 : {
3122
3165
  byModel: Object.fromEntries([...modelKeys].map((model) => {
3123
3166
  const live = perfModel.get(model);
@@ -3128,7 +3171,7 @@ function createUsageAggregator(persistence, options = {}) {
3128
3171
  byHourModel: Object.fromEntries([...hourKeys].sort().map((hour) => {
3129
3172
  const liveModels = perfHourModel.get(hour);
3130
3173
  const digestModels = perfHourDigest.get(hour);
3131
- const modelKeysInHour = /* @__PURE__ */ new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
3174
+ const modelKeysInHour = new Set([...liveModels?.keys() ?? [], ...digestModels?.keys() ?? []]);
3132
3175
  return [hour, Object.fromEntries([...modelKeysInHour].map((model) => {
3133
3176
  const live = liveModels?.get(model);
3134
3177
  const restored = digestModels?.get(model);
@@ -3331,11 +3374,7 @@ function tc3Authorization(secretId, secretKey, payload, timestamp) {
3331
3374
  const canonicalRequest = `POST\n/\n\n${`content-type:application/json; charset=utf-8\nhost:${TOKENHUB_HOST}\n`}\ncontent-type;host\n${createHash("sha256").update(payload).digest("hex")}`;
3332
3375
  const hashedCanonical = createHash("sha256").update(canonicalRequest).digest("hex");
3333
3376
  const stringToSign = `TC3-HMAC-SHA256\n${String(timestamp)}\n${date}/${TOKENHUB_SERVICE}/tc3_request\n${hashedCanonical}`;
3334
- const kDate = createHmac("sha256", date).update(secretKey).digest();
3335
- const kService = createHmac("sha256", kDate).update(TOKENHUB_SERVICE).digest();
3336
- const kSigning = createHmac("sha256", kService).update("tc3_request").digest();
3337
- const signature = createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3338
- return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${signature}`;
3377
+ return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${TOKENHUB_SERVICE}/tc3_request, SignedHeaders=content-type;host, Signature=${createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", date).update(secretKey).digest()).update(TOKENHUB_SERVICE).digest()).update("tc3_request").digest()).update(stringToSign).digest("hex")}`;
3339
3378
  }
3340
3379
  /**
3341
3380
  * 调用一次 TokenHub 管控面接口:TC3 签名 + 超时保护,返回响应 JSON 的 `Response`。
@@ -4099,7 +4138,7 @@ const declaredGate = createCooldownGate({
4099
4138
  cooldownMs: 6e4
4100
4139
  });
4101
4140
  /** 会触及原型链的路径段:一律拒绝(读结果不是文档自带字段)。 */
4102
- const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
4141
+ const FORBIDDEN_SEGMENTS = new Set([
4103
4142
  "__proto__",
4104
4143
  "constructor",
4105
4144
  "prototype"
@@ -4640,6 +4679,29 @@ function buildPrices(models) {
4640
4679
  function asPrice(value) {
4641
4680
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
4642
4681
  }
4682
+ /** models.dev 常把同一模型收录在多个 provider 下(厂商 + 转售)。这些 id 是模型
4683
+ * 自身的厂商,其报价视为权威:转售可能加价,也可能省略 cache_read 而落到下方
4684
+ * `input * 0.1` 的猜测值。去重时优先采用。 */
4685
+ const FIRST_PARTY_PRICE_SOURCES = new Set([
4686
+ "anthropic",
4687
+ "openai",
4688
+ "google",
4689
+ "mistral",
4690
+ "deepseek",
4691
+ "moonshotai",
4692
+ "zhipuai",
4693
+ "alibaba",
4694
+ "alibaba-cn",
4695
+ "xai",
4696
+ "meta",
4697
+ "cohere",
4698
+ "ai21",
4699
+ "minimax",
4700
+ "stepfun",
4701
+ "upstage",
4702
+ "perplexity",
4703
+ "morph"
4704
+ ]);
4643
4705
  /**
4644
4706
  * models.dev 响应 → 目录外补充条目。不再按厂商白名单过滤:凡是有有效
4645
4707
  * cost 的模型都纳入(探活模型可能来自任何预制厂商,白名单会漏掉)。厂商
@@ -4649,14 +4711,18 @@ function asPrice(value) {
4649
4711
  */
4650
4712
  function buildExtraModels(data) {
4651
4713
  if (data === null || typeof data !== "object") return [];
4652
- const catalogKeys = /* @__PURE__ */ new Set([...MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(MODEL_KEY_ALIASES).map((key) => MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4653
- const extras = [];
4714
+ const catalogKeys = new Set([...BUILTIN_MODEL_CATALOG.map((entry) => entry.key.toLowerCase()), ...Object.keys(BUILTIN_MODEL_KEY_ALIASES).map((key) => BUILTIN_MODEL_KEY_ALIASES[key]?.toLowerCase() ?? key.toLowerCase())]);
4715
+ const offers = /* @__PURE__ */ new Map();
4716
+ const familyVendorVotes = /* @__PURE__ */ new Map();
4654
4717
  for (const [providerId, providerDoc] of Object.entries(data)) {
4655
4718
  if (providerDoc === null || typeof providerDoc !== "object") continue;
4656
4719
  const models = providerDoc.models;
4657
4720
  if (models === null || typeof models !== "object") continue;
4721
+ const providerKey = providerId.toLowerCase();
4722
+ const providerName = providerDoc.name;
4723
+ const providerDisplay = MODELS_DEV_PROVIDERS[providerKey] ?? (typeof providerName === "string" && providerName !== "" ? providerName : providerId);
4658
4724
  for (const [modelId, modelDoc] of Object.entries(models)) {
4659
- const catalogKey = (MODEL_KEY_ALIASES[modelId] ?? modelId).toLowerCase();
4725
+ const catalogKey = (BUILTIN_MODEL_KEY_ALIASES[modelId] ?? modelId).toLowerCase();
4660
4726
  if (catalogKeys.has(catalogKey)) continue;
4661
4727
  const key = catalogKey;
4662
4728
  if (modelDoc === null || typeof modelDoc !== "object") continue;
@@ -4665,20 +4731,87 @@ function buildExtraModels(data) {
4665
4731
  const input = asPrice(cost.input);
4666
4732
  const output = asPrice(cost.output);
4667
4733
  if (input === void 0 || output === void 0) continue;
4668
- const cacheRead = asPrice(cost.cache_read) ?? input * .1;
4734
+ const declaredCacheRead = asPrice(cost.cache_read);
4735
+ const cacheRead = declaredCacheRead ?? input * .1;
4669
4736
  const name = modelDoc.name;
4670
- extras.push({
4671
- key,
4672
- name: typeof name === "string" && name !== "" ? name : modelId,
4673
- provider: MODELS_DEV_PROVIDERS[providerId.toLowerCase()] ?? providerId,
4674
- price: {
4675
- input,
4676
- cacheHit: cacheRead,
4677
- output
4737
+ const familyValue = modelDoc.family;
4738
+ const family = typeof familyValue === "string" && familyValue !== "" ? familyValue : void 0;
4739
+ const slash = modelId.indexOf("/");
4740
+ const namespace = slash > 0 ? modelId.slice(0, slash).toLowerCase() : "";
4741
+ const firstParty = FIRST_PARTY_PRICE_SOURCES.has(providerKey) || namespace !== "" && namespace === providerKey;
4742
+ if (firstParty && family !== void 0) {
4743
+ let votes = familyVendorVotes.get(family);
4744
+ if (votes === void 0) {
4745
+ votes = /* @__PURE__ */ new Map();
4746
+ familyVendorVotes.set(family, votes);
4678
4747
  }
4748
+ votes.set(providerDisplay, (votes.get(providerDisplay) ?? 0) + 1);
4749
+ }
4750
+ let list = offers.get(key);
4751
+ if (list === void 0) {
4752
+ list = [];
4753
+ offers.set(key, list);
4754
+ }
4755
+ list.push({
4756
+ entry: {
4757
+ key,
4758
+ name: typeof name === "string" && name !== "" ? name : modelId,
4759
+ provider: providerDisplay,
4760
+ price: {
4761
+ input,
4762
+ cacheHit: cacheRead,
4763
+ output
4764
+ }
4765
+ },
4766
+ input,
4767
+ output,
4768
+ pair: `${input}/${output}`,
4769
+ firstParty,
4770
+ declared: declaredCacheRead !== void 0,
4771
+ namespace,
4772
+ ...family === void 0 ? {} : { family },
4773
+ providerDisplay
4679
4774
  });
4680
4775
  }
4681
4776
  }
4777
+ const familyVendor = /* @__PURE__ */ new Map();
4778
+ for (const [family, votes] of familyVendorVotes) {
4779
+ let winner;
4780
+ let winnerVotes = 0;
4781
+ for (const [vendor, count] of votes) if (count > winnerVotes) {
4782
+ winnerVotes = count;
4783
+ winner = vendor;
4784
+ }
4785
+ if (winner !== void 0) familyVendor.set(family, winner);
4786
+ }
4787
+ const extras = [];
4788
+ for (const list of offers.values()) {
4789
+ const firstPartyRow = list.find((candidate) => candidate.firstParty);
4790
+ let pool;
4791
+ if (firstPartyRow !== void 0) pool = list.filter((candidate) => candidate.firstParty);
4792
+ else {
4793
+ const tally = /* @__PURE__ */ new Map();
4794
+ for (const candidate of list) tally.set(candidate.pair, (tally.get(candidate.pair) ?? 0) + 1);
4795
+ let consensus;
4796
+ let consensusCount = 0;
4797
+ for (const [pair, count] of tally) if (count > consensusCount) {
4798
+ consensusCount = count;
4799
+ consensus = pair;
4800
+ }
4801
+ if (consensusCount > 1) pool = list.filter((candidate) => candidate.pair === consensus);
4802
+ else {
4803
+ const sorted = [...list].sort((a, b) => a.input - b.input || a.output - b.output);
4804
+ const median = sorted[Math.floor((sorted.length - 1) / 2)];
4805
+ pool = median === void 0 ? [] : [median];
4806
+ }
4807
+ }
4808
+ const best = pool.find((candidate) => candidate.declared) ?? pool[0];
4809
+ if (best === void 0) continue;
4810
+ const familyName = list.find((candidate) => candidate.family !== void 0)?.family;
4811
+ const learnedVendor = familyName === void 0 ? void 0 : familyVendor.get(familyName);
4812
+ best.entry.provider = learnedVendor ?? (best.firstParty ? best.providerDisplay : best.namespace !== "" ? MODELS_DEV_PROVIDERS[best.namespace] ?? best.namespace : familyName !== void 0 ? familyName.charAt(0).toUpperCase() + familyName.slice(1) : best.providerDisplay);
4813
+ extras.push(best.entry);
4814
+ }
4682
4815
  return extras;
4683
4816
  }
4684
4817
  /**
@@ -5499,7 +5632,7 @@ const relayGate = createCooldownGate({
5499
5632
  /** Abort a relay fetch when the upstream hangs beyond this budget. */
5500
5633
  const FETCH_TIMEOUT_MS = 8e3;
5501
5634
  /** 指纹识别缓存 TTL(毫秒):识别结果低频变化,5 分钟内同 origin 不再重复探测。 */
5502
- const FINGERPRINT_TTL_MS = 3e5;
5635
+ const FINGERPRINT_TTL_MS = 300 * 1e3;
5503
5636
  /** 每 origin 的识别结果缓存:`kind` 是识别出的中转站程序,`at` 是探测时刻。 */
5504
5637
  const fingerprintCache = /* @__PURE__ */ new Map();
5505
5638
  /** Number, or null when the value is not a finite number (nor numeric string). */
@@ -5635,7 +5768,7 @@ function originOf(baseURL) {
5635
5768
  * 子路径只会得到 404/非中转站格式,因而面板应排除它们,避免误判为
5636
5769
  * 「未识别」。中转站面板只列真正的第三方中转程序。
5637
5770
  */
5638
- const OFFICIAL_HOSTS = /* @__PURE__ */ new Set([
5771
+ const OFFICIAL_HOSTS = new Set([
5639
5772
  "api.deepseek.com",
5640
5773
  "api.openai.com",
5641
5774
  "open.bigmodel.cn",
@@ -5839,11 +5972,23 @@ function isLoopbackPeer(req) {
5839
5972
  /** 校验 Host 头是本机回环(精确 127.0.0.0/8 / ::1 / localhost 或空,供 curl 不带 Host 的极简请求)。
5840
5973
  * 拒绝 `127.0.0.1.attacker.com` 这类以 `127.` 开头但解析到外部的 DNS rebinding 域名:
5841
5974
  * 只用 `startsWith('127.')` 会被它穿透,必须精确匹配回环 IP 的字面量。 */
5842
- function isLoopbackHost(req) {
5975
+ /**
5976
+ * 信任主机名归一化:去空白、去端口、转小写,丢弃空项。与请求侧
5977
+ * `host.split(':')[0].toLowerCase()` 同口径,因此匹配忽略大小写与端口。
5978
+ * @param hosts - 配置里的原始名单。
5979
+ * @returns 归一化后的主机名集合(空集 = 与历史版本行为一致)。
5980
+ */
5981
+ function normalizeTrustedHosts(hosts) {
5982
+ if (hosts === void 0) return /* @__PURE__ */ new Set();
5983
+ const names = hosts.filter((host) => typeof host === "string").map((host) => host.trim().split(":")[0]?.toLowerCase() ?? "").filter((host) => host !== "");
5984
+ return new Set(names);
5985
+ }
5986
+ function isLoopbackHost(req, trustedHosts = /* @__PURE__ */ new Set()) {
5843
5987
  const host = req.headers.host;
5844
5988
  if (host === void 0 || host === "") return true;
5845
5989
  const name = host.split(":")[0];
5846
- return name === "localhost" || name === "::1" || name !== void 0 && /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name);
5990
+ if (name === "localhost" || name === "::1" || name !== void 0 && /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
5991
+ return name !== void 0 && trustedHosts.has(name.toLowerCase());
5847
5992
  }
5848
5993
  /** 校验 Origin 头是否回环(写操作用,防止跨站表单/fetch 改写设置)。Origin 缺失
5849
5994
  * (curl 或同源 fetch 不带)视为放行,交给下方的 Content-Type 校验兜底;Origin 存在
@@ -5864,10 +6009,15 @@ function isLoopbackOrigin(origin) {
5864
6009
  * @param res - 当前响应。
5865
6010
  * @returns 是否放行;false = 已拒绝并结束响应。
5866
6011
  */
5867
- function guardLoopback(req, res) {
5868
- if (!(req.method === "GET" || req.method === "POST") || !isLoopbackPeer(req) || !isLoopbackHost(req)) {
6012
+ function guardLoopback(req, res, trustedHosts = /* @__PURE__ */ new Set()) {
6013
+ const methodOk = req.method === "GET" || req.method === "POST";
6014
+ const peerOk = isLoopbackPeer(req);
6015
+ const hostOk = isLoopbackHost(req, trustedHosts);
6016
+ if (!methodOk || !peerOk || !hostOk) {
5869
6017
  res.writeHead(403, { "content-type": "application/json; charset=utf-8" });
5870
- res.end(JSON.stringify({ error: "forbidden: loopback only" }));
6018
+ const host = req.headers.host;
6019
+ const hint = methodOk && peerOk && typeof host === "string" && host !== "" ? ` (host "${host.slice(0, 80)}" not in trustedHosts)` : "";
6020
+ res.end(JSON.stringify({ error: `forbidden: loopback only${hint}` }));
5871
6021
  return false;
5872
6022
  }
5873
6023
  return true;
@@ -5888,13 +6038,13 @@ const usageBillingSettingsNs = validateSettingsNamespace(BILLING_SETTINGS_NAMESP
5888
6038
  /** 该命名空间的 wire schema:`enableUsageStatsTool` 布尔,默认关闭(issue 诉求)。 */
5889
6039
  const UsageBillingSettingsSchema = z.object({ [ENABLE_USAGE_STATS_TOOL_FIELD]: z.boolean().default(false) });
5890
6040
  /** 实时定价的后台刷新间隔(毫秒):汇率/模型价低频变化,6 小时一次足够。 */
5891
- const PRICING_REFRESH_INTERVAL_MS = 216e5;
6041
+ const PRICING_REFRESH_INTERVAL_MS = 360 * 60 * 1e3;
5892
6042
  /** 历史回放预热的延迟:等宿主启动高峰(插件加载 / 路由挂载)过去再全量折叠,
5893
6043
  * 避免抢启动期的 CPU;纯延迟不阻塞任何请求,面板提前打开也只会提前聚合。 */
5894
6044
  const WARMUP_DELAY_MS = 3e3;
5895
6045
  /** 订阅套餐额度缓存时长(毫秒):上游配额 API 低频变化,5 分钟足够。 */
5896
- const SUBSCRIPTION_CACHE_MS = 3e5;
5897
- const BALANCE_CACHE_MS = 3e5;
6046
+ const SUBSCRIPTION_CACHE_MS = 300 * 1e3;
6047
+ const BALANCE_CACHE_MS = 300 * 1e3;
5898
6048
  /** DeepSeek 余额查询的默认凭据引用(与 llm-deepseek 的默认引用一致)。 */
5899
6049
  const DEFAULT_BALANCE_API_KEY_ENV = "DEEPSEEK_API_KEY";
5900
6050
  /**
@@ -5911,7 +6061,7 @@ const SNAPSHOT_INTERVAL_MS = 3e5;
5911
6061
  * 轻量用户暖缓存下的聚合是毫秒级,预算几乎不会触发。 */
5912
6062
  const RESPONSE_BUDGET_MS = 1500;
5913
6063
  /** 鉴权失败告警冷却(毫秒):同一 provider 在窗口内只提示一次,避免 30 秒轮询刷屏。 */
5914
- const AUTH_WARN_COOLDOWN_MS = 18e5;
6064
+ const AUTH_WARN_COOLDOWN_MS = 1800 * 1e3;
5915
6065
  /**
5916
6066
  * 鉴权失败分类告警(P1-5):余额 / 订阅查询返回 unauthorized 时,按
5917
6067
  * `source:provider` 去重并冷却告警,提示检查 llm-pi-ai 里该 provider 的 apiKeyEnv。
@@ -6243,6 +6393,7 @@ function adaptSessionPersistence(raw) {
6243
6393
  * @param config - optional statsPath override.
6244
6394
  */
6245
6395
  function apply(ctx, config = {}) {
6396
+ const trustedHosts = normalizeTrustedHosts(config.trustedHosts);
6246
6397
  let usageSettingsScope;
6247
6398
  const cwd = process.cwd();
6248
6399
  const dshHome = resolveDshHome();
@@ -6261,13 +6412,13 @@ function apply(ctx, config = {}) {
6261
6412
  })();
6262
6413
  const persistReconcileRef = () => {
6263
6414
  if (reconcileRef === null) return;
6264
- const payload = JSON.stringify({ ref: reconcileRef });
6265
- writeFileAtomic(reconcilePath, payload, {
6415
+ writeFileAtomic(reconcilePath, JSON.stringify({ ref: reconcileRef }), {
6266
6416
  mode: 384,
6267
6417
  dirMode: 448
6268
6418
  }).catch(() => {});
6269
6419
  };
6270
6420
  const workspaceTitleResolver = buildWorkspaceTitleResolver(ctx);
6421
+ applyBuiltinCatalog(BUILTIN_MODEL_CATALOG, BUILTIN_MODEL_KEY_ALIASES);
6271
6422
  applyUserModelAliases(config.modelKeyAliases);
6272
6423
  const aggregator = createUsageAggregator(adaptSessionPersistence(ctx.sessionPersistence), {
6273
6424
  ...config.subscriptionProviders === void 0 ? {} : { subscriptionProviders: config.subscriptionProviders },
@@ -6280,8 +6431,9 @@ function apply(ctx, config = {}) {
6280
6431
  ctx.effect(() => () => {
6281
6432
  aggregator.flush();
6282
6433
  }, "usage-billing: ledger flush on dispose");
6434
+ let pricingReady;
6283
6435
  const warmupTimer = setTimeout(() => {
6284
- aggregator.aggregate().then(() => {
6436
+ Promise.resolve(pricingReady).catch(() => {}).then(() => aggregator.aggregate()).then(() => {
6285
6437
  console.info("[usage-billing] historical replay warmed up; ledger ready");
6286
6438
  }).catch((error) => {
6287
6439
  console.warn("[usage-billing] historical replay warmup failed; will fold on first dashboard request:", error);
@@ -6334,7 +6486,7 @@ function apply(ctx, config = {}) {
6334
6486
  kind: "exact",
6335
6487
  path: "/api/billing/notify-claim",
6336
6488
  handler: async (req, res) => {
6337
- if (!guardLoopback(req, res)) return;
6489
+ if (!guardLoopback(req, res, trustedHosts)) return;
6338
6490
  if (req.method !== "POST") {
6339
6491
  res.writeHead(405, { "content-type": "application/json; charset=utf-8" });
6340
6492
  res.end(JSON.stringify({ error: "method not allowed" }));
@@ -6543,8 +6695,19 @@ function apply(ctx, config = {}) {
6543
6695
  live = await fetchLivePricing();
6544
6696
  pricingSyncedAt = Date.now();
6545
6697
  applyLivePricing(live);
6698
+ pricingBodyCache = void 0;
6699
+ };
6700
+ let pricingBodyCache;
6701
+ const pricingBody = () => {
6702
+ if (pricingBodyCache === void 0) pricingBodyCache = JSON.stringify({
6703
+ ...live,
6704
+ syncedAt: pricingSyncedAt,
6705
+ catalog: BUILTIN_MODEL_CATALOG,
6706
+ aliases: BUILTIN_MODEL_KEY_ALIASES
6707
+ });
6708
+ return pricingBodyCache;
6546
6709
  };
6547
- refreshPricing();
6710
+ pricingReady = refreshPricing();
6548
6711
  ctx.effect(() => {
6549
6712
  const timer = setInterval(() => {
6550
6713
  refreshPricing();
@@ -6557,19 +6720,16 @@ function apply(ctx, config = {}) {
6557
6720
  kind: "exact",
6558
6721
  path: "/api/billing/pricing",
6559
6722
  handler: async (req, res) => {
6560
- if (!guardLoopback(req, res)) return;
6723
+ if (!guardLoopback(req, res, trustedHosts)) return;
6561
6724
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6562
- res.end(JSON.stringify({
6563
- ...live,
6564
- syncedAt: pricingSyncedAt
6565
- }));
6725
+ res.end(pricingBody());
6566
6726
  }
6567
6727
  }), "usage-billing: pricing route");
6568
6728
  ctx.effect(() => ctx.webServer.register({
6569
6729
  kind: "exact",
6570
6730
  path: "/api/billing/pricing/refresh",
6571
6731
  handler: async (req, res) => {
6572
- if (!guardLoopback(req, res)) return;
6732
+ if (!guardLoopback(req, res, trustedHosts)) return;
6573
6733
  if (req.method !== "POST") {
6574
6734
  res.writeHead(405, { "content-type": "application/json; charset=utf-8" });
6575
6735
  res.end(JSON.stringify({ error: "method not allowed" }));
@@ -6587,10 +6747,7 @@ function apply(ctx, config = {}) {
6587
6747
  }
6588
6748
  await refreshPricing();
6589
6749
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6590
- res.end(JSON.stringify({
6591
- ...live,
6592
- syncedAt: pricingSyncedAt
6593
- }));
6750
+ res.end(pricingBody());
6594
6751
  }
6595
6752
  }), "usage-billing: pricing refresh route");
6596
6753
  let balanceCache = {
@@ -6601,7 +6758,7 @@ function apply(ctx, config = {}) {
6601
6758
  kind: "exact",
6602
6759
  path: "/api/billing/balance",
6603
6760
  handler: async (req, res) => {
6604
- if (!guardLoopback(req, res)) return;
6761
+ if (!guardLoopback(req, res, trustedHosts)) return;
6605
6762
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6606
6763
  const piProviders = await readPiAiProviders(ctx.settings);
6607
6764
  const providers = { ...piProviders };
@@ -6663,7 +6820,7 @@ function apply(ctx, config = {}) {
6663
6820
  kind: "exact",
6664
6821
  path: "/api/billing/usage-tool",
6665
6822
  handler: async (req, res) => {
6666
- if (!guardLoopback(req, res)) return;
6823
+ if (!guardLoopback(req, res, trustedHosts)) return;
6667
6824
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6668
6825
  const enabled = usageSettingsScope?.get().enableUsageStatsTool ?? false;
6669
6826
  if (req.method === "GET") {
@@ -6750,7 +6907,7 @@ function apply(ctx, config = {}) {
6750
6907
  kind: "exact",
6751
6908
  path: "/api/billing/subscriptions",
6752
6909
  handler: async (req, res) => {
6753
- if (!guardLoopback(req, res)) return;
6910
+ if (!guardLoopback(req, res, trustedHosts)) return;
6754
6911
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6755
6912
  if (Date.now() - quotaCache.at >= SUBSCRIPTION_CACHE_MS) try {
6756
6913
  await refreshQuotas();
@@ -6786,7 +6943,7 @@ function apply(ctx, config = {}) {
6786
6943
  kind: "exact",
6787
6944
  path: "/api/billing/relay-quotas",
6788
6945
  handler: async (req, res) => {
6789
- if (!guardLoopback(req, res)) return;
6946
+ if (!guardLoopback(req, res, trustedHosts)) return;
6790
6947
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6791
6948
  if (Date.now() - relayCache.at >= SUBSCRIPTION_CACHE_MS) try {
6792
6949
  await refreshRelay();
@@ -6808,7 +6965,7 @@ function apply(ctx, config = {}) {
6808
6965
  kind: "exact",
6809
6966
  path: "/api/billing/usage-stats",
6810
6967
  handler: async (req, res) => {
6811
- if (!guardLoopback(req, res)) return;
6968
+ if (!guardLoopback(req, res, trustedHosts)) return;
6812
6969
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
6813
6970
  const decorate = (doc) => ({
6814
6971
  ...doc,
@@ -6845,4 +7002,4 @@ function apply(ctx, config = {}) {
6845
7002
  }), "usage-billing: usage-stats route");
6846
7003
  }
6847
7004
  //#endregion
6848
- export { adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, readPiAiProviderRoutes, resolveSubscriptionKeys };
7005
+ export { adaptSessionPersistence, apply, createFileUsageLedgerStore, guardLoopback, inject, normalizeTrustedHosts, readPiAiProviderRoutes, resolveSubscriptionKeys };