返回 DeepSeek-Reasonix
billing_upgrade_test.go
根目录 / internal / config / billing_upgrade_test.go
1 package config
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9
10 "reasonix/internal/billing"
11 "reasonix/internal/provider"
12 )
13
14 // A config still holding the August Flash anchor must be quoted at the
15 // September rate: the cost a user sees is the vendor's current price, not the
16 // one their config was written with.
17 func TestStaleDeepSeekAnchorQuotesAtTheLiveRate(t *testing.T) {
18 p := &ProviderEntry{
19 Kind: "openai", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash",
20 Price: clonePricing(augustDeepSeekV4PricesCNY()["deepseek-v4-flash"]), BillingCurrency: "CNY",
21 }
22 ctx := p.PricingContextForModel(p.Model)
23 if ctx.ScheduleID != billing.ScheduleDeepSeekV4September2026 {
24 t.Fatalf("stale anchor bound schedule = %q, want the live schedule", ctx.ScheduleID)
25 }
26 // Monday 2026-09-14T06:00Z is inside the 06:00-10:00 UTC peak window.
27 q := billing.BuildQuote(billing.QuoteInput{
28 Usage: billing.UsageTokens{CompletionTokens: 1_000_000},
29 Rates: p.RateCardForModel(p.Model),
30 OccurredAt: time.Date(2026, 9, 14, 6, 0, 0, 0, time.UTC),
31 BillingMode: billing.BillingModePAYG, ProviderKind: "deepseek", ModelID: p.Model,
32 ScheduleID: ctx.ScheduleID, CatalogSource: ctx.CatalogSource,
33 })
34 if q.RateBand != billing.RateBandPeak || q.Original.Amount != "8" {
35 t.Fatalf("quote = %+v, want the September peak output rate of 8 CNY", q)
36 }
37 }
38
39 func TestBillingSplitUpgradeV5ToV6FreezesProviderCurrency(t *testing.T) {
40 dir := t.TempDir()
41 path := filepath.Join(dir, "config.toml")
42 body := `config_version = 5
43 [desktop]
44 currency = "CNY"
45 language = "zh"
46
47 [[providers]]
48 name = "deepseek-flash"
49 kind = "openai"
50 base_url = "https://api.deepseek.com"
51 model = "deepseek-v4-flash"
52 price = { cache_hit = 0.0028, input = 0.14, output = 0.28, currency = "$" }
53 `
54 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
55 t.Fatal(err)
56 }
57 changed, err := ApplyUserConfigUpgradesOnStartup(path)
58 if err != nil {
59 t.Fatal(err)
60 }
61 if !changed {
62 t.Fatal("expected upgrade rewrite")
63 }
64 raw, err := os.ReadFile(path)
65 if err != nil {
66 t.Fatal(err)
67 }
68 text := string(raw)
69 if !strings.Contains(text, "config_version = 10") {
70 t.Fatalf("missing v10:\n%s", text)
71 }
72 if !strings.Contains(text, "display_currency") && !strings.Contains(text, `currency = "CNY"`) {
73 t.Fatalf("display currency not migrated:\n%s", text)
74 }
75 cfg := LoadForEdit(path)
76 if got := cfg.DisplayCurrencyPref(); got != "CNY" {
77 t.Fatalf("display pref = %q", got)
78 }
79 flash, ok := cfg.Provider("deepseek-flash")
80 if !ok {
81 t.Fatal("missing flash")
82 }
83 // List price must stay USD official; display is CNY.
84 if flash.Price == nil || flash.Price.Currency != "$" || flash.Price.CacheHit != 0.006 || flash.Price.Input != 0.3 || flash.Price.Output != 1.2 {
85 t.Fatalf("list price rewritten: %+v", flash.Price)
86 }
87 if got := flash.ProviderBillingCurrency(); got != "USD" {
88 t.Fatalf("billing_currency = %q, want USD frozen from price", got)
89 }
90 }
91
92 func TestDeepSeekScheduledPricingUpgradeLeavesMixedOfficialTableUntouched(t *testing.T) {
93 path := filepath.Join(t.TempDir(), "config.toml")
94 body := `config_version = 6
95 [[providers]]
96 name = "deepseek"
97 kind = "responses"
98 base_url = "https://api.deepseek.com"
99 models = ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v5-future"]
100 billing_currency = "USD"
101 supported_efforts = ["disabled", "high"]
102 prices = { deepseek-v4-flash = { cache_hit = 0.0028, input = 0.14, output = 0.28, currency = "$" }, deepseek-v4-pro = { cache_hit = 9, input = 9, output = 9, currency = "$" }, deepseek-v5-future = { cache_hit = 8, input = 8, output = 8, currency = "$" } }
103 `
104 if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
105 t.Fatal(err)
106 }
107 if changed, err := ApplyUserConfigUpgradesOnStartup(path); err != nil || !changed {
108 t.Fatalf("upgrade changed=%v err=%v", changed, err)
109 }
110 cfg := LoadForEdit(path)
111 p, _ := cfg.Provider("deepseek")
112 if p.Prices["deepseek-v4-flash"].Input != 0.14 || p.Prices["deepseek-v4-pro"].Input != 9 || p.Prices["deepseek-v5-future"].Input != 8 {
113 t.Fatalf("mixed/custom table changed: %+v", p.Prices)
114 }
115 if strings.Join(p.SupportedEfforts, ",") != "disabled,high" {
116 t.Fatalf("custom supported_efforts changed: %v", p.SupportedEfforts)
117 }
118 }
119
120 func TestDeepSeekPricingContextSchedulesOnlyTrustedProtocolsAndAnchor(t *testing.T) {
121 anchor := deepSeekV4FlashPriceCNY()
122 for _, endpoint := range []struct{ kind, baseURL string }{
123 {kind: "openai", baseURL: "https://api.deepseek.com"},
124 {kind: "responses", baseURL: "https://api.deepseek.com"},
125 {kind: "anthropic", baseURL: "https://api.deepseek.com/anthropic"},
126 } {
127 p := &ProviderEntry{Kind: endpoint.kind, BaseURL: endpoint.baseURL, Model: "deepseek-v4-flash", Price: clonePricing(anchor), BillingCurrency: "CNY"}
128 if got := p.PricingContextForModel(p.Model).ScheduleID; got != billing.ScheduleDeepSeekV4September2026 {
129 t.Fatalf("%s schedule = %q", endpoint.kind, got)
130 }
131 // A config that has not been re-saved since the August price cut still
132 // proves it is an untouched official row, so it must bind the live
133 // schedule and be quoted at today's rate rather than the old one.
134 p.Price = clonePricing(augustDeepSeekV4PricesCNY()["deepseek-v4-flash"])
135 if got := p.PricingContextForModel(p.Model).ScheduleID; got != billing.ScheduleDeepSeekV4September2026 {
136 t.Fatalf("%s stale-anchor schedule = %q", endpoint.kind, got)
137 }
138 }
139 for _, p := range []*ProviderEntry{
140 {Kind: "ollama", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash", Price: clonePricing(anchor), BillingCurrency: "CNY"},
141 {Kind: "openai", BaseURL: "https://gateway.example/v1", Model: "deepseek-v4-flash", Price: clonePricing(anchor), BillingCurrency: "CNY"},
142 {Kind: "openai", BaseURL: "https://api.deepseek.com/custom", Model: "deepseek-v4-flash", Price: clonePricing(anchor), BillingCurrency: "CNY"},
143 {Kind: "anthropic", BaseURL: "https://api.deepseek.com/custom", Model: "deepseek-v4-flash", Price: clonePricing(anchor), BillingCurrency: "CNY"},
144 {Kind: "openai", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash", Price: &provider.Pricing{CacheHit: 9, Input: 9, Output: 9, Currency: "CNY"}, BillingCurrency: "CNY"},
145 } {
146 if got := p.PricingContextForModel(p.Model).ScheduleID; got != "" {
147 t.Fatalf("untrusted provider scheduled: %+v => %q", p, got)
148 }
149 }
150 }
151
152 func TestDeepSeekScheduledPricingUpgradeV6ToV7(t *testing.T) {
153 path := filepath.Join(t.TempDir(), "config.toml")
154 body := `config_version = 6
155 [[providers]]
156 name = "deepseek"
157 kind = "anthropic"
158 base_url = "https://api.deepseek.com/anthropic"
159 models = ["deepseek-v4-flash", "deepseek-v4-pro"]
160 billing_currency = "CNY"
161 prices = { deepseek-v4-flash = { cache_hit = 0.02, input = 1, output = 2, currency = "CNY" }, deepseek-v4-pro = { cache_hit = 0.025, input = 3, output = 6, currency = "¥" } }
162
163 [[providers]]
164 name = "custom-endpoint"
165 kind = "openai"
166 base_url = "https://gateway.example/v1"
167 model = "deepseek-v4-flash"
168 billing_currency = "USD"
169 price = { cache_hit = 0.0028, input = 0.14, output = 0.28, currency = "$" }
170
171 [[providers]]
172 name = "custom-price"
173 kind = "openai"
174 base_url = "https://api.deepseek.com"
175 model = "deepseek-v4-flash"
176 billing_currency = "USD"
177 price = { cache_hit = 9, input = 9, output = 9, currency = "$" }
178
179 [[providers]]
180 name = "custom-path"
181 kind = "openai"
182 base_url = "https://api.deepseek.com/custom"
183 model = "deepseek-v4-flash"
184 billing_currency = "USD"
185 price = { cache_hit = 0.0028, input = 0.14, output = 0.28, currency = "$" }
186 `
187 if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
188 t.Fatal(err)
189 }
190 changed, err := ApplyUserConfigUpgradesOnStartup(path)
191 if err != nil || !changed {
192 t.Fatalf("upgrade changed=%v err=%v", changed, err)
193 }
194 cfg := LoadForEdit(path)
195 official, _ := cfg.Provider("deepseek")
196 if got := official.Prices["deepseek-v4-flash"]; got == nil || got.CacheHit != 0.04 || got.Input != 2 || got.Output != 8 {
197 t.Fatalf("flash = %+v", got)
198 }
199 if got := official.Prices["deepseek-v4-pro"]; got == nil || got.CacheHit != 0.30 || got.Input != 9 || got.Output != 27 {
200 t.Fatalf("pro = %+v", got)
201 }
202 customEndpoint, _ := cfg.Provider("custom-endpoint")
203 if customEndpoint.Price.Input != 0.14 {
204 t.Fatalf("custom endpoint changed: %+v", customEndpoint.Price)
205 }
206 customPrice, _ := cfg.Provider("custom-price")
207 if customPrice.Price.Input != 9 {
208 t.Fatalf("custom price changed: %+v", customPrice.Price)
209 }
210 customPath, _ := cfg.Provider("custom-path")
211 if customPath.Price.Input != 0.14 {
212 t.Fatalf("custom path changed: %+v", customPath.Price)
213 }
214 if again, err := ApplyUserConfigUpgradesOnStartup(path); err != nil || again {
215 t.Fatalf("second upgrade changed=%v err=%v", again, err)
216 }
217 }
218
218 lines GO