返回 DeepSeek-Reasonix
auxiliary_provider.go
根目录 / internal / boot / auxiliary_provider.go
1 package boot
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "sync"
8
9 "reasonix/internal/config"
10 "reasonix/internal/extension"
11 "reasonix/internal/extension/protocol"
12 "reasonix/internal/extension/sidecar"
13 "reasonix/internal/provider"
14 )
15
16 // AuxiliaryProviderRequest describes a bounded provider-only operation that
17 // belongs to a durable session but must not construct a conversation
18 // controller. Extension sidecars receive the target session identity.
19 type AuxiliaryProviderRequest struct {
20 Config *config.Config
21 SessionID string
22 WorkspaceRoot string
23 ModelRef string
24 OnWarning func(string)
25 }
26
27 // AuxiliaryProviderHandle owns the resolver and any provider sidecars started
28 // for one auxiliary request. Close is idempotent.
29 type AuxiliaryProviderHandle struct {
30 Resolver provider.Resolver
31 Generation uint64
32
33 once sync.Once
34 manager *sidecar.Manager
35 }
36
37 // Close retires only the sidecars owned by this auxiliary generation.
38 func (h *AuxiliaryProviderHandle) Close() error {
39 if h == nil {
40 return nil
41 }
42 var err error
43 h.once.Do(func() {
44 if h.manager != nil {
45 err = h.manager.Close()
46 }
47 })
48 return err
49 }
50
51 // AcquireAuxiliaryProvider builds the same config+extension provider resolver
52 // used by normal boot without creating a Controller or publishing tools, MCP,
53 // hooks, commands, or extension UI into an active chat runtime.
54 func AcquireAuxiliaryProvider(ctx context.Context, request AuxiliaryProviderRequest) (*AuxiliaryProviderHandle, error) {
55 if request.Config == nil {
56 return nil, fmt.Errorf("auxiliary provider config is required")
57 }
58 sessionID := strings.TrimSpace(request.SessionID)
59 root := strings.TrimSpace(request.WorkspaceRoot)
60 if sessionID == "" || root == "" {
61 return nil, fmt.Errorf("auxiliary provider target identity is required")
62 }
63 generation := nextRuntimeGeneration()
64 base := NewLocalProviderResolverWithCapabilities(
65 request.Config,
66 request.Config.NetworkProxySpec(),
67 config.NewModelCapabilityResolver(),
68 )
69 modelRef := strings.TrimSpace(request.ModelRef)
70 if modelRef == "" {
71 modelRef = strings.TrimSpace(request.Config.DefaultModel)
72 }
73 // Config-backed providers need no sidecar at all. This is both cheaper and
74 // prevents unrelated mixed-capability extensions from receiving a session
75 // context for an operation they do not own.
76 if _, err := base.Resolve(provider.Selection{Ref: modelRef}); err == nil {
77 return &AuxiliaryProviderHandle{Resolver: base, Generation: generation}, nil
78 }
79 pluginName := auxiliaryProviderPluginName(modelRef)
80 if pluginName == "" {
81 return nil, fmt.Errorf("auxiliary provider model %q is unavailable", modelRef)
82 }
83 manager, warnings, err := sidecar.StartPackagesByName(ctx, config.ReasonixHomeDir(), protocol.SessionContext{
84 SessionID: sessionID, WorkspaceRoot: root, Generation: generation,
85 }, nil, pluginName)
86 for _, warning := range warnings {
87 if request.OnWarning != nil {
88 request.OnWarning(warning)
89 }
90 }
91 if err != nil {
92 return nil, err
93 }
94 handle := &AuxiliaryProviderHandle{Resolver: base, Generation: generation, manager: manager}
95 if manager == nil {
96 return handle, nil
97 }
98 claims, err := resolveReplacementClaims(manager.Contributions())
99 if err != nil {
100 _ = handle.Close()
101 return nil, err
102 }
103 merged, err := mergeSidecarProviders(base, manager, claims, extension.NewRuntimeOwner())
104 if err != nil {
105 _ = handle.Close()
106 return nil, err
107 }
108 installSidecarStreamRouters(manager, merged)
109 if _, err := merged.Resolve(provider.Selection{Ref: modelRef}); err != nil {
110 _ = handle.Close()
111 return nil, fmt.Errorf("auxiliary provider model %q is unavailable: %w", modelRef, err)
112 }
113 handle.Resolver = merged
114 return handle, nil
115 }
116
117 func auxiliaryProviderPluginName(modelRef string) string {
118 parts := strings.Split(strings.TrimSpace(modelRef), "/")
119 if len(parts) < 4 || parts[0] != "plugin" {
120 return ""
121 }
122 return strings.TrimSpace(parts[1])
123 }
124
124 lines GO