返回 DeepSeek-Reasonix
model_runtime_settings.go
根目录 / internal / config / model_runtime_settings.go
1 package config
2
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "fmt"
8 "os"
9 "path/filepath"
10 "reflect"
11 "strings"
12
13 "github.com/BurntSushi/toml"
14 )
15
16 func NewModelSettingsOfferID() (string, error) {
17 var id [16]byte
18 if _, err := rand.Read(id[:]); err != nil {
19 return "", err
20 }
21 return hex.EncodeToString(id[:]), nil
22 }
23
24 // ModelRuntimeSettings is an in-memory model resolver bundle. Desktop sends
25 // only tunnel tokens in Credentials; actual provider credentials remain local.
26 // It is never serialized into session history or a system prompt.
27 type ModelRuntimeSettings struct {
28 SourceToken string `json:"sourceToken,omitempty"`
29 OfferID string `json:"offerID,omitempty"`
30 ProxyURL string `json:"proxyURL"`
31 Revision string `json:"revision"`
32 Providers []ProviderEntry `json:"providers"`
33 Credentials map[string]string `json:"credentials"`
34 References map[string]string `json:"references"`
35 Preferences ModelRuntimePreferences `json:"preferences"`
36 }
37
38 // ModelSettingsSourceRequest is an authenticated, transient tunnel exchange.
39 // Offer ownership protects candidate routes until Serve publishes or rejects
40 // them. IDs are random correlation values and never enter user configuration.
41 type ModelSettingsSourceRequest struct {
42 ModelSettingsOwnership
43 Mode string `json:"mode"`
44 OfferID string `json:"offerID"`
45 PreviousOfferID string `json:"previousOfferID,omitempty"`
46 Model string `json:"model,omitempty"`
47 AppliedRevision string `json:"appliedRevision,omitempty"`
48 RemotePort int `json:"remotePort,omitempty"`
49 OwnedRevisions []string `json:"ownedRevisions"`
50 UnversionedOwners bool `json:"unversionedOwners"`
51 }
52
53 // ModelSettingsOwnership orders complete owner snapshots within one Serve.
54 type ModelSettingsOwnership struct {
55 OwnershipIncarnation string `json:"ownershipIncarnation"`
56 OwnershipSeq uint64 `json:"ownershipSeq"`
57 }
58
59 type ModelSettingsSourceResponse struct {
60 CanContinue bool `json:"canContinue,omitempty"`
61 ConnectionTarget string `json:"connectionTarget,omitempty"`
62 ContinuationUnavailable string `json:"continuationUnavailable,omitempty"`
63 Version int `json:"version"`
64 Revision string `json:"revision"`
65 Ref string `json:"ref,omitempty"`
66 Settings *ModelRuntimeSettings `json:"settings,omitempty"`
67 }
68
69 type ModelRuntimePreferences struct {
70 PlannerModel string `json:"plannerModel" toml:"planner_model"`
71 VisionModel string `json:"visionModel" toml:"vision_model"`
72 WebSearchModel string `json:"webSearchModel" toml:"web_search_model"`
73 GuardianModel string `json:"guardianModel" toml:"guardian_model"`
74 RecoveryModel string `json:"recoveryModel" toml:"recovery_model"`
75 SubagentModel string `json:"subagentModel" toml:"subagent_model"`
76 SubagentModels map[string]string `json:"subagentModels" toml:"subagent_models"`
77 SubagentEffort string `json:"subagentEffort" toml:"subagent_effort"`
78 SubagentEfforts map[string]string `json:"subagentEfforts" toml:"subagent_efforts"`
79 MaxSubagentDepth int `json:"maxSubagentDepth" toml:"max_subagent_depth"`
80 MaxSubagentConcurrency int `json:"maxSubagentConcurrency" toml:"max_subagent_concurrency"`
81 MaxParallelWriters int `json:"maxParallelWriters" toml:"max_parallel_writers"`
82 }
83
84 func (c *Config) RuntimeModelPreferences() ModelRuntimePreferences {
85 var out ModelRuntimePreferences
86 src, dst := reflect.ValueOf(c.Agent), reflect.ValueOf(&out).Elem()
87 for i := range dst.NumField() {
88 dst.Field(i).Set(src.FieldByName(dst.Type().Field(i).Name))
89 }
90 return out
91 }
92
93 // Apply overlays the desktop-managed resolver, retaining explicitly configured
94 // project models/preferences. The loaded Config is private to this boot; clone
95 // the input first so concurrent boot builders never share mutable maps.
96 func (settings *ModelRuntimeSettings) Apply(c *Config, root string) error {
97 if settings == nil {
98 return nil
99 }
100 raw, err := json.Marshal(settings)
101 if err != nil {
102 return fmt.Errorf("encode model snapshot: %w", err)
103 }
104 var frozen ModelRuntimeSettings
105 if err := json.Unmarshal(raw, &frozen); err != nil {
106 return err
107 }
108 var project map[string]any
109 projectRaw, err := os.ReadFile(filepath.Join(root, "reasonix.toml"))
110 if err != nil && !os.IsNotExist(err) {
111 return err
112 }
113 if len(projectRaw) > 0 {
114 if _, err := toml.Decode(string(projectRaw), &project); err != nil {
115 return err
116 }
117 }
118 projectProviders := map[string]bool{}
119 if entries, ok := project["providers"].([]map[string]any); ok {
120 for _, entry := range entries {
121 if name, ok := entry["name"].(string); ok {
122 projectProviders[name] = true
123 }
124 }
125 }
126 retained := make([]ProviderEntry, 0, len(projectProviders))
127 projectEntries := map[string]ProviderEntry{}
128 for _, p := range c.Providers {
129 if projectProviders[p.Name] {
130 retained = append(retained, p)
131 projectEntries[p.Name] = p
132 }
133 }
134 // The Desktop expands multi-model providers into separately credentialed
135 // routes. Preserve project overrides under those transport aliases too.
136 sourceNames := map[string]string{}
137 for source, target := range frozen.References {
138 sourceName, _, _ := strings.Cut(source, "/")
139 targetName, _, _ := strings.Cut(target, "/")
140 sourceNames[targetName] = sourceName
141 }
142 c.Providers = nil
143 seen := map[string]bool{}
144 for _, p := range frozen.Providers {
145 if strings.TrimSpace(p.Name) == "" || seen[p.Name] {
146 return fmt.Errorf("model snapshot has an empty or duplicate provider")
147 }
148 seen[p.Name] = true
149 if projectEntry, ok := projectEntries[sourceNames[p.Name]]; ok {
150 alias := projectEntry
151 alias.Name = p.Name
152 c.Providers = append(c.Providers, alias)
153 continue
154 }
155 if projectProviders[p.Name] {
156 continue
157 }
158 p.resolvedAPIKey, p.credentialsFrozen = frozen.Credentials[p.Name], true
159 p.credentialProxyURL = frozen.ProxyURL
160 c.Providers = append(c.Providers, p)
161 }
162 c.Providers = append(c.Providers, retained...)
163 c.Desktop.ProviderAccess = nil
164 for _, p := range c.Providers {
165 c.Desktop.ProviderAccess = append(c.Desktop.ProviderAccess, p.Name)
166 }
167 declared, _ := project["agent"].(map[string]any)
168 src, dst := reflect.ValueOf(frozen.Preferences), reflect.ValueOf(&c.Agent).Elem()
169 for i := range src.NumField() {
170 field := src.Type().Field(i)
171 if _, explicit := declared[field.Tag.Get("toml")]; !explicit {
172 dst.FieldByName(field.Name).Set(src.Field(i))
173 }
174 }
175 // Explicit project assignments still use the user's provider names. Route
176 // them to the managed alias only when the project has no provider override.
177 mapRef := func(ref string) string {
178 name, _, _ := strings.Cut(ref, "/")
179 if mapped := frozen.References[ref]; mapped != "" && !projectProviders[name] {
180 return mapped
181 }
182 return ref
183 }
184 for _, ref := range []*string{&c.Agent.PlannerModel, &c.Agent.VisionModel, &c.Agent.WebSearchModel, &c.Agent.GuardianModel, &c.Agent.RecoveryModel, &c.Agent.SubagentModel} {
185 *ref = mapRef(*ref)
186 }
187 for name, ref := range c.Agent.SubagentModels {
188 c.Agent.SubagentModels[name] = mapRef(ref)
189 }
190 return nil
191 }
192
193 func (e *ProviderEntry) CredentialProxyURL() string { return e.credentialProxyURL }
194
194 lines GO