返回 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 {
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 func openCodeGoModelRoutes(model string) []string {
119 model = strings.TrimSpace(model)
120 if model == "" {
121 return nil
122 }
123 routes := make([]string, 0, 3)
124 if _, ok := provider.LookupOfficialOpenCodeGo("openai", "https://opencode.ai/zen/go/v1", model); ok {
125 routes = append(routes, provider.OpenCodeGoRouteChat)
126 }
127 if _, ok := provider.LookupOfficialOpenCodeGo("anthropic", "https://opencode.ai/zen/go", model); ok {
128 routes = append(routes, provider.OpenCodeGoRouteAnthropic)
129 }
130 if _, ok := provider.LookupOfficialOpenCodeGo("responses", "https://opencode.ai/zen/go/v1", model); ok {
131 routes = append(routes, provider.OpenCodeGoRouteResponses)
132 }
133 return routes
134 }
135
136 func providerModelSupportsOpenCodeGoRoute(route, baseURL, kind, model string) bool {
137 _, ok := provider.LookupOfficialOpenCodeGo(kind, baseURL, model)
138 if ok {
139 return true
140 }
141 // A DeepSeek model is intentionally supported by all three OpenCode Go
142 // adapters; the lookup above already captures that special compatibility
143 // path when the current route is official.
144 return route == provider.OpenCodeGoRouteChat && strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek-")
145 }
146
147 func firstOpenCodeGoGroup(groups map[string][]string) string {
148 for _, route := range []string{provider.OpenCodeGoRouteChat, provider.OpenCodeGoRouteAnthropic, provider.OpenCodeGoRouteResponses} {
149 if len(groups[route]) > 0 {
150 return route
151 }
152 }
153 return ""
154 }
155
156 func setOpenCodeGoRoute(p *ProviderEntry, route string) {
157 if p == nil {
158 return
159 }
160 switch route {
161 case provider.OpenCodeGoRouteChat:
162 p.Kind = "openai"
163 p.BaseURL = "https://opencode.ai/zen/go/v1"
164 p.ResponsesMode = ""
165 case provider.OpenCodeGoRouteAnthropic:
166 p.Kind = "anthropic"
167 p.BaseURL = "https://opencode.ai/zen/go"
168 p.ResponsesMode = ""
169 case provider.OpenCodeGoRouteResponses:
170 p.Kind = "responses"
171 p.BaseURL = "https://opencode.ai/zen/go/v1"
172 p.ResponsesMode = "stateless"
173 }
174 }
175
176 func openCodeGoPresetIDForRoute(route string) string {
177 switch route {
178 case provider.OpenCodeGoRouteAnthropic:
179 return "opencode-go-anthropic"
180 case provider.OpenCodeGoRouteResponses:
181 return "opencode-go-responses"
182 default:
183 return "opencode-go"
184 }
185 }
186
187 func openCodeGoCanonicalContextWindow(route string) int {
188 switch route {
189 case provider.OpenCodeGoRouteAnthropic:
190 return legacyOpenCodeGoAnthropicWindow
191 case provider.OpenCodeGoRouteResponses:
192 return 500_000
193 default:
194 return 128_000
195 }
196 }
197
198 func mergeOpenCodeGoRouteDefaults(p *ProviderEntry, route string) bool {
199 if p == nil {
200 return false
201 }
202 preset, ok := CuratedProviderPreset(openCodeGoPresetIDForRoute(route))
203 if !ok || len(preset.Entries) != 1 {
204 return false
205 }
206 return mergeMissingOpenCodeGoContextOverrides(p, preset.Entries[0].ModelOverrides)
207 }
208
209 func applyOpenCodeGoModelGroup(p *ProviderEntry, models []string) {
210 if p == nil || len(models) == 0 {
211 return
212 }
213 originalDefault := strings.TrimSpace(p.DefaultModel())
214 if len(p.Models) > 0 || len(models) > 1 {
215 p.Models = append([]string(nil), models...)
216 p.Model = ""
217 } else {
218 p.Models = nil
219 p.Model = models[0]
220 }
221 if containsModelFold(models, originalDefault) {
222 p.Default = originalDefault
223 } else {
224 p.Default = models[0]
225 }
226 filterOpenCodeGoModelFields(p, models)
227 }
228
229 func filterOpenCodeGoModelFields(p *ProviderEntry, models []string) {
230 if p == nil {
231 return
232 }
233 wanted := map[string]bool{}
234 for _, model := range models {
235 wanted[strings.ToLower(strings.TrimSpace(model))] = true
236 }
237 filterStrings := func(values []string) []string {
238 if values == nil {
239 return nil
240 }
241 out := make([]string, 0, len(values))
242 for _, value := range values {
243 if wanted[strings.ToLower(strings.TrimSpace(value))] {
244 out = append(out, value)
245 }
246 }
247 return out
248 }
249 p.VisionModels = filterStrings(p.VisionModels)
250 for model := range p.ModelOverrides {
251 if !wanted[strings.ToLower(strings.TrimSpace(model))] {
252 delete(p.ModelOverrides, model)
253 }
254 }
255 for model := range p.Prices {
256 if !wanted[strings.ToLower(strings.TrimSpace(model))] {
257 delete(p.Prices, model)
258 }
259 }
260 }
261
262 func containsModelFold(models []string, want string) bool {
263 for _, model := range models {
264 if strings.EqualFold(strings.TrimSpace(model), strings.TrimSpace(want)) {
265 return true
266 }
267 }
268 return false
269 }
270
271 func uniqueOpenCodeGoSiblingName(c *Config, base, route string) string {
272 name := strings.TrimSpace(base) + "-" + route
273 if _, ok := c.Provider(name); !ok {
274 return name
275 }
276 for n := 2; ; n++ {
277 candidate := name + "-" + strconv.Itoa(n)
278 if _, ok := c.Provider(candidate); !ok {
279 return candidate
280 }
281 }
282 }
283
284 func addOpenCodeGoAccess(c *Config, names ...string) {
285 if c == nil || c.Desktop.ProviderAccess == nil {
286 return
287 }
288 seen := desktopProviderAccessMap(c.Desktop.ProviderAccess)
289 for _, name := range names {
290 name = strings.TrimSpace(name)
291 if name != "" && !seen[name] {
292 c.Desktop.ProviderAccess = append(c.Desktop.ProviderAccess, name)
293 seen[name] = true
294 }
295 }
296 }
297
298 func retargetOpenCodeGoRefs(c *Config, oldName string, models []string, newName, oldDefault string) {
299 if c == nil {
300 return
301 }
302 wanted := map[string]bool{}
303 for _, model := range models {
304 wanted[strings.ToLower(strings.TrimSpace(model))] = true
305 }
306 rewrite := func(ref string) string {
307 ref = strings.TrimSpace(ref)
308 if model, ok := strings.CutPrefix(ref, oldName+"/"); ok {
309 if wanted[strings.ToLower(strings.TrimSpace(model))] {
310 return newName + "/" + model
311 }
312 }
313 if strings.EqualFold(ref, oldName) && wanted[strings.ToLower(strings.TrimSpace(oldDefault))] {
314 return newName + "/" + oldDefault
315 }
316 if wanted[strings.ToLower(ref)] {
317 return newName + "/" + ref
318 }
319 return ref
320 }
321 c.DefaultModel = rewrite(c.DefaultModel)
322 c.Agent.PlannerModel = rewrite(c.Agent.PlannerModel)
323 c.Agent.VisionModel = rewrite(c.Agent.VisionModel)
324 c.Agent.GuardianModel = rewrite(c.Agent.GuardianModel)
325 c.Agent.RecoveryModel = rewrite(c.Agent.RecoveryModel)
326 c.Agent.SubagentModel = rewrite(c.Agent.SubagentModel)
327 for name, ref := range c.Agent.SubagentModels {
328 c.Agent.SubagentModels[name] = rewrite(ref)
329 }
330 c.Bot.Model = rewrite(c.Bot.Model)
331 for i := range c.Bot.Connections {
332 c.Bot.Connections[i].Model = rewrite(c.Bot.Connections[i].Model)
333 }
334 }
335
336 func normalizeLegacyOpenCodeGoBilling(c *Config) bool {
337 if c == nil {
338 return false
339 }
340 changed := false
341 for i := range c.Providers {
342 p := &c.Providers[i]
343 if _, ok := provider.OfficialOpenCodeGoRoute(p.Kind, p.BaseURL); !ok || strings.TrimSpace(p.BillingMode) != "" {
344 continue
345 }
346 p.BillingMode = billing.BillingModeSubscriptionEquivalent
347 changed = true
348 }
349 return changed
350 }
351
352 func officialOpenCodeGoKindURL(kind, baseURL string) bool {
353 _, ok := provider.OfficialOpenCodeGoRoute(kind, baseURL)
354 return ok
355 }
356
357 func openCodeGoPresetIDForMigration(p ProviderEntry) string {
358 presetID := strings.TrimSpace(p.PresetID)
359 if presetID == "" {
360 presetID = strings.TrimSpace(p.Name)
361 }
362 switch presetID {
363 case "opencode-go", "opencode-go-anthropic", "opencode-go-deepseek-anthropic", "opencode-go-deepseek-responses":
364 return presetID
365 default:
366 return ""
367 }
368 }
369
370 func mergeMissingOpenCodeGoContextOverrides(p *ProviderEntry, defaults map[string]ProviderModelOverride) bool {
371 if p == nil || len(defaults) == 0 {
372 return false
373 }
374 if p.ModelOverrides == nil {
375 p.ModelOverrides = map[string]ProviderModelOverride{}
376 }
377 changed := false
378 for defaultKey, defaultOverride := range defaults {
379 overrideKey := defaultKey
380 for key := range p.ModelOverrides {
381 if strings.EqualFold(strings.TrimSpace(key), defaultKey) {
382 overrideKey = key
383 break
384 }
385 }
386 override := p.ModelOverrides[overrideKey]
387 if override.ContextWindow == 0 && defaultOverride.ContextWindow > 0 {
388 override.ContextWindow = defaultOverride.ContextWindow
389 p.ModelOverrides[overrideKey] = override
390 changed = true
391 }
392 if override.MaxOutputTokens == 0 && defaultOverride.MaxOutputTokens != 0 {
393 override.MaxOutputTokens = defaultOverride.MaxOutputTokens
394 p.ModelOverrides[overrideKey] = override
395 changed = true
396 }
397 }
398 return changed
399 }
400
401 func openCodeGoCatalogMatches(presetID string, p, canonical ProviderEntry) bool {
402 if strings.TrimSpace(p.Model) != "" {
403 return false
404 }
405 if stringSlicesEqual(p.Models, canonical.Models) {
406 return true
407 }
408 return presetID == "opencode-go" && (stringSlicesEqual(p.Models, opencodeGoModels) || stringSlicesEqual(p.Models, legacyOpenCodeGoModels))
409 }
410
411 func migrateOpenCodeGoProviderWindow(p *ProviderEntry, canonical ProviderEntry, legacy int) bool {
412 if p.ContextWindow != 0 && p.ContextWindow != legacy {
413 return false
414 }
415 if p.ContextWindow == canonical.ContextWindow {
416 return mergeMissingOpenCodeGoContextOverrides(p, canonical.ModelOverrides)
417 }
418 p.ContextWindow = canonical.ContextWindow
419 _ = mergeMissingOpenCodeGoContextOverrides(p, canonical.ModelOverrides)
420 return true
421 }
422
423 // normalizeLegacyOpenCodeGoContextWindows upgrades only unmodified official
424 // OpenCode Go presets that still carry the old conservative provider window.
425 // Custom catalogs, endpoints, and positive context/output overrides stay put.
426 func normalizeLegacyOpenCodeGoContextWindows(c *Config) bool {
427 if c == nil {
428 return false
429 }
430 changed := false
431 for i := range c.Providers {
432 p := &c.Providers[i]
433 presetID := openCodeGoPresetIDForMigration(*p)
434 preset, ok := CuratedProviderPreset(presetID)
435 if presetID == "" || !ok || len(preset.Entries) != 1 {
436 continue
437 }
438 canonical := preset.Entries[0]
439 if !strings.EqualFold(strings.TrimSpace(p.Kind), strings.TrimSpace(canonical.Kind)) ||
440 !officialOpenCodeGoKindURL(canonical.Kind, p.BaseURL) ||
441 normalizedBaseURLForMigration(p.BaseURL) != normalizedBaseURLForMigration(canonical.BaseURL) ||
442 !openCodeGoCatalogMatches(presetID, *p, canonical) {
443 continue
444 }
445 legacy := legacyOpenCodeGoChatWindow
446 if presetID == "opencode-go-anthropic" || presetID == "opencode-go-deepseek-anthropic" {
447 legacy = legacyOpenCodeGoAnthropicWindow
448 }
449 if migrateOpenCodeGoProviderWindow(p, canonical, legacy) {
450 changed = true
451 }
452 }
453 return changed
454 }
455
455 lines GO