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