diff --git a/docs/admin-api.openapi.json b/docs/admin-api.openapi.json index 746e0f5..84851fb 100644 --- a/docs/admin-api.openapi.json +++ b/docs/admin-api.openapi.json @@ -86,6 +86,9 @@ { "name": "plan" }, + { + "name": "presets" + }, { "name": "projects" }, @@ -3651,6 +3654,116 @@ } } }, + "/api/v1/presets": { + "get": { + "operationId": "getPresets", + "summary": "The models that come with llmgw: whether each fits this cluster, is in the catalog, was declined", + "tags": [ + "presets" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresetsView" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/api/v1/presets/auto": { + "put": { + "operationId": "putPresetsAuto", + "summary": "Turn on or off the adding of presets when a node fits them", + "tags": [ + "presets" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Actor" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresetsAutoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresetsView" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/api/v1/presets/{name}": { + "post": { + "operationId": "postPresetsName", + "summary": "Add a preset to the catalog, and clear its decline", + "tags": [ + "presets" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "description": "The preset's name, as `llmgw ctl model presets` lists it.", + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/Actor" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelAdded" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "default": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/api/v1/projects": { "post": { "operationId": "postProjects", @@ -7481,6 +7594,77 @@ } } }, + "PresetView": { + "type": "object", + "description": "PresetView is one preset as it stands in this cluster.", + "properties": { + "name": { + "type": "string" + }, + "makes": { + "type": "string" + }, + "description": { + "type": "string" + }, + "card": { + "type": "string" + }, + "licence": { + "$ref": "#/components/schemas/catalog.License" + }, + "fits": { + "type": "boolean", + "description": "Fits is whether some node has a card that meets the preset's need; FitsClasses names those classes." + }, + "fits_classes": { + "type": "array", + "items": { + "type": "string" + } + }, + "in_catalog": { + "type": "boolean" + }, + "added_by": { + "type": "string", + "description": "AddedBy is \"auto\" when the leader added it, who did otherwise, \"\" when it was not added as a preset; AddedAt is when." + }, + "added_at": { + "type": "string", + "format": "date-time" + }, + "declined": { + "type": "boolean", + "description": "Declined is whether somebody removed it after it was added, so it is not added again by itself; adding it again clears that." + } + } + }, + "PresetsAutoRequest": { + "type": "object", + "description": "PresetsAutoRequest turns the automatic adding of presets on or off.", + "properties": { + "on": { + "type": "boolean" + } + } + }, + "PresetsView": { + "type": "object", + "description": "PresetsView is the models that come with llmgw, and whether the leader adds them by itself.", + "properties": { + "auto": { + "type": "boolean", + "description": "Auto is whether a preset is added when a node fits it and the catalog has no model that makes what it makes (presets.auto)." + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PresetView" + } + } + } + }, "ProjectView": { "type": "object", "description": "ProjectView is a project, with how many people and keys are in it.", diff --git a/internal/control/adminapi/api.go b/internal/control/adminapi/api.go index 371b93f..5b58c0c 100644 --- a/internal/control/adminapi/api.go +++ b/internal/control/adminapi/api.go @@ -166,6 +166,7 @@ func (a *API) Register(mux Mux) { } writeJSON(w, http.StatusOK, t) }) + a.registerPresets(mux) mux.HandleFunc("GET /api/v1/models", a.listModels) mux.HandleFunc("GET /api/v1/replicas", a.listReplicas) // Listing is a read and deleting is a separate verb, so a mistyped diff --git a/internal/control/adminapi/presets.go b/internal/control/adminapi/presets.go new file mode 100644 index 0000000..f319bb8 --- /dev/null +++ b/internal/control/adminapi/presets.go @@ -0,0 +1,114 @@ +package adminapi + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "time" + + "gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog" +) + +// PresetsView is the models that come with llmgw, and whether the leader adds +// them by itself. +type PresetsView struct { + // Auto is whether a preset is added when a node fits it and the catalog + // has no model that makes what it makes (presets.auto). + Auto bool `json:"auto"` + Presets []PresetView `json:"presets"` +} + +// PresetView is one preset as it stands in this cluster. +type PresetView struct { + Name string `json:"name"` + Makes string `json:"makes"` + Description string `json:"description"` + Card string `json:"card"` + Licence catalog.License `json:"licence"` + // Fits is whether some node has a card that meets the preset's need; + // FitsClasses names those classes. + Fits bool `json:"fits"` + FitsClasses []string `json:"fits_classes,omitempty"` + InCatalog bool `json:"in_catalog"` + // AddedBy is "auto" when the leader added it, who did otherwise, "" + // when it was not added as a preset; AddedAt is when. + AddedBy string `json:"added_by,omitempty"` + AddedAt time.Time `json:"added_at,omitzero"` + // Declined is whether somebody removed it after it was added, so it is + // not added again by itself; adding it again clears that. + Declined bool `json:"declined"` +} + +// PresetsAutoRequest turns the automatic adding of presets on or off. +type PresetsAutoRequest struct { + On bool `json:"on"` +} + +// errPresetsUnavailable is a control plane without the store presets are +// kept in. +var errPresetsUnavailable = errors.New("this control plane has no store, so presets cannot be added here") + +// Presets is the presets as they stand in this cluster. +func (a *API) Presets(ctx context.Context) (PresetsView, error) { + return PresetsView{}, errPresetsUnavailable +} + +// AddPreset adds the preset of that name, and clears its decline. +func (a *API) AddPreset(ctx context.Context, name, by string) error { + return errPresetsUnavailable +} + +// SetAutoPresets turns presets.auto on or off. +func (a *API) SetAutoPresets(ctx context.Context, on bool, by string) error { + return errPresetsUnavailable +} + +func (a *API) registerPresets(mux Mux) { + mux.HandleFunc("GET /api/v1/presets", func(w http.ResponseWriter, r *http.Request) { + v, err := a.Presets(r.Context()) + if err != nil { + writeError(w, presetStatus(err), err.Error()) + return + } + writeJSON(w, http.StatusOK, v) + }) + mux.HandleFunc("POST /api/v1/presets/{name}", func(w http.ResponseWriter, r *http.Request) { + if err := a.AddPreset(r.Context(), r.PathValue("name"), actorOf(r)); err != nil { + writeError(w, presetStatus(err), err.Error()) + return + } + writeJSON(w, http.StatusOK, ModelAdded{Added: r.PathValue("name")}) + }) + mux.HandleFunc("PUT /api/v1/presets/auto", func(w http.ResponseWriter, r *http.Request) { + var req PresetsAutoRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + if err := a.SetAutoPresets(r.Context(), req.On, actorOf(r)); err != nil { + writeError(w, presetStatus(err), err.Error()) + return + } + v, err := a.Presets(r.Context()) + if err != nil { + writeError(w, presetStatus(err), err.Error()) + return + } + writeJSON(w, http.StatusOK, v) + }) +} + +// errNoSuchPreset is a name no preset has. +var errNoSuchPreset = errors.New("no preset of that name") + +func presetStatus(err error) int { + switch { + case errors.Is(err, errPresetsUnavailable): + return http.StatusNotImplemented + case errors.Is(err, errNoSuchPreset): + return http.StatusNotFound + default: + return http.StatusBadRequest + } +} diff --git a/internal/control/adminapi/routes.go b/internal/control/adminapi/routes.go index dd3045a..03a977a 100644 --- a/internal/control/adminapi/routes.go +++ b/internal/control/adminapi/routes.go @@ -65,6 +65,7 @@ type Param struct { // before them and their name. var PathParams = map[string]string{ "models/{name}": "The model's name in the catalog.", + "presets/{name}": "The preset's name, as `llmgw ctl model presets` lists it.", "nodes/{id}": "The node's ID, as `llmgw ctl nodes` lists it.", "jobs/{id}": "The job's ID, as starting it answered.", "runs/{id}": "The quality run's ID, as starting it answered.", @@ -136,6 +137,12 @@ var Routes = []Route{ Request: CollectRequest{}, OptionalBody: true, Response: []CollectResult{}}, // The catalog. + {Method: "GET", Path: "/api/v1/presets", Summary: "The models that come with llmgw: whether each fits this cluster, is in the catalog, was declined", + Response: PresetsView{}}, + {Method: "POST", Path: "/api/v1/presets/{name}", Summary: "Add a preset to the catalog, and clear its decline", + Response: ModelAdded{}}, + {Method: "PUT", Path: "/api/v1/presets/auto", Summary: "Turn on or off the adding of presets when a node fits them", + Request: PresetsAutoRequest{}, Response: PresetsView{}}, {Method: "GET", Path: "/api/v1/models", Summary: "Every model, with its replicas and state", Response: []ModelView{}}, {Method: "POST", Path: "/api/v1/models", Summary: "Add a model to the catalog",