Requests kept and replayed: keys requests per project with retention and a share, ctl request --body and --replay
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
841ee21c12
commit
32ff1dbd6f
22 files changed
+757
-10
No files matched your search
@@ -41,6 +41,8 @@ import (
|
||||
|
||||
// API serves the management endpoints.
|
||||
type API struct {
|
||||
// replay sends a kept request again; nil without a router here.
|
||||
replay Replayer
|
||||
// notifier is the webhooks `ctl notify test` reaches; nil without any.
|
||||
notifier *notify.Notifier
|
||||
// releases is where release binaries are fetched from, for upgrades.
|
||||
@@ -159,6 +161,8 @@ func (a *API) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/v1/plan", a.planModel)
|
||||
mux.HandleFunc("POST /api/v1/metrics/range", a.metricsBatch)
|
||||
mux.HandleFunc("GET /api/v1/requests/{id}", a.request)
|
||||
mux.HandleFunc("GET /api/v1/requests/{id}/body", a.requestBody)
|
||||
mux.HandleFunc("POST /api/v1/requests/{id}/replay", a.replayRequest)
|
||||
mux.HandleFunc("POST /api/v1/models/{name}/retry", a.retryModel)
|
||||
mux.HandleFunc("GET /api/v1/models/{name}/headroom", func(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package adminapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
||||
)
|
||||
|
||||
type bodyFinder interface {
|
||||
RequestBodyByID(ctx context.Context, id string) (store.RequestBody, error)
|
||||
}
|
||||
|
||||
// Replayer sends a kept request again to a model: the router's Replay.
|
||||
type Replayer func(ctx context.Context, path, model string, body []byte) (int, []byte, time.Duration, error)
|
||||
|
||||
// SetReplayer gives the API the router to replay kept requests through.
|
||||
func (a *API) SetReplayer(f Replayer) { a.replay = f }
|
||||
|
||||
// ReplayView is a kept request sent again, beside what it got the first time.
|
||||
type ReplayView struct {
|
||||
Original store.RequestBody `json:"original"`
|
||||
Model string `json:"model"`
|
||||
Status int `json:"status"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
TookMs int64 `json:"took_ms"`
|
||||
}
|
||||
|
||||
func (a *API) keptBody(w http.ResponseWriter, r *http.Request) (store.RequestBody, bool) {
|
||||
f, ok := a.usage.(bodyFinder)
|
||||
if !ok {
|
||||
writeError(w, http.StatusNotImplemented, "this control plane keeps no requests")
|
||||
return store.RequestBody{}, false
|
||||
}
|
||||
b, err := f.RequestBodyByID(r.Context(), r.PathValue("id"))
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
writeError(w, http.StatusNotFound, "no request with that id is kept: its project keeps none "+
|
||||
"(llmgw keys requests --project P --retain-hours N), it fell outside the share kept, or it has expired")
|
||||
return store.RequestBody{}, false
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return store.RequestBody{}, false
|
||||
}
|
||||
return b, true
|
||||
}
|
||||
|
||||
func (a *API) requestBody(w http.ResponseWriter, r *http.Request) {
|
||||
if b, ok := a.keptBody(w, r); ok {
|
||||
writeJSON(w, http.StatusOK, b)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) replayRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.replay == nil {
|
||||
writeError(w, http.StatusNotImplemented, "this control plane does not route requests")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Model string `json:"model"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<12)).Decode(&req); err != nil || req.Model == "" {
|
||||
writeError(w, http.StatusBadRequest, "say which model: {\"model\": \"...\"}")
|
||||
return
|
||||
}
|
||||
b, ok := a.keptBody(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
status, body, took, err := a.replay(r.Context(), b.Path, req.Model, []byte(b.Request))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
v := ReplayView{Original: b, Model: req.Model, Status: status, TookMs: took.Milliseconds()}
|
||||
if json.Valid(body) {
|
||||
v.Body = body
|
||||
} else {
|
||||
v.Body, _ = json.Marshal(string(body))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
}
|
||||
Reference in new issue
Block a user