返回 DeepSeek-Reasonix
model_settings.go
根目录 / desktop / model_settings.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "log/slog"
7 "strings"
8
9 "reasonix/internal/config"
10 "reasonix/internal/control"
11 "reasonix/internal/extension/providerext"
12 )
13
14 var errModelSettingsSuperseded = errors.New("model settings changed while building the session")
15
16 type tabModelApplicationState struct {
17 startupRetry bool
18 failure *modelSettingsApplyFailure
19 }
20
21 type modelSettingsApplyFailure struct {
22 controller control.SessionAPI
23 revision string
24 message string
25 }
26
27 type modelSettingsSnapshot interface {
28 ModelSettingsState() (applied, desired string, err error)
29 }
30
31 func modelSettingsNeedApply(ctrl control.SessionAPI) (bool, error) {
32 snapshot, ok := ctrl.(modelSettingsSnapshot)
33 if !ok {
34 return false, nil
35 }
36 applied, desired, err := snapshot.ModelSettingsState()
37 return applied != desired, err
38 }
39
40 // Model writes own only persisted configuration. Runtime admission applies the
41 // latest effective snapshot before accepting the next run, including when the
42 // write came from another process. Saving never requires a visible session.
43 func (a *App) applyModelConfigChange(mutate func(*config.Config) error) error {
44 _, err := a.applyModelConfigChangeWithWarning("model settings", mutate)
45 return err
46 }
47
48 func (a *App) applyModelConfigChangeWithWarning(setting string, mutate func(*config.Config) error) (string, error) {
49 return a.applyModelConfigChangeWithSave(setting, mutate, nil)
50 }
51
52 func (a *App) applyModelConfigChangeWithSave(setting string, mutate func(*config.Config) error, save func(*config.Config, string) error) (string, error) {
53 err := func() error {
54 unlock := config.LockUserConfigEdits()
55 defer unlock()
56 unlockCredentials, err := config.LockUserCredentialEdits()
57 if err != nil {
58 return err
59 }
60 defer unlockCredentials()
61 cfg, path, err := a.loadDesktopUserConfigForEdit()
62 if err != nil {
63 return err
64 }
65 baseline := cfg.ModelSettingsBaseline()
66 defer cfg.CleanupStagedModelCredentialsLocked(path)
67 if err := mutate(cfg); err != nil {
68 return err
69 }
70 if save == nil {
71 return cfg.SaveModelSettingsTo(path, baseline)
72 }
73 return save(cfg, path)
74 }()
75 if err != nil {
76 return "", err
77 }
78 a.modelSettingsSaved(setting)
79 return "", nil
80 }
81
82 func (a *App) modelSettingsSaved(setting string) {
83 a.invalidateAuxiliaryProviderOperations()
84 a.mu.RLock()
85 count, active := len(a.tabs), a.activeTabID != ""
86 var retry []*WorkspaceTab
87 var apply []string
88 for _, tab := range a.tabs {
89 if tab != nil && tab.Ctrl == nil && tab.modelApplication.startupRetry {
90 retry = append(retry, tab)
91 }
92 if tab != nil && tab.Ctrl != nil {
93 apply = append(apply, tab.ID)
94 }
95 }
96 a.mu.RUnlock()
97 slog.Debug("model settings persisted", "setting", setting, "visibleSessions", count, "hasActiveSession", active)
98 a.refreshActiveTabMetaExtras()
99 for _, id := range apply {
100 a.scheduleDeferredRebuild(id, "saved model settings")
101 }
102 if a.ctx != nil {
103 for _, tab := range retry {
104 a.scheduleDeferredStartupBuild(tab.ID)
105 }
106 }
107 }
108
109 // refreshTabModelSettings runs outside the turn admission read lock. The
110 // existing rebuild owner protects build/swap; no second scheduling authority.
111 func (a *App) refreshTabModelSettings(tab *WorkspaceTab) error {
112 a.runtimeRebuildMu.Lock()
113 defer a.runtimeRebuildMu.Unlock()
114 tab.turnStartMu.Lock()
115 defer tab.turnStartMu.Unlock()
116 for {
117 if err := a.reqCtx().Err(); err != nil {
118 return err
119 }
120 current := a.controllerForTab(tab)
121 snapshot, ok := current.(modelSettingsSnapshot)
122 if !ok {
123 return nil
124 }
125 applied, attempted, err := snapshot.ModelSettingsState()
126 if err != nil {
127 return fmt.Errorf("read saved model settings: %w", err)
128 }
129 if applied == attempted {
130 return nil
131 }
132 a.mu.RLock()
133 failure := tab.modelApplication.failure
134 a.mu.RUnlock()
135 if failure != nil && failure.controller == current && failure.revision == attempted {
136 return newModelApplicationError(current, errors.New(failure.message))
137 }
138 if err := a.rebuildSettingTurnLocked("saved model settings", tab, false, true); err != nil {
139 if errors.Is(err, errModelSettingsSuperseded) {
140 continue
141 }
142 var busy *rebuildBusyError
143 if errors.As(err, &busy) {
144 a.scheduleDeferredRebuild(tab.ID, "saved model settings")
145 }
146 if current != nil && !errors.As(err, &busy) {
147 a.mu.Lock()
148 if a.ownsRuntimeTabLocked(tab) && tab.Ctrl == current {
149 tab.modelApplication.failure = &modelSettingsApplyFailure{current, attempted, modelSettingsIssue("apply_failed", err).Message}
150 }
151 a.mu.Unlock()
152 }
153 return newModelApplicationError(current, err)
154 }
155 return nil
156 }
157 }
158
159 // Preserve a selected model while it is still accessible; a removed selection
160 // may only fall back to an explicitly available and configured Desktop model.
161 func resolveModelSettingsRuntime(c *config.Config, model string) (string, error) {
162 if providerext.PluginRefOwner(model) != "" {
163 return model, nil
164 }
165 if p, ok := c.ResolveModel(model); ok && modelProviderAccessAllowed(c.Desktop.ProviderAccess, p.Name) && p.Configured() {
166 return p.Name + "/" + p.Model, nil
167 }
168 if ref := resolveNewSessionModel(c); strings.TrimSpace(ref) != "" {
169 if p, ok := c.ResolveModel(ref); ok && modelProviderAccessAllowed(c.Desktop.ProviderAccess, p.Name) && p.Configured() {
170 return p.Name + "/" + p.Model, nil
171 }
172 }
173 return "", fmt.Errorf("no configured model is available; choose a model in Settings before starting another run")
174 }
175
176 func (a *App) sessionPathForSettingsRebuild(tab *WorkspaceTab) string {
177 if path := a.reconciledSessionPathForTab(tab); path != "" {
178 return path
179 }
180 return a.currentSessionPathFor(tab)
181 }
182
183 func validateModelSettingsReplacement(ctrl, previous control.SessionAPI) error {
184 if stale, err := modelSettingsNeedApply(ctrl); err != nil || stale {
185 if ctrl != previous {
186 discardReplacementController(ctrl, previous)
187 }
188 if err != nil {
189 return err
190 }
191 return errModelSettingsSuperseded
192 }
193 return nil
194 }
195
195 lines GO