返回 DeepSeek-Reasonix
independent_web_search.go
根目录 / internal / config / independent_web_search.go
1 package config
2
3 import (
4 "fmt"
5 "reasonix/internal/provider"
6 "slices"
7 "strings"
8 )
9
10 // IsOfficialDeepSeekSearchEndpoint also recognizes Chat Completions accounts:
11 // their search requests use a separate official Messages endpoint. Explicit
12 // request URL overrides are never redirected to a different endpoint.
13 func IsOfficialDeepSeekSearchEndpoint(e *ProviderEntry) bool {
14 if e == nil || overridesBaseRoute(e, e.RequestURL) || overridesBaseRoute(e, e.ChatURL) {
15 return false
16 }
17 if IsOfficialDeepSeekWebSearchEndpoint(e) {
18 return true
19 }
20 if !strings.EqualFold(e.Kind, "openai") {
21 return false
22 }
23 copy := *e
24 copy.Kind = "responses"
25 copy.BaseURL = strings.TrimSuffix(strings.TrimRight(e.BaseURL, "/"), "/v1")
26 return IsOfficialDeepSeekWebSearchEndpoint(&copy)
27 }
28
29 // overridesBaseRoute reports whether an explicit URL names anything other than
30 // the standard route derived from the base URL. The Settings editor submits the
31 // derived route it displays, and that is not a user-owned override.
32 func overridesBaseRoute(e *ProviderEntry, raw string) bool {
33 if strings.TrimSpace(raw) == "" {
34 return false
35 }
36 explicit, ok := normalizedExactProviderRequestURL(raw)
37 if !ok {
38 return true
39 }
40 derived, ok := normalizedExactProviderRequestURL(ProviderRequestURL(e.Kind, e.BaseURL))
41 return !ok || explicit != derived
42 }
43
44 // EffectiveIndependentWebSearch preserves the existing tri-state switch while
45 // allowing official Chat Completions accounts to supply an auxiliary search.
46 func EffectiveIndependentWebSearch(e *ProviderEntry) bool {
47 if e == nil || (!SupportsServerWebSearch(e) && !IsOfficialDeepSeekSearchEndpoint(e)) {
48 return false
49 }
50 if e.WebSearch != nil {
51 return *e.WebSearch
52 }
53 return EffectiveWebSearch(e) || IsOfficialDeepSeekSearchEndpoint(e)
54 }
55
56 // ResolveWebSearchProvider freezes one search route for a runtime assembly.
57 // Prefer the selected chat account; otherwise use the first enabled configured
58 // search account. An explicit disable on a search-capable current account wins
59 // over fallback. Exact official routes use the source-rich Messages API.
60 // Compatible providers keep their own endpoint, credentials and model.
61 func (c *Config) resolveAutomaticWebSearchProvider(current *ProviderEntry) *ProviderEntry {
62 if c == nil {
63 return nil
64 }
65 if current != nil && current.WebSearch != nil && !*current.WebSearch &&
66 (SupportsServerWebSearch(current) || IsOfficialDeepSeekSearchEndpoint(current) || isOpenCodeGoEntry(current)) {
67 return nil
68 }
69 resolve := func(e *ProviderEntry) *ProviderEntry {
70 if !EffectiveIndependentWebSearch(e) || !e.Configured() || e.Model == "" {
71 return nil
72 }
73 copy := cloneProviderEntry(*e)
74 if IsOfficialDeepSeekSearchEndpoint(e) {
75 copy.Kind = "anthropic"
76 copy.BaseURL = "https://api.deepseek.com/anthropic"
77 copy.RequestURL = ""
78 copy.ChatURL = ""
79 copy.ReasoningProtocol = ReasoningProtocolDeepSeek
80 }
81 copy.WebSearch = boolPointer(true)
82 copy.ResponsesStateful = boolPointer(false)
83 copy.ResponsesMode = "stateless"
84 return &copy
85 }
86 if selected := resolve(current); selected != nil {
87 return selected
88 }
89 if isOpenCodeGoEntry(current) {
90 // OpenCode auto search is account-bound, including when the migrated
91 // connection has since been edited. Never fall through to another account.
92 return c.resolveOpenCodeGoAutomaticSearch(current, resolve)
93 }
94 for i := range c.Providers {
95 entry, ok := c.ResolveModel(c.Providers[i].Name)
96 if ok {
97 if selected := resolve(entry); selected != nil {
98 return selected
99 }
100 }
101 }
102 return nil
103 }
104
105 // WebSearchResolution distinguishes an invalid explicit assignment from automatic
106 // absence. Entry is a detached route frozen for one runtime.
107 type WebSearchResolution struct {
108 Entry *ProviderEntry
109 Status string
110 Reason string
111 }
112
113 // ResolveWebSearchModel resolves an exact account/model without legacy account
114 // retargeting: an explicit search assignment must never silently change accounts.
115 func (c *Config) ResolveWebSearchModel(ref string) (*ProviderEntry, error) {
116 name, model, exact := strings.Cut(strings.TrimSpace(ref), "/")
117 entry, exists := c.Provider(name)
118 if !exact || !exists || !entry.HasModel(model) {
119 var aliasErr error
120 ref, aliasErr = c.resolveOpenCodeGoAlias(ref, true)
121 if aliasErr != nil {
122 return nil, aliasErr
123 }
124 }
125 name, model, ok := strings.Cut(strings.TrimSpace(ref), "/")
126 if !ok {
127 return nil, fmt.Errorf("search model must use provider/model")
128 }
129 if c.Desktop.ProviderAccess != nil && !slices.Contains(c.Desktop.ProviderAccess, name) {
130 return nil, fmt.Errorf("search connection is not added")
131 }
132 e, found := c.Provider(name)
133 if !found || !e.HasModel(model) {
134 return nil, fmt.Errorf("search model is no longer available")
135 }
136 cp := cloneProviderEntry(*e)
137 cp.Model = model
138 cp.applyModelPrice()
139 cp.applyModelOverride()
140 if !SupportsServerWebSearch(&cp) && !IsOfficialDeepSeekSearchEndpoint(&cp) {
141 return nil, fmt.Errorf("connection does not support the native search protocol")
142 }
143 if !EffectiveIndependentWebSearch(&cp) {
144 return nil, fmt.Errorf("search is disabled on this connection")
145 }
146 if !cp.Configured() {
147 return nil, fmt.Errorf("search connection has no credentials")
148 }
149 return &cp, nil
150 }
151
152 func (c *Config) SetWebSearchModel(ref string) error {
153 ref = strings.TrimSpace(ref)
154 if ref == "" || strings.EqualFold(ref, "auto") {
155 c.Agent.WebSearchModel = "auto"
156 return nil
157 }
158 e, err := c.ResolveWebSearchModel(ref)
159 if err != nil {
160 return err
161 }
162 c.Agent.WebSearchModel = e.Name + "/" + e.Model
163 return nil
164 }
165
166 func (c *Config) ResolveWebSearch(current *ProviderEntry) WebSearchResolution {
167 if c == nil {
168 return WebSearchResolution{Status: "unavailable"}
169 }
170 if c.Environment.Offline || (len(c.Tools.Enabled) > 0 && !slices.Contains(c.Tools.Enabled, "web_search")) {
171 return WebSearchResolution{Status: "disabled"}
172 }
173 ref := strings.TrimSpace(c.Agent.WebSearchModel)
174 if ref != "" && !strings.EqualFold(ref, "auto") {
175 entry, err := c.ResolveWebSearchModel(ref)
176 if err != nil {
177 return WebSearchResolution{Status: "invalid", Reason: err.Error()}
178 }
179 route := c.resolveAutomaticWebSearchProvider(entry)
180 return WebSearchResolution{Entry: route, Status: "ready"}
181 }
182 if current != nil && current.WebSearch != nil && !*current.WebSearch && (SupportsServerWebSearch(current) || IsOfficialDeepSeekSearchEndpoint(current) || isOpenCodeGoEntry(current)) {
183 return WebSearchResolution{Status: "disabled"}
184 }
185 if isOpenCodeGoEntry(current) && c.openCodeGoJournal != nil {
186 // A saved search identity is an assignment, including in auto mode.
187 // Its disappearance must not select another account from the catalog.
188 refs := make([]string, 0, len(c.openCodeGoJournal.SearchAliases))
189 for old, alias := range c.openCodeGoJournal.SearchAliases {
190 if alias.Identity == openCodeGoIdentity(*current) {
191 refs = append(refs, old)
192 }
193 }
194 slices.Sort(refs)
195 for _, old := range refs {
196 if _, err := c.resolveHistoricalWebSearchModel(old); err != nil {
197 return WebSearchResolution{Status: "invalid", Reason: err.Error()}
198 }
199 }
200 }
201 if entry := c.resolveAutomaticWebSearchProvider(current); entry != nil {
202 return WebSearchResolution{Entry: entry, Status: "ready"}
203 }
204 return WebSearchResolution{Status: "unavailable"}
205 }
206
207 func isOpenCodeGoEntry(e *ProviderEntry) bool {
208 if e == nil {
209 return false
210 }
211 _, ok := provider.OpenCodeGoRequestRoute(e.Kind, e.BaseURL, e.RequestURL, e.ChatURL)
212 return ok
213 }
214
215 // ResolveWebSearchProvider retains the existing API for consumers that only need
216 // the route. Runtime registration uses ResolveWebSearch to surface invalid refs.
217 func (c *Config) ResolveWebSearchProvider(current *ProviderEntry) *ProviderEntry {
218 return c.ResolveWebSearch(current).Entry
219 }
220
220 lines GO