返回 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 Version int `json:"version"`
61 Revision string `json:"revision"`
62 Ref string `json:"ref,omitempty"`
63 Settings *ModelRuntimeSettings `json:"settings,omitempty"`
64 }
65
66 type ModelRuntimePreferences struct {
67 PlannerModel string `json:"plannerModel" toml:"planner_model"`
68 VisionModel string `json:"visionModel" toml:"vision_model"`
69 WebSearchModel string `json:"webSearchModel" toml:"web_search_model"`
70 GuardianModel string `json:"guardianModel" toml:"guardian_model"`
71 RecoveryModel string `json:"recoveryModel" toml:"recovery_model"`
72 SubagentModel string `json:"subagentModel" toml:"subagent_model"`
73 SubagentModels map[string]string `json:"subagentModels" toml:"subagent_models"`
74 SubagentEffort string `json:"subagentEffort" toml:"subagent_effort"`
75 SubagentEfforts map[string]string `json:"subagentEfforts" toml:"subagent_efforts"`
76 MaxSubagentDepth int `json:"maxSubagentDepth" toml:"max_subagent_depth"`
77 MaxSubagentConcurrency int `json:"maxSubagentConcurrency" toml:"max_subagent_concurrency"`
78 MaxParallelWriters int `json:"maxParallelWriters" toml:"max_parallel_writers"`
79 }
80
81 func (c *Config) RuntimeModelPreferences() ModelRuntimePreferences {
82 var out ModelRuntimePreferences
83 src, dst := reflect.ValueOf(c.Agent), reflect.ValueOf(&out).Elem()
84 for i := range dst.NumField() {
85 dst.Field(i).Set(src.FieldByName(dst.Type().Field(i).Name))
86 }
87 return out
88 }
89
90 // Apply overlays the desktop-managed resolver, retaining explicitly configured
91 // project models/preferences. The loaded Config is private to this boot; clone
92 // the input first so concurrent boot builders never share mutable maps.
93 func (settings *ModelRuntimeSettings) Apply(c *Config, root string) error {
94 if settings == nil {
95 return nil
96 }
97 raw, err := json.Marshal(settings)
98 if err != nil {
99 return fmt.Errorf("encode model snapshot: %w", err)
100 }
101 var frozen ModelRuntimeSettings
102 if err := json.Unmarshal(raw, &frozen); err != nil {
103 return err
104 }
105 var project map[string]any
106 projectRaw, err := os.ReadFile(filepath.Join(root, "reasonix.toml"))
107 if err != nil && !os.IsNotExist(err) {
108 return err
109 }
110 if len(projectRaw) > 0 {
111 if _, err := toml.Decode(string(projectRaw), &project); err != nil {
112 return err
113 }
114 }
115 projectProviders := map[string]bool{}
116 if entries, ok := project["providers"].([]map[string]any); ok {
117 for _, entry := range entries {
118 if name, ok := entry["name"].(string); ok {
119 projectProviders[name] = true
120 }
121 }
122 }
123 retained := make([]ProviderEntry, 0, len(projectProviders))
124 projectEntries := map[string]ProviderEntry{}
125 for _, p := range c.Providers {
126 if projectProviders[p.Name] {
127 retained = append(retained, p)
128 projectEntries[p.Name] = p
129 }
130 }
131 // The Desktop expands multi-model providers into separately credentialed
132 // routes. Preserve project overrides under those transport aliases too.
133 sourceNames := map[string]string{}
134 for source, target := range frozen.References {
135 sourceName, _, _ := strings.Cut(source, "/")
136 targetName, _, _ := strings.Cut(target, "/")
137 sourceNames[targetName] = sourceName
138 }
139 c.Providers = nil
140 seen := map[string]bool{}
141 for _, p := range frozen.Providers {
142 if strings.TrimSpace(p.Name) == "" || seen[p.Name] {
143 return fmt.Errorf("model snapshot has an empty or duplicate provider")
144 }
145 seen[p.Name] = true
146 if projectEntry, ok := projectEntries[sourceNames[p.Name]]; ok {
147 alias := projectEntry
148 alias.Name = p.Name
149 c.Providers = append(c.Providers, alias)
150 continue
151 }
152 if projectProviders[p.Name] {
153 continue
154 }
155 p.resolvedAPIKey, p.credentialsFrozen = frozen.Credentials[p.Name], true
156 p.credentialProxyURL = frozen.ProxyURL
157 c.Providers = append(c.Providers, p)
158 }
159 c.Providers = append(c.Providers, retained...)
160 c.Desktop.ProviderAccess = nil
161 for _, p := range c.Providers {
162 c.Desktop.ProviderAccess = append(c.Desktop.ProviderAccess, p.Name)
163 }
164 declared, _ := project["agent"].(map[string]any)
165 src, dst := reflect.ValueOf(frozen.Preferences), reflect.ValueOf(&c.Agent).Elem()
166 for i := range src.NumField() {
167 field := src.Type().Field(i)
168 if _, explicit := declared[field.Tag.Get("toml")]; !explicit {
169 dst.FieldByName(field.Name).Set(src.Field(i))
170 }
171 }
172 // Explicit project assignments still use the user's provider names. Route
173 // them to the managed alias only when the project has no provider override.
174 mapRef := func(ref string) string {
175 name, _, _ := strings.Cut(ref, "/")
176 if mapped := frozen.References[ref]; mapped != "" && !projectProviders[name] {
177 return mapped
178 }
179 return ref
180 }
181 for _, ref := range []*string{&c.Agent.PlannerModel, &c.Agent.VisionModel, &c.Agent.WebSearchModel, &c.Agent.GuardianModel, &c.Agent.RecoveryModel, &c.Agent.SubagentModel} {
182 *ref = mapRef(*ref)
183 }
184 for name, ref := range c.Agent.SubagentModels {
185 c.Agent.SubagentModels[name] = mapRef(ref)
186 }
187 return nil
188 }
189
190 func (e *ProviderEntry) CredentialProxyURL() string { return e.credentialProxyURL }
191
191 lines GO