Canary of a model's newer weights: ctl model canary, promote and canaries, rolled back by itself when it serves worse

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-04 20:31:26 +02:00
1 parent 0a4076928a
commit a1dfd4df1d
13 files changed
+750 -18

No files matched your search

+6
View File
@@ -154,6 +154,7 @@ func (a *API) Register(mux *http.ServeMux) {
mux.HandleFunc("POST /api/v1/updates/check", a.checkUpdates)
mux.HandleFunc("POST /api/v1/models/{name}/update", a.updateModel)
mux.HandleFunc("POST /api/v1/models/{name}/rollback", a.rollbackModel)
a.registerCanary(mux)
mux.HandleFunc("POST /api/v1/models/{name}/origin", a.setOrigin)
mux.HandleFunc("GET /api/v1/plan", a.planModel)
mux.HandleFunc("POST /api/v1/metrics/range", a.metricsBatch)
@@ -330,6 +331,10 @@ func loadView(l *inventory.Load, now time.Time) *LoadView {
// ModelView is a model as `ctl models` shows it.
type ModelView struct {
Name string `json:"name"`
// Canary is a share of its requests sent to a candidate; CanaryOf is
// set on the candidate, naming the model it is a canary of.
Canary *catalog.Canary `json:"canary,omitempty"`
CanaryOf string `json:"canary_of,omitempty"`
// Description is what the model is for, when somebody said.
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases,omitempty"`
@@ -651,6 +656,7 @@ func (a *API) Models() []ModelView {
Parked: h.Parked, Reason: h.Reason, Unplaceable: h.Unplaceable,
Class: m.EffectiveClass(), Owner: m.Owner, Preempted: h.Preempted, HeldFor: h.HeldFor,
External: externalLabel(m), Adapters: m.Adapters,
Canary: m.Canary, CanaryOf: a.ctl.Catalog().CanaryOf(m.Name),
}
for _, p := range m.Profiles {
v.Profiles = append(v.Profiles, p.Name)
+84
View File
@@ -0,0 +1,84 @@
package adminapi
import (
"encoding/json"
"io"
"net/http"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
)
func (a *API) registerCanary(mux *http.ServeMux) {
need := func(w http.ResponseWriter) bool {
if a.serve == nil {
writeError(w, http.StatusNotImplemented, "a canary needs a mirror and a store, and this control plane lacks one")
return false
}
return true
}
mux.HandleFunc("GET /api/v1/canaries", func(w http.ResponseWriter, r *http.Request) {
if !need(w) {
return
}
writeJSON(w, http.StatusOK, a.serve.Canaries())
})
// Start one: the newer weights fetched and run beside, then given
// their share.
mux.HandleFunc("POST /api/v1/models/{name}/canary", func(w http.ResponseWriter, r *http.Request) {
if !need(w) {
return
}
var cn catalog.Canary
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<12)).Decode(&cn); err != nil {
writeError(w, http.StatusBadRequest, "the request is not valid JSON: "+err.Error())
return
}
job, err := a.serve.StartCanary(r.PathValue("name"), cn, actorOf(r))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusAccepted, job)
})
// Change its share.
mux.HandleFunc("PUT /api/v1/models/{name}/canary", func(w http.ResponseWriter, r *http.Request) {
if !need(w) {
return
}
var req struct {
Percent int `json:"percent"`
}
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<12)).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "the request is not valid JSON: "+err.Error())
return
}
msg, err := a.serve.SetCanaryPercent(r.Context(), r.PathValue("name"), req.Percent, actorOf(r))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"result": msg})
})
mux.HandleFunc("DELETE /api/v1/models/{name}/canary", func(w http.ResponseWriter, r *http.Request) {
if !need(w) {
return
}
msg, err := a.serve.AbortCanary(r.Context(), r.PathValue("name"), actorOf(r))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"result": msg})
})
mux.HandleFunc("POST /api/v1/models/{name}/promote", func(w http.ResponseWriter, r *http.Request) {
if !need(w) {
return
}
job, err := a.serve.StartPromote(r.PathValue("name"), actorOf(r))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusAccepted, job)
})
}