Files
llmgw/internal/control/adminapi/requestbody.go
T
Ilker ManapandClaude Opus 5.5 a95c519bc5 The admin API described in OpenAPI 3.1: generated from a route table by docgen, served at /api/v1/openapi.json
adminapi.Routes lists every endpoint with its query parameters and Go values
of the bodies it reads and answers with; internal/docgen/openapi turns that
into docs/admin-api.openapi.json by reflection, the way encoding/json encodes
the types, with the fields' comments as descriptions. A test fails when an
endpoint is registered and not in the table, another when the committed
document is stale. The binary embeds it and serves it with the token.

Handlers that wrote map literals and decoded anonymous structs now use named
types, so the document can see them. GET /api/v1/models/{name} answers with a
model's whole definition as it is in force, which a tool managing it needs.
docs/admin-api.md: reaching the API, the token, what a request must carry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 23:47:59 +02:00

85 lines
2.6 KiB
Go

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 ReplayRequest
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)
}