返回 DeepSeek-Reasonix
billing.go
根目录 / internal / doctor / billing.go
1 package doctor
2
3 import (
4 "fmt"
5 "strings"
6 "time"
7
8 "reasonix/internal/billing"
9 "reasonix/internal/config"
10 )
11
12 // BillingReport is the structured output of `reasonix doctor billing`.
13 type BillingReport struct {
14 DisplayCurrencyPref string `json:"display_currency_pref"`
15 DisplayCurrency string `json:"display_currency"`
16 FX FXReport `json:"fx"`
17 Providers []ProviderBillingInfo `json:"providers"`
18 CatalogNotes []string `json:"catalog_notes,omitempty"`
19 }
20
21 // FXReport is retained for JSON compatibility. Runtime FX is intentionally
22 // disabled; old readers may continue to expect the object.
23 type FXReport struct {
24 Enabled bool `json:"enabled"`
25 Source string `json:"source"`
26 CachePath string `json:"cache_path,omitempty"`
27 FetchedAt time.Time `json:"fetched_at,omitempty"`
28 Stale bool `json:"stale"`
29 HasTable bool `json:"has_table"`
30 Observation string `json:"latest_observation,omitempty"`
31 }
32
33 // ProviderBillingInfo describes one provider's frozen list-price currency.
34 type ProviderBillingInfo struct {
35 Name string `json:"name"`
36 Model string `json:"model,omitempty"`
37 BillingCurrency string `json:"billing_currency,omitempty"`
38 BillingMode string `json:"billing_mode,omitempty"`
39 PriceCurrency string `json:"price_currency,omitempty"`
40 PriceInput float64 `json:"price_input,omitempty"`
41 PriceOutput float64 `json:"price_output,omitempty"`
42 PriceCacheHit float64 `json:"price_cache_hit,omitempty"`
43 CatalogMatch bool `json:"catalog_match"`
44 CatalogSource string `json:"catalog_source,omitempty"`
45 Fingerprint string `json:"pricing_fingerprint,omitempty"`
46 CustomPrice bool `json:"custom_price"`
47 }
48
49 // CollectBilling builds a billing diagnostics report.
50 func CollectBilling(cfg *config.Config) BillingReport {
51 if cfg == nil {
52 cfg = config.Default()
53 }
54 rep := BillingReport{
55 DisplayCurrencyPref: prefLabel(cfg.DisplayCurrencyPref()),
56 DisplayCurrency: cfg.ExplicitDisplayCurrency(),
57 FX: FXReport{
58 Enabled: false,
59 Source: "disabled",
60 },
61 }
62
63 for i := range cfg.Providers {
64 p := &cfg.Providers[i]
65 info := ProviderBillingInfo{
66 Name: p.Name,
67 Model: p.Model,
68 BillingCurrency: p.ProviderBillingCurrency(),
69 BillingMode: p.ProviderBillingMode(),
70 }
71 price := p.PriceForModel(p.Model)
72 if price != nil {
73 info.PriceCurrency = billing.NormalizeCurrency(price.Currency)
74 info.PriceInput = price.Input
75 info.PriceOutput = price.Output
76 info.PriceCacheHit = price.CacheHit
77 card := billing.RateCard{
78 CacheHit: price.CacheHit, Input: price.Input, Output: price.Output,
79 Currency: info.PriceCurrency,
80 }
81 info.Fingerprint = billing.PricingFingerprint(card)
82 providerKind := officialKindForBilling(p)
83 if entry, ok := billing.MatchesCatalog(providerKind, p.Model, card); ok {
84 info.CatalogMatch = true
85 info.CatalogSource = entry.DocURL
86 } else if price != nil {
87 info.CustomPrice = !info.CatalogMatch
88 }
89 }
90 rep.Providers = append(rep.Providers, info)
91 }
92 rep.CatalogNotes = []string{
93 "User-custom prices always win over the official catalog.",
94 "MiMo Token Plan costs are pay-as-you-go equivalents, not plan invoices.",
95 "Runtime FX is disabled; only identity and official regional rate-card estimates are used.",
96 "Switching display_currency never rewrites provider billing_currency or list prices.",
97 }
98 return rep
99 }
100
101 func prefLabel(pref string) string {
102 if pref == "" {
103 return "auto"
104 }
105 return pref
106 }
107
108 func officialKindForBilling(p *config.ProviderEntry) string {
109 if p == nil {
110 return ""
111 }
112 name := strings.ToLower(p.Name)
113 base := strings.ToLower(p.BaseURL)
114 switch {
115 case strings.Contains(base, "deepseek") || strings.Contains(name, "deepseek"):
116 return "deepseek"
117 case strings.Contains(base, "longcat") || strings.Contains(name, "longcat"):
118 return "longcat"
119 case strings.Contains(base, "mimo") || strings.Contains(name, "mimo"):
120 return "mimo"
121 default:
122 return name
123 }
124 }
125
126 // RenderBillingText formats a human-readable billing doctor report.
127 func RenderBillingText(r BillingReport) string {
128 var b strings.Builder
129 fmt.Fprintf(&b, "reasonix doctor billing\n")
130 fmt.Fprintf(&b, " display preference: %s\n", r.DisplayCurrencyPref)
131 fmt.Fprintf(&b, " display resolved: %s\n", r.DisplayCurrency)
132 fmt.Fprintf(&b, " fx source: %s\n", r.FX.Source)
133 fmt.Fprintf(&b, " fx enabled: %v\n", r.FX.Enabled)
134 fmt.Fprintf(&b, " auto strategy: wallet currency, then pricing basis\n")
135 b.WriteString("\n providers:\n")
136 for _, p := range r.Providers {
137 fmt.Fprintf(&b, " - %s model=%s billing=%s mode=%s", p.Name, p.Model, p.BillingCurrency, p.BillingMode)
138 if p.PriceCurrency != "" {
139 fmt.Fprintf(&b, " price=%s/%.4g/%.4g", p.PriceCurrency, p.PriceInput, p.PriceOutput)
140 }
141 if p.CatalogMatch {
142 fmt.Fprintf(&b, " [official: %s]", p.CatalogSource)
143 } else if p.CustomPrice {
144 b.WriteString(" [custom price protected]")
145 }
146 b.WriteByte('\n')
147 if p.Fingerprint != "" {
148 fmt.Fprintf(&b, " fingerprint=%s\n", p.Fingerprint)
149 }
150 }
151 if len(r.CatalogNotes) > 0 {
152 b.WriteString("\n notes:\n")
153 for _, n := range r.CatalogNotes {
154 fmt.Fprintf(&b, " - %s\n", n)
155 }
156 }
157 return b.String()
158 }
159
159 lines GO