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