返回 DeepSeek-Reasonix
opencode_go_aliases.go
根目录 / internal / config / opencode_go_aliases.go
1 package config
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "maps"
8 "os"
9 "slices"
10 "strings"
11
12 "reasonix/internal/provider"
13 )
14
15 // ErrMigratedModelUnavailable marks a saved selection whose migrated OpenCode
16 // Go connection no longer matches; a new explicit selection resolves it.
17 var ErrMigratedModelUnavailable = errors.New("MIGRATED_MODEL_UNAVAILABLE")
18
19 func normalizeRuntimeConfigWithMigrationJournal(cfg *Config) error {
20 cfg.loadOpenCodeGoJournal(userConfigLoadPath())
21 return normalizeLoadedConfig(cfg)
22 }
23
24 func (c *Config) loadOpenCodeGoJournal(path string) {
25 if c == nil || path == "" {
26 return
27 }
28 resolved, exists, err := statConfigPath(path)
29 if err != nil || !exists {
30 return
31 }
32 raw, err := os.ReadFile(resolved)
33 if err != nil {
34 return
35 }
36 c.openCodeGoJournal = readOpenCodeGoJournal(resolved, raw)
37 }
38
39 // Project files are never rewritten by startup. Resolve their legacy routes in
40 // memory, while version-10 global connections retain later manual API choices.
41 func normalizeOpenCodeGoRuntimeCompatibility(c *Config) {
42 if c == nil {
43 return
44 }
45 previous := c.openCodeGoJournal
46 j, _ := planOpenCodeGoUpgradeFiltered(c, func(p ProviderEntry) bool {
47 return c.ConfigVersion < openCodeGoUpgradeVersion || c.providerSources[providerMergeKey(p)] == providerSourceProject
48 })
49 if previous != nil {
50 maps.Copy(j.Aliases, previous.Aliases)
51 maps.Copy(j.SearchAliases, previous.SearchAliases)
52 j.Connections = append(j.Connections, previous.Connections...)
53 j.Skipped = append(j.Skipped, previous.Skipped...)
54 }
55 if len(j.Aliases) > 0 || len(j.SearchAliases) > 0 || previous != nil {
56 c.openCodeGoJournal = &j
57 }
58 }
59
60 func (c *Config) resolveOpenCodeGoAlias(ref string, search bool) (string, error) {
61 if c == nil || c.openCodeGoJournal == nil {
62 return ref, nil
63 }
64 aliases := c.openCodeGoJournal.Aliases
65 if search {
66 aliases = c.openCodeGoJournal.SearchAliases
67 }
68 alias, ok := aliases[strings.TrimSpace(ref)]
69 if !ok {
70 return ref, nil
71 }
72 name, model, ok := strings.Cut(alias.Target, "/")
73 p, found := c.Provider(name)
74 if !ok || !found || !p.HasModel(model) {
75 return "", fmt.Errorf("%w: %q moved to %q; restore that OpenCode Go connection in model settings", ErrMigratedModelUnavailable, ref, alias.Target)
76 }
77 if _, official := provider.OpenCodeGoRequestRoute(p.Kind, p.BaseURL, p.RequestURL, p.ChatURL); !official || openCodeGoIdentity(*p) != alias.Identity {
78 return "", fmt.Errorf("%w: account or endpoint for %q has changed; restore the original connection for %q", ErrMigratedModelUnavailable, alias.Target, ref)
79 }
80 return alias.Target, nil
81 }
82
83 // ModelReferenceError checks alias fallback only when the reference is absent
84 // from the current catalog. Historical callers must use ResolveHistoricalModel.
85 func (c *Config) ModelReferenceError(ref string) error {
86 if _, ok := c.resolveCurrentModel(ref); ok {
87 return nil
88 }
89 _, err := c.resolveOpenCodeGoAlias(ref, false)
90 return err
91 }
92
93 // ResolveHistoricalModel validates the original migration identity before
94 // resolving a saved selection. Unknown non-migrated refs are retained so the
95 // owning runtime can apply its existing plugin and stale-selection policy.
96 func (c *Config) ResolveHistoricalModel(ref string) (string, error) {
97 target, err := c.resolveOpenCodeGoAlias(ref, false)
98 if err != nil {
99 return "", err
100 }
101 if entry, ok := c.resolveCurrentModel(target); ok {
102 return entry.Name + "/" + entry.Model, nil
103 }
104 return target, nil
105 }
106
107 // ModelSelectionIdentity records only a digest, never resolved credentials.
108 // Current selections capture endpoint and model as well as the transport
109 // identity, so a later resume cannot silently adopt a different connection.
110 func (c *Config) ModelSelectionIdentity(ref string) string {
111 entry, ok := c.resolveCurrentModel(ref)
112 if !ok {
113 return ""
114 }
115 tracked := isOpenCodeGoEntry(entry)
116 if c.openCodeGoJournal != nil {
117 for _, alias := range c.openCodeGoJournal.Aliases {
118 if alias.Target == entry.Name+"/"+entry.Model {
119 tracked = true
120 break
121 }
122 }
123 }
124 if !tracked {
125 return ""
126 }
127 b, _ := json.Marshal([]string{entry.Name, entry.Model, entry.Kind, entry.BaseURL, entry.RequestURL, entry.ChatURL, openCodeGoIdentity(*entry)})
128 return openCodeGoDigest(b)
129 }
130
131 // ResolveSavedModel uses an explicitly persisted choice when present; legacy
132 // sidecars without that choice retain the original migration protection.
133 func (c *Config) ResolveSavedModel(ref, identity string) (string, error) {
134 if identity == "" {
135 return c.ResolveHistoricalModel(ref)
136 }
137 if current := c.ModelSelectionIdentity(ref); current == "" || current != identity {
138 return "", fmt.Errorf("%w: saved connection for %q has changed; explicitly select a model to use the current connection", ErrMigratedModelUnavailable, ref)
139 }
140 entry, _ := c.resolveCurrentModel(ref)
141 return entry.Name + "/" + entry.Model, nil
142 }
143
144 func (c *Config) resolveHistoricalWebSearchModel(ref string) (*ProviderEntry, error) {
145 target, err := c.resolveOpenCodeGoAlias(ref, true)
146 if err != nil {
147 return nil, err
148 }
149 return c.ResolveWebSearchModel(target)
150 }
151
152 // OpenCodeGoUpgradeSummary is consumed by the existing startup notice channel.
153 func (c *Config) OpenCodeGoUpgradeSummary() string {
154 if c == nil || c.openCodeGoJournal == nil {
155 return ""
156 }
157 j := c.openCodeGoJournal
158 var parts []string
159 if len(j.Connections) > 0 {
160 parts = append(parts, "OpenCode Go connections organized by model API: "+strings.Join(j.Connections, ", "))
161 }
162 if len(j.Skipped) > 0 {
163 parts = append(parts, "Preserved custom connections: "+strings.Join(j.Skipped, "; "))
164 }
165 return strings.Join(parts, ". ")
166 }
167
168 func (c *Config) resolveOpenCodeGoAutomaticSearch(current *ProviderEntry, resolve func(*ProviderEntry) *ProviderEntry) *ProviderEntry {
169 if isOpenCodeGoEntry(current) {
170 // A migrated chat connection keeps its auxiliary search on the same
171 // credential reference, even when another account appears earlier.
172 if c.openCodeGoJournal != nil {
173 for _, modelSpecific := range []bool{true, false} {
174 refs := make([]string, 0, len(c.openCodeGoJournal.SearchAliases))
175 for ref := range c.openCodeGoJournal.SearchAliases {
176 refs = append(refs, ref)
177 }
178 slices.Sort(refs)
179 for _, ref := range refs {
180 alias := c.openCodeGoJournal.SearchAliases[ref]
181 _, model, _ := strings.Cut(alias.Target, "/")
182 if alias.Identity != openCodeGoIdentity(*current) || (modelSpecific && model != current.Model) {
183 continue
184 }
185 if entry, err := c.resolveHistoricalWebSearchModel(ref); err == nil {
186 if selected := resolve(entry); selected != nil {
187 return selected
188 }
189 }
190 }
191 }
192 }
193 for i := range c.Providers {
194 if openCodeGoIdentity(c.Providers[i]) != openCodeGoIdentity(*current) || !isOpenCodeGoEntry(&c.Providers[i]) {
195 continue
196 }
197 entry, ok := c.ResolveModel(c.Providers[i].Name)
198 if ok {
199 if selected := resolve(entry); selected != nil {
200 return selected
201 }
202 }
203 }
204 }
205 return nil
206 }
207
207 lines GO