返回 DeepSeek-Reasonix
opencode_go_context.go
根目录 / internal / config / opencode_go_context.go
1 package config
2
3 import (
4 "strconv"
5 "strings"
6
7 "reasonix/internal/billing"
8 "reasonix/internal/provider"
9 )
10
11 const (
12 legacyOpenCodeGoChatWindow = 128000
13 legacyOpenCodeGoAnthropicWindow = 262144
14 )
15
16 func normalizeLegacyOpenCodeGoInstalls(c *Config) bool {
17 changed := normalizeLegacyOpenCodeGoKimiK3Catalog(c)
18 changed = normalizeLegacyOpenCodeGoVisionCatalog(c) || changed
19 changed = normalizeLegacyOpenCodeGoRouteCatalog(c) || changed
20 changed = normalizeLegacyOpenCodeGoContextWindows(c) || changed
21 changed = normalizeLegacyOpenCodeGoBilling(c) || changed
22 return changed
23 }
24
25 // normalizeLegacyOpenCodeGoRouteCatalog repairs the old aggregate model list
26 // shape. OpenCode Go exposes one /models catalog, but the wire protocol is
27 // route-specific: Chat, Anthropic Messages, and Responses do not accept the
28 // same model IDs. Older installs could therefore persist Grok/Qwen under the
29 // wrong provider kind and only discover the mistake after a paid request.
30 //
31 // The migration is additive and non-destructive. The original provider name is
32 // kept for the route it already serves; known models belonging to another
33 // route are moved to a sibling provider with the same key, headers, prices,
34 // overrides, and user-owned fields. Unknown/future model IDs stay on the
35 // original route so forward compatibility is preserved.
36 func normalizeLegacyOpenCodeGoRouteCatalog(c *Config) bool {
37 if c == nil {
38 return false
39 }
40 changed := false
41 for i := 0; i < len(c.Providers); i++ {
42 p := &c.Providers[i]
43 if strings.TrimSpace(p.RequestURL) != "" {
44 // An explicit request_url is a user-owned escape hatch; the base URL
45 // alone cannot prove which wire route that custom URL implements.
46 continue
47 }
48 currentRoute, ok := provider.OfficialOpenCodeGoRoute(p.Kind, p.BaseURL)
49 if !ok || len(p.ModelList()) == 0 {
50 continue
51 }
52 models := append([]string(nil), p.ModelList()...)
53 groups := map[string][]string{}
54 for _, model := range models {
55 routes := openCodeGoModelRoutes(model)
56 route := currentRoute
57 if !providerModelSupportsOpenCodeGoRoute(currentRoute, p.BaseURL, p.Kind, model) && len(routes) == 1 && !openCodeGoKeepsCorrectedModel(*p, model) {
58 route = routes[0]
59 }
60 groups[route] = append(groups[route], model)
61 }
62 if len(groups) == 1 {
63 if target := firstOpenCodeGoGroup(groups); target != currentRoute {
64 setOpenCodeGoRoute(p, target)
65 p.PresetID = openCodeGoPresetIDForRoute(target)
66 p.PresetVersion = ProviderPresetVersion
67 changed = true
68 }
69 continue
70 }
71 currentModels := groups[currentRoute]
72 if len(currentModels) == 0 {
73 // Preserve the old provider identity on the route selected by its
74 // default model; other groups become siblings below.
75 preferred := strings.TrimSpace(p.DefaultModel())
76 if routes := openCodeGoModelRoutes(preferred); len(routes) == 1 {
77 currentRoute = routes[0]
78 setOpenCodeGoRoute(p, currentRoute)
79 currentModels = groups[currentRoute]
80 changed = true
81 }
82 }
83 if len(currentModels) == 0 {
84 continue
85 }
86 original := cloneProviderEntry(*p)
87 originalDefault := original.DefaultModel()
88 if !stringSlicesEqual(p.ModelList(), currentModels) || len(p.Models) == 0 && len(p.Model) > 0 {
89 applyOpenCodeGoModelGroup(p, currentModels)
90 changed = true
91 }
92 if mergeOpenCodeGoRouteDefaults(p, currentRoute) {
93 changed = true
94 }
95 for route, routeModels := range groups {
96 if route == currentRoute || len(routeModels) == 0 {
97 continue
98 }
99 sibling := cloneProviderEntry(original)
100 sibling.Name = uniqueOpenCodeGoSiblingName(c, p.Name, route)
101 sibling.PresetID = openCodeGoPresetIDForRoute(route)
102 sibling.PresetVersion = ProviderPresetVersion
103 setOpenCodeGoRoute(&sibling, route)
104 if sibling.PresetID == "opencode-go" && sibling.ContextWindow == legacyOpenCodeGoChatWindow {
105 sibling.ContextWindow = openCodeGoCanonicalContextWindow(route)
106 }
107 applyOpenCodeGoModelGroup(&sibling, routeModels)
108 mergeOpenCodeGoRouteDefaults(&sibling, route)
109 retargetOpenCodeGoRefs(c, p.Name, routeModels, sibling.Name, originalDefault)
110 addOpenCodeGoAccess(c, p.Name, sibling.Name)
111 c.Providers = append(c.Providers, sibling)
112 changed = true
113 }
114 }
115 return changed
116 }
117
118 // openCodeGoKeepsCorrectedModel keeps a route-corrected model where the user
119 // put it unless the connection still holds a chat preset list we shipped.
120 func openCodeGoKeepsCorrectedModel(p ProviderEntry, model string) bool {
121 if !provider.OpenCodeGoRouteCorrected(model) {
122 return false
123 }
124 route, ok := provider.OfficialOpenCodeGoRoute(p.Kind, p.BaseURL)
125 if !ok || route != provider.OpenCodeGoRouteChat || strings.TrimSpace(p.PresetID) != "opencode-go" || strings.TrimSpace(p.Model) != "" {
126 return true
127 }
128 for _, shipped := range shippedMiniMaxChatOpenCodeGoModels {
129 if stringSlicesEqual(p.Models, shipped) {
130 return false
131 }
132 }
133 return true
134 }
135
136 func openCodeGoModelRoutes(model string) []string {
137 model = strings.TrimSpace(model)
138 if model == "" {
139 return nil
140 }
141 routes := make([]string, 0, 3)
142 if _, ok := provider.LookupOfficialOpenCodeGo("openai", "https://opencode.ai/zen/go/v1", model); ok {
143 routes = append(routes, provider.OpenCodeGoRouteChat)
144 }
145 if _, ok := provider.LookupOfficialOpenCodeGo("anthropic", "https://opencode.ai/zen/go", model); ok {
146 routes = append(routes, provider.OpenCodeGoRouteAnthropic)
147 }
148 if _, ok := provider.LookupOfficialOpenCodeGo("responses", "https://opencode.ai/zen/go/v1", model); ok {
149 routes = append(routes, provider.OpenCodeGoRouteResponses)
150 }
151 return routes
152 }
153
154 func providerModelSupportsOpenCodeGoRoute(route, baseURL, kind, model string) bool {
155 _, ok := provider.LookupOfficialOpenCodeGo(kind, baseURL, model)
156 if ok {
157 return true
158 }
159 // A DeepSeek model is intentionally supported by all three OpenCode Go
160 // adapters; the lookup above already captures that special compatibility
161 // path when the current route is official.
162 return route == provider.OpenCodeGoRouteChat && strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek-")
163 }
164
165 func firstOpenCodeGoGroup(groups map[string][]string) string {
166 for _, route := range []string{provider.OpenCodeGoRouteChat, provider.OpenCodeGoRouteAnthropic, provider.OpenCodeGoRouteResponses} {
167 if len(groups[route]) > 0 {
168 return route
169 }
170 }
171 return ""
172 }
173
174 func setOpenCodeGoRoute(p *ProviderEntry, route string) {
175 if p == nil {
176 return
177 }
178 switch route {
179 case provider.OpenCodeGoRouteChat:
180 p.Kind = "openai"
181 p.BaseURL = "https://opencode.ai/zen/go/v1"
182 p.ResponsesMode = ""
183 case provider.OpenCodeGoRouteAnthropic:
184 p.Kind = "anthropic"
185 p.BaseURL = "https://opencode.ai/zen/go"
186 p.ResponsesMode = ""
187 case provider.OpenCodeGoRouteResponses:
188 p.Kind = "responses"
189 p.BaseURL = "https://opencode.ai/zen/go/v1"
190 p.ResponsesMode = "stateless"
191 }
192 }
193
194 func openCodeGoPresetIDForRoute(route string) string {
195 switch route {
196 case provider.OpenCodeGoRouteAnthropic:
197 return "opencode-go-anthropic"
198 case provider.OpenCodeGoRouteResponses:
199 return "opencode-go-responses"
200 default:
201 return "opencode-go"
202 }
203 }
204
205 func openCodeGoCanonicalContextWindow(route string) int {
206 switch route {
207 case provider.OpenCodeGoRouteAnthropic:
208 return legacyOpenCodeGoAnthropicWindow
209 case provider.OpenCodeGoRouteResponses:
210 return 500_000
211 default:
212 return 128_000
213 }
214 }
215
216 func mergeOpenCodeGoRouteDefaults(p *ProviderEntry, route string) bool {
217 if p == nil {
218 return false
219 }
220 preset, ok := CuratedProviderPreset(openCodeGoPresetIDForRoute(route))
221 if !ok || len(preset.Entries) != 1 {
222 return false
223 }
224 return mergeMissingOpenCodeGoContextOverrides(p, preset.Entries[0].ModelOverrides)
225 }
226
227 func applyOpenCodeGoModelGroup(p *ProviderEntry, models []string) {
228 if p == nil || len(models) == 0 {
229 return
230 }
231 originalDefault := strings.TrimSpace(p.DefaultModel())
232 if len(p.Models) > 0 || len(models) > 1 {
233 p.Models = append([]string(nil), models...)
234 p.Model = ""
235 } else {
236 p.Models = nil
237 p.Model = models[0]
238 }
239 if containsModelFold(models, originalDefault) {
240 p.Default = originalDefault
241 } else {
242 p.Default = models[0]
243 }
244 filterOpenCodeGoModelFields(p, models)
245 }
246
247 func filterOpenCodeGoModelFields(p *ProviderEntry, models []string) {
248 if p == nil {
249 return
250 }
251 wanted := map[string]bool{}
252 for _, model := range models {
253 wanted[strings.ToLower(strings.TrimSpace(model))] = true
254 }
255 filterStrings := func(values []string) []string {
256 if values == nil {
257 return nil
258 }
259 out := make([]string, 0, len(values))
260 for _, value := range values {
261 if wanted[strings.ToLower(strings.TrimSpace(value))] {
262 out = append(out, value)
263 }
264 }
265 return out
266 }
267 p.VisionModels = filterStrings(p.VisionModels)
268 for model := range p.ModelOverrides {
269 if !wanted[strings.ToLower(strings.TrimSpace(model))] {
270 delete(p.ModelOverrides, model)
271 }
272 }
273 for model := range p.Prices {
274 if !wanted[strings.ToLower(strings.TrimSpace(model))] {
275 delete(p.Prices, model)
276 }
277 }
278 }
279
280 func containsModelFold(models []string, want string) bool {
281 for _, model := range models {
282 if strings.EqualFold(strings.TrimSpace(model), strings.TrimSpace(want)) {
283 return true
284 }
285 }
286 return false
287 }
288
289 func uniqueOpenCodeGoSiblingName(c *Config, base, route string) string {
290 name := strings.TrimSpace(base) + "-" + route
291 if _, ok := c.Provider(name); !ok {
292 return name
293 }
294 for n := 2; ; n++ {
295 candidate := name + "-" + strconv.Itoa(n)
296 if _, ok := c.Provider(candidate); !ok {
297 return candidate
298 }
299 }
300 }
301
302 func addOpenCodeGoAccess(c *Config, names ...string) {
303 if c == nil || c.Desktop.ProviderAccess == nil {
304 return
305 }
306 seen := desktopProviderAccessMap(c.Desktop.ProviderAccess)
307 for _, name := range names {
308 name = strings.TrimSpace(name)
309 if name != "" && !seen[name] {
310 c.Desktop.ProviderAccess = append(c.Desktop.ProviderAccess, name)
311 seen[name] = true
312 }
313 }
314 }
315
316 func retargetOpenCodeGoRefs(c *Config, oldName string, models []string, newName, oldDefault string) {
317 if c == nil {
318 return
319 }
320 wanted := map[string]bool{}
321 for _, model := range models {
322 wanted[strings.ToLower(strings.TrimSpace(model))] = true
323 }
324 rewrite := func(ref string) string {
325 ref = strings.TrimSpace(ref)
326 if model, ok := strings.CutPrefix(ref, oldName+"/"); ok {
327 if wanted[strings.ToLower(strings.TrimSpace(model))] {
328 return newName + "/" + model
329 }
330 }
331 if strings.EqualFold(ref, oldName) && wanted[strings.ToLower(strings.TrimSpace(oldDefault))] {
332 return newName + "/" + oldDefault
333 }
334 if wanted[strings.ToLower(ref)] {
335 return newName + "/" + ref
336 }
337 return ref
338 }
339 c.DefaultModel = rewrite(c.DefaultModel)
340 c.Agent.PlannerModel = rewrite(c.Agent.PlannerModel)
341 c.Agent.VisionModel = rewrite(c.Agent.VisionModel)
342 c.Agent.GuardianModel = rewrite(c.Agent.GuardianModel)
343 c.Agent.RecoveryModel = rewrite(c.Agent.RecoveryModel)
344 c.Agent.SubagentModel = rewrite(c.Agent.SubagentModel)
345 for name, ref := range c.Agent.SubagentModels {
346 c.Agent.SubagentModels[name] = rewrite(ref)
347 }
348 c.Bot.Model = rewrite(c.Bot.Model)
349 for i := range c.Bot.Connections {
350 c.Bot.Connections[i].Model = rewrite(c.Bot.Connections[i].Model)
351 }
352 }
353
354 func normalizeLegacyOpenCodeGoBilling(c *Config) bool {
355 if c == nil {
356 return false
357 }
358 changed := false
359 for i := range c.Providers {
360 p := &c.Providers[i]
361 if _, ok := provider.OfficialOpenCodeGoRoute(p.Kind, p.BaseURL); !ok || strings.TrimSpace(p.BillingMode) != "" {
362 continue
363 }
364 p.BillingMode = billing.BillingModeSubscriptionEquivalent
365 changed = true
366 }
367 return changed
368 }
369
370 func officialOpenCodeGoKindURL(kind, baseURL string) bool {
371 _, ok := provider.OfficialOpenCodeGoRoute(kind, baseURL)
372 return ok
373 }
374
375 func openCodeGoPresetIDForMigration(p ProviderEntry) string {
376 presetID := strings.TrimSpace(p.PresetID)
377 if presetID == "" {
378 presetID = strings.TrimSpace(p.Name)
379 }
380 switch presetID {
381 case "opencode-go", "opencode-go-anthropic", "opencode-go-deepseek-anthropic", "opencode-go-deepseek-responses":
382 return presetID
383 default:
384 return ""
385 }
386 }
387
388 func mergeMissingOpenCodeGoContextOverrides(p *ProviderEntry, defaults map[string]ProviderModelOverride) bool {
389 if p == nil || len(defaults) == 0 {
390 return false
391 }
392 if p.ModelOverrides == nil {
393 p.ModelOverrides = map[string]ProviderModelOverride{}
394 }
395 changed := false
396 for defaultKey, defaultOverride := range defaults {
397 overrideKey := defaultKey
398 for key := range p.ModelOverrides {
399 if strings.EqualFold(strings.TrimSpace(key), defaultKey) {
400 overrideKey = key
401 break
402 }
403 }
404 override := p.ModelOverrides[overrideKey]
405 if override.ContextWindow == 0 && defaultOverride.ContextWindow > 0 {
406 override.ContextWindow = defaultOverride.ContextWindow
407 p.ModelOverrides[overrideKey] = override
408 changed = true
409 }
410 if override.MaxOutputTokens == 0 && defaultOverride.MaxOutputTokens != 0 {
411 override.MaxOutputTokens = defaultOverride.MaxOutputTokens
412 p.ModelOverrides[overrideKey] = override
413 changed = true
414 }
415 }
416 return changed
417 }
418
419 func openCodeGoCatalogMatches(presetID string, p, canonical ProviderEntry) bool {
420 if strings.TrimSpace(p.Model) != "" {
421 return false
422 }
423 if stringSlicesEqual(p.Models, canonical.Models) {
424 return true
425 }
426 return presetID == "opencode-go" && (stringSlicesEqual(p.Models, opencodeGoModels) || stringSlicesEqual(p.Models, legacyOpenCodeGoModels))
427 }
428
429 func migrateOpenCodeGoProviderWindow(p *ProviderEntry, canonical ProviderEntry, legacy int) bool {
430 if p.ContextWindow != 0 && p.ContextWindow != legacy {
431 return false
432 }
433 if p.ContextWindow == canonical.ContextWindow {
434 return mergeMissingOpenCodeGoContextOverrides(p, canonical.ModelOverrides)
435 }
436 p.ContextWindow = canonical.ContextWindow
437 _ = mergeMissingOpenCodeGoContextOverrides(p, canonical.ModelOverrides)
438 return true
439 }
440
441 // normalizeLegacyOpenCodeGoContextWindows upgrades only unmodified official
442 // OpenCode Go presets that still carry the old conservative provider window.
443 // Custom catalogs, endpoints, and positive context/output overrides stay put.
444 func normalizeLegacyOpenCodeGoContextWindows(c *Config) bool {
445 if c == nil {
446 return false
447 }
448 changed := false
449 for i := range c.Providers {
450 p := &c.Providers[i]
451 presetID := openCodeGoPresetIDForMigration(*p)
452 preset, ok := CuratedProviderPreset(presetID)
453 if presetID == "" || !ok || len(preset.Entries) != 1 {
454 continue
455 }
456 canonical := preset.Entries[0]
457 if !strings.EqualFold(strings.TrimSpace(p.Kind), strings.TrimSpace(canonical.Kind)) ||
458 !officialOpenCodeGoKindURL(canonical.Kind, p.BaseURL) ||
459 normalizedBaseURLForMigration(p.BaseURL) != normalizedBaseURLForMigration(canonical.BaseURL) ||
460 !openCodeGoCatalogMatches(presetID, *p, canonical) {
461 continue
462 }
463 legacy := legacyOpenCodeGoChatWindow
464 if presetID == "opencode-go-anthropic" || presetID == "opencode-go-deepseek-anthropic" {
465 legacy = legacyOpenCodeGoAnthropicWindow
466 }
467 if migrateOpenCodeGoProviderWindow(p, canonical, legacy) {
468 changed = true
469 }
470 }
471 return changed
472 }
473
473 lines GO