dsh-cost-meter 1.5.5
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/LICENSE +21 -0
- package/README.en.md +316 -0
- package/README.md +320 -0
- package/cordis.patch.yml +6 -0
- package/docs/provider-pricing.json +777 -0
- package/lib/backfill.js +405 -0
- package/lib/client.js +4116 -0
- package/lib/coding-plans.js +346 -0
- package/lib/custom-balance.js +147 -0
- package/lib/index.js +975 -0
- package/lib/pricing.js +799 -0
- package/lib/store.js +990 -0
- package/lib/typert.host.js +402 -0
- package/package.json +76 -0
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek 官方定价模型:价格表、官方文档解析、计费数学。
|
|
3
|
+
*
|
|
4
|
+
* 价格单位:美元 / 1M tokens(与官方文档一致)。账本中的成本恒以美元存储,
|
|
5
|
+
* 币种/汇率仅是展示层换算(config.exchangeRate)。
|
|
6
|
+
*
|
|
7
|
+
* 官方页面(2026-08-15 抓取)要点:
|
|
8
|
+
* - 现为纯峰谷两档计价:空闲时段(OFF-PEAK)价格 = 高峰时段(PEAK)价格的一半;
|
|
9
|
+
* deepseek-v4-flash 空闲 命中 $0.007 / 未命中 $0.22 / 输出 $0.66,
|
|
10
|
+
* 高峰 命中 $0.014 / 未命中 $0.44 / 输出 $1.32;
|
|
11
|
+
* deepseek-v4-pro 空闲 命中 $0.022 / 未命中 $0.66 / 输出 $1.98,
|
|
12
|
+
* 高峰 命中 $0.044 / 未命中 $1.32 / 输出 $3.96。
|
|
13
|
+
* - 峰时段为 01:00-04:00 与 06:00-10:00 UTC,其余为空闲时段;
|
|
14
|
+
* - 页面已不再列出基础价档与生效时间(两档方案即时生效);本插件把空闲档
|
|
15
|
+
* 同时作为「基础档」存储,未启用峰谷计价时按空闲档计费。
|
|
16
|
+
* - 页面未单列 cache write 价格,历史定价中 cache write 按 cache hit 计,
|
|
17
|
+
* 本插件沿用该规则(cacheRead + cacheWrite 均按命中价计)。
|
|
18
|
+
*
|
|
19
|
+
* 价格表写法:
|
|
20
|
+
* - 三桶:{ cacheHit, cacheMiss, output }(DeepSeek 官方结构);
|
|
21
|
+
* - 两档简写:{ input, output }(Anthropic / Gemini / Mistral 等无缓存折扣模型);
|
|
22
|
+
* - 任意子集皆可:cacheMiss 缺省取 input,cacheHit 缺省取 cacheMiss(无缓存折扣
|
|
23
|
+
* 时命中价 = 未命中价),output 缺省为 0;峰谷子档(offPeak/peak/legacyBase)
|
|
24
|
+
* 同样适用该补齐规则。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 官方定价页(英文版,服务端预渲染,可解析)。 */
|
|
28
|
+
export const OFFICIAL_PRICING_URL = 'https://api-docs.deepseek.com/quick_start/pricing'
|
|
29
|
+
|
|
30
|
+
/** 峰谷计价生效时间(UTC)。两档方案已即时生效:置为过去时刻,门控恒通过。 */
|
|
31
|
+
export const DEFAULT_PEAK_EFFECTIVE_AT = '2026-08-01T00:00:00Z'
|
|
32
|
+
|
|
33
|
+
/** 峰谷时代分界(2026-08-16 16:00 UTC):此前的计费按当时的基础价执行(历史正确性)。 */
|
|
34
|
+
export const LEGACY_BASE_BOUNDARY = '2026-08-16T16:00:00Z'
|
|
35
|
+
|
|
36
|
+
/** 峰谷时代之前的官方基础价(美元 / 1M tokens),历史计费按此执行。 */
|
|
37
|
+
export const LEGACY_BASE_PRICES = {
|
|
38
|
+
'deepseek-v4-flash': { cacheHit: 0.0028, cacheMiss: 0.14, output: 0.28 },
|
|
39
|
+
'deepseek-v4-pro': { cacheHit: 0.003625, cacheMiss: 0.435, output: 0.87 },
|
|
40
|
+
// 旧模型别名:其基础价即历史价(官方已下架)。
|
|
41
|
+
'deepseek-chat': { cacheHit: 0.07, cacheMiss: 0.27, output: 1.1 },
|
|
42
|
+
'deepseek-reasoner': { cacheHit: 0.14, cacheMiss: 0.55, output: 2.19 },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 峰时段窗口(UTC 小时,半开区间 [start, end))。 */
|
|
46
|
+
export const DEFAULT_PEAK_WINDOWS = [
|
|
47
|
+
{ start: 1, end: 4 },
|
|
48
|
+
{ start: 6, end: 10 },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/** 首批人工核对的非 DeepSeek 官方 token 价格(USD / 1M tokens)。 */
|
|
52
|
+
export const DEFAULT_PROVIDER_PRICE_TABLE = {
|
|
53
|
+
openai: {
|
|
54
|
+
models: {
|
|
55
|
+
'gpt-5.6-sol': { input: 5, cachedInput: 0.5, output: 30, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤272K 档;超过 272K 输入按 $10/$45 计;缓存写入 $6.25' },
|
|
56
|
+
'gpt-5.6-terra': { input: 2, cachedInput: 0.2, output: 12, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤272K 档;超过 272K 按 $4/$18 计;缓存写入 $2.50' },
|
|
57
|
+
'gpt-5.6-luna': { input: 0.2, cachedInput: 0.02, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤272K 档;超过 272K 按 $0.40/$1.80 计;缓存写入 $0.25' },
|
|
58
|
+
'gpt-5.5': { input: 5, cachedInput: 0.5, output: 30, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤272K 档;超过 272K 按 $10/$45 计' },
|
|
59
|
+
'gpt-5.5-pro': { input: 30, output: 180, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
60
|
+
'gpt-5.4': { input: 2.5, cachedInput: 0.25, output: 15, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤272K 档;超过 272K 按 $5/$22.5 计' },
|
|
61
|
+
'gpt-5.4-pro': { input: 30, output: 180, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
62
|
+
'gpt-5.4-mini': { input: 0.75, cachedInput: 0.075, output: 4.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
63
|
+
'gpt-5.4-nano': { input: 0.2, cachedInput: 0.02, output: 1.25, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
64
|
+
'gpt-5.3-codex': { input: 1.75, cachedInput: 0.175, output: 14, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
65
|
+
'gpt-5.3-codex-spark': { input: 1.75, cachedInput: 0.175, output: 14, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
66
|
+
'gpt-5.2': { input: 1.75, cachedInput: 0.175, output: 14, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
67
|
+
'gpt-5.2-codex': { input: 1.75, cachedInput: 0.175, output: 14, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-07-23 起弃用' },
|
|
68
|
+
'gpt-5.1': { input: 1.07, cachedInput: 0.107, output: 8.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
69
|
+
'gpt-5.1-codex': { input: 1.07, cachedInput: 0.107, output: 8.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-07-23 起弃用' },
|
|
70
|
+
'gpt-5.1-codex-max': { input: 1.25, cachedInput: 0.125, output: 10, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-07-23 起弃用' },
|
|
71
|
+
'gpt-5.1-codex-mini': { input: 0.25, cachedInput: 0.025, output: 2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-07-23 起弃用' },
|
|
72
|
+
'gpt-5': { input: 1.07, cachedInput: 0.107, output: 8.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
73
|
+
'gpt-5-codex': { input: 1.07, cachedInput: 0.107, output: 8.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-07-23 起弃用' },
|
|
74
|
+
'gpt-5-nano': { input: 0.05, cachedInput: 0.005, output: 0.4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
75
|
+
'gpt-5-2025-08-07': { input: 1.25, cachedInput: 0.13, output: 10, billingMode: 'flat', notes: '2025-08 首发价;当前同名 gpt-5 已调价,历史 id 保留' },
|
|
76
|
+
'gpt-4.1-2025-04-14': { input: 2, cachedInput: 0.5, output: 8, billingMode: 'flat' },
|
|
77
|
+
'gpt-4.1-mini-2025-04-14': { input: 0.4, cachedInput: 0.1, output: 1.6, billingMode: 'flat' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
anthropic: {
|
|
81
|
+
models: {
|
|
82
|
+
'claude-fable-5': { input: 10, cachedInput: 1, output: 50, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '缓存写入 $12.50' },
|
|
83
|
+
'claude-opus-5': { input: 5, cachedInput: 0.5, output: 25, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '缓存写入 $6.25' },
|
|
84
|
+
'claude-opus-4-8': { input: 5, cachedInput: 0.5, output: 25, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '缓存写入 $6.25' },
|
|
85
|
+
'claude-opus-4-7': { input: 5, cachedInput: 0.5, output: 25, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
86
|
+
'claude-opus-4-6': { input: 5, cachedInput: 0.5, output: 25, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
87
|
+
'claude-opus-4-5': { input: 5, cachedInput: 0.5, output: 25, billingMode: 'flat', notes: '缓存写入 $6.25' },
|
|
88
|
+
'claude-sonnet-5': { input: 2, cachedInput: 0.2, output: 10, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '早鸟价(至 2026-08-31);标准价 $3/$15' },
|
|
89
|
+
'claude-sonnet-4-6': { input: 3, cachedInput: 0.3, output: 15, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '缓存写入 $3.75' },
|
|
90
|
+
'claude-sonnet-4-5': { input: 3, cachedInput: 0.3, output: 15, billingMode: 'flat', notes: '≤200K 档;>200K 按 $6/$22.5 计;缓存写入 $3.75' },
|
|
91
|
+
'claude-haiku-4-5': { input: 1, cachedInput: 0.1, output: 5, billingMode: 'flat', notes: '缓存写入 $1.25' },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
google: {
|
|
95
|
+
models: {
|
|
96
|
+
'gemini-3.7-flash': { input: 0.75, output: 3.75, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17', notes: '2026 年底前促销价;2027 起 $1.5/$7.5;Go 网关按 $1.5/$7.5' },
|
|
97
|
+
'gemini-3.6-flash': { input: 1.5, cachedInput: 0.15, output: 7.5, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17' },
|
|
98
|
+
'gemini-3.5-flash': { input: 1.5, cachedInput: 0.15, output: 9, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17' },
|
|
99
|
+
'gemini-3.5-flash-lite': { input: 0.3, cachedInput: 0.03, output: 2.5, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17' },
|
|
100
|
+
'gemini-3.1-pro-preview': { input: 2, cachedInput: 0.2, output: 12, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17', notes: '≤200K 档;>200K 按 $4/$18 计' },
|
|
101
|
+
'gemini-3-flash': { input: 0.5, cachedInput: 0.05, output: 3, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17', notes: 'Preview' },
|
|
102
|
+
'gemini-2.5-pro': { input: 1.25, cachedInput: 0.125, output: 10, billingMode: 'flat', notes: '≤200K 档;>200K 按 $2.5/$15 计' },
|
|
103
|
+
'gemini-2.5-flash': { input: 0.3, cachedInput: 0.03, output: 2.5, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17' },
|
|
104
|
+
'gemini-2.5-flash-lite': { input: 0.1, cachedInput: 0.01, output: 0.4, billingMode: 'flat', sourceUrl: 'https://ai.google.dev/gemini-api/docs/pricing', checkedAt: '2026-08-17' },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
moonshot: {
|
|
108
|
+
models: {
|
|
109
|
+
'kimi-k3': { input: 3, cachedInput: 0.3, output: 15, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '官方人民币价 ¥20/¥100/缓存¥2 每百万;此处为 Go 网关美元价' },
|
|
110
|
+
'kimi-k2.7-code': { input: 0.95, cachedInput: 0.19, output: 4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
111
|
+
'kimi-k2.6': { input: 0.95, cachedInput: 0.16, output: 4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
112
|
+
'kimi-k2.5': { input: 0.6, cachedInput: 0.1, output: 3, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-08-05 起弃用' },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
'z-ai': {
|
|
116
|
+
models: {
|
|
117
|
+
'glm-5.3': { unpriced: true, billingMode: 'flat', notes: 'OpenCode Go 目录在册;官方/Zen 均未公布单价,不编造价格' },
|
|
118
|
+
'glm-5.2': { input: 1.4, cachedInput: 0.26, output: 4.4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
119
|
+
'glm-5.1': { input: 1.4, cachedInput: 0.26, output: 4.4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
120
|
+
'glm-5': { input: 1, cachedInput: 0.2, output: 3.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-05-14 起弃用' },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
xai: {
|
|
124
|
+
models: {
|
|
125
|
+
'grok-4.6': { input: 2, cachedInput: 0.5, output: 6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤200K 档;>200K 按 $4/$12 计' },
|
|
126
|
+
'grok-4.5': { input: 2, cachedInput: 0.3, output: 6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '≤200K 档;>200K 按 $4/$12 计' },
|
|
127
|
+
'grok-4.3': { input: 1.25, output: 2.5, billingMode: 'flat', sourceUrl: 'https://docs.x.ai/developers/models/grok-4.3' },
|
|
128
|
+
'grok-build-0.1': { input: 1, cachedInput: 0.2, output: 2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
alibaba: {
|
|
132
|
+
models: {
|
|
133
|
+
'qwen3.8-max': { input: 2, cachedInput: 0.25, output: 6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: '缓存写入 $2.50' },
|
|
134
|
+
'qwen3.7-max': { input: 2.5, cachedInput: 0.5, output: 7.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: '缓存写入 $3.125' },
|
|
135
|
+
'qwen3.7-plus': { input: 0.4, cachedInput: 0.04, output: 1.6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: '≤256K 档;>256K 按 $1.2/$4.8 计;缓存写入 $0.50' },
|
|
136
|
+
'qwen3.6-plus': { input: 0.5, cachedInput: 0.05, output: 3, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: '≤256K 档;>256K 按 $2/$6 计;缓存写入 $0.625' },
|
|
137
|
+
'qwen3.5-plus': { input: 0.2, cachedInput: 0.02, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '缓存写入 $0.25' },
|
|
138
|
+
'qwen3-plus': { input: 0.4, output: 1.2, billingMode: 'flat', sourceUrl: 'https://www.alibabacloud.com/help/en/model-studio/model-pricing' },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
minimax: {
|
|
142
|
+
models: {
|
|
143
|
+
'minimax-m3': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
144
|
+
'minimax-m2.7': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17' },
|
|
145
|
+
'minimax-m2.5': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/zen', checkedAt: '2026-08-17', notes: '2026-08-05 起弃用' },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
tencent: {
|
|
149
|
+
models: {
|
|
150
|
+
'hunyuan-a13b': { input: 0.5, output: 2, billingMode: 'flat', notes: 'Official price is CNY; catalog value requires currency conversion before use.' },
|
|
151
|
+
'hy3': { input: 0.14, cachedInput: 0.035, output: 0.58, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17', notes: 'OpenCode Go 目录价;厂商归属未官宣' },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
xiaomi: {
|
|
155
|
+
models: {
|
|
156
|
+
'mimo-v2.5': { input: 0.14, cachedInput: 0.0028, output: 0.28, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
157
|
+
'mimo-v2.5-pro': { input: 0.435, cachedInput: 0.003625, output: 0.87, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
upstage: {
|
|
161
|
+
models: {
|
|
162
|
+
'solar-pro4': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://www.upstage.ai/pricing/api' },
|
|
163
|
+
'solar-pro3': { input: 0.15, cachedInput: 0.015, output: 0.6, billingMode: 'flat', sourceUrl: 'https://www.upstage.ai/pricing/api' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
nvidia: { models: { 'nvidia/nemotron-3-ultra-550b-a55b': { unpriced: true, billingMode: 'flat', notes: 'Official catalog does not publish token price.' } } },
|
|
167
|
+
mistral: {
|
|
168
|
+
models: {
|
|
169
|
+
'mistral-large-2512': { input: 0.5, cachedInput: 0.05, output: 1.5, billingMode: 'flat' },
|
|
170
|
+
'mistral-medium-3.5': { input: 1.5, cachedInput: 0.15, output: 7.5, billingMode: 'flat' },
|
|
171
|
+
'mistral-small-4.0': { input: 0.15, cachedInput: 0.015, output: 0.6, billingMode: 'flat' },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
// OpenCode Go 订阅($10/月)包含的模型中非 DeepSeek 的 17 个:订阅制下请求不按 token 扣费,
|
|
175
|
+
// 此处为官方公布的参考单价(用于成本估算/对比),来源 opencode.ai/docs/go。
|
|
176
|
+
// DeepSeek V4 Flash/Pro 与官方主表重复,以官方为准(含峰谷两档),Go 目录不重复收录(v1.5.2 移除)。
|
|
177
|
+
'opencode-go': {
|
|
178
|
+
models: {
|
|
179
|
+
'grok-4.5': { input: 2, cachedInput: 0.3, output: 6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
180
|
+
'glm-5.3': { unpriced: true, billingMode: 'flat', notes: 'Go 目录在册;官方未公布单价' },
|
|
181
|
+
'glm-5.2': { input: 1.4, cachedInput: 0.26, output: 4.4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
182
|
+
'glm-5.1': { input: 1.4, cachedInput: 0.26, output: 4.4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
183
|
+
'gpt-5.6-luna': { input: 0.2, cachedInput: 0.02, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
184
|
+
'kimi-k3': { input: 3, cachedInput: 0.3, output: 15, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
185
|
+
'kimi-k2.7-code': { input: 0.95, cachedInput: 0.19, output: 4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
186
|
+
'kimi-k2.6': { input: 0.95, cachedInput: 0.16, output: 4, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
187
|
+
'mimo-v2.5': { input: 0.14, cachedInput: 0.0028, output: 0.28, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
188
|
+
'mimo-v2.5-pro': { input: 0.435, cachedInput: 0.003625, output: 0.87, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
189
|
+
'minimax-m3': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
190
|
+
'minimax-m2.7': { input: 0.3, cachedInput: 0.06, output: 1.2, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
191
|
+
'qwen3.8-max': { input: 2, cachedInput: 0.25, output: 6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
192
|
+
'qwen3.7-max': { input: 2.5, cachedInput: 0.5, output: 7.5, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
193
|
+
'qwen3.7-plus': { input: 0.4, cachedInput: 0.04, output: 1.6, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
194
|
+
'qwen3.6-plus': { input: 0.5, cachedInput: 0.05, output: 3, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
195
|
+
'hy3': { input: 0.14, cachedInput: 0.035, output: 0.58, billingMode: 'flat', sourceUrl: 'https://opencode.ai/docs/go', checkedAt: '2026-08-17' },
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 拓展价格表目录的模型家族分组(展示用;未列出的模型自成一家)。 */
|
|
201
|
+
export const PROVIDER_MODEL_FAMILIES = {
|
|
202
|
+
deepseek: { 'deepseek-v4-flash': 'DeepSeek v4', 'deepseek-v4-pro': 'DeepSeek v4' },
|
|
203
|
+
openai: {
|
|
204
|
+
'gpt-5.6-sol': 'GPT-5.6', 'gpt-5.6-terra': 'GPT-5.6', 'gpt-5.6-luna': 'GPT-5.6',
|
|
205
|
+
'gpt-5.5': 'GPT-5.5', 'gpt-5.5-pro': 'GPT-5.5',
|
|
206
|
+
'gpt-5.4': 'GPT-5.4', 'gpt-5.4-pro': 'GPT-5.4', 'gpt-5.4-mini': 'GPT-5.4', 'gpt-5.4-nano': 'GPT-5.4',
|
|
207
|
+
'gpt-5.3-codex': 'GPT-5.3 Codex', 'gpt-5.3-codex-spark': 'GPT-5.3 Codex',
|
|
208
|
+
'gpt-5.2': 'GPT-5.2', 'gpt-5.2-codex': 'GPT-5.2',
|
|
209
|
+
'gpt-5.1': 'GPT-5.1', 'gpt-5.1-codex': 'GPT-5.1', 'gpt-5.1-codex-max': 'GPT-5.1', 'gpt-5.1-codex-mini': 'GPT-5.1',
|
|
210
|
+
'gpt-5': 'GPT-5', 'gpt-5-codex': 'GPT-5', 'gpt-5-nano': 'GPT-5', 'gpt-5-2025-08-07': 'GPT-5',
|
|
211
|
+
'gpt-4.1': 'GPT-4.1', 'gpt-4.1-mini': 'GPT-4.1',
|
|
212
|
+
},
|
|
213
|
+
anthropic: {
|
|
214
|
+
'claude-fable-5': 'Claude Fable',
|
|
215
|
+
'claude-opus-5': 'Claude Opus', 'claude-opus-4-8': 'Claude Opus', 'claude-opus-4-7': 'Claude Opus', 'claude-opus-4-6': 'Claude Opus', 'claude-opus-4-5': 'Claude 4.5',
|
|
216
|
+
'claude-sonnet-5': 'Claude Sonnet', 'claude-sonnet-4-6': 'Claude Sonnet', 'claude-sonnet-4-5': 'Claude 4.5',
|
|
217
|
+
'claude-haiku-4-5': 'Claude 4.5',
|
|
218
|
+
},
|
|
219
|
+
google: {
|
|
220
|
+
'gemini-3.7-flash': 'Gemini 3.7 Flash',
|
|
221
|
+
'gemini-3.6-flash': 'Gemini 3.6 Flash',
|
|
222
|
+
'gemini-3.5-flash': 'Gemini 3.5 Flash', 'gemini-3.5-flash-lite': 'Gemini 3.5 Flash',
|
|
223
|
+
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro',
|
|
224
|
+
'gemini-3-flash': 'Gemini 3 Flash',
|
|
225
|
+
'gemini-2.5-pro': 'Gemini 2.5', 'gemini-2.5-flash': 'Gemini 2.5', 'gemini-2.5-flash-lite': 'Gemini 2.5',
|
|
226
|
+
},
|
|
227
|
+
moonshot: { 'kimi-k3': 'Kimi K3', 'kimi-k2.7-code': 'Kimi K2', 'kimi-k2.6': 'Kimi K2', 'kimi-k2.5': 'Kimi K2' },
|
|
228
|
+
'z-ai': { 'glm-5.3': 'GLM-5', 'glm-5.2': 'GLM-5', 'glm-5.1': 'GLM-5', 'glm-5': 'GLM-5' },
|
|
229
|
+
xai: { 'grok-4.6': 'Grok 4', 'grok-4.5': 'Grok 4', 'grok-4.3': 'Grok 4', 'grok-build-0.1': 'Grok Build' },
|
|
230
|
+
alibaba: { 'qwen3.8-max': 'Qwen3.8', 'qwen3.7-max': 'Qwen3.7', 'qwen3.7-plus': 'Qwen3.7', 'qwen3.6-plus': 'Qwen3.6', 'qwen3.5-plus': 'Qwen3.5', 'qwen3-plus': 'Qwen3' },
|
|
231
|
+
minimax: { 'minimax-m3': 'MiniMax M3', 'minimax-m2.7': 'MiniMax M2', 'minimax-m2.5': 'MiniMax M2' },
|
|
232
|
+
tencent: { 'hunyuan-a13b': '混元', 'hy3': 'Hy3' },
|
|
233
|
+
xiaomi: { 'mimo-v2.5': 'MiMo V2.5', 'mimo-v2.5-pro': 'MiMo V2.5' },
|
|
234
|
+
upstage: { 'solar-pro4': 'Solar', 'solar-pro3': 'Solar' },
|
|
235
|
+
nvidia: { 'nvidia/nemotron-3-ultra-550b-a55b': 'Nemotron' },
|
|
236
|
+
mistral: { 'mistral-large-2512': 'Mistral Large', 'mistral-medium-3.5': 'Mistral Medium', 'mistral-small-4.0': 'Mistral Small' },
|
|
237
|
+
'opencode-go': {
|
|
238
|
+
'gpt-5.6-luna': 'GPT', 'grok-4.5': 'Grok', 'glm-5.3': 'GLM', 'glm-5.2': 'GLM', 'glm-5.1': 'GLM',
|
|
239
|
+
'kimi-k3': 'Kimi', 'kimi-k2.7-code': 'Kimi', 'kimi-k2.6': 'Kimi',
|
|
240
|
+
'mimo-v2.5': 'MiMo', 'mimo-v2.5-pro': 'MiMo', 'minimax-m3': 'MiniMax', 'minimax-m2.7': 'MiniMax',
|
|
241
|
+
'qwen3.8-max': 'Qwen', 'qwen3.7-max': 'Qwen', 'qwen3.7-plus': 'Qwen', 'qwen3.6-plus': 'Qwen',
|
|
242
|
+
'hy3': 'Hy3',
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 构建扩展价格表目录:provider → family → modelId → 价格条目。
|
|
248
|
+
* 内置只读目录(含 DeepSeek 当前模型);「挂载」= 把条目复制进可编辑价格表。
|
|
249
|
+
*/
|
|
250
|
+
export function buildPriceCatalog() {
|
|
251
|
+
const catalog = {}
|
|
252
|
+
const put = (provider, id, entry) => {
|
|
253
|
+
const family = PROVIDER_MODEL_FAMILIES[provider]?.[id] ?? id
|
|
254
|
+
if (catalog[provider] === undefined) catalog[provider] = {}
|
|
255
|
+
if (catalog[provider][family] === undefined) catalog[provider][family] = {}
|
|
256
|
+
catalog[provider][family][id] = entry
|
|
257
|
+
}
|
|
258
|
+
for (const [id, entry] of Object.entries(DEFAULT_PRICE_TABLE.models)) put('deepseek', id, entry)
|
|
259
|
+
for (const [provider, table] of Object.entries(DEFAULT_PROVIDER_PRICE_TABLE)) {
|
|
260
|
+
for (const [id, entry] of Object.entries(table.models)) put(provider, id, entry)
|
|
261
|
+
}
|
|
262
|
+
return catalog
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 内置默认 DeepSeek 价格表(与官方页面当前数字一致,供首次启动使用;基础档 = 空闲档)。 */
|
|
266
|
+
export const DEFAULT_PRICE_TABLE = {
|
|
267
|
+
models: {
|
|
268
|
+
'deepseek-v4-flash': {
|
|
269
|
+
cacheHit: 0.007,
|
|
270
|
+
cacheMiss: 0.22,
|
|
271
|
+
output: 0.66,
|
|
272
|
+
offPeak: { cacheHit: 0.007, cacheMiss: 0.22, output: 0.66 },
|
|
273
|
+
peak: { cacheHit: 0.014, cacheMiss: 0.44, output: 1.32 },
|
|
274
|
+
legacyBase: { cacheHit: 0.0028, cacheMiss: 0.14, output: 0.28 },
|
|
275
|
+
},
|
|
276
|
+
'deepseek-v4-pro': {
|
|
277
|
+
cacheHit: 0.022,
|
|
278
|
+
cacheMiss: 0.66,
|
|
279
|
+
output: 1.98,
|
|
280
|
+
offPeak: { cacheHit: 0.022, cacheMiss: 0.66, output: 1.98 },
|
|
281
|
+
peak: { cacheHit: 0.044, cacheMiss: 1.32, output: 3.96 },
|
|
282
|
+
legacyBase: { cacheHit: 0.003625, cacheMiss: 0.435, output: 0.87 },
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
default: { cacheHit: 0.007, cacheMiss: 0.22, output: 0.66 },
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 补齐一档价格:支持多种模型计费写法。
|
|
290
|
+
* - 三桶写法:{ cacheHit, cacheMiss, output }(DeepSeek 官方结构);
|
|
291
|
+
* - 两档简写:{ input, output }(Anthropic / Gemini / Mistral 等无缓存折扣模型
|
|
292
|
+
* 的价表通常只给输入/输出两档);
|
|
293
|
+
* - 混合:{ cacheMiss, output } 等任意子集。
|
|
294
|
+
* 补齐规则:
|
|
295
|
+
* - cacheMiss 缺省 → 取 input;两者都缺 → 0;
|
|
296
|
+
* - cacheHit 缺省 → 取 cacheMiss(无缓存折扣时命中价 = 未命中价);
|
|
297
|
+
* - output 缺省 → 0。
|
|
298
|
+
* 显式给出的数字恒优先;非负有限数字才被接受。
|
|
299
|
+
* @param raw - 任意一档价格对象。
|
|
300
|
+
* @returns 补全后的三桶价格 { cacheHit, cacheMiss, output },或 undefined。
|
|
301
|
+
*/
|
|
302
|
+
function completeTier(raw) {
|
|
303
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return undefined
|
|
304
|
+
const n = key => {
|
|
305
|
+
const v = raw[key]
|
|
306
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined
|
|
307
|
+
}
|
|
308
|
+
const cacheMiss = n('cacheMiss') ?? n('input') ?? 0
|
|
309
|
+
const cacheHit = n('cacheHit') ?? n('cachedInput') ?? n('cacheRead') ?? cacheMiss
|
|
310
|
+
const output = n('output') ?? 0
|
|
311
|
+
const reasoning = n('reasoning')
|
|
312
|
+
return reasoning === undefined ? { cacheHit, cacheMiss, output } : { cacheHit, cacheMiss, output, reasoning }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 规范化一条价格记录:按 completeTier 补齐缺失字段,剥离未知字段。
|
|
317
|
+
* @param value - 任意解析结果。
|
|
318
|
+
* @returns 规范化后的价格记录,或 null。
|
|
319
|
+
*/
|
|
320
|
+
export function normalizePrice(value) {
|
|
321
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null
|
|
322
|
+
if (value.unpriced === true) {
|
|
323
|
+
const entry = { unpriced: true }
|
|
324
|
+
for (const key of ['billingMode', 'sourceUrl', 'checkedAt', 'notes']) if (typeof value[key] === 'string') entry[key] = value[key]
|
|
325
|
+
return entry
|
|
326
|
+
}
|
|
327
|
+
if (!('cacheHit' in value) && !('cacheMiss' in value) && !('output' in value) && !('input' in value)) return null
|
|
328
|
+
const entry = completeTier(value)
|
|
329
|
+
if (value.legacy === true) entry.legacy = true
|
|
330
|
+
if (value.billingMode === 'flat' || value.billingMode === 'deepseek-peak' || value.billingMode === 'batch') entry.billingMode = value.billingMode
|
|
331
|
+
for (const key of ['sourceUrl', 'checkedAt', 'notes']) if (typeof value[key] === 'string') entry[key] = value[key]
|
|
332
|
+
const offPeak = completeTier(value.offPeak)
|
|
333
|
+
if (offPeak !== undefined) entry.offPeak = offPeak
|
|
334
|
+
const peak = completeTier(value.peak)
|
|
335
|
+
if (peak !== undefined) entry.peak = peak
|
|
336
|
+
const legacyBase = completeTier(value.legacyBase)
|
|
337
|
+
if (legacyBase !== undefined) entry.legacyBase = legacyBase
|
|
338
|
+
return entry
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 全部价格为 0 的记录视为空记录。 */
|
|
342
|
+
export function isZeroPrice(entry) {
|
|
343
|
+
return entry !== null && entry.cacheHit === 0 && entry.cacheMiss === 0 && entry.output === 0
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 按模型 id 解析价格记录:精确匹配 → default 回退。
|
|
348
|
+
* @param modelId - 请求中的模型 id。
|
|
349
|
+
* @param table - { models, default } 价格表。
|
|
350
|
+
* @returns 价格记录。
|
|
351
|
+
*/
|
|
352
|
+
export function priceEntryFor(modelId, table) {
|
|
353
|
+
const models = table?.models ?? {}
|
|
354
|
+
if (typeof modelId === 'string' && modelId.length > 0) {
|
|
355
|
+
const exact = models[modelId]
|
|
356
|
+
if (exact !== undefined) return exact
|
|
357
|
+
// 别名匹配:deepseek-chat → 任何以 '-' 连接的相近 id 不再猜测,直接回退 default。
|
|
358
|
+
}
|
|
359
|
+
return table?.default ?? { cacheHit: 0, cacheMiss: 0, output: 0 }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── 模型名自动匹配(精确 → 手动覆盖 → 去后缀/前缀/家族相似) ───────────
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 模型名归一化:小写,去掉括号括起的附注(如 (go)),再只保留字母与数字。
|
|
366
|
+
* 大小写、空格、横杠、下划线、点号等差异全部忽略:'GPT-5.6 Luna (Go)' → 'gpt56luna'。
|
|
367
|
+
*/
|
|
368
|
+
export function canonModelId(id) {
|
|
369
|
+
return String(id ?? '').toLowerCase()
|
|
370
|
+
.replace(/\([^)]*\)/g, ' ')
|
|
371
|
+
.replace(/([^)]*)/g, ' ')
|
|
372
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function stripIdDecor(id) {
|
|
376
|
+
let out = String(id).toLowerCase()
|
|
377
|
+
// 去掉日期后缀(-2026-01-01 / -20260101 / @2026-01-01)与带 v 的版本号后缀(-v2 / -v1.5)。
|
|
378
|
+
// 注意:不剥裸数字后缀——'glm-5.3' 的 '-5.3' 是模型名本体而非版本后缀,
|
|
379
|
+
// 剥掉会让 glm-5.3/glm-5.2 都退化成 'glm' 而互配,把订阅制模型记到同家族付费价上(issue #18)。
|
|
380
|
+
out = out.replace(/[-@]\d{4}-?\d{2}-?\d{2}$/, '')
|
|
381
|
+
out = out.replace(/[-@]v\d+(\.\d+)*$/, '')
|
|
382
|
+
return out
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const tokensOf = id => stripIdDecor(id).split(/[-_./:]+/).filter(Boolean)
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 把请求模型 id 匹配到候选价格表 id。
|
|
389
|
+
* 顺序:精确 → 归一化等价(忽略大小写/空格/横杠/点号/括号附注) → 宽泛包含
|
|
390
|
+
* (请求名归一化后包含候选名即算命中,取最长候选) → 去日期/版本后缀精确
|
|
391
|
+
* → 候选前缀(取最长) → 家族 token 相似(≥2 个前缀 token 且最长者胜)。
|
|
392
|
+
* @param modelId - 请求模型 id。
|
|
393
|
+
* @param candidates - 候选 id 数组。
|
|
394
|
+
* @returns 命中的候选 id,或 null。
|
|
395
|
+
*/
|
|
396
|
+
export function matchModelId(modelId, candidates) {
|
|
397
|
+
if (typeof modelId !== 'string' || modelId.length === 0) return null
|
|
398
|
+
const list = Array.isArray(candidates) ? candidates.filter(c => typeof c === 'string' && c.length > 0) : []
|
|
399
|
+
if (list.length === 0) return null
|
|
400
|
+
const exact = list.find(c => c === modelId)
|
|
401
|
+
if (exact !== undefined) return exact
|
|
402
|
+
const canon = canonModelId(modelId)
|
|
403
|
+
if (canon.length === 0) return null
|
|
404
|
+
// 归一化后等价:'GPT-5.6 Luna' ≡ 'gpt-5.6-luna'。
|
|
405
|
+
const byCanon = list.find(c => canonModelId(c) === canon)
|
|
406
|
+
if (byCanon !== undefined) return byCanon
|
|
407
|
+
// 宽泛包含:'gpt5.6 luna(go)' 归一化后包含 'gpt56luna' 即命中;取最长候选,过短候选(≤3)防误配。
|
|
408
|
+
let containHit = null
|
|
409
|
+
let containLen = 0
|
|
410
|
+
for (const c of list) {
|
|
411
|
+
const cc = canonModelId(c)
|
|
412
|
+
if (cc.length < 4 || cc === canon) continue
|
|
413
|
+
if (canon.includes(cc) && cc.length > containLen) { containHit = c; containLen = cc.length }
|
|
414
|
+
}
|
|
415
|
+
if (containHit !== null) return containHit
|
|
416
|
+
const stripped = stripIdDecor(modelId)
|
|
417
|
+
const byStripped = list.find(c => stripIdDecor(c) === stripped)
|
|
418
|
+
if (byStripped !== undefined) return byStripped
|
|
419
|
+
// 前缀匹配:modelId(去饰后)以候选(去饰后)开头且紧接分隔符,取最长候选。
|
|
420
|
+
let prefixHit = null
|
|
421
|
+
for (const c of list) {
|
|
422
|
+
const cs = stripIdDecor(c)
|
|
423
|
+
if (cs.length === 0 || cs === stripped) continue
|
|
424
|
+
if (stripped.startsWith(cs) && /^[\-_./:]/.test(stripped.slice(cs.length))) {
|
|
425
|
+
if (prefixHit === null || stripIdDecor(prefixHit).length < cs.length) prefixHit = c
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (prefixHit !== null) return prefixHit
|
|
429
|
+
// 家族 token 相似:前缀公共 token ≥2,取公共最长者;同长取候选最短(更泛化的家族)。
|
|
430
|
+
const mt = tokensOf(modelId)
|
|
431
|
+
if (mt.length < 2) return null
|
|
432
|
+
let best = null
|
|
433
|
+
let bestLen = 0
|
|
434
|
+
for (const c of list) {
|
|
435
|
+
const ct = tokensOf(c)
|
|
436
|
+
let n = 0
|
|
437
|
+
while (n < mt.length && n < ct.length && mt[n] === ct[n]) n += 1
|
|
438
|
+
// 防跨版本误配(issue #18):分歧位置两侧都有数字/版本号 token(如 glm-5.3 vs glm-5.2)
|
|
439
|
+
// 视为不同模型拒绝匹配——订阅制/新版本模型不应落到同家族其它版本的付费单价。
|
|
440
|
+
if (n < mt.length && n < ct.length && /^\d+$/.test(mt[n]) && /^\d+$/.test(ct[n])) continue
|
|
441
|
+
if (n >= 2 && (n > bestLen || (n === bestLen && best !== null && c.length < best.length))) {
|
|
442
|
+
best = c
|
|
443
|
+
bestLen = n
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return best
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* provider-aware 价格查找。provider 缺失时保持旧版 DeepSeek 行为;
|
|
451
|
+
* 已知非 DeepSeek provider 未配置模型时返回 null,避免误套 DeepSeek default。
|
|
452
|
+
* @param options - { mode: 'auto'|'exact', overrides: { 'provider:modelId': '目标' } };
|
|
453
|
+
* overrides 目标可为同 provider 模型 id,或 'provider:modelId' 跨 provider 引用;
|
|
454
|
+
* 'deepseek:__default__' 表示回退 DeepSeek 默认价。
|
|
455
|
+
*/
|
|
456
|
+
export function providerPriceEntryFor(provider, modelId, prices, options) {
|
|
457
|
+
const rawProvider = typeof provider === 'string' ? provider.trim().toLowerCase() : ''
|
|
458
|
+
const normalized = rawProvider.startsWith('llm-') ? rawProvider.slice(4) : rawProvider
|
|
459
|
+
const mode = options?.mode === 'exact' ? 'exact' : 'auto'
|
|
460
|
+
const overrides = options?.overrides !== null && typeof options?.overrides === 'object' ? options.overrides : {}
|
|
461
|
+
// 手动覆盖优先于自动匹配。
|
|
462
|
+
let targetModel = modelId
|
|
463
|
+
let targetProvider = normalized
|
|
464
|
+
const overrideKey = (normalized === '' ? 'deepseek' : normalized) + ':' + modelId
|
|
465
|
+
const override = overrides[overrideKey]
|
|
466
|
+
if (typeof override === 'string' && override.length > 0) {
|
|
467
|
+
const sep = override.indexOf(':')
|
|
468
|
+
if (sep > 0 && override.slice(sep + 1).length > 0) {
|
|
469
|
+
targetProvider = override.slice(0, sep).trim().toLowerCase()
|
|
470
|
+
targetModel = override.slice(sep + 1)
|
|
471
|
+
} else {
|
|
472
|
+
targetModel = override
|
|
473
|
+
}
|
|
474
|
+
if (targetProvider === 'deepseek' && targetModel === '__default__') {
|
|
475
|
+
return { entry: prices?.default ?? { cacheHit: 0, cacheMiss: 0, output: 0 }, billingMode: 'deepseek-peak', priced: true }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (targetProvider === '' || targetProvider === 'deepseek' || targetProvider.includes('deepseek')) {
|
|
479
|
+
const models = prices?.models ?? {}
|
|
480
|
+
const hit = models[targetModel] !== undefined
|
|
481
|
+
? targetModel
|
|
482
|
+
: (mode === 'auto' ? matchModelId(targetModel, Object.keys(models)) : null)
|
|
483
|
+
if (hit !== null && hit !== undefined) {
|
|
484
|
+
return { entry: models[hit], billingMode: 'deepseek-peak', priced: true }
|
|
485
|
+
}
|
|
486
|
+
const entry = priceEntryFor(targetModel, prices)
|
|
487
|
+
return { entry, billingMode: 'deepseek-peak', priced: entry?.unpriced !== true }
|
|
488
|
+
}
|
|
489
|
+
const providerTable = prices?.providers?.[targetProvider]
|
|
490
|
+
const catalog = providerTable?.models ?? {}
|
|
491
|
+
let hit = catalog[targetModel] !== undefined ? targetModel : null
|
|
492
|
+
if (hit === null && mode === 'auto') hit = matchModelId(targetModel, Object.keys(catalog))
|
|
493
|
+
if (hit === null && mode === 'auto') {
|
|
494
|
+
// 跨厂商兑底:请求携带的 provider 未在价格表登记(opencode / zen 等路由入口)时,
|
|
495
|
+
// 按模型名全库查找——先查 DeepSeek 主表(保留峰谷两档),再取其余厂商中归一化最长命中。
|
|
496
|
+
const dsModels = prices?.models ?? {}
|
|
497
|
+
const dsHit = matchModelId(targetModel, Object.keys(dsModels))
|
|
498
|
+
if (dsHit !== null) return { entry: dsModels[dsHit], billingMode: 'deepseek-peak', priced: true }
|
|
499
|
+
let bestEntry = null
|
|
500
|
+
let bestLen = 0
|
|
501
|
+
let bestMode = 'flat'
|
|
502
|
+
for (const [prov, table] of Object.entries(prices?.providers ?? {})) {
|
|
503
|
+
if (prov === targetProvider) continue
|
|
504
|
+
const models = table?.models ?? {}
|
|
505
|
+
const h = matchModelId(targetModel, Object.keys(models))
|
|
506
|
+
if (h === null) continue
|
|
507
|
+
const entry = normalizePrice(models[h])
|
|
508
|
+
if (entry === null || entry.unpriced === true) continue
|
|
509
|
+
const score = canonModelId(h).length
|
|
510
|
+
if (score > bestLen) {
|
|
511
|
+
bestEntry = entry
|
|
512
|
+
bestLen = score
|
|
513
|
+
bestMode = models[h]?.billingMode === 'deepseek-peak' ? 'deepseek-peak' : 'flat'
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (bestEntry !== null) return { entry: bestEntry, billingMode: bestMode, priced: true }
|
|
517
|
+
}
|
|
518
|
+
if (hit === null) return { entry: null, billingMode: 'flat', priced: false }
|
|
519
|
+
const entry = normalizePrice(catalog[hit])
|
|
520
|
+
if (entry === null || entry.unpriced === true) return { entry: null, billingMode: 'flat', priced: false }
|
|
521
|
+
return { entry, billingMode: catalog[hit].billingMode === 'deepseek-peak' ? 'deepseek-peak' : 'flat', priced: true }
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 某一时刻是否处于峰时段。
|
|
526
|
+
* @param atMs - 时刻(epoch ms)。
|
|
527
|
+
* @param effectiveAtMs - 峰谷计价生效时刻(epoch ms)。
|
|
528
|
+
* @param windows - 峰时段窗口数组({start,end} UTC 小时,半开区间)。
|
|
529
|
+
* @returns 峰时段返回 true;生效前或窗口外返回 false。
|
|
530
|
+
*/
|
|
531
|
+
export function isPeakHour(atMs, effectiveAtMs, windows) {
|
|
532
|
+
if (!Array.isArray(windows) || windows.length === 0) return false
|
|
533
|
+
if (Number.isFinite(effectiveAtMs) && atMs < effectiveAtMs) return false
|
|
534
|
+
const hour = new Date(atMs).getUTCHours()
|
|
535
|
+
return windows.some(w => {
|
|
536
|
+
const start = Number(w?.start)
|
|
537
|
+
const end = Number(w?.end)
|
|
538
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return false
|
|
539
|
+
if (start < end) return hour >= start && hour < end
|
|
540
|
+
// 跨午夜窗口(本配置不会出现,兼容处理)。
|
|
541
|
+
return hour >= start || hour < end
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* 某一时刻所处的峰谷相位与相邻相位切换点(供倒计时/进度条展示)。
|
|
547
|
+
* 窗口为半开区间 [start, end)(UTC 小时),兼容跨午夜窗口(end <= start)。
|
|
548
|
+
* @param atMs - 时刻(epoch ms)。
|
|
549
|
+
* @param windows - 峰时段窗口数组。
|
|
550
|
+
* @returns { inPeak, prevAtMs, nextAtMs, nextIntoPeak },或 null(无有效窗口/时刻)。
|
|
551
|
+
* prevAtMs = 当前相位起点,nextAtMs = 下一次切换时刻,nextIntoPeak = 该次切换是否进入峰时段。
|
|
552
|
+
*/
|
|
553
|
+
export function peakPhaseAt(atMs, windows) {
|
|
554
|
+
if (!Array.isArray(windows) || windows.length === 0 || !Number.isFinite(atMs)) return null
|
|
555
|
+
const hourAt = (dayOffset, hour) => {
|
|
556
|
+
const date = new Date(atMs)
|
|
557
|
+
date.setUTCDate(date.getUTCDate() + dayOffset)
|
|
558
|
+
date.setUTCHours(hour, 0, 0, 0)
|
|
559
|
+
return date.getTime()
|
|
560
|
+
}
|
|
561
|
+
// 收集前一天到后一天的全部切换点,保证任意时刻都能取到前后相邻切换点。
|
|
562
|
+
const points = []
|
|
563
|
+
for (let day = -1; day <= 1; day += 1) {
|
|
564
|
+
for (const w of windows) {
|
|
565
|
+
const start = Number(w?.start)
|
|
566
|
+
const end = Number(w?.end)
|
|
567
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) continue
|
|
568
|
+
points.push({ at: hourAt(day, start), intoPeak: true })
|
|
569
|
+
// 跨午夜窗口的结束点落在次日。
|
|
570
|
+
points.push({ at: hourAt(end <= start ? day + 1 : day, end), intoPeak: false })
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const inPeak = isPeakHour(atMs, undefined, windows)
|
|
574
|
+
let prev = null
|
|
575
|
+
let next = null
|
|
576
|
+
for (const p of points) {
|
|
577
|
+
if (p.at <= atMs && (prev === null || p.at > prev.at)) prev = p
|
|
578
|
+
if (p.at > atMs && (next === null || p.at < next.at)) next = p
|
|
579
|
+
}
|
|
580
|
+
if (prev === null || next === null) return null
|
|
581
|
+
return { inPeak, prevAtMs: prev.at, nextAtMs: next.at, nextIntoPeak: next.intoPeak }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 为一次用量挑选价格档位:生效后峰时段 → peak;生效后谷时段 → offPeak;
|
|
586
|
+
* 生效前(或禁用峰谷)→ 基础价格。cache write 与 cache hit 同价。
|
|
587
|
+
* @param entry - 模型价格记录。
|
|
588
|
+
* @param atMs - 计费时刻。
|
|
589
|
+
* @param peak - { enabled, effectiveAtMs, windows } 峰谷配置。
|
|
590
|
+
* @returns 三档价格 { cacheHit, cacheMiss, output }。
|
|
591
|
+
*/
|
|
592
|
+
export function tierFor(entry, atMs, peak) {
|
|
593
|
+
const base = entry ?? { cacheHit: 0, cacheMiss: 0, output: 0 }
|
|
594
|
+
const asTier = price => price.reasoning === undefined
|
|
595
|
+
? { cacheHit: price.cacheHit, cacheMiss: price.cacheMiss, output: price.output }
|
|
596
|
+
: { cacheHit: price.cacheHit, cacheMiss: price.cacheMiss, output: price.output, reasoning: price.reasoning }
|
|
597
|
+
// 峰谷时代之前(2026-08-16 16:00 UTC 前):按当时的基础价计费(历史正确性)。
|
|
598
|
+
if (Number.isFinite(atMs) && atMs < Date.parse(LEGACY_BASE_BOUNDARY)) {
|
|
599
|
+
const lb = base.legacyBase
|
|
600
|
+
return lb === undefined ? asTier(base) : asTier(lb)
|
|
601
|
+
}
|
|
602
|
+
if (peak?.enabled !== true) return asTier(base)
|
|
603
|
+
const effectiveAtMs = typeof peak.effectiveAtMs === 'number' ? peak.effectiveAtMs : undefined
|
|
604
|
+
if (isPeakHour(atMs, effectiveAtMs, peak.windows)) {
|
|
605
|
+
const p = base.peak
|
|
606
|
+
return p === undefined ? asTier(base) : asTier(p)
|
|
607
|
+
}
|
|
608
|
+
if (effectiveAtMs !== undefined && atMs >= effectiveAtMs) {
|
|
609
|
+
const off = base.offPeak
|
|
610
|
+
return off === undefined ? asTier(base) : asTier(off)
|
|
611
|
+
}
|
|
612
|
+
return asTier(base)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 一次调用的美元成本。
|
|
617
|
+
* @param tokens - { input, output, cacheRead, cacheWrite } 各桶 token 数。
|
|
618
|
+
* @param entry - 模型价格记录。
|
|
619
|
+
* @param atMs - 计费时刻。
|
|
620
|
+
* @param peak - 峰谷配置。
|
|
621
|
+
* @returns 美元成本(非负)。
|
|
622
|
+
*/
|
|
623
|
+
export function costOf(tokens, entry, atMs, peak) {
|
|
624
|
+
const tier = tierFor(entry, atMs, peak)
|
|
625
|
+
const input = Math.max(0, Number(tokens?.input) || 0)
|
|
626
|
+
const output = Math.max(0, Number(tokens?.output) || 0)
|
|
627
|
+
const cacheRead = Math.max(0, Number(tokens?.cacheRead) || 0)
|
|
628
|
+
const cacheWrite = Math.max(0, Number(tokens?.cacheWrite) || 0)
|
|
629
|
+
const reasoning = Math.max(0, Number(tokens?.reasoning) || 0)
|
|
630
|
+
const reasoningPrice = typeof tier.reasoning === 'number' ? tier.reasoning : 0
|
|
631
|
+
const cost = (input * tier.cacheMiss
|
|
632
|
+
+ output * tier.output
|
|
633
|
+
+ (cacheRead + cacheWrite) * tier.cacheHit
|
|
634
|
+
+ reasoning * reasoningPrice) / 1_000_000
|
|
635
|
+
return Math.max(0, cost)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** 金额显示:美元成本 × 汇率,按币种格式化,截断而非四舍五入进位。 */
|
|
639
|
+
export function formatMoney(usdCost, display) {
|
|
640
|
+
const rate = Number(display?.exchangeRate)
|
|
641
|
+
const value = usdCost * (Number.isFinite(rate) && rate > 0 ? rate : 1)
|
|
642
|
+
const symbol = typeof display?.symbol === 'string' && display.symbol.length > 0 ? display.symbol : '$'
|
|
643
|
+
const decimals = Math.max(0, Math.min(10, Math.floor(Number(display?.decimals) || 2)))
|
|
644
|
+
// 数值过小时自动放宽小数位,避免显示成 0。
|
|
645
|
+
let effective = decimals
|
|
646
|
+
if (value > 0 && value < 10 ** -decimals) effective = decimals + 2
|
|
647
|
+
const fixed = value.toFixed(effective)
|
|
648
|
+
const trimmed = fixed.includes('.') ? fixed.replace(/0+$/, '').replace(/\.$/, '') : fixed
|
|
649
|
+
return `${symbol}${trimmed}`
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── 官方页面解析 ──────────────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
function decodeEntities(text) {
|
|
655
|
+
return text
|
|
656
|
+
.replace(/ /g, ' ')
|
|
657
|
+
.replace(/&/g, '&')
|
|
658
|
+
.replace(/</g, '<')
|
|
659
|
+
.replace(/>/g, '>')
|
|
660
|
+
.replace(/"/g, '"')
|
|
661
|
+
.replace(/'/g, "'")
|
|
662
|
+
.replace(/—/g, '—')
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function stripTags(html) {
|
|
666
|
+
return decodeEntities(String(html).replace(/<[^>]*>/g, ' ')).replace(/\s+/g, ' ').trim()
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** 取出页面内所有 <table> 块,解析为行 × 单元格文本。 */
|
|
670
|
+
function parseTables(html) {
|
|
671
|
+
const blocks = String(html).match(/<table[\s\S]*?<\/table>/gi) ?? []
|
|
672
|
+
return blocks.map(block => {
|
|
673
|
+
const rows = []
|
|
674
|
+
const trs = block.match(/<tr[\s\S]*?<\/tr>/gi) ?? []
|
|
675
|
+
for (const tr of trs) {
|
|
676
|
+
const cells = tr.match(/<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi) ?? []
|
|
677
|
+
const row = cells.map(cell => stripTags(cell.replace(/^<t[dh][^>]*>/, '').replace(/<\/t[dh]>$/, '')))
|
|
678
|
+
if (row.length > 0) rows.push(row)
|
|
679
|
+
}
|
|
680
|
+
return rows
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** 单元格内的美元金额,取第一个 $ 数字。 */
|
|
685
|
+
function cellMoney(cell) {
|
|
686
|
+
const m = /(?:^|\s)\$([0-9]+(?:\.[0-9]+)?)/.exec(cell ?? '')
|
|
687
|
+
if (m === null) return null
|
|
688
|
+
const value = Number(m[1])
|
|
689
|
+
return Number.isFinite(value) ? value : null
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const MODEL_ID = /deepseek-[a-z0-9_.-]+/i
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* 解析官方定价页 HTML。
|
|
696
|
+
*
|
|
697
|
+
* 页面为一张表(服务端预渲染,结构与 2026-08-15 抓取一致):
|
|
698
|
+
* - 首行 [MODEL, <模型id>...] 给出全部模型 id;
|
|
699
|
+
* - 计价行按指标分组:指标标签行 [1M INPUT TOKENS (CACHE HIT), OFF-PEAK, $hit, $hit]
|
|
700
|
+
* 后跟 PEAK 续行 [PEAK, $hit, $hit](首两格被上一行 rowspan 合并);
|
|
701
|
+
* - 每个指标给出 OFF-PEAK / PEAK 两档各模型价格,空闲档 = 高峰档的一半;
|
|
702
|
+
* - 页面已不再列出基础价档与生效时间(两档方案即时生效),因此 models 的
|
|
703
|
+
* 基础档直接取空闲档数值,effectiveAt 返回 null。
|
|
704
|
+
* @param html - 页面源文本。
|
|
705
|
+
* @returns { models, effectiveAt, peakWindows } 解析结果。
|
|
706
|
+
* @throws 无法识别价格表时抛出带说明的 Error。
|
|
707
|
+
*/
|
|
708
|
+
export function parsePricingHtml(html) {
|
|
709
|
+
const tables = parseTables(html)
|
|
710
|
+
const modelIds = []
|
|
711
|
+
/** metricKey -> { offPeak: number[], peak: number[] }(按模型顺序)。 */
|
|
712
|
+
const tiers = {}
|
|
713
|
+
const metricOf = cell => {
|
|
714
|
+
const text = (cell ?? '').trim().toUpperCase()
|
|
715
|
+
if (text.includes('CACHE HIT')) return 'cacheHit'
|
|
716
|
+
if (text.includes('CACHE MISS')) return 'cacheMiss'
|
|
717
|
+
if (text.includes('OUTPUT TOKENS')) return 'output'
|
|
718
|
+
return null
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
for (const rows of tables) {
|
|
722
|
+
let lastMetric = null
|
|
723
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
724
|
+
const row = rows[i]
|
|
725
|
+
const first = (row[0] ?? '').trim()
|
|
726
|
+
// 模型表头行:MODEL 后跟全部模型 id。
|
|
727
|
+
if (/^MODEL$/i.test(first)) {
|
|
728
|
+
const ids = row.slice(1).map(cell => (MODEL_ID.exec(cell ?? '') ?? [])[0]).filter(Boolean)
|
|
729
|
+
if (ids.length > 0) modelIds.splice(0, modelIds.length, ...ids)
|
|
730
|
+
continue
|
|
731
|
+
}
|
|
732
|
+
// 指标标签可能在本行任意单元格(含 rowspan 合并布局);PEAK 续行沿用上一行指标。
|
|
733
|
+
const metric = metricOf(row.join(' ')) ?? lastMetric
|
|
734
|
+
if (metric !== null) lastMetric = metric
|
|
735
|
+
// 档位标签:OFF-PEAK / PEAK,价格紧跟其后。
|
|
736
|
+
const tierIdx = row.findIndex(cell => /^OFF-PEAK$/i.test((cell ?? '').trim()) || /^PEAK$/i.test((cell ?? '').trim()))
|
|
737
|
+
if (tierIdx < 0) continue
|
|
738
|
+
if (metric === null || modelIds.length === 0) continue
|
|
739
|
+
const label = /^PEAK$/i.test((row[tierIdx] ?? '').trim()) ? 'peak' : 'offPeak'
|
|
740
|
+
const prices = row.slice(tierIdx + 1, tierIdx + 1 + modelIds.length).map(cellMoney)
|
|
741
|
+
if (prices.some(v => v === null)) continue
|
|
742
|
+
if (tiers[metric] === undefined) tiers[metric] = { offPeak: [], peak: [] }
|
|
743
|
+
tiers[metric][label] = prices
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const models = {}
|
|
748
|
+
for (let k = 0; k < modelIds.length; k += 1) {
|
|
749
|
+
const id = modelIds[k].toLowerCase()
|
|
750
|
+
const off = {
|
|
751
|
+
cacheHit: tiers.cacheHit?.offPeak?.[k],
|
|
752
|
+
cacheMiss: tiers.cacheMiss?.offPeak?.[k],
|
|
753
|
+
output: tiers.output?.offPeak?.[k],
|
|
754
|
+
}
|
|
755
|
+
const pk = {
|
|
756
|
+
cacheHit: tiers.cacheHit?.peak?.[k],
|
|
757
|
+
cacheMiss: tiers.cacheMiss?.peak?.[k],
|
|
758
|
+
output: tiers.output?.peak?.[k],
|
|
759
|
+
}
|
|
760
|
+
if (off.cacheHit === undefined || off.cacheMiss === undefined || off.output === undefined) continue
|
|
761
|
+
models[id] = {
|
|
762
|
+
cacheHit: off.cacheHit,
|
|
763
|
+
cacheMiss: off.cacheMiss,
|
|
764
|
+
output: off.output,
|
|
765
|
+
offPeak: off,
|
|
766
|
+
peak: {
|
|
767
|
+
cacheHit: pk.cacheHit ?? off.cacheHit,
|
|
768
|
+
cacheMiss: pk.cacheMiss ?? off.cacheMiss,
|
|
769
|
+
output: pk.output ?? off.output,
|
|
770
|
+
},
|
|
771
|
+
}
|
|
772
|
+
// 峰谷时代前的历史基础价(官方页面已不再列出,按历史公告数字附带)。
|
|
773
|
+
const legacy = LEGACY_BASE_PRICES[id]
|
|
774
|
+
if (legacy !== undefined) models[id].legacyBase = { ...legacy }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (Object.keys(models).length === 0) {
|
|
778
|
+
// code 供上层按语言渲染提示(见 index.js 的 ERR_NO_MODELS 分支)。
|
|
779
|
+
const error = new Error('官方页面中未解析出任何模型价格,页面结构可能已变化,请稍后重试或手动编辑价格')
|
|
780
|
+
error.code = 'ERR_NO_MODELS'
|
|
781
|
+
throw error
|
|
782
|
+
}
|
|
783
|
+
// 生效时间:页面已不再给出(两档方案即时生效)→ null。
|
|
784
|
+
const effectiveAt = null
|
|
785
|
+
// 峰时段窗口。
|
|
786
|
+
let peakWindows = null
|
|
787
|
+
const plain = stripTags(html)
|
|
788
|
+
const win = /Peak hours are\s+(.+?)\s+UTC/.exec(plain)
|
|
789
|
+
if (win !== null) {
|
|
790
|
+
const pairs = win[1].match(/\d{1,2}:\d{2}/g) ?? []
|
|
791
|
+
peakWindows = []
|
|
792
|
+
for (let i = 0; i + 1 < pairs.length; i += 2) {
|
|
793
|
+
const start = Number(pairs[i].split(':')[0])
|
|
794
|
+
const end = Number(pairs[i + 1].split(':')[0])
|
|
795
|
+
if (Number.isFinite(start) && Number.isFinite(end)) peakWindows.push({ start, end })
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return { models, effectiveAt, peakWindows }
|
|
799
|
+
}
|