返回 DeepSeek-Reasonix
catalog.go
根目录 / internal / billing / catalog.go
1 package billing
2
3 import (
4 "strings"
5 "time"
6 )
7
8 const (
9 ScheduleDeepSeekV4August2026 = "deepseek-v4-2026-08-17"
10 ScheduleDeepSeekV4September2026 = "deepseek-v4-2026-09-10"
11 RateBandPeak = "peak"
12 RateBandOffPeak = "off_peak"
13 RateBandMixed = "mixed"
14 )
15
16 var (
17 deepSeekV4August2026EffectiveAt = time.Date(2026, time.August, 16, 16, 0, 0, 0, time.UTC)
18 // 2026-09-10 12:00 Beijing: the V4.1 Flash price cut plus its rename.
19 deepSeekV4September2026EffectiveAt = time.Date(2026, time.September, 10, 4, 0, 0, 0, time.UTC)
20 )
21
22 // deepSeekScheduleStart maps a schedule to the instant its peak/off-peak banding
23 // begins. A schedule absent from this map has no banded rows.
24 var deepSeekScheduleStart = map[string]time.Time{
25 ScheduleDeepSeekV4August2026: deepSeekV4August2026EffectiveAt,
26 ScheduleDeepSeekV4September2026: deepSeekV4September2026EffectiveAt,
27 }
28
29 // DeepSeekScheduledIDs lists the banded schedule ids newest first, so callers
30 // that bind a configured rate to its anchor always prefer the current one.
31 func DeepSeekScheduledIDs() []string {
32 return []string{ScheduleDeepSeekV4September2026, ScheduleDeepSeekV4August2026}
33 }
34
35 // CatalogEntry is one official list price for a model in a billing currency.
36 type CatalogEntry struct {
37 Provider string // deepseek | longcat | mimo
38 Model string
39 Currency string // ISO billing currency for this row
40 CacheHit float64
41 Input float64
42 Output float64
43 ScheduleID string
44 RateBand string // peak | off_peak; empty for static or legacy rows
45 EffectiveFrom time.Time
46 EffectiveTo time.Time
47 DocURL string
48 BillingMode string // payg | subscription_equivalent
49 Notes string
50 Fingerprint string // filled by OfficialCatalog
51 }
52
53 // ResolvedRate is the occurrence-time rate selected from an official schedule.
54 type ResolvedRate struct {
55 Card RateCard
56 RateBand string
57 ScheduleID string
58 OccurredAt time.Time
59 }
60
61 const (
62 DocDeepSeekPricing = "https://api-docs.deepseek.com/quick_start/pricing"
63 DocLongCatPricingUSD = "https://longcat.chat/platform/docs/Pricing/LongCat-2.0.html"
64 DocLongCatPricingCNY = "https://longcat.chat/platform/docs/zh/pricing/long-cat-2.0"
65 DocMiMoPAYG = "https://mimo.mi.com/docs/price/pay-as-you-go"
66 DocMiMoTokenPlan = "https://platform.xiaomimimo.com/token-plan"
67 )
68
69 // OfficialCatalog is the built-in price book. DeepSeek's scheduled rows use
70 // exact UTC instants so historical quote fixtures do not depend on host locale.
71 func OfficialCatalog() []CatalogEntry {
72 cutover := deepSeekV4August2026EffectiveAt
73 sept := deepSeekV4September2026EffectiveAt
74 entries := []CatalogEntry{
75 // August DeepSeek V4 regional tables. Peak rows are the persisted config
76 // anchors; occurrence-time resolution substitutes off-peak rows. Closed at
77 // the September cutover so the superseded prices stay queryable.
78 {Provider: "deepseek", Model: "deepseek-v4-flash", Currency: "CNY", CacheHit: 0.10, Input: 3, Output: 9, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
79 {Provider: "deepseek", Model: "deepseek-v4-flash", Currency: "CNY", CacheHit: 0.05, Input: 1.5, Output: 4.5, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandOffPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
80 {Provider: "deepseek", Model: "deepseek-v4-flash-vision-exp", Currency: "CNY", CacheHit: 0.10, Input: 3, Output: 9, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
81 {Provider: "deepseek", Model: "deepseek-v4-flash-vision-exp", Currency: "CNY", CacheHit: 0.05, Input: 1.5, Output: 4.5, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandOffPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
82 {Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "CNY", CacheHit: 0.30, Input: 9, Output: 27, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
83 {Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "CNY", CacheHit: 0.15, Input: 4.5, Output: 13.5, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandOffPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
84 {Provider: "deepseek", Model: "deepseek-v4-flash", Currency: "USD", CacheHit: 0.014, Input: 0.44, Output: 1.32, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
85 {Provider: "deepseek", Model: "deepseek-v4-flash", Currency: "USD", CacheHit: 0.007, Input: 0.22, Output: 0.66, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandOffPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
86 {Provider: "deepseek", Model: "deepseek-v4-flash-vision-exp", Currency: "USD", CacheHit: 0.014, Input: 0.44, Output: 1.32, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
87 {Provider: "deepseek", Model: "deepseek-v4-flash-vision-exp", Currency: "USD", CacheHit: 0.007, Input: 0.22, Output: 0.66, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandOffPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
88 {Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "USD", CacheHit: 0.044, Input: 1.32, Output: 3.96, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
89 {Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "USD", CacheHit: 0.022, Input: 0.66, Output: 1.98, ScheduleID: ScheduleDeepSeekV4August2026, RateBand: RateBandOffPeak, EffectiveFrom: cutover, EffectiveTo: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
90
91 // Historical DeepSeek prices are available only to explicitly dated
92 // schedule resolution. Persisted quotes are never repriced from these rows.
93 {Provider: "deepseek", Model: "deepseek-v4-flash", Currency: "CNY", CacheHit: 0.02, Input: 1, Output: 2, ScheduleID: ScheduleDeepSeekV4August2026, EffectiveTo: cutover, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
94 {Provider: "deepseek", Model: "deepseek-v4-flash-vision-exp", Currency: "CNY", CacheHit: 0.02, Input: 1, Output: 2, ScheduleID: ScheduleDeepSeekV4August2026, EffectiveTo: cutover, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
95 {Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "CNY", CacheHit: 0.025, Input: 3, Output: 6, ScheduleID: ScheduleDeepSeekV4August2026, EffectiveTo: cutover, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
96 {Provider: "deepseek", Model: "deepseek-v4-flash", Currency: "USD", CacheHit: 0.0028, Input: 0.14, Output: 0.28, ScheduleID: ScheduleDeepSeekV4August2026, EffectiveTo: cutover, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
97 {Provider: "deepseek", Model: "deepseek-v4-flash-vision-exp", Currency: "USD", CacheHit: 0.0028, Input: 0.14, Output: 0.28, ScheduleID: ScheduleDeepSeekV4August2026, EffectiveTo: cutover, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
98 {Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "USD", CacheHit: 0.003625, Input: 0.435, Output: 0.87, ScheduleID: ScheduleDeepSeekV4August2026, EffectiveTo: cutover, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
99
100 {Provider: "longcat", Model: "LongCat-2.0", Currency: "CNY", CacheHit: 0.04, Input: 2, Output: 8, DocURL: DocLongCatPricingCNY, BillingMode: BillingModePAYG},
101 {Provider: "longcat", Model: "LongCat-2.0", Currency: "USD", CacheHit: 0.006, Input: 0.30, Output: 1.20, DocURL: DocLongCatPricingUSD, BillingMode: BillingModePAYG},
102 {Provider: "mimo", Model: "mimo-v2.5-pro", Currency: "CNY", CacheHit: 0.025, Input: 3, Output: 6, DocURL: DocMiMoPAYG, BillingMode: BillingModePAYG},
103 {Provider: "mimo", Model: "mimo-v2.5", Currency: "CNY", CacheHit: 0.02, Input: 1, Output: 2, DocURL: DocMiMoPAYG, BillingMode: BillingModePAYG},
104 {Provider: "mimo", Model: "mimo-v2-flash", Currency: "CNY", CacheHit: 0.07, Input: 0.70, Output: 2.10, DocURL: DocMiMoPAYG, BillingMode: BillingModePAYG},
105 {Provider: "mimo", Model: "mimo-v2.5-pro", Currency: "CNY", CacheHit: 0.025, Input: 3, Output: 6, DocURL: DocMiMoTokenPlan, BillingMode: BillingModeSubscriptionEquivalent, Notes: "payg_equivalent_not_plan_bill"},
106 {Provider: "mimo", Model: "mimo-v2.5", Currency: "CNY", CacheHit: 0.02, Input: 1, Output: 2, DocURL: DocMiMoTokenPlan, BillingMode: BillingModeSubscriptionEquivalent, Notes: "payg_equivalent_not_plan_bill"},
107 }
108 // September: V4.1 Flash cut its price and the vendor serves the retired
109 // deepseek-v4-flash / -vision-exp ids from it, so all three share one rate.
110 for _, model := range []string{"deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"} {
111 entries = append(entries,
112 CatalogEntry{Provider: "deepseek", Model: model, Currency: "CNY", CacheHit: 0.04, Input: 2, Output: 8, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
113 CatalogEntry{Provider: "deepseek", Model: model, Currency: "CNY", CacheHit: 0.02, Input: 1, Output: 4, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandOffPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
114 CatalogEntry{Provider: "deepseek", Model: model, Currency: "USD", CacheHit: 0.006, Input: 0.3, Output: 1.2, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
115 CatalogEntry{Provider: "deepseek", Model: model, Currency: "USD", CacheHit: 0.003, Input: 0.15, Output: 0.6, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandOffPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG})
116 }
117 // V4 Pro keeps its own price until the vendor routes it to V4.1 Flash.
118 entries = append(entries,
119 CatalogEntry{Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "CNY", CacheHit: 0.30, Input: 9, Output: 27, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
120 CatalogEntry{Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "CNY", CacheHit: 0.15, Input: 4.5, Output: 13.5, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandOffPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
121 CatalogEntry{Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "USD", CacheHit: 0.044, Input: 1.32, Output: 3.96, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG},
122 CatalogEntry{Provider: "deepseek", Model: "deepseek-v4-pro", Currency: "USD", CacheHit: 0.022, Input: 0.66, Output: 1.98, ScheduleID: ScheduleDeepSeekV4September2026, RateBand: RateBandOffPeak, EffectiveFrom: sept, DocURL: DocDeepSeekPricing, BillingMode: BillingModePAYG})
123 for i := range entries {
124 entries[i].Fingerprint = PricingFingerprint(RateCardFromCatalog(entries[i]))
125 }
126 return entries
127 }
128
129 func normalizeBillingMode(mode string) string {
130 if strings.TrimSpace(mode) == "" {
131 return BillingModePAYG
132 }
133 return strings.TrimSpace(mode)
134 }
135
136 func catalogIdentityMatches(e CatalogEntry, provider, model, currency, billingMode string) bool {
137 return e.Provider == strings.ToLower(strings.TrimSpace(provider)) &&
138 e.Model == strings.TrimSpace(model) &&
139 NormalizeCurrency(e.Currency) == NormalizeCurrency(currency) &&
140 e.BillingMode == normalizeBillingMode(billingMode)
141 }
142
143 func catalogEntryEffective(e CatalogEntry, at time.Time) bool {
144 if !e.EffectiveFrom.IsZero() && at.Before(e.EffectiveFrom) {
145 return false
146 }
147 return e.EffectiveTo.IsZero() || at.Before(e.EffectiveTo)
148 }
149
150 // beijingLocal anchors DeepSeek's published peak windows: PRC standard time
151 // has no DST, so a fixed +8 offset stays exact year-round.
152 var beijingLocal = time.FixedZone("Asia/Shanghai", 8*60*60)
153
154 // chineseStatutoryHolidays lists the PRC public-holiday dates (Beijing local
155 // dates) on which DeepSeek bills everything off-peak, per the pricing page
156 // ("peak hours are weekdays only, excluding Chinese public holidays"). Dates
157 // come from the State Council's annual notice; extend the table each year
158 // when the next notice is published (the 2027 one is due around November
159 // 2026). Make-up workdays (调休上班的周末) are deliberately not listed: they
160 // are calendar weekends, and the same page keeps weekends off-peak in full.
161 var chineseStatutoryHolidays = map[string]struct{}{
162 // 元旦 (New Year's Day): Jan 1-3.
163 "2026-01-01": {}, "2026-01-02": {}, "2026-01-03": {},
164 // 春节 (Spring Festival): Feb 15-23.
165 "2026-02-15": {}, "2026-02-16": {}, "2026-02-17": {}, "2026-02-18": {},
166 "2026-02-19": {}, "2026-02-20": {}, "2026-02-21": {}, "2026-02-22": {},
167 "2026-02-23": {},
168 // 清明节 (Qingming Festival): Apr 4-6.
169 "2026-04-04": {}, "2026-04-05": {}, "2026-04-06": {},
170 // 劳动节 (Labor Day): May 1-5.
171 "2026-05-01": {}, "2026-05-02": {}, "2026-05-03": {}, "2026-05-04": {},
172 "2026-05-05": {},
173 // 端午节 (Dragon Boat Festival): Jun 19-21.
174 "2026-06-19": {}, "2026-06-20": {}, "2026-06-21": {},
175 // 中秋节 (Mid-Autumn Festival): Sep 25-27.
176 "2026-09-25": {}, "2026-09-26": {}, "2026-09-27": {},
177 // 国庆节 (National Day): Oct 1-7.
178 "2026-10-01": {}, "2026-10-02": {}, "2026-10-03": {}, "2026-10-04": {},
179 "2026-10-05": {}, "2026-10-06": {}, "2026-10-07": {},
180 }
181
182 // DeepSeekRateBand selects the documented Beijing weekday peak windows by
183 // their stable UTC equivalents. Weekends and Chinese public holidays are
184 // off-peak in full.
185 func DeepSeekRateBand(at time.Time) string {
186 at = at.UTC()
187 if _, holiday := chineseStatutoryHolidays[at.In(beijingLocal).Format("2006-01-02")]; holiday {
188 return RateBandOffPeak
189 }
190 if at.Weekday() == time.Saturday || at.Weekday() == time.Sunday {
191 return RateBandOffPeak
192 }
193 minutes := at.Hour()*60 + at.Minute()
194 if (minutes >= 60 && minutes < 240) || (minutes >= 360 && minutes < 600) {
195 return RateBandPeak
196 }
197 return RateBandOffPeak
198 }
199
200 // ResolveScheduledRate resolves an official occurrence-time rate. The schedule
201 // id must come from resolved official-provider config; a model name is not enough.
202 func ResolveScheduledRate(provider, model, currency, billingMode, scheduleID string, at time.Time) (ResolvedRate, bool) {
203 if strings.TrimSpace(scheduleID) == "" {
204 return ResolvedRate{}, false
205 }
206 if at.IsZero() {
207 at = time.Now().UTC()
208 } else {
209 at = at.UTC()
210 }
211 band := ""
212 if start, banded := deepSeekScheduleStart[scheduleID]; banded && !at.Before(start) {
213 band = DeepSeekRateBand(at)
214 }
215 for _, e := range OfficialCatalog() {
216 if e.ScheduleID != scheduleID || e.RateBand != band || !catalogEntryEffective(e, at) {
217 continue
218 }
219 if catalogIdentityMatches(e, provider, model, currency, billingMode) {
220 return ResolvedRate{Card: RateCardFromCatalog(e), RateBand: band, ScheduleID: scheduleID, OccurredAt: at}, true
221 }
222 }
223 return ResolvedRate{}, false
224 }
225
226 // LookupCatalog finds the preferred current official entry. Scheduled models
227 // expose their peak row as the stable config anchor.
228 func LookupCatalog(provider, model, currency, billingMode string) (CatalogEntry, bool) {
229 var anchor, fallback CatalogEntry
230 for _, e := range OfficialCatalog() {
231 if !catalogIdentityMatches(e, provider, model, currency, billingMode) {
232 continue
233 }
234 if e.ScheduleID == "" {
235 return e, true
236 }
237 // A model carries one peak anchor per dated schedule; the config anchor is
238 // always the newest, so resolution must not depend on catalog order.
239 if e.RateBand == RateBandPeak {
240 if anchor.Provider == "" || e.EffectiveFrom.After(anchor.EffectiveFrom) {
241 anchor = e
242 }
243 continue
244 }
245 if fallback.Provider == "" {
246 fallback = e
247 }
248 }
249 if anchor.Provider != "" {
250 return anchor, true
251 }
252 return fallback, fallback.Provider != ""
253 }
254
255 // LookupCatalogAt returns the matching occurrence-time peer row used for an
256 // official dual-currency valuation.
257 func LookupCatalogAt(provider, model, currency, billingMode, scheduleID, rateBand string, at time.Time) (CatalogEntry, bool) {
258 for _, e := range OfficialCatalog() {
259 if e.ScheduleID != scheduleID || e.RateBand != rateBand || !catalogEntryEffective(e, at) {
260 continue
261 }
262 if catalogIdentityMatches(e, provider, model, currency, billingMode) {
263 return e, true
264 }
265 }
266 return CatalogEntry{}, false
267 }
268
269 func RateCardFromCatalog(e CatalogEntry) RateCard {
270 return RateCard{CacheHit: e.CacheHit, Input: e.Input, Output: e.Output, Currency: e.Currency}
271 }
272
273 func MatchesCatalog(provider, model string, rates RateCard) (CatalogEntry, bool) {
274 cur := NormalizeCurrency(rates.Currency)
275 for _, e := range OfficialCatalog() {
276 // Historical and off-peak rows require a trusted schedule resolution.
277 // Static custom rates that merely equal one of those rows are not official.
278 if e.ScheduleID != "" && e.RateBand != RateBandPeak {
279 continue
280 }
281 if e.Provider != strings.ToLower(strings.TrimSpace(provider)) || e.Model != strings.TrimSpace(model) || NormalizeCurrency(e.Currency) != cur {
282 continue
283 }
284 if e.CacheHit == rates.CacheHit && e.Input == rates.Input && e.Output == rates.Output {
285 return e, true
286 }
287 }
288 return CatalogEntry{}, false
289 }
290
291 // MatchesOfficialPeakAnchor reports whether rates is an unedited peak anchor for
292 // this model in any recorded schedule. A config that has not been re-saved since
293 // a dated price change still holds the previous generation's anchor; that is
294 // still an official row, so occurrence-time resolution must be allowed to price
295 // it. Which schedule that resolution uses is the caller's decision, not this
296 // check's — custom and off-peak-looking static prices still fail here.
297 func MatchesOfficialPeakAnchor(provider, model, currency, billingMode string, rates RateCard) bool {
298 for _, e := range OfficialCatalog() {
299 if e.ScheduleID == "" || e.RateBand != RateBandPeak {
300 continue
301 }
302 if !catalogIdentityMatches(e, provider, model, currency, billingMode) {
303 continue
304 }
305 if e.CacheHit == rates.CacheHit && e.Input == rates.Input && e.Output == rates.Output {
306 return true
307 }
308 }
309 return false
310 }
311
312 // MatchesScheduleAnchor verifies that configured rates are the peak anchor of
313 // the named schedule. It is the stricter, schedule-scoped form of
314 // MatchesOfficialPeakAnchor.
315 func MatchesScheduleAnchor(provider, model, scheduleID string, rates RateCard) bool {
316 for _, e := range OfficialCatalog() {
317 if e.ScheduleID != scheduleID || e.RateBand != RateBandPeak {
318 continue
319 }
320 if !catalogIdentityMatches(e, provider, model, rates.Currency, BillingModePAYG) {
321 continue
322 }
323 return e.CacheHit == rates.CacheHit && e.Input == rates.Input && e.Output == rates.Output
324 }
325 return false
326 }
327
327 lines GO