返回 DeepSeek-Reasonix
pricing.go
根目录 / internal / config / pricing.go
1 package config
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/provider"
8 )
9
10 func deepSeekV4FlashPriceCNY() *provider.Pricing {
11 return &provider.Pricing{CacheHit: 0.02, Input: 1, Output: 2, Currency: "¥"}
12 }
13
14 func deepSeekV4ProPriceCNY() *provider.Pricing {
15 return &provider.Pricing{CacheHit: 0.025, Input: 3, Output: 6, Currency: "¥"}
16 }
17
18 func deepSeekV4PricesCNY() map[string]*provider.Pricing {
19 return map[string]*provider.Pricing{
20 "deepseek-v4-flash": deepSeekV4FlashPriceCNY(),
21 "deepseek-v4-pro": deepSeekV4ProPriceCNY(),
22 }
23 }
24
25 func deepSeekV4FlashPriceUSD() *provider.Pricing {
26 return &provider.Pricing{CacheHit: 0.0028, Input: 0.14, Output: 0.28, Currency: "$"}
27 }
28
29 func deepSeekV4ProPriceUSD() *provider.Pricing {
30 return &provider.Pricing{CacheHit: 0.003625, Input: 0.435, Output: 0.87, Currency: "$"}
31 }
32
33 func deepSeekV4PricesUSD() map[string]*provider.Pricing {
34 return map[string]*provider.Pricing{
35 "deepseek-v4-flash": deepSeekV4FlashPriceUSD(),
36 "deepseek-v4-pro": deepSeekV4ProPriceUSD(),
37 }
38 }
39
40 // DeepSeekV4PricesForCurrency returns the official regional price table.
41 // Persisted custom prices still win; this is only used for built-in templates
42 // and known-default refreshes.
43 func DeepSeekV4PricesForCurrency(currency string) map[string]*provider.Pricing {
44 if normalizeDeepSeekPricingCurrency(currency) == "CNY" {
45 return deepSeekV4PricesCNY()
46 }
47 return deepSeekV4PricesUSD()
48 }
49
50 // DeepSeekV4PricesForLanguage is retained for compatibility with older call
51 // sites. New desktop code should pass an explicit pricing currency.
52 func DeepSeekV4PricesForLanguage(lang string) map[string]*provider.Pricing {
53 if normalizeDeepSeekPricingLanguage(lang) == "zh" {
54 return DeepSeekV4PricesForCurrency("CNY")
55 }
56 return DeepSeekV4PricesForCurrency("USD")
57 }
58
59 func deepSeekV4PricesForConfig(c *Config) map[string]*provider.Pricing {
60 return DeepSeekV4PricesForCurrency(c.DeepSeekOfficialPricingCurrency())
61 }
62
63 func deepSeekV4PriceForModel(currency, model string) *provider.Pricing {
64 return clonePricing(DeepSeekV4PricesForCurrency(currency)[strings.TrimSpace(model)])
65 }
66
67 // DeepSeekOfficialPricingCurrency resolves the regional pricing table used for
68 // official DeepSeek defaults. An explicit desktop currency wins; auto follows
69 // desktop language, then CLI language, and finally defaults to USD.
70 func (c *Config) DeepSeekOfficialPricingCurrency() string {
71 if c != nil {
72 if currency := c.DesktopCurrency(); currency != "" {
73 return currency
74 }
75 if normalizeDeepSeekPricingLanguage(c.Desktop.Language) == "zh" {
76 return "CNY"
77 }
78 if normalizeDeepSeekPricingLanguage(c.Desktop.Language) == "en" {
79 return "USD"
80 }
81 if normalizeDeepSeekPricingLanguage(c.Language) == "zh" {
82 return "CNY"
83 }
84 }
85 return "USD"
86 }
87
88 // DesktopPricingFollowsDetectedLocale reports whether the desktop may supply
89 // its browser/OS locale as the official pricing region. Explicit currency or
90 // language preferences always win.
91 func (c *Config) DesktopPricingFollowsDetectedLocale() bool {
92 return c != nil && c.DesktopCurrency() == "" && c.DesktopLanguage() == "" && c.ResponseLanguage() == "auto"
93 }
94
95 // ApplyRuntimeAutoPricingCurrency applies a frontend-detected pricing region
96 // to this in-memory config only. Callers must not persist the resulting copy,
97 // because doing so would turn the user's Auto choice into an explicit region.
98 func (c *Config) ApplyRuntimeAutoPricingCurrency(currency string) {
99 if !c.DesktopPricingFollowsDetectedLocale() {
100 return
101 }
102 normalized := normalizeDeepSeekPricingCurrency(currency)
103 if normalized == "" {
104 return
105 }
106 c.Desktop.Currency = normalized
107 applyDeepSeekOfficialDefaultPricingWithOverride(c, false)
108 }
109
110 // DeepSeekOfficialPricingLanguage is retained for older settings/template call
111 // sites that still express the pricing region as a language.
112 func (c *Config) DeepSeekOfficialPricingLanguage() string {
113 if c.DeepSeekOfficialPricingCurrency() == "CNY" {
114 return "zh"
115 }
116 return "en"
117 }
118
119 func normalizeDeepSeekPricingCurrency(currency string) string {
120 switch strings.ToUpper(strings.TrimSpace(currency)) {
121 case "CNY", "RMB", "CNH", "¥", "¥":
122 return "CNY"
123 case "USD", "$", "US$":
124 return "USD"
125 default:
126 return ""
127 }
128 }
129
130 func normalizeDeepSeekPricingLanguage(lang string) string {
131 switch strings.ToLower(strings.TrimSpace(lang)) {
132 case "zh", "zh-cn", "zh-hans", "cn", "chinese", "中文", "zh-tw", "zh-hant", "zh-hk", "zh-mo":
133 return "zh"
134 case "en", "en-us", "en-gb", "english":
135 return "en"
136 default:
137 return ""
138 }
139 }
140
141 // ApplyDeepSeekOfficialDefaultPricing refreshes built-in/official DeepSeek
142 // prices that still match known official defaults. Custom user prices are left
143 // untouched.
144 func (c *Config) ApplyDeepSeekOfficialDefaultPricing() {
145 applyDeepSeekOfficialDefaultPricing(c)
146 }
147
148 func applyDeepSeekOfficialDefaultPricing(c *Config) {
149 applyDeepSeekOfficialDefaultPricingWithOverride(c, c != nil && c.DesktopCurrency() != "")
150 }
151
152 func applyDeepSeekOfficialDefaultPricingWithOverride(c *Config, overridePersisted bool) {
153 if c == nil {
154 return
155 }
156 currency := c.DeepSeekOfficialPricingCurrency()
157 for i := range c.Providers {
158 p := &c.Providers[i]
159 if officialProviderKind(p) != "deepseek" {
160 continue
161 }
162 if isKnownDeepSeekOfficialPricing(p.Model, p.Price) && (overridePersisted || p.persistedOfficialCurrency == "") {
163 p.Price = deepSeekV4PriceForModel(currency, p.Model)
164 }
165 for model, price := range p.Prices {
166 if isKnownDeepSeekOfficialPricing(model, price) && (overridePersisted || p.persistedOfficialCurrency == "") {
167 p.Prices[model] = deepSeekV4PriceForModel(currency, model)
168 }
169 }
170 }
171 }
172
173 // markPersistedDeepSeekOfficialPricing records which recognized regional
174 // prices came from TOML. Auto locale refreshes preserve those values, while an
175 // explicit currency choice can still replace them with the selected table.
176 func markPersistedDeepSeekOfficialPricing(c *Config) {
177 if c == nil {
178 return
179 }
180 for i := range c.Providers {
181 p := &c.Providers[i]
182 if officialProviderKind(p) != "deepseek" {
183 continue
184 }
185 p.persistedOfficialCurrency = completeDeepSeekOfficialPricingCurrency(p)
186 if c.ConfigVersion >= Default().ConfigVersion && isStandardDeepSeekProviderTemplate(p) {
187 p.persistedOfficialCurrency = ""
188 }
189 }
190 }
191
192 func isStandardDeepSeekProviderTemplate(p *ProviderEntry) bool {
193 if p == nil || officialProviderKind(p) != "deepseek" {
194 return false
195 }
196 return strings.TrimSpace(p.APIKeyEnv) == "DEEPSEEK_API_KEY" &&
197 strings.TrimSpace(p.BalanceURL) == "https://api.deepseek.com/user/balance" &&
198 p.ContextWindow == 1_000_000
199 }
200
201 func completeDeepSeekOfficialPricingCurrency(p *ProviderEntry) string {
202 if p == nil {
203 return ""
204 }
205 models := p.ModelList()
206 if len(models) == 1 && isKnownDeepSeekOfficialPricing(models[0], p.Price) {
207 return normalizeDeepSeekPricingCurrency(p.Price.Currency)
208 }
209 if len(models) == 0 || p.Price != nil {
210 return ""
211 }
212 currency := ""
213 for _, model := range models {
214 price := p.Prices[strings.TrimSpace(model)]
215 if !isKnownDeepSeekOfficialPricing(model, price) {
216 return ""
217 }
218 nextCurrency := normalizeDeepSeekPricingCurrency(price.Currency)
219 if nextCurrency == "" {
220 return ""
221 }
222 if currency == "" {
223 currency = nextCurrency
224 } else if currency != nextCurrency {
225 return ""
226 }
227 }
228 return currency
229 }
230
231 func mimoV25ProPrice() *provider.Pricing {
232 return &provider.Pricing{CacheHit: 0.025, Input: 3, Output: 6, Currency: "¥"}
233 }
234
235 func mimoV25Price() *provider.Pricing {
236 return &provider.Pricing{CacheHit: 0.02, Input: 1, Output: 2, Currency: "¥"}
237 }
238
239 func mimoV2FlashPrice() *provider.Pricing {
240 return &provider.Pricing{CacheHit: 0.07, Input: 0.70, Output: 2.10, Currency: "¥"}
241 }
242
243 func mimoDomesticPrices(models []string) map[string]*provider.Pricing {
244 prices := map[string]*provider.Pricing{}
245 for _, model := range models {
246 switch strings.TrimSpace(model) {
247 case "mimo-v2.5-pro", "mimo-v2-pro":
248 prices[model] = mimoV25ProPrice()
249 case "mimo-v2.5", "mimo-v2-omni":
250 prices[model] = mimoV25Price()
251 case "mimo-v2-flash":
252 prices[model] = mimoV2FlashPrice()
253 }
254 }
255 return prices
256 }
257
258 func longCat20Price() *provider.Pricing {
259 return &provider.Pricing{CacheHit: 0.04, Input: 2, Output: 8, Currency: "¥"}
260 }
261
262 func longCat20Prices(models []string) map[string]*provider.Pricing {
263 prices := map[string]*provider.Pricing{}
264 for _, model := range models {
265 switch strings.TrimSpace(model) {
266 case "LongCat-2.0":
267 prices[model] = longCat20Price()
268 }
269 }
270 return prices
271 }
272
273 const (
274 deepSeekPricingResetConfigVersion = 3
275 windowsBashSandboxDefaultConfigVersion = 4
276 retiredAutoPlanConfigVersion = 5
277 )
278
279 // ApplyUserConfigUpgradesOnStartup applies one-time startup migrations. It
280 // intentionally runs from the desktop and CLI startup paths, not every config
281 // Load(), so user edits made after the upgrade are preserved.
282 func ApplyUserConfigUpgradesOnStartup(path string) (bool, error) {
283 path = strings.TrimSpace(path)
284 if path == "" {
285 return false, nil
286 }
287 unlock, err := LockConfigFileEdits(path)
288 if err != nil {
289 return false, err
290 }
291 defer unlock()
292
293 _, exists, err := statConfigPath(path)
294 if err != nil {
295 return false, err
296 }
297 if !exists {
298 return false, nil
299 }
300 var header Config
301 if _, err := decodeTOMLFile(path, &header); err != nil {
302 return false, fmt.Errorf("config %s: %w", path, err)
303 }
304 if header.ConfigVersion >= Default().ConfigVersion {
305 return false, nil
306 }
307 cfg := LoadForEdit(path)
308 changed := false
309 if header.ConfigVersion < deepSeekPricingResetConfigVersion {
310 resetOfficialProviderPricingDefaults(cfg)
311 changed = true
312 }
313 if shouldMarkWindowsBashSandboxDefaultUpgrade(header.ConfigVersion) {
314 resetWindowsBashSandboxDefaultOnUpgrade(cfg)
315 // Mark the Windows v4 migration even when the user was already on off,
316 // so a later manual enforce choice is not treated as the old template default.
317 changed = true
318 }
319 if header.ConfigVersion < retiredAutoPlanConfigVersion {
320 normalizeRetiredAutoPlan(cfg)
321 // Mark every older config as migrated even when Auto Plan was already off;
322 // the v5 renderer removes both retired keys so older binaries also observe
323 // the manual-only default after a downgrade.
324 changed = true
325 }
326 if !changed {
327 return false, nil
328 }
329 cfg.ConfigVersion = Default().ConfigVersion
330 if err := cfg.SaveTo(path); err != nil {
331 return false, err
332 }
333 return true, nil
334 }
335
336 // ResetOfficialProviderPricingOnUpgrade is retained for older call sites.
337 func ResetOfficialProviderPricingOnUpgrade(path string) (bool, error) {
338 return ApplyUserConfigUpgradesOnStartup(path)
339 }
340
341 func shouldMarkWindowsBashSandboxDefaultUpgrade(fromVersion int) bool {
342 return runtimeGOOS == "windows" && fromVersion < windowsBashSandboxDefaultConfigVersion
343 }
344
345 func resetWindowsBashSandboxDefaultOnUpgrade(c *Config) {
346 if c == nil {
347 return
348 }
349 if strings.TrimSpace(c.Sandbox.Bash) != "enforce" {
350 return
351 }
352 c.Sandbox.Bash = "off"
353 }
354
355 func resetOfficialProviderPricingDefaults(c *Config) {
356 if c == nil {
357 return
358 }
359 for i := range c.Providers {
360 p := &c.Providers[i]
361 switch {
362 case officialProviderKind(p) == "deepseek":
363 resetDeepSeekOfficialPricing(p, deepSeekV4PricesForConfig(c))
364 }
365 }
366 }
367
368 func resetDeepSeekOfficialPricing(p *ProviderEntry, defaults map[string]*provider.Pricing) {
369 if p == nil {
370 return
371 }
372 p.Price = nil
373 if strings.TrimSpace(p.Model) != "" && len(p.Models) == 0 {
374 if price := defaults[strings.TrimSpace(p.Model)]; price != nil {
375 p.Price = clonePricing(price)
376 p.Prices = nil
377 return
378 }
379 }
380 if p.Prices == nil {
381 p.Prices = map[string]*provider.Pricing{}
382 }
383 for model, price := range defaults {
384 if p.HasModel(model) {
385 p.Prices[model] = clonePricing(price)
386 }
387 }
388 }
389
390 func isKnownDeepSeekOfficialPricing(model string, price *provider.Pricing) bool {
391 model = strings.TrimSpace(model)
392 if model == "" || price == nil {
393 return false
394 }
395 for _, prices := range []map[string]*provider.Pricing{deepSeekV4PricesCNY(), deepSeekV4PricesUSD()} {
396 if samePricing(price, prices[model]) {
397 return true
398 }
399 }
400 return false
401 }
402
403 // IsOfficialDeepSeekProvider reports whether an entry targets DeepSeek's
404 // official API endpoint. Desktop telemetry uses this after a regional-currency
405 // change so custom endpoints and rates stay untouched.
406 func IsOfficialDeepSeekProvider(p *ProviderEntry) bool {
407 return officialProviderKind(p) == "deepseek"
408 }
409
410 // IsKnownDeepSeekOfficialPricing reports whether price is one of Reasonix's
411 // built-in DeepSeek regional defaults for model.
412 func IsKnownDeepSeekOfficialPricing(model string, price *provider.Pricing) bool {
413 return isKnownDeepSeekOfficialPricing(model, price)
414 }
415
416 func samePricing(a, b *provider.Pricing) bool {
417 if a == nil || b == nil {
418 return false
419 }
420 return a.CacheHit == b.CacheHit && a.Input == b.Input && a.Output == b.Output && a.Currency == b.Currency
421 }
422
422 lines GO