返回 DeepSeek-Reasonix
model_settings.go
根目录 / internal / serve / model_settings.go
1 package serve
2
3 import (
4 "crypto/rand"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "sort"
9 "strings"
10
11 "reasonix/internal/config"
12 "reasonix/internal/control"
13 )
14
15 const modelSettingsProtocolVersion = 1
16
17 func currentModelRef(c control.SessionAPI) string {
18 ref := strings.TrimSpace(c.ModelRef())
19 if ref != "" {
20 return ref
21 }
22 return strings.TrimSpace(c.Label())
23 }
24
25 type modelSettingsStatusView struct {
26 config.ModelSettingsOwnership
27 Version int `json:"version"`
28 Revision string `json:"revision"`
29 Model string `json:"model"`
30 SessionPath string `json:"sessionPath"`
31 OwnedRevisions []string `json:"ownedRevisions"`
32 UnversionedOwners bool `json:"unversionedOwners"`
33 }
34
35 func (s *Server) modelSettingsStatus(w http.ResponseWriter, r *http.Request) {
36 s.bindMu.Lock()
37 defer s.bindMu.Unlock()
38 if !s.validateExpectedSessionLocked(w, r) {
39 return
40 }
41 writeJSON(w, s.modelSettingsStatusLocked())
42 }
43
44 // Caller holds bindMu across validation, refresh and turn admission.
45 func (s *Server) admitModelSettingsRunLocked(w http.ResponseWriter, r *http.Request) bool {
46 if !s.validateExpectedSessionLocked(w, r) {
47 return false
48 }
49 if s.rejectMirroredForegroundLocked(w) {
50 return false
51 }
52 if err := s.refreshRunModelSettingsLocked(r.Context()); err != nil {
53 http.Error(w, err.Error(), http.StatusConflict)
54 return false
55 }
56 if !s.validateExpectedSessionLocked(w, r) {
57 return false
58 }
59 return true
60 }
61
62 func (s *Server) modelSettingsStatusLocked() modelSettingsStatusView {
63 if s.modelSettingsOwnership.OwnershipIncarnation == "" {
64 s.modelSettingsOwnership.OwnershipIncarnation = rand.Text()
65 }
66 s.modelSettingsOwnership.OwnershipSeq++
67 current := s.ctl()
68 view := modelSettingsStatusView{Version: modelSettingsProtocolVersion, Model: current.ModelRef(), SessionPath: current.SessionPath(), OwnedRevisions: []string{}}
69 view.ModelSettingsOwnership = s.modelSettingsOwnership
70 owners := []control.SessionAPI{current}
71 s.detachedMu.Lock()
72 for _, detached := range s.detached {
73 if detached != nil && detached.ctrl != nil {
74 owners = append(owners, detached.ctrl)
75 }
76 }
77 s.detachedMu.Unlock()
78 seen := map[string]bool{}
79 for _, owner := range owners {
80 revision := ""
81 if snapshot, ok := owner.(interface{ ModelSettingsSourceRevision() string }); ok {
82 revision = snapshot.ModelSettingsSourceRevision()
83 }
84 if owner == current {
85 view.Revision = revision
86 }
87 if revision == "" {
88 view.UnversionedOwners = true
89 } else if !seen[revision] {
90 seen[revision] = true
91 view.OwnedRevisions = append(view.OwnedRevisions, revision)
92 }
93 }
94 sort.Strings(view.OwnedRevisions)
95 return view
96 }
97
98 // applyModelSettings publishes a complete resolver at the same session binding
99 // boundary as /model. No provider config or real credential is written remotely.
100 // A failed build leaves the old controller and its bundle intact.
101 func (s *Server) applyModelSettings(w http.ResponseWriter, r *http.Request) {
102 var request struct {
103 Version int `json:"version"`
104 Ref string `json:"ref"`
105 Settings config.ModelRuntimeSettings `json:"settings"`
106 }
107 decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20))
108 decoder.DisallowUnknownFields()
109 if err := decoder.Decode(&request); err != nil || request.Version != modelSettingsProtocolVersion || strings.TrimSpace(request.Settings.Revision) == "" {
110 http.Error(w, "invalid model settings snapshot", http.StatusBadRequest)
111 return
112 }
113 ref, ok := modelSettingsCatalogRef(request.Settings.Providers, request.Ref)
114 if !ok {
115 http.Error(w, "model is not included in the snapshot", http.StatusBadRequest)
116 return
117 }
118 s.bindMu.Lock()
119 defer s.bindMu.Unlock()
120 if !s.validateExpectedSessionLocked(w, r) {
121 return
122 }
123 if controllerHasActiveRuntimeWork(s.ctl()) {
124 http.Error(w, "cannot apply model settings while active work or background jobs are running", http.StatusConflict)
125 return
126 }
127 if view := s.modelSettingsStatusLocked(); view.Revision == request.Settings.Revision && view.Model == ref {
128 writeJSON(w, view)
129 return
130 }
131 previous := s.managedModels
132 s.managedModels = &request.Settings
133 if err := s.switchModelLocked(r.Context(), ref); err != nil {
134 s.managedModels = previous
135 http.Error(w, fmt.Sprintf("apply saved model settings: %s", err), runtimeSwitchErrorStatus(err))
136 return
137 }
138 writeJSON(w, s.modelSettingsStatusLocked())
139 }
140
141 func modelSettingsCatalogRef(providers []config.ProviderEntry, requested string) (string, bool) {
142 requested = strings.TrimSpace(requested)
143 for _, entry := range providers {
144 models := entry.ChatModelList()
145 if len(models) == 0 {
146 models = entry.ModelList()
147 }
148 for _, model := range models {
149 ref := entry.Name + "/" + model
150 if requested == ref {
151 return ref, true
152 }
153 }
154 }
155 return "", false
156 }
157
157 lines GO