返回 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 // DeepSeekRateBand selects the documented Beijing weekday peak windows by
151 // their stable UTC equivalents.
152 func DeepSeekRateBand(at time.Time) string {
153 at = at.UTC()
154 if at.Weekday() == time.Saturday || at.Weekday() == time.Sunday {
155 return RateBandOffPeak
156 }
157 minutes := at.Hour()*60 + at.Minute()
158 if (minutes >= 60 && minutes < 240) || (minutes >= 360 && minutes < 600) {
159 return RateBandPeak
160 }
161 return RateBandOffPeak
162 }
163
164 // ResolveScheduledRate resolves an official occurrence-time rate. The schedule
165 // id must come from resolved official-provider config; a model name is not enough.
166 func ResolveScheduledRate(provider, model, currency, billingMode, scheduleID string, at time.Time) (ResolvedRate, bool) {
167 if strings.TrimSpace(scheduleID) == "" {
168 return ResolvedRate{}, false
169 }
170 if at.IsZero() {
171 at = time.Now().UTC()
172 } else {
173 at = at.UTC()
174 }
175 band := ""
176 if start, banded := deepSeekScheduleStart[scheduleID]; banded && !at.Before(start) {
177 band = DeepSeekRateBand(at)
178 }
179 for _, e := range OfficialCatalog() {
180 if e.ScheduleID != scheduleID || e.RateBand != band || !catalogEntryEffective(e, at) {
181 continue
182 }
183 if catalogIdentityMatches(e, provider, model, currency, billingMode) {
184 return ResolvedRate{Card: RateCardFromCatalog(e), RateBand: band, ScheduleID: scheduleID, OccurredAt: at}, true
185 }
186 }
187 return ResolvedRate{}, false
188 }
189
190 // LookupCatalog finds the preferred current official entry. Scheduled models
191 // expose their peak row as the stable config anchor.
192 func LookupCatalog(provider, model, currency, billingMode string) (CatalogEntry, bool) {
193 var anchor, fallback CatalogEntry
194 for _, e := range OfficialCatalog() {
195 if !catalogIdentityMatches(e, provider, model, currency, billingMode) {
196 continue
197 }
198 if e.ScheduleID == "" {
199 return e, true
200 }
201 // A model carries one peak anchor per dated schedule; the config anchor is
202 // always the newest, so resolution must not depend on catalog order.
203 if e.RateBand == RateBandPeak {
204 if anchor.Provider == "" || e.EffectiveFrom.After(anchor.EffectiveFrom) {
205 anchor = e
206 }
207 continue
208 }
209 if fallback.Provider == "" {
210 fallback = e
211 }
212 }
213 if anchor.Provider != "" {
214 return anchor, true
215 }
216 return fallback, fallback.Provider != ""
217 }
218
219 // LookupCatalogAt returns the matching occurrence-time peer row used for an
220 // official dual-currency valuation.
221 func LookupCatalogAt(provider, model, currency, billingMode, scheduleID, rateBand string, at time.Time) (CatalogEntry, bool) {
222 for _, e := range OfficialCatalog() {
223 if e.ScheduleID != scheduleID || e.RateBand != rateBand || !catalogEntryEffective(e, at) {
224 continue
225 }
226 if catalogIdentityMatches(e, provider, model, currency, billingMode) {
227 return e, true
228 }
229 }
230 return CatalogEntry{}, false
231 }
232
233 func RateCardFromCatalog(e CatalogEntry) RateCard {
234 return RateCard{CacheHit: e.CacheHit, Input: e.Input, Output: e.Output, Currency: e.Currency}
235 }
236
237 func MatchesCatalog(provider, model string, rates RateCard) (CatalogEntry, bool) {
238 cur := NormalizeCurrency(rates.Currency)
239 for _, e := range OfficialCatalog() {
240 // Historical and off-peak rows require a trusted schedule resolution.
241 // Static custom rates that merely equal one of those rows are not official.
242 if e.ScheduleID != "" && e.RateBand != RateBandPeak {
243 continue
244 }
245 if e.Provider != strings.ToLower(strings.TrimSpace(provider)) || e.Model != strings.TrimSpace(model) || NormalizeCurrency(e.Currency) != cur {
246 continue
247 }
248 if e.CacheHit == rates.CacheHit && e.Input == rates.Input && e.Output == rates.Output {
249 return e, true
250 }
251 }
252 return CatalogEntry{}, false
253 }
254
255 // MatchesOfficialPeakAnchor reports whether rates is an unedited peak anchor for
256 // this model in any recorded schedule. A config that has not been re-saved since
257 // a dated price change still holds the previous generation's anchor; that is
258 // still an official row, so occurrence-time resolution must be allowed to price
259 // it. Which schedule that resolution uses is the caller's decision, not this
260 // check's — custom and off-peak-looking static prices still fail here.
261 func MatchesOfficialPeakAnchor(provider, model, currency, billingMode string, rates RateCard) bool {
262 for _, e := range OfficialCatalog() {
263 if e.ScheduleID == "" || e.RateBand != RateBandPeak {
264 continue
265 }
266 if !catalogIdentityMatches(e, provider, model, currency, billingMode) {
267 continue
268 }
269 if e.CacheHit == rates.CacheHit && e.Input == rates.Input && e.Output == rates.Output {
270 return true
271 }
272 }
273 return false
274 }
275
276 // MatchesScheduleAnchor verifies that configured rates are the peak anchor of
277 // the named schedule. It is the stricter, schedule-scoped form of
278 // MatchesOfficialPeakAnchor.
279 func MatchesScheduleAnchor(provider, model, scheduleID string, rates RateCard) bool {
280 for _, e := range OfficialCatalog() {
281 if e.ScheduleID != scheduleID || e.RateBand != RateBandPeak {
282 continue
283 }
284 if !catalogIdentityMatches(e, provider, model, rates.Currency, BillingModePAYG) {
285 continue
286 }
287 return e.CacheHit == rates.CacheHit && e.Input == rates.Input && e.Output == rates.Output
288 }
289 return false
290 }
291
291 lines GO