The console starts a serve or update job over the admin API and stays, instead of closing to follow the download. Jobs lists them with their progress and steps; the sidebar counts those running. ctl model serve and update take --detach; ctl jobs lists jobs and follows one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
103 lines
3.0 KiB
Go
103 lines
3.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/modelserve"
|
|
)
|
|
|
|
// Jobs: serving a model or moving one to new weights is a job on the control
|
|
// plane — a download that can take an hour, then a replica starting. Started
|
|
// from the console, it runs there whatever this screen does; the console is
|
|
// free at once, and this view follows it.
|
|
|
|
// startedSays is the result of starting a job.
|
|
func startedSays(raw json.RawMessage) string {
|
|
var j modelserve.Job
|
|
if json.Unmarshal(raw, &j) != nil || j.ID == "" {
|
|
return "Started."
|
|
}
|
|
return fmt.Sprintf("Started %s. It runs on the control plane — leaving this screen does not stop it.\n"+
|
|
"Jobs, under Watch, shows how far it has got.", j.ID)
|
|
}
|
|
|
|
// runningJobs counts the jobs still under way.
|
|
func runningJobs(jobs []modelserve.Job) int {
|
|
n := 0
|
|
for _, j := range jobs {
|
|
if j.State == modelserve.Running {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func jobState(j modelserve.Job) string {
|
|
switch j.State {
|
|
case modelserve.Running:
|
|
return sWarn.Render("running")
|
|
case modelserve.Failed:
|
|
return sBad.Render("failed")
|
|
}
|
|
for _, st := range j.Steps {
|
|
if st.Name == "placement" && st.State == modelserve.Skipped {
|
|
return sWarn.Render("waiting for room")
|
|
}
|
|
}
|
|
return sOK.Render("done")
|
|
}
|
|
|
|
// jobNow is the step a job is at, in a line.
|
|
func jobNow(j modelserve.Job) string {
|
|
if j.State == modelserve.Failed {
|
|
return j.Error
|
|
}
|
|
if len(j.Steps) == 0 {
|
|
return ""
|
|
}
|
|
st := j.Steps[len(j.Steps)-1]
|
|
return st.Name + ": " + st.Detail
|
|
}
|
|
|
|
func (m consoleModel) jobsPanel(pw int) (string, string, string) {
|
|
keys := "→ or Enter open · ↑↓ choose · Q leave"
|
|
if m.inPanel {
|
|
keys = "↑↓ row · Enter its steps · ← back · Q leave"
|
|
}
|
|
head := "\n " + sDim.Render("Models being served or updated, on the control plane; kept until it restarts.") + "\n\n"
|
|
var rows [][]string
|
|
for _, j := range m.data.jobs {
|
|
kind := j.Kind
|
|
if kind == "" {
|
|
kind = "serve"
|
|
}
|
|
what := j.Model
|
|
if what == "" {
|
|
what = j.Request.Repo
|
|
}
|
|
rows = append(rows, []string{ageShort(j.Started), kind, what, jobState(j), jobNow(j)})
|
|
}
|
|
if len(rows) == 0 {
|
|
return "Jobs", head + " " + sDim.Render("None since the control plane started. Serve a model, under Do, starts one."), keys
|
|
}
|
|
return "Jobs", head + table([]string{"STARTED", "KIND", "MODEL", "STATE", "NOW"}, rows, m.row, pw, m.inPanel), keys
|
|
}
|
|
|
|
// jobDialog is a job's steps, each as it stands.
|
|
func jobDialog(j modelserve.Job) (string, string) {
|
|
var b strings.Builder
|
|
mark := map[string]string{modelserve.Running: sWarn.Render(".."), modelserve.Done: sOK.Render("ok"),
|
|
modelserve.Failed: sBad.Render("!!"), modelserve.Skipped: sDim.Render("--")}
|
|
for _, st := range j.Steps {
|
|
fmt.Fprintf(&b, "%s %-11s %s\n", mark[st.State], st.Name, wrap(st.Detail, 80, strings.Repeat(" ", 15)))
|
|
}
|
|
if j.Reply != "" {
|
|
fmt.Fprintf(&b, "\n%s said %q at %.1f tokens/s\n", j.Model, j.Reply, j.TokensPerSecond)
|
|
}
|
|
fmt.Fprintf(&b, "\n%s, started %s\n\n%s", j.ID, j.Started.Format(time.DateTime), sDim.Render("any key"))
|
|
return j.ID, b.String()
|
|
}
|