返回 DeepSeek-Reasonix
reasoning_preflight.go
根目录 / internal / boot / reasoning_preflight.go
1 package boot
2
3 import (
4 "fmt"
5 "slices"
6 "strings"
7
8 "reasonix/internal/config"
9 "reasonix/internal/event"
10 "reasonix/internal/extension/providerext"
11 "reasonix/internal/provider"
12 )
13
14 // RoleReasoningError retains the adapter error for errors.As while giving every
15 // frontend enough context to fix the role that actually prevented assembly.
16 type RoleReasoningError struct {
17 Role, Ref, Effort, Source, Kind string
18 Supported []string
19 Err error
20 }
21
22 func (e *RoleReasoningError) Error() string {
23 return fmt.Sprintf("%s model %q: effort=%q (source=%s, API=%s, supported=%v): %v", e.Role, e.Ref, e.Effort, e.Source, e.Kind, e.Supported, e.Err)
24 }
25
26 func (e *RoleReasoningError) Unwrap() error { return e.Err }
27
28 // ValidateReasoningSnapshot lets Desktop validate a pending configuration
29 // before a workspace correction is allowed to retire its current controller.
30 func ValidateReasoningSnapshot(cfg *config.Config, opts Options) error {
31 opts = rebindReasoningSelection(cfg, opts)
32 return preflightRoleReasoning(cfg, opts, opts.ProviderResolver, false)
33 }
34
35 func rebindReasoningSelection(cfg *config.Config, opts Options) Options {
36 if opts.EffortModel != "" {
37 target := opts.Model
38 if target == "" {
39 target, _, _ = cfg.ResolveNewSessionChatModel()
40 }
41 opts.EffortOverride = config.RebindSessionEffort(cfg, opts.EffortModel, target, opts.EffortOverride)
42 }
43 return opts
44 }
45
46 func resolveBuildSelection(root string, opts Options) (*config.Config, Options, error) {
47 cfg, err := resolveBuildConfiguration(root, opts.Model, opts.ConfigSnapshot)
48 if err != nil {
49 return nil, opts, err
50 }
51 if err := opts.ModelSettings.Apply(cfg, root); err != nil {
52 return nil, opts, err
53 }
54 return cfg, rebindReasoningSelection(cfg, opts), nil
55 }
56
57 // resolveBuildConfiguration gives a caller-owned snapshot the runtime contract
58 // LoadModelRuntimeSnapshot provides: legacy refs expanded, then credentials
59 // frozen so no lazy resolver rereads the store during the runtime's lifetime.
60 func resolveBuildConfiguration(root, modelRef string, snapshot *config.Config) (*config.Config, error) {
61 if snapshot != nil {
62 config.NormalizeLegacyMimoCustomProvidersForRefs(snapshot, modelRef)
63 snapshot.FreezeProviderCredentials()
64 return snapshot, nil
65 }
66 return config.LoadModelRuntimeSnapshot(root, modelRef)
67 }
68
69 // explicitVisionModel returns the configured vision reference, or "" when the
70 // runtime selects one within the active provider ("auto" or unset).
71 func explicitVisionModel(cfg *config.Config) string {
72 ref := strings.TrimSpace(cfg.Agent.VisionModel)
73 if strings.EqualFold(ref, "auto") {
74 return ""
75 }
76 return ref
77 }
78
79 // preflightRoleReasoning uses the same immutable config snapshot as assembly.
80 // The first pass excludes extension refs because their adapter declarations are
81 // obtained by the extension handshake; the second pass validates those refs
82 // before any session, workspace lease, MCP tools or controller is constructed.
83 func preflightRoleReasoning(cfg *config.Config, opts Options, resolver provider.Resolver, extensionsOnly bool) error {
84 model := strings.TrimSpace(opts.Model)
85 if model == "" {
86 model, _, _ = cfg.ResolveNewSessionChatModel()
87 }
88 type roleSelection struct {
89 role, ref, source string
90 effort *string
91 // optional roles are constructed lazily at use time, so an unresolvable
92 // reference keeps that fallback; only a resolvable one is validated.
93 optional bool
94 }
95 roles := []roleSelection{
96 {role: "execution", ref: model, source: "session effort override", effort: opts.EffortOverride},
97 {role: "planner", ref: effectivePlannerModel(cfg, opts)},
98 {role: "vision", ref: explicitVisionModel(cfg), optional: true},
99 {role: "guardian", ref: cfg.Agent.GuardianModel},
100 }
101 subagentModel := strings.TrimSpace(cfg.Agent.SubagentModel)
102 if subagentModel == "" {
103 subagentModel = model
104 }
105 subagentEffort := preflightSubagentEffort(cfg, resolver, model)
106 if cfg.Agent.SubagentModel != "" || subagentEffort != nil {
107 roles = append(roles, roleSelection{role: "subagent", ref: subagentModel, source: "agent.subagent_effort", effort: subagentEffort, optional: true})
108 }
109 // Seed capacity from one map only. Adding the two attacker-controlled map
110 // lengths can overflow before make validates the allocation size.
111 keys := make([]string, 0, len(cfg.Agent.SubagentModels))
112 for key := range cfg.Agent.SubagentModels {
113 keys = append(keys, key)
114 }
115 for key := range cfg.Agent.SubagentEfforts {
116 if !slices.Contains(keys, key) {
117 keys = append(keys, key)
118 }
119 }
120 slices.Sort(keys)
121 for _, key := range keys {
122 ref := strings.TrimSpace(cfg.Agent.SubagentModels[key])
123 if ref == "" {
124 ref = subagentModel
125 }
126 effort, source := subagentEffort, "agent.subagent_effort"
127 if value := cfg.Agent.SubagentEfforts[key]; value != "" {
128 effort, source = &value, "agent.subagent_efforts."+key
129 }
130 roles = append(roles, roleSelection{role: "subagent[" + key + "]", ref: ref, source: source, effort: effort, optional: true})
131 }
132 for _, selection := range roles {
133 ref := strings.TrimSpace(selection.ref)
134 if ref == "" || (providerext.PluginRefOwner(ref) != "") != extensionsOnly {
135 continue
136 }
137 entry, resolved, err := resolveModelEntry(resolver, cfg, ref)
138 if err != nil {
139 // A migrated account identity must still fail closed, and a
140 // configured invalid effort is checked whenever the model resolves.
141 if selection.optional && cfg.ModelReferenceError(ref) == nil && !extensionsOnly {
142 continue
143 }
144 return fmt.Errorf("%s_model %q: %w", selection.role, ref, err)
145 }
146 copy := *config.ResolveReasoningEntry(entry)
147 source := "provider default"
148 if copy.Effort != "" {
149 source = "providers." + copy.Name + ".effort"
150 } else if copy.DefaultEffort != "" {
151 source = "providers." + copy.Name + ".default_effort"
152 if raw, ok := cfg.Provider(copy.Name); ok && raw.ModelOverrides[copy.Model].DefaultEffort != "" {
153 source = "providers." + copy.Name + ".model_overrides." + copy.Model + ".default_effort"
154 }
155 }
156 if selection.effort != nil {
157 copy.Effort, source = *selection.effort, selection.source
158 if copy.Kind == "anthropic" && copy.Effort != "" && copy.Thinking == "" {
159 copy.Thinking = "adaptive"
160 }
161 }
162 cap := config.ReasoningCapabilityForEntry(&copy)
163 effort := config.EffectiveEffort(&copy)
164 if err := cap.Validate(copy.Model, effort); err != nil {
165 if effort == "" {
166 effort = cap.Default
167 }
168 return &RoleReasoningError{selection.role, resolved, effort, source, copy.Kind, cap.IDs(), err}
169 }
170 }
171 return nil
172 }
173
174 // inheritedSubagentEffort is agent.subagent_effort as a subagent that follows
175 // the execution model receives it. The setting is a cross-model default, so a
176 // model that cannot take it falls back to its own default and the dropped value
177 // is returned for a notice; paired with agent.subagent_model it stays strict.
178 func inheritedSubagentEffort(cfg *config.Config, execution *config.ProviderEntry) (effort, dropped string) {
179 effort = strings.TrimSpace(cfg.Agent.SubagentEffort)
180 if effort == "" || strings.TrimSpace(cfg.Agent.SubagentModel) != "" || execution == nil {
181 return effort, ""
182 }
183 entry := *config.ResolveReasoningEntry(execution)
184 entry.Effort = effort
185 if entry.Kind == "anthropic" && entry.Thinking == "" {
186 entry.Thinking = "adaptive"
187 }
188 if config.ReasoningCapabilityForEntry(&entry).Validate(entry.Model, config.EffectiveEffort(&entry)) != nil {
189 return "", effort
190 }
191 return effort, ""
192 }
193
194 func preflightSubagentEffort(cfg *config.Config, resolver provider.Resolver, model string) *string {
195 value := strings.TrimSpace(cfg.Agent.SubagentEffort)
196 if execution, _, err := resolveModelEntry(resolver, cfg, model); err == nil {
197 value, _ = inheritedSubagentEffort(cfg, execution)
198 }
199 if value == "" {
200 return nil
201 }
202 return &value
203 }
204
205 // subagentEffortDefault is the agent.subagent_effort the assembled runtime
206 // hands its subagents, with a notice naming a value the model could not take.
207 func subagentEffortDefault(cfg *config.Config, execution *config.ProviderEntry, sink event.Sink) string {
208 effort, dropped := inheritedSubagentEffort(cfg, execution)
209 if dropped != "" {
210 sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Text: "Subagents use the model's default effort.", Detail: fmt.Sprintf("agent.subagent_effort = %q is not supported by %q, which subagents inherit because agent.subagent_model is unset; the setting is kept and applies again on a model that supports it.", dropped, execution.Model)})
211 }
212 return effort
213 }
214
214 lines GO