@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/README.en.md +3 -2
- package/README.md +3 -2
- package/lib/client.js +5 -5
- package/lib/index.js +801 -644
- package/lib/types/aggregate.d.ts +3 -2
- package/lib/types/builtin-catalog.d.ts +15 -0
- package/lib/types/client/TrendChart.d.ts +2 -1
- package/lib/types/client/heatmap.d.ts +12 -6
- package/lib/types/client/label-display.d.ts +19 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/pricing.d.ts +9 -6
- package/lib/types/client/round-chart.d.ts +1 -1
- package/lib/types/client/usage-billing-settings.d.ts +15 -1
- package/lib/types/index.d.ts +19 -1
- package/package.json +1 -1
- package/lib/types/client/vendor-logos.d.ts +0 -12
package/lib/index.js
CHANGED
|
@@ -198,7 +198,7 @@ let liveExtraModels;
|
|
|
198
198
|
/**
|
|
199
199
|
* 用户自定义模型别名(插件配置 `modelKeyAliases`,聚合启动时注入):真实日志
|
|
200
200
|
* model id → 计费目录键。优先级高于内置别名表——目录外的新模型无需等发版,
|
|
201
|
-
*
|
|
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 +
|
|
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 +
|
|
332
|
+
return ((timeMs + 8 * 36e5) % 864e5 + 864e5) % 864e5;
|
|
333
333
|
}
|
|
334
334
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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: "
|
|
456
|
-
input:
|
|
457
|
-
cacheHit: .
|
|
458
|
-
output:
|
|
494
|
+
currency: "USD",
|
|
495
|
+
input: live.input,
|
|
496
|
+
cacheHit: live.cacheHit,
|
|
497
|
+
output: live.output
|
|
459
498
|
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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: "
|
|
468
|
-
input:
|
|
469
|
-
cacheHit: .
|
|
470
|
-
output:
|
|
509
|
+
currency: "USD",
|
|
510
|
+
input: extra.price.input,
|
|
511
|
+
cacheHit: extra.price.cacheHit,
|
|
512
|
+
output: extra.price.output
|
|
471
513
|
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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: "
|
|
487
|
-
name: "
|
|
488
|
-
provider: "
|
|
489
|
-
colorVar: "
|
|
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:
|
|
493
|
-
cacheHit:
|
|
494
|
-
output:
|
|
689
|
+
input: 2,
|
|
690
|
+
cacheHit: .04,
|
|
691
|
+
output: 8,
|
|
692
|
+
offPeak: {
|
|
693
|
+
input: 1,
|
|
694
|
+
cacheHit: .02,
|
|
695
|
+
output: 4
|
|
696
|
+
}
|
|
495
697
|
},
|
|
496
|
-
|
|
698
|
+
peakHours: DEEPSEEK_PEAK_HOURS
|
|
497
699
|
},
|
|
498
700
|
{
|
|
499
|
-
key: "
|
|
500
|
-
name: "
|
|
501
|
-
provider: "
|
|
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:
|
|
506
|
-
cacheHit:
|
|
507
|
-
output:
|
|
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: "
|
|
512
|
-
name: "
|
|
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:
|
|
518
|
-
cacheHit:
|
|
519
|
-
output:
|
|
744
|
+
input: 8,
|
|
745
|
+
cacheHit: 2,
|
|
746
|
+
output: 28
|
|
520
747
|
}
|
|
521
748
|
},
|
|
522
749
|
{
|
|
523
|
-
key: "glm-
|
|
524
|
-
name: "GLM-
|
|
750
|
+
key: "glm-5.3",
|
|
751
|
+
name: "GLM-5.3",
|
|
525
752
|
provider: "智谱 AI",
|
|
526
|
-
colorVar: "
|
|
753
|
+
colorVar: "ds-blue",
|
|
527
754
|
price: {
|
|
528
755
|
currency: "CNY",
|
|
529
|
-
input:
|
|
530
|
-
cacheHit:
|
|
531
|
-
output:
|
|
756
|
+
input: 8,
|
|
757
|
+
cacheHit: 2,
|
|
758
|
+
output: 28
|
|
532
759
|
}
|
|
533
760
|
},
|
|
534
761
|
{
|
|
535
|
-
key: "
|
|
536
|
-
name: "
|
|
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
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1796
|
+
currency: "CNY",
|
|
1797
|
+
input: .6,
|
|
1798
|
+
cacheHit: .12,
|
|
1799
|
+
output: 3.6
|
|
1784
1800
|
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1817
|
+
* 真实 provider model id → 计费目录键(`MODEL_CATALOG[].key`)的映射。未知 id
|
|
1818
|
+
* 原样保留并落回 `other`(未知模型不估算费用)。聚合层(aggregate.ts)在折叠时
|
|
1819
|
+
* 用同一张表把日志里的 model id 归并为目录键,客户端渲染(`modelOf`)也按它
|
|
1820
|
+
* 解析,两侧共用一份映射,避免同一模型两侧不一致导致「未收录」。
|
|
1866
1821
|
*/
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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 =
|
|
3120
|
-
const hourKeys =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
4653
|
-
const
|
|
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 = (
|
|
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
|
|
4734
|
+
const declaredCacheRead = asPrice(cost.cache_read);
|
|
4735
|
+
const cacheRead = declaredCacheRead ?? input * .1;
|
|
4669
4736
|
const name = modelDoc.name;
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
5897
|
-
const BALANCE_CACHE_MS =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 };
|