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