返回 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 the key before publishing the config
70 // reference: in place when its providers alone in the user config read their
71 // stored variable, otherwise to a fresh private slot.
72 func CommitConnectionCredential(req ConnectionCredentialRequest) (ConnectionCredentialResult, error) {
73 var result ConnectionCredentialResult
74 path := strings.TrimSpace(req.ConfigPath)
75 if path == "" {
76 return result, fmt.Errorf("config path is required")
77 }
78 if len(req.ProviderNames) == 0 {
79 return result, fmt.Errorf("at least one provider is required")
80 }
81 if strings.TrimSpace(req.RequestID) == "" {
82 return result, fmt.Errorf("request ID is required")
83 }
84 if strings.ContainsAny(req.Key, "\r\n") {
85 return result, fmt.Errorf("credential value contains a newline")
86 }
87 digest, err := connectionCredentialRequestDigest(req)
88 if err != nil {
89 return result, err
90 }
91 if receipt, ok := LookupModelSettingsReceipt(strings.TrimSpace(req.RequestID)); ok {
92 return connectionCredentialReceiptResult(receipt, digest)
93 }
94 unlock, err := LockConfigFileEdits(path)
95 if err != nil {
96 return result, err
97 }
98 defer unlock()
99 unlockCredentials, err := LockUserCredentialEdits()
100 if err != nil {
101 return result, err
102 }
103 defer unlockCredentials()
104 if err := RecoverModelCredentialCommitsLocked(path); err != nil {
105 return result, err
106 }
107 if receipt, ok := LookupModelSettingsReceipt(strings.TrimSpace(req.RequestID)); ok {
108 return connectionCredentialReceiptResult(receipt, digest)
109 }
110 if req.ExpectedRevision != "" && fileContentRevision(path) != req.ExpectedRevision {
111 return result, fmt.Errorf("model settings changed; reload before saving")
112 }
113 cfg, err := LoadForEditReadOnlyStrict(path)
114 if err != nil {
115 return result, err
116 }
117 if err := cfg.BeginModelCredentialCommitLocked(path, req.RequestID, digest); err != nil {
118 return result, err
119 }
120 defer cfg.CleanupStagedModelCredentialsLocked(path)
121 baseline := cfg.ModelSettingsBaseline()
122 slot, err := cfg.RotateModelCredentialLocked(req.ProviderNames, req.Key)
123 if err != nil {
124 return result, err
125 }
126 seen := map[string]bool{}
127 for _, rawName := range req.ProviderNames {
128 name := strings.TrimSpace(rawName)
129 if name == "" || seen[name] {
130 continue
131 }
132 seen[name] = true
133 entry, ok := cfg.Provider(name)
134 if !ok {
135 return result, fmt.Errorf("unknown provider %q", name)
136 }
137 updated := *entry
138 updated.APIKeyEnv = slot
139 if err := cfg.UpsertProvider(updated); err != nil {
140 return result, err
141 }
142 }
143 if len(seen) == 0 {
144 return result, fmt.Errorf("at least one provider is required")
145 }
146 err = cfg.SaveModelSettingsTo(path, baseline)
147 if err != nil {
148 return result, err
149 }
150 result.Persisted = true
151 result.Slot = slot
152 result.Revision = fileContentRevision(path)
153 if err := cfg.MarkModelCredentialConfigCommittedLocked(path, result.Revision); err != nil {
154 return result, err
155 }
156 saved, err := LoadForEditReadOnlyStrict(path)
157 if err != nil {
158 return result, err
159 }
160 for name := range seen {
161 entry, ok := saved.Provider(name)
162 if !ok || entry.APIKeyEnv != slot {
163 return result, fmt.Errorf("saved provider %q did not reference the new credential", name)
164 }
165 }
166 if err := cfg.CompleteModelCredentialCommitLocked(); err != nil {
167 return result, err
168 }
169 return result, nil
170 }
171
171 lines GO