A suite is a set of cases in YAML or JSON lines, each scored by exact, contains, regex, a number within a tolerance, a JSON Schema subset, or a judge model with a rubric; weighed into one score against a threshold. internal/bench/eval runs one on the control plane through Router.Replay, as the gateway's own requests, and keeps every run and its cases in the store (migration 0028: eval_runs, eval_cases). - llmgw ctl eval MODEL --suite FILE|NAME, --history, --run, --suites; exits non-zero below the score. Suites by name from control.eval_dir, or the two built in from deploy/eval (arithmetic, json-format). - model update and promote take --eval SUITE --min-score N, or a model's standing gate (ctl model gate, eval: in the catalog): a promote asks the canary first and leaves it below the score; an update tries the new weights as a canary given no share, then promotes or removes it. - The console's model menu: ask a suite with its progress, its runs, its gate. - The audit middleware no longer hands a handler a body cut off at 64 KiB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
163 lines
5.8 KiB
Go
163 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/modelserve"
|
|
)
|
|
|
|
func newModelUpdatesCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
|
|
var check bool
|
|
c := &cobra.Command{
|
|
Use: "updates [MODEL]",
|
|
Short: "Show which models' repositories have published new weights",
|
|
Long: "A model served from the registry remembers the repository and commit its\n" +
|
|
"files came from, and the control plane asks the registry every\n" +
|
|
"mirror.update_check (six hours by default) whether those files have\n" +
|
|
"changed there. A new commit that leaves them alone is not an update.\n\n" +
|
|
"--check asks now rather than showing the last answer.",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var list []modelserve.UpdateInfo
|
|
if check || len(args) == 1 {
|
|
req := map[string]string{}
|
|
if len(args) == 1 {
|
|
req["model"] = args[0]
|
|
}
|
|
body, _ := json.Marshal(req)
|
|
if err := client().post(cmd.Context(), "/api/v1/updates/check", body, &list); err != nil {
|
|
return err
|
|
}
|
|
} else if err := client().get(cmd.Context(), "/api/v1/updates", &list); err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return emitJSON(list)
|
|
}
|
|
if len(list) == 0 {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "no model has an origin to compare yet; one is checked a few minutes after the control plane starts")
|
|
return nil
|
|
}
|
|
w := tabTo(cmd.OutOrStdout())
|
|
fmt.Fprintln(w, "MODEL\tREPOSITORY\tRUNS\tLATEST\tUPDATE\tCHECKED")
|
|
for _, u := range list {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", u.Model, u.Repo, catalog.ShortRevision(u.Current),
|
|
catalog.ShortRevision(u.Latest), updateCell(u), ago(u.CheckedAt, time.Now()))
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return err
|
|
}
|
|
for _, u := range list {
|
|
if u.Available {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "\nTo move one to its new weights: llmgw ctl model update MODEL\n")
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
c.Flags().BoolVar(&check, "check", false, "ask the registry now")
|
|
return c
|
|
}
|
|
|
|
// updateCell says what an update would be, or why there is none.
|
|
func updateCell(u modelserve.UpdateInfo) string {
|
|
switch {
|
|
case u.Err != "":
|
|
return "could not ask: " + truncate(u.Err, 60)
|
|
case u.Available:
|
|
return fmt.Sprintf("yes: %d file(s), %s", len(u.Changed), bytesShort(u.Bytes))
|
|
case len(u.Missing) > 0:
|
|
return "the repository no longer has " + strings.Join(u.Missing, ", ")
|
|
case u.Latest != "" && u.Latest != u.Current:
|
|
return "no: new commit, same weights"
|
|
}
|
|
return "-"
|
|
}
|
|
|
|
func newModelUpdateCmd(client func() *ctlClient) *cobra.Command {
|
|
var (
|
|
detach bool
|
|
gate gateFlags
|
|
)
|
|
c := &cobra.Command{
|
|
Use: "update MODEL",
|
|
Short: "Move a model to the weights its repository publishes now",
|
|
Long: "The new files are fetched into the mirror beside the ones in use and\n" +
|
|
"checked against the registry's checksums and the nodes' llama.cpp; then\n" +
|
|
"the catalog names them and the replicas are replaced one at a time, each\n" +
|
|
"new one up before an old one stops, and one is asked to say hello.\n\n" +
|
|
"Held to a quality suite (--eval, or the model's own: `llmgw ctl model\n" +
|
|
"gate`), the new weights first run beside the old as MODEL-canary, given\n" +
|
|
"none of its requests, and are asked the suite; below its score they are\n" +
|
|
"taken out again and the model keeps its weights. That needs room for one\n" +
|
|
"more replica while it runs.\n\n" +
|
|
"The old files stay on the mirror: `llmgw ctl model rollback MODEL` goes\n" +
|
|
"back to them. A model from server.yaml is changed there instead.",
|
|
Example: " llmgw ctl model update qwen-7b\n" +
|
|
" llmgw ctl model update qwen-7b --eval arithmetic --min-score 0.8",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
body, err := gate.body()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var job modelserve.Job
|
|
if err := client().post(cmd.Context(), "/api/v1/models/"+args[0]+"/update", body, &job); err != nil {
|
|
return err
|
|
}
|
|
if detach {
|
|
return detached(cmd, job)
|
|
}
|
|
return followJob(cmd, client, job, "runs the new weights")
|
|
},
|
|
}
|
|
c.Flags().BoolVar(&detach, "detach", false, "start it and return: it runs on the control plane; `llmgw ctl jobs` follows it")
|
|
gate.add(c)
|
|
return c
|
|
}
|
|
|
|
func newModelRollbackCmd(client func() *ctlClient) *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "rollback MODEL",
|
|
Short: "Move a model back to the weights its last update replaced",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
var out map[string]string
|
|
if err := client().post(cmd.Context(), "/api/v1/models/"+args[0]+"/rollback", []byte("{}"), &out); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(cmd.OutOrStdout(), out["result"])
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func newModelOriginCmd(client func() *ctlClient) *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "origin MODEL OWNER/REPO",
|
|
Short: "Say which registry repository a model's weights came from",
|
|
Long: "For a model fetched before llmgw kept this, and that it could not find on\n" +
|
|
"its own. The repository is checked: it must have the commit the mirror's\n" +
|
|
"directory is named after, holding the model's files at the sizes the\n" +
|
|
"mirror has. From then on its updates are watched.",
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
body, _ := json.Marshal(map[string]string{"repo": args[1]})
|
|
var o struct{ Repo, Revision string }
|
|
if err := client().post(cmd.Context(), "/api/v1/models/"+args[0]+"/origin", body, &o); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%s came from %s at %s; its updates are watched\n", args[0], o.Repo,
|
|
catalog.ShortRevision(o.Revision))
|
|
return nil
|
|
},
|
|
}
|
|
}
|