返回 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 }
20
21 func NewLocalProviderResolver(cfg *config.Config, proxy netclient.ProxySpec) *LocalProviderResolver {
22 return &LocalProviderResolver{cfg: cfg, proxy: proxy}
23 }
24
25 func (r *LocalProviderResolver) Catalog() []provider.Descriptor {
26 if r == nil || r.cfg == nil {
27 return nil
28 }
29 out := make([]provider.Descriptor, 0, len(r.cfg.Providers))
30 for i := range r.cfg.Providers {
31 e := &r.cfg.Providers[i]
32 ref := modelRefFromEntry(e)
33 d := provider.Descriptor{
34 Ref: ref, DisplayName: e.Name, Model: e.Model,
35 ContextWindow: e.ContextWindow, Vision: config.EffectiveVision(e),
36 Tools: true, DefaultEffort: config.EffectiveEffort(e),
37 }
38 if price := e.PriceForModel(e.Model); price != nil {
39 d.PricingCurrency = price.Currency
40 d.CacheHitPerMillion = price.CacheHit
41 d.InputPerMillion = price.Input
42 d.OutputPerMillion = price.Output
43 }
44 if len(e.SupportedEfforts) > 0 {
45 d.Efforts = append([]string(nil), e.SupportedEfforts...)
46 d.Reasoning = true
47 }
48 if config.ReasoningProtocolForEntry(e) == config.ReasoningProtocolDeepSeek {
49 d.ToolCallReasoning = true
50 d.Reasoning = true
51 }
52 out = append(out, d)
53 }
54 return out
55 }
56
57 func (r *LocalProviderResolver) Resolve(selection provider.Selection) (provider.Provider, error) {
58 if r == nil || r.cfg == nil {
59 return nil, fmt.Errorf("local provider resolver is not configured")
60 }
61 ref := strings.TrimSpace(selection.Ref)
62 if ref == "" {
63 return nil, fmt.Errorf("provider selection ref is required")
64 }
65 entry, ok := r.cfg.ResolveModel(ref)
66 if !ok {
67 return nil, fmt.Errorf("%w %q", ErrUnknownModel, ref)
68 }
69 if selection.Effort != nil {
70 entry.Effort = *selection.Effort
71 }
72 return NewProviderWithProxy(entry, r.proxy)
73 }
74
75 func resolveProvider(resolver provider.Resolver, cfg *config.Config, proxy netclient.ProxySpec, selection provider.Selection) (provider.Provider, error) {
76 if resolver != nil {
77 return resolver.Resolve(selection)
78 }
79 return NewLocalProviderResolver(cfg, proxy).Resolve(selection)
80 }
81
82 // mergeSidecarProviders wraps the build's resolver with the extension-hosted
83 // provider adapter (stage 7) whenever a started sidecar declared providers in
84 // its handshake. The base resolver — the caller-owned broker when
85 // opts.ProviderResolver is set, the local config-backed one otherwise — keeps
86 // serving every non-plugin ref; plugin providers are additive on top of it,
87 // and an exact-ref collision without the plugin's provider:<ref> claim is a
88 // *providerext.ConflictError (fatal at the call site). The merged Resolver is
89 // also the sidecar clients' stream router, installed here so inbound
90 // stream/chunk and stream/end notifications reach the buffered streams.
91 func mergeSidecarProviders(base provider.Resolver, mgr *sidecar.Manager, claims map[extension.Slot]extension.ContributionSource) (provider.Resolver, error) {
92 if mgr == nil {
93 return base, nil
94 }
95 declares := false
96 for _, client := range mgr.Clients() {
97 if len(client.Handshake().Providers) > 0 {
98 declares = true
99 break
100 }
101 }
102 if !declares {
103 return base, nil
104 }
105 clientsFn := func() []providerext.ProviderClient {
106 clients := mgr.Clients()
107 out := make([]providerext.ProviderClient, 0, len(clients))
108 for _, client := range clients {
109 out = append(out, client)
110 }
111 return out
112 }
113 merged, err := providerext.New(base, clientsFn, claims)
114 if err != nil {
115 return nil, err
116 }
117 for _, client := range mgr.Clients() {
118 client.SetStreamRouter(merged)
119 }
120 return merged, nil
121 }
122
123 func modelRefFromEntry(e *config.ProviderEntry) string {
124 if e == nil {
125 return ""
126 }
127 if strings.TrimSpace(e.Model) == "" {
128 return e.Name
129 }
130 return e.Name + "/" + e.Model
131 }
132
133 // resolveModelEntry synthesizes only non-secret metadata from the resolver
134 // catalog. A caller-owned (or extension-merged) resolver is authoritative even
135 // when the credential-free Host happens to contain a provider with the same
136 // ref. The unknown-model error names every ref the session could have used,
137 // including plugin-namespaced refs a merged extension resolver serves.
138 func resolveModelEntry(resolver provider.Resolver, cfg *config.Config, modelName string) (*config.ProviderEntry, string, error) {
139 if resolver != nil {
140 entry := syntheticEntryFromResolver(resolver, modelName)
141 if strings.TrimSpace(entry.Name) != "" {
142 return entry, modelRefFromEntry(entry), nil
143 }
144 }
145 if entry, ok := cfg.ResolveModel(modelName); ok {
146 return entry, modelRefFromEntry(entry), nil
147 }
148 available := providerNames(cfg)
149 if pluginRefs := extensionCatalogRefs(resolver); len(pluginRefs) > 0 {
150 if available != "" {
151 available += "/"
152 }
153 available += strings.Join(pluginRefs, "/")
154 }
155 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)
156 }
157
158 // extensionCatalogRefs returns the plugin-namespaced refs a resolver's catalog
159 // serves, for error messages and pickers that merge extension providers with
160 // the config's own. Nil-safe: no resolver (or no plugin refs) → nil.
161 func extensionCatalogRefs(resolver provider.Resolver) []string {
162 if resolver == nil {
163 return nil
164 }
165 var out []string
166 for _, d := range resolver.Catalog() {
167 if providerext.PluginRefOwner(d.Ref) != "" {
168 out = append(out, d.Ref)
169 }
170 }
171 return out
172 }
173
174 func resolveOptionalEntry(resolver provider.Resolver, cfg *config.Config, ref string) (*config.ProviderEntry, bool) {
175 if resolver != nil {
176 entry := syntheticEntryFromResolver(resolver, ref)
177 if strings.TrimSpace(entry.Name) != "" {
178 return entry, true
179 }
180 }
181 entry, ok := cfg.ResolveModel(ref)
182 return entry, ok
183 }
184
185 func syntheticEntryFromResolver(r provider.Resolver, ref string) *config.ProviderEntry {
186 ref = strings.TrimSpace(ref)
187 if r == nil || ref == "" {
188 return &config.ProviderEntry{}
189 }
190 var match *provider.Descriptor
191 for _, d := range r.Catalog() {
192 if d.Ref == ref || d.DisplayName == ref || d.Model == ref || strings.HasPrefix(d.Ref, ref+"/") {
193 copy := d
194 match = &copy
195 break
196 }
197 }
198 if match == nil {
199 return &config.ProviderEntry{}
200 }
201 name, model := splitProviderRef(match.Ref)
202 if model == "" {
203 model = match.Model
204 }
205 if name == "" {
206 name = match.DisplayName
207 }
208 contextWindow := match.ContextWindow
209 if contextWindow <= 0 {
210 contextWindow = 128_000
211 }
212 entry := &config.ProviderEntry{
213 Name: name, Model: model, ContextWindow: contextWindow,
214 SupportedEfforts: append([]string(nil), match.Efforts...),
215 DefaultEffort: match.DefaultEffort, Vision: match.Vision,
216 }
217 if match.CacheHitPerMillion > 0 || match.InputPerMillion > 0 || match.OutputPerMillion > 0 {
218 entry.Price = &provider.Pricing{CacheHit: match.CacheHitPerMillion, Input: match.InputPerMillion, Output: match.OutputPerMillion, Currency: match.PricingCurrency}
219 }
220 return entry
221 }
222
223 func splitProviderRef(ref string) (string, string) {
224 ref = strings.TrimSpace(ref)
225 if i := strings.IndexByte(ref, '/'); i >= 0 {
226 return ref[:i], ref[i+1:]
227 }
228 return ref, ""
229 }
230
230 lines GO