返回 DeepSeek-Reasonix
submission.go
根目录 / internal / serve / submission.go
1 package serve
2
3 import (
4 "net/http"
5 "reasonix/internal/control"
6 "reasonix/internal/provider"
7 "strings"
8 )
9
10 func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
11 body, trimmed, ok := decodeSubmitRequest(w, r)
12 if !ok {
13 return
14 }
15 // Session rotations must complete while bindMu is held. Controller.Submit
16 // dispatches these verbs asynchronously, which would let a following model,
17 // resume, or extension command cross the rotation generation boundary.
18 switch trimmed {
19 case "/new":
20 s.newSessionFromSubmit(w, r)
21 return
22 case "/clear":
23 s.clearSessionFromSubmit(w, r)
24 return
25 }
26 // Intercept /model <ref> for runtime model switching (the controller's
27 // Submit path only lists models — switching is frontend-specific).
28 if s.submitModelCommand(w, r, trimmed) {
29 return
30 }
31 // Intercept /effort <level> for reasoning effort switching.
32 if strings.HasPrefix(trimmed, "/effort ") {
33 level := strings.TrimSpace(strings.TrimPrefix(trimmed, "/effort"))
34 if level != "" {
35 if err := s.switchEffortExpected(r.Context(), level, r.Header.Get(expectedSessionPathHeader)); err != nil {
36 http.Error(w, err.Error(), runtimeSwitchErrorStatus(err))
37 return
38 }
39 w.WriteHeader(http.StatusNoContent)
40 return
41 }
42 }
43 // Admission and controller replacement share one ownership boundary.
44 s.bindMu.Lock()
45 // Known durable receipts win before configuration validation on a retry.
46 identity := control.SubmissionRequest{ID: body.SubmissionID, HTTP: true, Input: body.Input, Format: body.Format, Action: body.Action, RecoveryID: body.RecoveryID}
47 if !s.validateExpectedSessionLocked(w, r) {
48 s.bindMu.Unlock()
49 return
50 }
51 if !s.admitModelSubmissionLocked(w, r, body.ModelApplication, trimmed, identity) {
52 s.bindMu.Unlock()
53 return
54 }
55 ctrl := s.ctl()
56 // Fix false 202 while a turn is active: SubmitHTTPFormat silently drops
57 // concurrent input. Clients must use POST /inbox/items for durable follow-up.
58 identified, identifiedOK := ctrl.(*control.Controller)
59 if ctrl.Running() {
60 s.bindMu.Unlock()
61 if _, err := control.MaintenanceCommandConflict(ctrl, trimmed); err != nil {
62 http.Error(w, err.Error(), http.StatusConflict)
63 return
64 }
65 http.Error(w, SubmitBusyMessage+"; use POST /inbox/items for durable follow-up", http.StatusConflict)
66 return
67 }
68 if body.Action == control.ProtocolRecoveryAction {
69 pending, ok := ctrl.(interface {
70 PendingProtocolRecovery() *provider.ProtocolRecoveryAction
71 })
72 var action *provider.ProtocolRecoveryAction
73 if ok {
74 action = pending.PendingProtocolRecovery()
75 }
76 if action == nil || action.ID != body.RecoveryID {
77 s.bindMu.Unlock()
78 http.Error(w, "protocol recovery is unavailable or stale", http.StatusConflict)
79 return
80 }
81 }
82 if routing, ok := ctrl.(interface{ SetTurnSubmissionID(string) }); ok {
83 routing.SetTurnSubmissionID(body.SubmissionID)
84 }
85 if identifiedOK && !isServeManagementCommand(trimmed) {
86 _, err := identified.SubmitIdentified(identity)
87 if err != nil {
88 s.bindMu.Unlock()
89 writeSubmissionFailure(w, err)
90 return
91 }
92 s.bindMu.Unlock()
93 w.WriteHeader(http.StatusAccepted)
94 return
95 }
96 submitWithAction(ctrl, body.Input, body.Format, body.Action, body.RecoveryID)
97 if isServeManagementCommand(trimmed) && !ctrl.Running() && !ctrl.RuntimeStatus().PendingPrompt {
98 // Management notices/status are successful non-turn operations.
99 s.bindMu.Unlock()
100 w.WriteHeader(http.StatusNoContent)
101 return
102 }
103 // Legacy clients without receipts still use the synchronous running gate.
104 if !ctrl.Running() && !ctrl.RuntimeStatus().PendingPrompt {
105 s.bindMu.Unlock()
106 http.Error(w, "input was not admitted; session is rotating, closed, or finishing — use POST /inbox/items", http.StatusConflict)
107 return
108 }
109 s.bindMu.Unlock()
110 w.WriteHeader(http.StatusAccepted)
111 }
112
112 lines GO