返回 DeepSeek-Reasonix
resolver.go
根目录 / internal / boot / resolver.go
1 package boot
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/config"
8 "reasonix/internal/extension"
9 "reasonix/internal/extension/providerext"
10 "reasonix/internal/extension/sidecar"
11 "reasonix/internal/netclient"
12 "reasonix/internal/provider"
13 )
14
15 // LocalProviderResolver preserves the historical config-backed provider path.
16 type LocalProviderResolver struct {
17 cfg *config.Config
18 proxy netclient.ProxySpec
19 capabilities *config.ModelCapabilityResolver
20 }
21
22 func NewLocalProviderResolver(cfg *config.Config, proxy netclient.ProxySpec) *LocalProviderResolver {
23 return NewLocalProviderResolverWithCapabilities(cfg, proxy, nil)
24 }
25
26 func NewLocalProviderResolverWithCapabilities(cfg *config.Config, proxy netclient.ProxySpec, capabilities *config.ModelCapabilityResolver) *LocalProviderResolver {
27 if capabilities == nil {
28 capabilities = config.NewModelCapabilityResolver()
29 }
30 return &LocalProviderResolver{cfg: cfg, proxy: proxy, capabilities: capabilities}
31 }
32
33 func (r *LocalProviderResolver) Catalog() []provider.Descriptor {
34 if r == nil || r.cfg == nil {
35 return nil
36 }
37 out := make([]provider.Descriptor, 0, len(r.cfg.Providers))
38 for i := range r.cfg.Providers {
39 base := &r.cfg.Providers[i]
40 models := base.ModelList()
41 if len(models) == 0 {
42 models = []string{base.Model}
43 }
44 for _, model := range models {
45 entry := *base
46 entry.Model = model
47 if selected, ok := r.cfg.ResolveModel(base.Name + "/" + model); ok {
48 entry = *selected
49 }
50 ref := modelRefFromEntry(&entry)
51 if resolved, ok := r.cfg.ResolveModel(ref); ok {
52 entry = *resolved
53 }
54 capability := config.ResolvedModelCapability{State: config.CapabilityUnknown}
55 if r.capabilities != nil {
56 capability = r.capabilities.Resolve(&entry)
57 }
58 d := provider.Descriptor{
59 Ref: ref, DisplayName: entry.Name, Model: entry.Model,
60 ContextWindow: entry.ContextWindow, Vision: capability.State == config.CapabilitySupported,
61 InputModalities: append([]provider.ModelModality(nil), capability.InputModalities...),
62 Tools: true, DefaultEffort: config.EffectiveEffort(&entry),
63 }
64 if capability.ModelInfo.ContextWindow > 0 && d.ContextWindow == 0 {
65 d.ContextWindow = capability.ModelInfo.ContextWindow
66 }
67 if capability.ModelInfo.Reasoning {
68 d.Reasoning = true
69 }
70 if price := entry.PriceForModel(entry.Model); price != nil {
71 d.PricingCurrency = price.Currency
72 d.CacheHitPerMillion = price.CacheHit
73 d.InputPerMillion = price.Input
74 d.OutputPerMillion = price.Output
75 }
76 reasoning := config.ReasoningCapabilityForEntry(&entry)
77 d.ReasoningUnknown = reasoning.Unknown
78 if len(reasoning.Options) > 0 {
79 d.Efforts = reasoning.IDs()
80 d.Reasoning = true
81 }
82 if config.ReasoningProtocolForEntry(&entry) == config.ReasoningProtocolDeepSeek {
83 d.ToolCallReasoning = true
84 d.Reasoning = true
85 }
86 out = append(out, d)
87 }
88 }
89 return out
90 }
91
92 func (r *LocalProviderResolver) Resolve(selection provider.Selection) (provider.Provider, error) {
93 if r == nil || r.cfg == nil {
94 return nil, fmt.Errorf("local provider resolver is not configured")
95 }
96 ref := strings.TrimSpace(selection.Ref)
97 if ref == "" {
98 return nil, fmt.Errorf("provider selection ref is required")
99 }
100 entry, ok := r.cfg.ResolveModel(ref)
101 if !ok {
102 return nil, fmt.Errorf("%w %q", ErrUnknownModel, ref)
103 }
104 if selection.Effort != nil {
105 if *selection.Effort != "" {
106 if _, err := config.NormalizeEffort(entry, *selection.Effort); err != nil {
107 return nil, err
108 }
109 }
110 entry.Effort = *selection.Effort
111 }
112 var modelInfo *provider.ModelInfo
113 if r.capabilities != nil {
114 resolved := r.capabilities.Resolve(entry)
115 info := resolved.ModelInfo
116 if info.ID == "" {
117 info = provider.ModelInfo{ID: resolved.Model, InputModalities: resolved.InputModalities}
118 }
119 modelInfo = &info
120 }
121 return NewProviderWithProxyAndModelInfo(entry, r.proxy, modelInfo)
122 }
123
124 func resolveProvider(resolver provider.Resolver, cfg *config.Config, proxy netclient.ProxySpec, selection provider.Selection) (provider.Provider, error) {
125 if resolver != nil {
126 return resolver.Resolve(selection)
127 }
128 return NewLocalProviderResolver(cfg, proxy).Resolve(selection)
129 }
130
131 // mergeSidecarProviders wraps the build's resolver with the extension-hosted
132 // provider adapter (stage 7) whenever a started sidecar declared providers.
133 // It does not install stream routers — call installSidecarStreamRouters after
134 // commit so failed narrow rebuilds never leave adopted clients on a discarded
135 // generation resolver.
136 func mergeSidecarProviders(base provider.Resolver, mgr *sidecar.Manager, claims map[extension.Slot]extension.ContributionSource, owners ...*extension.RuntimeOwner) (provider.Resolver, error) {
137 if mgr == nil {
138 return base, nil
139 }
140 declares := false
141 for _, client := range mgr.Clients() {
142 if len(client.Handshake().Providers) > 0 {
143 declares = true
144 break
145 }
146 }
147 if !declares {
148 return base, nil
149 }
150 clientsFn := func() []providerext.ProviderClient {
151 clients := mgr.Clients()
152 out := make([]providerext.ProviderClient, 0, len(clients))
153 for _, client := range clients {
154 out = append(out, client)
155 }
156 return out
157 }
158 return providerext.New(base, clientsFn, claims, owners...)
159 }
160
161 // installSidecarStreamRouters binds merged as the stream router on every live
162 // client. Call only after cold-start success or narrow-rebuild commit.
163 func installSidecarStreamRouters(mgr *sidecar.Manager, merged provider.Resolver) {
164 if mgr == nil || merged == nil {
165 return
166 }
167 router, ok := merged.(sidecar.StreamRouter)
168 if !ok {
169 return
170 }
171 for _, client := range mgr.Clients() {
172 client.SetStreamRouter(router)
173 }
174 }
175
176 func modelRefFromEntry(e *config.ProviderEntry) string {
177 if e == nil {
178 return ""
179 }
180 if strings.TrimSpace(e.Model) == "" {
181 return e.Name
182 }
183 return e.Name + "/" + e.Model
184 }
185
186 // resolveModelEntry synthesizes only non-secret metadata from the resolver
187 // catalog. A caller-owned (or extension-merged) resolver is authoritative even
188 // when the credential-free Host happens to contain a provider with the same
189 // ref. The unknown-model error names every ref the session could have used,
190 // including plugin-namespaced refs a merged extension resolver serves.
191 func resolveModelEntry(resolver provider.Resolver, cfg *config.Config, modelName string) (*config.ProviderEntry, string, error) {
192 if err := cfg.ModelReferenceError(modelName); err != nil {
193 return nil, "", err
194 }
195 if resolver != nil {
196 entry := syntheticEntryFromResolver(resolver, modelName)
197 if strings.TrimSpace(entry.Name) != "" {
198 return entry, modelRefFromEntry(entry), nil
199 }
200 }
201 if entry, ok := cfg.ResolveModel(modelName); ok {
202 return entry, modelRefFromEntry(entry), nil
203 }
204 available := providerNames(cfg)
205 if pluginRefs := extensionCatalogRefs(resolver); len(pluginRefs) > 0 {
206 if available != "" {
207 available += "/"
208 }
209 available += strings.Join(pluginRefs, "/")
210 }
211 return nil, "", fmt.Errorf("%w %q (configured: %s); note: defining [[providers]] replaces the built-in presets, so add a [[providers]] entry for it or use a configured name, or run `reasonix setup` to reconfigure", ErrUnknownModel, modelName, available)
212 }
213
214 // extensionCatalogRefs returns the plugin-namespaced refs a resolver's catalog
215 // serves, for error messages and pickers that merge extension providers with
216 // the config's own. Nil-safe: no resolver (or no plugin refs) → nil.
217 func extensionCatalogRefs(resolver provider.Resolver) []string {
218 if resolver == nil {
219 return nil
220 }
221 var out []string
222 for _, d := range resolver.Catalog() {
223 if providerext.PluginRefOwner(d.Ref) != "" {
224 out = append(out, d.Ref)
225 }
226 }
227 return out
228 }
229
230 func resolveOptionalEntry(resolver provider.Resolver, cfg *config.Config, ref string) (*config.ProviderEntry, bool) {
231 if resolver != nil {
232 entry := syntheticEntryFromResolver(resolver, ref)
233 if strings.TrimSpace(entry.Name) != "" {
234 return entry, true
235 }
236 }
237 entry, ok := cfg.ResolveModel(ref)
238 return entry, ok
239 }
240
241 func syntheticEntryFromResolver(r provider.Resolver, ref string) *config.ProviderEntry {
242 ref = strings.TrimSpace(ref)
243 if r == nil || ref == "" {
244 return &config.ProviderEntry{}
245 }
246 var match *provider.Descriptor
247 for _, d := range r.Catalog() {
248 if d.Ref == ref || d.DisplayName == ref || d.Model == ref || strings.HasPrefix(d.Ref, ref+"/") {
249 copy := d
250 match = &copy
251 break
252 }
253 }
254 if match == nil {
255 return &config.ProviderEntry{}
256 }
257 name, model := splitProviderRef(match.Ref)
258 if model == "" {
259 model = match.Model
260 }
261 if name == "" {
262 name = match.DisplayName
263 }
264 contextWindow := match.ContextWindow
265 if contextWindow <= 0 {
266 contextWindow = 128_000
267 }
268 entry := &config.ProviderEntry{
269 Name: name, Model: model, ContextWindow: contextWindow,
270 SupportedEfforts: append([]string(nil), match.Efforts...),
271 ReasoningMetadataUnknown: match.ReasoningUnknown,
272 DefaultEffort: match.DefaultEffort, Vision: match.Vision,
273 }
274 if match.CacheHitPerMillion > 0 || match.InputPerMillion > 0 || match.OutputPerMillion > 0 {
275 entry.Price = &provider.Pricing{CacheHit: match.CacheHitPerMillion, Input: match.InputPerMillion, Output: match.OutputPerMillion, Currency: match.PricingCurrency}
276 }
277 return entry
278 }
279
280 func splitProviderRef(ref string) (string, string) {
281 ref = strings.TrimSpace(ref)
282 if before, after, ok := strings.Cut(ref, "/"); ok {
283 return before, after
284 }
285 return ref, ""
286 }
287
287 lines GO