返回 DeepSeek-Reasonix
mimo_catalog_upgrade.go
根目录 / internal / config / mimo_catalog_upgrade.go
1 package config
2
3 import (
4 "fmt"
5 "os"
6 "reflect"
7 "slices"
8 "strings"
9
10 "github.com/BurntSushi/toml"
11
12 fileencoding "reasonix/internal/fileutil/encoding"
13 )
14
15 const mimoCatalogUpgradeVersion = 12
16
17 type mimoCatalogProviderUpdate struct {
18 models []string
19 visionModels []string
20 prices map[string]map[string]any
21 }
22
23 // The caller holds the config edit lock. Model additions and the migration
24 // marker are committed together so a model the user later removes stays gone.
25 func upgradeMimoCatalogFileLocked(path string, write func(string, []byte, os.FileMode) error) (bool, error) {
26 resolved, exists, err := statConfigPath(path)
27 if err != nil || !exists {
28 return false, err
29 }
30 raw, err := os.ReadFile(resolved)
31 if err != nil {
32 return false, err
33 }
34 info, err := os.Stat(resolved)
35 if err != nil {
36 return false, err
37 }
38 encoding, data := fileencoding.Detect(raw)
39 body := string(fileencoding.Decode(data, encoding))
40 crlf := strings.Contains(body, "\r\n")
41 if crlf {
42 body = strings.ReplaceAll(body, "\r\n", "\n")
43 }
44 next, changed, err := rewriteMimoCatalogUpgrade(body)
45 if err != nil || !changed {
46 return false, err
47 }
48 if crlf {
49 next = strings.ReplaceAll(next, "\n", "\r\n")
50 }
51 encoded, err := fileencoding.Encode(next, encoding)
52 if err != nil {
53 return false, fmt.Errorf("commit MiMo catalog upgrade: %w", err)
54 }
55 if err := write(resolved, encoded, info.Mode().Perm()); err != nil {
56 return false, fmt.Errorf("commit MiMo catalog upgrade: %w", err)
57 }
58 return true, nil
59 }
60
61 func rewriteMimoCatalogUpgrade(body string) (string, bool, error) {
62 var cfg Config
63 if _, err := toml.Decode(body, &cfg); err != nil {
64 return body, false, err
65 }
66 if cfg.ConfigVersion >= mimoCatalogUpgradeVersion {
67 return body, false, nil
68 }
69 var expected map[string]any
70 if _, err := toml.Decode(body, &expected); err != nil {
71 return body, false, err
72 }
73 expected["config_version"] = int64(mimoCatalogUpgradeVersion)
74 providerTables, _ := deepSeekCatalogDocumentValue(expected["providers"]).([]any)
75 updates := make(map[int]mimoCatalogProviderUpdate)
76 for i := range cfg.Providers {
77 p := &cfg.Providers[i]
78 if !isOfficialMimoCatalogUpgradeEntry(p) || len(p.ModelList()) == 0 ||
79 (!p.HasModel("mimo-v2.5-pro") && !p.HasModel("mimo-v2.5")) {
80 continue
81 }
82 update := mimoCatalogProviderUpdate{models: append([]string(nil), p.ModelList()...)}
83 for _, model := range []string{"mimo-v2.6-pro", "mimo-v2.6-flash"} {
84 if !p.HasModel(model) {
85 update.models = append(update.models, model)
86 }
87 }
88 if stringSlicesEqual(p.VisionModels, []string{"mimo-v2.5"}) {
89 update.visionModels = append(append([]string(nil), p.VisionModels...), "mimo-v2.6-pro", "mimo-v2.6-flash")
90 }
91 missingPrices := make([]string, 0, 2)
92 for _, model := range []string{"mimo-v2.6-pro", "mimo-v2.6-flash"} {
93 if p.Prices[model] == nil {
94 missingPrices = append(missingPrices, model)
95 }
96 }
97 update.prices = mimoCatalogPriceDocuments(missingPrices)
98 if stringSlicesEqual(update.models, p.ModelList()) && update.visionModels == nil && len(update.prices) == 0 {
99 continue
100 }
101 updates[i] = update
102
103 fields, ok := providerTables[i].(map[string]any)
104 if !ok {
105 return body, false, fmt.Errorf("MiMo catalog upgrade cannot map provider %d", i)
106 }
107 fields["models"] = stringSliceDocument(update.models)
108 if update.visionModels != nil {
109 fields["vision_models"] = stringSliceDocument(update.visionModels)
110 }
111 if len(update.prices) > 0 {
112 table, _ := fields["prices"].(map[string]any)
113 if table == nil {
114 table = make(map[string]any)
115 fields["prices"] = table
116 }
117 for model, price := range update.prices {
118 encoded, err := rawTOMLValue(price)
119 if err != nil {
120 return body, false, err
121 }
122 var rendered map[string]any
123 if _, err := toml.Decode("price = "+encoded, &rendered); err != nil {
124 return body, false, err
125 }
126 table[model] = rendered["price"]
127 }
128 }
129 }
130 next := body
131 var err error
132 if len(updates) > 0 {
133 next, err = rewriteMimoCatalogProviders(next, len(cfg.Providers), updates)
134 if err != nil {
135 return body, false, err
136 }
137 }
138 next, err = rawTOMLSet(next, []string{"config_version"}, mimoCatalogUpgradeVersion)
139 if err != nil {
140 return body, false, err
141 }
142 var actual map[string]any
143 if _, err := toml.Decode(next, &actual); err != nil {
144 return body, false, fmt.Errorf("MiMo catalog upgrade readback: %w", err)
145 }
146 if !reflect.DeepEqual(deepSeekCatalogDocumentValue(actual), deepSeekCatalogDocumentValue(expected)) {
147 return body, false, fmt.Errorf("MiMo catalog upgrade changed unplanned fields; original configuration retained")
148 }
149 return next, true, nil
150 }
151
152 func isOfficialMimoCatalogUpgradeEntry(p *ProviderEntry) bool {
153 if p == nil || strings.TrimSpace(p.RequestURL) != "" || strings.TrimSpace(p.ChatURL) != "" {
154 return false
155 }
156 host := officialProviderHost(p.BaseURL)
157 switch host {
158 case "api.xiaomimimo.com", "token-plan-cn.xiaomimimo.com", "token-plan-sgp.xiaomimimo.com", "token-plan-ams.xiaomimimo.com":
159 default:
160 return false
161 }
162 suffix := "/v1"
163 if normalizedProviderProtocol(p.Kind) == "anthropic" {
164 suffix = "/anthropic"
165 } else if normalizedProviderProtocol(p.Kind) != "openai" && normalizedProviderProtocol(p.Kind) != "responses" {
166 return false
167 }
168 return normalizedBaseURLForMigration(p.BaseURL) == "https://"+host+suffix
169 }
170
171 func stringSliceDocument(values []string) []any {
172 out := make([]any, len(values))
173 for i, value := range values {
174 out[i] = value
175 }
176 return out
177 }
178
179 func mimoCatalogPriceDocuments(models []string) map[string]map[string]any {
180 out := make(map[string]map[string]any)
181 for model, price := range mimoDomesticPrices(models) {
182 out[model] = map[string]any{
183 "cache_hit": price.CacheHit,
184 "input": price.Input,
185 "output": price.Output,
186 "currency": price.Currency,
187 }
188 }
189 return out
190 }
191
192 func rewriteMimoCatalogProviders(body string, count int, updates map[int]mimoCatalogProviderUpdate) (string, error) {
193 lines := strings.Split(body, "\n")
194 blocks := providerTOMLBlocks(lines)
195 if len(blocks) != count {
196 expanded, err := expandOpenCodeGoInlineProviders(body)
197 if err != nil {
198 return body, err
199 }
200 if len(providerTOMLBlocks(strings.Split(expanded, "\n"))) != count {
201 return body, fmt.Errorf("MiMo catalog upgrade cannot map provider tables")
202 }
203 return rewriteMimoCatalogProviders(expanded, count, updates)
204 }
205 for i := range slices.Backward(blocks) {
206 update, ok := updates[i]
207 if !ok {
208 continue
209 }
210 b := blocks[i]
211 for b.end < len(lines) {
212 header := tomlSectionHeader(lines[b.end])
213 path := rawTOMLKeyPath(strings.Trim(header, "[]"))
214 if header != "" && (len(path) < 2 || path[0] != "providers") {
215 break
216 }
217 b.end++
218 }
219 part := strings.Join(lines[b.start+1:b.end], "\n")
220 var err error
221 part, err = rawTOMLSet(part, []string{"models"}, update.models)
222 if err != nil {
223 return body, err
224 }
225 if update.visionModels != nil {
226 part, err = rawTOMLSet(part, []string{"vision_models"}, update.visionModels)
227 if err != nil {
228 return body, err
229 }
230 }
231 part, err = appendMimoCatalogPriceTables(part, update.prices)
232 if err != nil {
233 return body, err
234 }
235 lines = append(lines[:b.start+1], append(strings.Split(part, "\n"), lines[b.end:]...)...)
236 }
237 return strings.Join(lines, "\n"), nil
238 }
239
240 func appendMimoCatalogPriceTables(body string, prices map[string]map[string]any) (string, error) {
241 for _, model := range []string{"mimo-v2.6-pro", "mimo-v2.6-flash"} {
242 price := prices[model]
243 if price == nil {
244 continue
245 }
246 // Write leaves so inline prices are extended in place and absent prices
247 // create distinct model tables, without redeclaring an implicit parent.
248 for _, key := range []string{"cache_hit", "input", "output", "currency"} {
249 var err error
250 body, err = rawTOMLSet(body, []string{"providers", "prices", model, key}, price[key])
251 if err != nil {
252 return body, err
253 }
254 }
255 }
256 return body, nil
257 }
258
258 lines GO