返回 DeepSeek-Reasonix
pricing.go
根目录 / internal / config / pricing.go
1 package config
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/fileutil"
8 "reasonix/internal/provider"
9 "reasonix/internal/provider/openai"
10 )
11
12 func deepSeekV4FlashPriceCNY() *provider.Pricing {
13 return &provider.Pricing{CacheHit: 0.04, Input: 2, Output: 8, Currency: "¥"}
14 }
15
16 func deepSeekV4ProPriceCNY() *provider.Pricing {
17 return &provider.Pricing{CacheHit: 0.30, Input: 9, Output: 27, Currency: "¥"}
18 }
19
20 // deepSeekV4FlashModelIDs are the ids the vendor serves at the Flash price: the
21 // V4.1 name, and the retired V4 ids it still routes there.
22 func deepSeekV4FlashModelIDs() []string {
23 return []string{"deepseek-flash", "deepseek-v4-flash", openai.OfficialDeepSeekVisionModel}
24 }
25
26 func deepSeekV4PricesCNY() map[string]*provider.Pricing {
27 prices := map[string]*provider.Pricing{"deepseek-v4-pro": deepSeekV4ProPriceCNY()}
28 for _, model := range deepSeekV4FlashModelIDs() {
29 prices[model] = deepSeekV4FlashPriceCNY()
30 }
31 return prices
32 }
33
34 func deepSeekV4FlashPriceUSD() *provider.Pricing {
35 return &provider.Pricing{CacheHit: 0.006, Input: 0.3, Output: 1.2, Currency: "$"}
36 }
37
38 func deepSeekV4ProPriceUSD() *provider.Pricing {
39 return &provider.Pricing{CacheHit: 0.044, Input: 1.32, Output: 3.96, Currency: "$"}
40 }
41
42 func deepSeekV4PricesUSD() map[string]*provider.Pricing {
43 prices := map[string]*provider.Pricing{"deepseek-v4-pro": deepSeekV4ProPriceUSD()}
44 for _, model := range deepSeekV4FlashModelIDs() {
45 prices[model] = deepSeekV4FlashPriceUSD()
46 }
47 return prices
48 }
49
50 // DeepSeekV4PricesForCurrency returns the official regional price table.
51 // Persisted custom prices still win; this is only used for built-in templates
52 // and known-default refreshes.
53 func DeepSeekV4PricesForCurrency(currency string) map[string]*provider.Pricing {
54 if normalizeDeepSeekPricingCurrency(currency) == "CNY" {
55 return deepSeekV4PricesCNY()
56 }
57 return deepSeekV4PricesUSD()
58 }
59
60 // DeepSeekV4PricesForLanguage is retained for compatibility with older call
61 // sites. New desktop code should pass an explicit pricing currency.
62 func DeepSeekV4PricesForLanguage(lang string) map[string]*provider.Pricing {
63 if normalizeDeepSeekPricingLanguage(lang) == "zh" {
64 return DeepSeekV4PricesForCurrency("CNY")
65 }
66 return DeepSeekV4PricesForCurrency("USD")
67 }
68
69 func deepSeekV4PricesForConfig(c *Config) map[string]*provider.Pricing {
70 return DeepSeekV4PricesForCurrency(c.DeepSeekOfficialPricingCurrency())
71 }
72
73 func deepSeekV4PriceForModel(currency, model string) *provider.Pricing {
74 return clonePricing(DeepSeekV4PricesForCurrency(currency)[strings.TrimSpace(model)])
75 }
76
77 // DeepSeekOfficialPricingLanguage is retained for older settings/template call
78 // sites that still express the pricing region as a language. List-price region
79 // is frozen per provider (billing_currency), not the global display currency.
80 func (c *Config) DeepSeekOfficialPricingLanguage() string {
81 if c.DeepSeekOfficialPricingCurrency() == "CNY" {
82 return "zh"
83 }
84 return "en"
85 }
86
87 func normalizeDeepSeekPricingCurrency(currency string) string {
88 switch strings.ToUpper(strings.TrimSpace(currency)) {
89 case "CNY", "RMB", "CNH", "¥", "¥":
90 return "CNY"
91 case "USD", "$", "US$":
92 return "USD"
93 default:
94 return ""
95 }
96 }
97
98 func normalizeDeepSeekPricingLanguage(lang string) string {
99 switch strings.ToLower(strings.TrimSpace(lang)) {
100 case "zh", "zh-cn", "zh-hans", "cn", "chinese", "中文", "zh-tw", "zh-hant", "zh-hk", "zh-mo":
101 return "zh"
102 case "en", "en-us", "en-gb", "english":
103 return "en"
104 default:
105 return ""
106 }
107 }
108
109 // ApplyDeepSeekOfficialDefaultPricing refreshes built-in/official DeepSeek
110 // prices that still match known official defaults for each provider's frozen
111 // billing_currency. Custom user prices and display-currency switches never
112 // rewrite list prices.
113 func (c *Config) ApplyDeepSeekOfficialDefaultPricing() {
114 applyDeepSeekOfficialDefaultPricing(c)
115 }
116
117 func applyDeepSeekOfficialDefaultPricing(c *Config) {
118 applyDeepSeekOfficialDefaultPricingWithOverride(c, false)
119 }
120
121 func applyDeepSeekOfficialDefaultPricingWithOverride(c *Config, overridePersisted bool) {
122 if c == nil {
123 return
124 }
125 for i := range c.Providers {
126 p := &c.Providers[i]
127 if officialProviderKind(p) != "deepseek" || !isOfficialDeepSeekBillingEndpoint(p) {
128 continue
129 }
130 currency := p.ProviderBillingCurrency()
131 if currency == "" {
132 currency = "USD"
133 }
134 // Only refresh when the row still matches a known official default in
135 // the provider's own billing currency. Display currency must not win.
136 if isKnownDeepSeekOfficialPricing(p.Model, p.Price) && (overridePersisted || p.persistedOfficialCurrency == "" || p.persistedOfficialCurrency == currency) {
137 if samePricing(p.Price, deepSeekV4PriceForModel(currency, p.Model)) || overridePersisted {
138 p.Price = deepSeekV4PriceForModel(currency, p.Model)
139 }
140 }
141 for model, price := range p.Prices {
142 if isKnownDeepSeekOfficialPricing(model, price) && (overridePersisted || p.persistedOfficialCurrency == "" || p.persistedOfficialCurrency == currency) {
143 if samePricing(price, deepSeekV4PriceForModel(currency, model)) || overridePersisted {
144 p.Prices[model] = deepSeekV4PriceForModel(currency, model)
145 }
146 }
147 }
148 if strings.TrimSpace(p.BillingCurrency) == "" {
149 p.BillingCurrency = currency
150 }
151 }
152 }
153
154 // markPersistedDeepSeekOfficialPricing records which recognized regional
155 // prices came from TOML. Auto locale refreshes preserve those values, while an
156 // explicit currency choice can still replace them with the selected table.
157 func markPersistedDeepSeekOfficialPricing(c *Config) {
158 if c == nil {
159 return
160 }
161 for i := range c.Providers {
162 p := &c.Providers[i]
163 if officialProviderKind(p) != "deepseek" || !isOfficialDeepSeekBillingEndpoint(p) {
164 continue
165 }
166 p.persistedOfficialCurrency = completeDeepSeekOfficialPricingCurrency(p)
167 if c.ConfigVersion >= Default().ConfigVersion && isStandardDeepSeekProviderTemplate(p) {
168 p.persistedOfficialCurrency = ""
169 }
170 }
171 }
172
173 func isStandardDeepSeekProviderTemplate(p *ProviderEntry) bool {
174 if p == nil || officialProviderKind(p) != "deepseek" {
175 return false
176 }
177 return strings.TrimSpace(p.APIKeyEnv) == "DEEPSEEK_API_KEY" &&
178 strings.TrimSpace(p.BalanceURL) == "https://api.deepseek.com/user/balance" &&
179 p.ContextWindow == 1_000_000
180 }
181
182 func completeDeepSeekOfficialPricingCurrency(p *ProviderEntry) string {
183 if p == nil {
184 return ""
185 }
186 models := p.ModelList()
187 if len(models) == 1 && isKnownDeepSeekOfficialPricing(models[0], p.Price) {
188 return normalizeDeepSeekPricingCurrency(p.Price.Currency)
189 }
190 if len(models) == 0 || p.Price != nil {
191 return ""
192 }
193 currency := ""
194 for _, model := range models {
195 price := p.Prices[strings.TrimSpace(model)]
196 if !isKnownDeepSeekOfficialPricing(model, price) {
197 return ""
198 }
199 nextCurrency := normalizeDeepSeekPricingCurrency(price.Currency)
200 if nextCurrency == "" {
201 return ""
202 }
203 if currency == "" {
204 currency = nextCurrency
205 } else if currency != nextCurrency {
206 return ""
207 }
208 }
209 return currency
210 }
211
212 func mimoV25ProPrice() *provider.Pricing {
213 return &provider.Pricing{CacheHit: 0.025, Input: 3, Output: 6, Currency: "¥"}
214 }
215
216 func mimoV25Price() *provider.Pricing {
217 return &provider.Pricing{CacheHit: 0.02, Input: 1, Output: 2, Currency: "¥"}
218 }
219
220 func mimoV2FlashPrice() *provider.Pricing {
221 return &provider.Pricing{CacheHit: 0.07, Input: 0.70, Output: 2.10, Currency: "¥"}
222 }
223
224 func mimoDomesticPrices(models []string) map[string]*provider.Pricing {
225 prices := map[string]*provider.Pricing{}
226 for _, model := range models {
227 switch strings.TrimSpace(model) {
228 case "mimo-v2.6-pro", "mimo-v2.5-pro", "mimo-v2-pro":
229 prices[model] = mimoV25ProPrice()
230 case "mimo-v2.6-flash", "mimo-v2.5", "mimo-v2-omni":
231 prices[model] = mimoV25Price()
232 case "mimo-v2-flash":
233 prices[model] = mimoV2FlashPrice()
234 }
235 }
236 return prices
237 }
238
239 func longCat20Price() *provider.Pricing {
240 return &provider.Pricing{CacheHit: 0.04, Input: 2, Output: 8, Currency: "¥"}
241 }
242
243 func longCat20Prices(models []string) map[string]*provider.Pricing {
244 prices := map[string]*provider.Pricing{}
245 for _, model := range models {
246 switch strings.TrimSpace(model) {
247 case "LongCat-2.0":
248 prices[model] = longCat20Price()
249 }
250 }
251 return prices
252 }
253
254 const (
255 deepSeekPricingResetConfigVersion = 3
256 windowsBashSandboxDefaultConfigVersion = 4
257 retiredAutoPlanConfigVersion = 5
258 billingSplitConfigVersion = 6
259 deepSeekScheduledPricingConfigVersion = 7
260 )
261
262 // ApplyUserConfigUpgradesOnStartup applies one-time startup migrations. It
263 // intentionally runs from the desktop and CLI startup paths, not every config
264 // Load(), so user edits made after the upgrade are preserved.
265 func ApplyUserConfigUpgradesOnStartup(path string) (bool, error) {
266 path = strings.TrimSpace(path)
267 if path == "" {
268 return false, nil
269 }
270 unlock, err := LockConfigFileEdits(path)
271 if err != nil {
272 return false, err
273 }
274 defer unlock()
275
276 changed, err := applyUserConfigUpgradesThroughV10Locked(path)
277 if err != nil {
278 return changed, err
279 }
280 upgraded, err := upgradeDeepSeekCatalogFileLocked(path, fileutil.AtomicWriteFile)
281 changed = changed || upgraded
282 if err != nil {
283 return changed, err
284 }
285 upgraded, err = upgradeMimoCatalogFileLocked(path, fileutil.AtomicWriteFile)
286 return changed || upgraded, err
287 }
288
289 func applyUserConfigUpgradesThroughV10Locked(path string) (bool, error) {
290 _, exists, err := statConfigPath(path)
291 if err != nil {
292 return false, err
293 }
294 if !exists {
295 return false, nil
296 }
297 var header Config
298 if _, err := decodeTOMLFile(path, &header); err != nil {
299 return false, fmt.Errorf("config %s: %w", path, err)
300 }
301 defaultVersion := Default().ConfigVersion
302 if header.ConfigVersion > defaultVersion {
303 return false, nil
304 }
305 if header.ConfigVersion >= openCodeGoUpgradeVersion {
306 resolved, _, err := statConfigPath(path)
307 if err != nil {
308 return false, err
309 }
310 if err := finalizeOpenCodeGoJournal(resolved); err != nil {
311 return false, err
312 }
313 }
314 classicDesktopLayout := strings.EqualFold(strings.TrimSpace(header.Desktop.LayoutStyle), "classic")
315 if header.ConfigVersion == defaultVersion && !classicDesktopLayout {
316 return repairProviderEndpointContractsOnStartup(path, false)
317 }
318 // Versions 7-9 commit the protocol upgrades and final version together,
319 // preserving the original bytes in one backup before any replacement.
320 if header.ConfigVersion >= deepSeekScheduledPricingConfigVersion && header.ConfigVersion < openCodeGoUpgradeVersion {
321 return upgradeOpenCodeGoAndRepairProviderEndpoints(path)
322 }
323 cfg := LoadForEdit(path)
324 changed := false
325 if classicDesktopLayout {
326 cfg.Desktop.LayoutStyle = "workbench"
327 changed = true
328 }
329 if header.ConfigVersion < deepSeekPricingResetConfigVersion {
330 resetOfficialProviderPricingDefaults(cfg)
331 changed = true
332 }
333 if shouldMarkWindowsBashSandboxDefaultUpgrade(header.ConfigVersion) {
334 resetWindowsBashSandboxDefaultOnUpgrade(cfg)
335 // Mark the Windows v4 migration even when the user was already on off,
336 // so a later manual enforce choice is not treated as the old template default.
337 changed = true
338 }
339 if header.ConfigVersion < retiredAutoPlanConfigVersion {
340 normalizeRetiredAutoPlan(cfg)
341 // Mark every older config as migrated even when Auto Plan was already off;
342 // the v5 renderer removes both retired keys so older binaries also observe
343 // the manual-only default after a downgrade.
344 changed = true
345 }
346 if header.ConfigVersion < billingSplitConfigVersion {
347 migrateBillingDisplayCurrency(cfg)
348 freezeProviderBillingCurrencies(cfg)
349 changed = true
350 }
351 if header.ConfigVersion < deepSeekScheduledPricingConfigVersion {
352 migrateDeepSeekScheduledPricingDefaults(cfg)
353 // Mark every older config, including custom-price configs, so their values
354 // remain user-owned on later startups instead of being reconsidered.
355 changed = true
356 }
357 if header.ConfigVersion < deepSeekChatDefaultConfigVersion {
358 restoreDeepSeekChatDefaults(cfg)
359 changed = true
360 }
361 if header.ConfigVersion < deepSeekOfficialChatUpgradeConfigVersion {
362 migrateOfficialDeepSeekChat(cfg)
363 changed = true
364 }
365 if !changed {
366 return repairProviderEndpointContractsOnStartup(path, false)
367 }
368 if header.ConfigVersion < openCodeGoUpgradeVersion {
369 cfg.ConfigVersion = deepSeekOfficialChatUpgradeConfigVersion
370 }
371 if err := cfg.SaveTo(path); err != nil {
372 return false, err
373 }
374 if header.ConfigVersion < openCodeGoUpgradeVersion {
375 if _, err := upgradeOpenCodeGoFileLocked(path); err != nil {
376 return false, err
377 }
378 }
379 return repairProviderEndpointContractsOnStartup(path, true)
380 }
381
382 func upgradeOpenCodeGoAndRepairProviderEndpoints(path string) (bool, error) {
383 upgraded, err := upgradeOpenCodeGoFileLocked(path)
384 if err != nil {
385 return false, err
386 }
387 return repairProviderEndpointContractsOnStartup(path, upgraded)
388 }
389
390 func repairProviderEndpointContractsOnStartup(path string, changed bool) (bool, error) {
391 repairs, err := repairProviderEndpointContractsFileLocked(path)
392 if err != nil {
393 return false, err
394 }
395 recordProviderEndpointRepairs(path, repairs)
396 return changed || len(repairs) > 0, nil
397 }
398
399 // ResetOfficialProviderPricingOnUpgrade is retained for older call sites.
400 func ResetOfficialProviderPricingOnUpgrade(path string) (bool, error) {
401 return ApplyUserConfigUpgradesOnStartup(path)
402 }
403
404 func shouldMarkWindowsBashSandboxDefaultUpgrade(fromVersion int) bool {
405 // Windows resolves every [sandbox].bash value to off at load time (see
406 // BashModeForGOOS), so no persisted rewrite is needed; explicit values stay
407 // readable and doctor reports them as ignored.
408 return false
409 }
410
411 func resetWindowsBashSandboxDefaultOnUpgrade(c *Config) {
412 if c == nil {
413 return
414 }
415 if strings.TrimSpace(c.Sandbox.Bash) != "enforce" {
416 return
417 }
418 c.Sandbox.Bash = "off"
419 }
420
421 func resetOfficialProviderPricingDefaults(c *Config) {
422 if c == nil {
423 return
424 }
425 for i := range c.Providers {
426 p := &c.Providers[i]
427 switch {
428 case officialProviderKind(p) == "deepseek":
429 resetDeepSeekOfficialPricing(p, deepSeekV4PricesForConfig(c))
430 }
431 }
432 }
433
434 func resetDeepSeekOfficialPricing(p *ProviderEntry, defaults map[string]*provider.Pricing) {
435 if p == nil {
436 return
437 }
438 p.Price = nil
439 if strings.TrimSpace(p.Model) != "" && len(p.Models) == 0 {
440 if price := defaults[strings.TrimSpace(p.Model)]; price != nil {
441 p.Price = clonePricing(price)
442 p.Prices = nil
443 return
444 }
445 }
446 if p.Prices == nil {
447 p.Prices = map[string]*provider.Pricing{}
448 }
449 for model, price := range defaults {
450 if p.HasModel(model) {
451 p.Prices[model] = clonePricing(price)
452 }
453 }
454 }
455
456 func legacyDeepSeekV4PricesCNY() map[string]*provider.Pricing {
457 return map[string]*provider.Pricing{
458 "deepseek-v4-flash": {CacheHit: 0.02, Input: 1, Output: 2, Currency: "¥"},
459 "deepseek-v4-pro": {CacheHit: 0.025, Input: 3, Output: 6, Currency: "¥"},
460 }
461 }
462
463 func legacyDeepSeekV4PricesUSD() map[string]*provider.Pricing {
464 return map[string]*provider.Pricing{
465 "deepseek-v4-flash": {CacheHit: 0.0028, Input: 0.14, Output: 0.28, Currency: "$"},
466 "deepseek-v4-pro": {CacheHit: 0.003625, Input: 0.435, Output: 0.87, Currency: "$"},
467 }
468 }
469
470 // augustDeepSeekV4Prices* are the 2026-08-17 table. They are recognized so a
471 // config that never left that generation is still refreshed to the current
472 // prices, and so it is not mistaken for a hand-edited custom rate.
473 func augustDeepSeekV4PricesCNY() map[string]*provider.Pricing {
474 return map[string]*provider.Pricing{
475 "deepseek-v4-flash": {CacheHit: 0.10, Input: 3, Output: 9, Currency: "¥"},
476 openai.OfficialDeepSeekVisionModel: {CacheHit: 0.10, Input: 3, Output: 9, Currency: "¥"},
477 "deepseek-v4-pro": {CacheHit: 0.30, Input: 9, Output: 27, Currency: "¥"},
478 }
479 }
480
481 func augustDeepSeekV4PricesUSD() map[string]*provider.Pricing {
482 return map[string]*provider.Pricing{
483 "deepseek-v4-flash": {CacheHit: 0.014, Input: 0.44, Output: 1.32, Currency: "$"},
484 openai.OfficialDeepSeekVisionModel: {CacheHit: 0.014, Input: 0.44, Output: 1.32, Currency: "$"},
485 "deepseek-v4-pro": {CacheHit: 0.044, Input: 1.32, Output: 3.96, Currency: "$"},
486 }
487 }
488
489 // migrateDeepSeekScheduledPricingDefaults replaces only the exact pre-August
490 // official defaults. Custom endpoints and any edited numeric rate remain intact.
491 func migrateDeepSeekScheduledPricingDefaults(c *Config) {
492 if c == nil {
493 return
494 }
495 for i := range c.Providers {
496 p := &c.Providers[i]
497 if officialProviderKind(p) != "deepseek" || !isOfficialDeepSeekBillingEndpoint(p) {
498 continue
499 }
500 currency := p.ProviderBillingCurrency()
501 if currency == "" {
502 currency = "USD"
503 }
504 legacy := legacyDeepSeekV4PricesUSD()
505 if normalizeDeepSeekPricingCurrency(currency) == "CNY" {
506 legacy = legacyDeepSeekV4PricesCNY()
507 }
508 migrate := func(model string, price *provider.Pricing) *provider.Pricing {
509 old := legacy[strings.TrimSpace(model)]
510 if !samePricingNormalizedCurrency(price, old) {
511 return price
512 }
513 return deepSeekV4PriceForModel(currency, model)
514 }
515 if p.Price != nil {
516 p.Price = migrate(p.Model, p.Price)
517 }
518 // Treat a multi-model official table atomically. A mixture of an old
519 // default row and a user-edited row is custom as a whole; partially
520 // rewriting it would leave an incoherent price book.
521 canMigrateTable := false
522 for model, price := range p.Prices {
523 old, known := legacy[strings.TrimSpace(model)]
524 if !known {
525 continue
526 }
527 canMigrateTable = true
528 if !samePricingNormalizedCurrency(price, old) {
529 canMigrateTable = false
530 break
531 }
532 }
533 if canMigrateTable {
534 for model, price := range p.Prices {
535 p.Prices[model] = migrate(model, price)
536 }
537 }
538 }
539 }
540
541 func isKnownDeepSeekOfficialPricing(model string, price *provider.Pricing) bool {
542 model = strings.TrimSpace(model)
543 if model == "" || price == nil {
544 return false
545 }
546 for _, prices := range []map[string]*provider.Pricing{
547 deepSeekV4PricesCNY(), deepSeekV4PricesUSD(),
548 augustDeepSeekV4PricesCNY(), augustDeepSeekV4PricesUSD(),
549 legacyDeepSeekV4PricesCNY(), legacyDeepSeekV4PricesUSD(),
550 } {
551 if samePricingNormalizedCurrency(price, prices[model]) {
552 return true
553 }
554 }
555 return false
556 }
557
558 // IsOfficialDeepSeekProvider reports whether an entry targets DeepSeek's
559 // official API endpoint. Desktop telemetry uses this after a regional-currency
560 // change so custom endpoints and rates stay untouched.
561 func IsOfficialDeepSeekProvider(p *ProviderEntry) bool {
562 return officialProviderKind(p) == "deepseek"
563 }
564
565 // IsKnownDeepSeekOfficialPricing reports whether price is one of Reasonix's
566 // built-in DeepSeek regional defaults for model.
567 func IsKnownDeepSeekOfficialPricing(model string, price *provider.Pricing) bool {
568 return isKnownDeepSeekOfficialPricing(model, price)
569 }
570
571 func samePricing(a, b *provider.Pricing) bool {
572 if a == nil || b == nil {
573 return false
574 }
575 return a.CacheHit == b.CacheHit && a.Input == b.Input && a.Output == b.Output && a.Currency == b.Currency
576 }
577
578 func samePricingNormalizedCurrency(a, b *provider.Pricing) bool {
579 if a == nil || b == nil {
580 return false
581 }
582 return a.CacheHit == b.CacheHit && a.Input == b.Input && a.Output == b.Output &&
583 normalizeDeepSeekPricingCurrency(a.Currency) == normalizeDeepSeekPricingCurrency(b.Currency)
584 }
585
585 lines GO