返回 DeepSeek-Reasonix
model_settings_service.go
根目录 / internal / config / model_settings_service.go
1 package config
2
3 import (
4 "encoding/json"
5 "fmt"
6 "path/filepath"
7 "strings"
8 )
9
10 // ConnectionCredentialRequest is the shared CLI/Desktop credential mutation.
11 // ExpectedRevision is optional for compatibility; interactive editors should
12 // supply the revision they loaded to avoid overwriting a concurrent edit.
13 type ConnectionCredentialRequest struct {
14 RequestID string
15 ConfigPath string
16 ProviderNames []string
17 Key string
18 ExpectedRevision string
19 }
20
21 type ConnectionCredentialResult struct {
22 Persisted bool
23 Revision string
24 Slot string
25 }
26
27 func connectionCredentialRequestDigest(req ConnectionCredentialRequest) (string, error) {
28 raw, err := json.Marshal(struct {
29 ConfigPath string
30 ProviderNames []string
31 Key string
32 Revision string
33 }{filepath.Clean(req.ConfigPath), req.ProviderNames, req.Key, req.ExpectedRevision})
34 if err != nil {
35 return "", err
36 }
37 return ModelSettingsRequestDigest(raw)
38 }
39
40 func ConfigFileRevision(path string) string { return fileContentRevision(path) }
41
42 func connectionCredentialReceiptResult(receipt ModelSettingsReceipt, digest string) (ConnectionCredentialResult, error) {
43 if !strings.HasPrefix(receipt.RequestDigest, "hmac-v1:") {
44 return ConnectionCredentialResult{}, fmt.Errorf("unknown_result: legacy receipt content cannot be verified; reload current settings")
45 }
46 if receipt.RequestDigest != digest {
47 return ConnectionCredentialResult{}, fmt.Errorf("request_conflict: request ID was already used for a different connection edit")
48 }
49 revision := receipt.ResultRevision
50 if revision == "" {
51 revision = receipt.AfterRevision
52 }
53 return ConnectionCredentialResult{Persisted: true, Revision: revision}, nil
54 }
55
56 // ProviderEditPath follows the source selected by the runtime merge, including
57 // project entries that replace built-in defaults but not user-owned entries.
58 func (c *Config) ProviderEditPath(root, name string) (string, error) {
59 entry, ok := c.Provider(name)
60 if !ok {
61 return "", fmt.Errorf("unknown provider %q", name)
62 }
63 if c.providerSources[providerMergeKey(*entry)] == providerSourceProject {
64 return filepath.Join(root, "reasonix.toml"), nil
65 }
66 return UserConfigPath(), nil
67 }
68
69 // CommitConnectionCredential writes a fresh private slot before publishing the
70 // config reference. It never overwrites a legacy/shared credential variable.
71 func CommitConnectionCredential(req ConnectionCredentialRequest) (ConnectionCredentialResult, error) {
72 var result ConnectionCredentialResult
73 path := strings.TrimSpace(req.ConfigPath)
74 if path == "" {
75 return result, fmt.Errorf("config path is required")
76 }
77 if len(req.ProviderNames) == 0 {
78 return result, fmt.Errorf("at least one provider is required")
79 }
80 if strings.TrimSpace(req.RequestID) == "" {
81 return result, fmt.Errorf("request ID is required")
82 }
83 if strings.ContainsAny(req.Key, "\r\n") {
84 return result, fmt.Errorf("credential value contains a newline")
85 }
86 digest, err := connectionCredentialRequestDigest(req)
87 if err != nil {
88 return result, err
89 }
90 if receipt, ok := LookupModelSettingsReceipt(strings.TrimSpace(req.RequestID)); ok {
91 return connectionCredentialReceiptResult(receipt, digest)
92 }
93 unlock, err := LockConfigFileEdits(path)
94 if err != nil {
95 return result, err
96 }
97 defer unlock()
98 unlockCredentials, err := LockUserCredentialEdits()
99 if err != nil {
100 return result, err
101 }
102 defer unlockCredentials()
103 if err := RecoverModelCredentialCommitsLocked(path); err != nil {
104 return result, err
105 }
106 if receipt, ok := LookupModelSettingsReceipt(strings.TrimSpace(req.RequestID)); ok {
107 return connectionCredentialReceiptResult(receipt, digest)
108 }
109 if req.ExpectedRevision != "" && fileContentRevision(path) != req.ExpectedRevision {
110 return result, fmt.Errorf("model settings changed; reload before saving")
111 }
112 cfg, err := LoadForEditReadOnlyStrict(path)
113 if err != nil {
114 return result, err
115 }
116 if err := cfg.BeginModelCredentialCommitLocked(path, req.RequestID, digest); err != nil {
117 return result, err
118 }
119 defer cfg.CleanupStagedModelCredentialsLocked(path)
120 baseline := cfg.ModelSettingsBaseline()
121 slot, err := cfg.StageModelCredentialLocked(req.Key)
122 if err != nil {
123 return result, err
124 }
125 seen := map[string]bool{}
126 for _, rawName := range req.ProviderNames {
127 name := strings.TrimSpace(rawName)
128 if name == "" || seen[name] {
129 continue
130 }
131 seen[name] = true
132 entry, ok := cfg.Provider(name)
133 if !ok {
134 return result, fmt.Errorf("unknown provider %q", name)
135 }
136 updated := *entry
137 updated.APIKeyEnv = slot
138 if err := cfg.UpsertProvider(updated); err != nil {
139 return result, err
140 }
141 }
142 if len(seen) == 0 {
143 return result, fmt.Errorf("at least one provider is required")
144 }
145 err = cfg.SaveModelSettingsTo(path, baseline)
146 if err != nil {
147 return result, err
148 }
149 result.Persisted = true
150 result.Slot = slot
151 result.Revision = fileContentRevision(path)
152 if err := cfg.MarkModelCredentialConfigCommittedLocked(path, result.Revision); err != nil {
153 return result, err
154 }
155 saved, err := LoadForEditReadOnlyStrict(path)
156 if err != nil {
157 return result, err
158 }
159 for name := range seen {
160 entry, ok := saved.Provider(name)
161 if !ok || entry.APIKeyEnv != slot {
162 return result, fmt.Errorf("saved provider %q did not reference the new credential", name)
163 }
164 }
165 if err := cfg.CompleteModelCredentialCommitLocked(); err != nil {
166 return result, err
167 }
168 return result, nil
169 }
170
170 lines GO