Say plainly whether a model is serving, and why not
CI / test (push) Successful in 1m28s

Reported after watching a model appear in ctl models before it could answer
anything. Checked: clients are not told early — /v1/models listed it only
once a replica was ready, and the chat picker reads the same source. The
operator view showing it is deliberate, because a model coming up is
exactly what someone running the cluster wants to see.

But the report pointed at something real. That view distinguished "a
replica is coming up" from "nothing could be placed" by a parenthetical —
"0/1 ready (1 total)" against "0/1 ready" — and those need opposite
responses: one means wait, the other means look at explain-placement. On
four models the difference is findable. On forty it is invisible, and a
model that will never start reads exactly like one that is thirty seconds
away.

SERVING now answers the question that was being asked of the replica counts
— can a client use this right now — and STATUS says which of ready,
starting, not placed or stopped is the case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5 committed 2026-09-17 06:34:04 +02:00
1 parent 331de9b495
commit d07dddb1bb
2 files changed
+60 -6

No files matched your search

+34 -6
View File
@@ -172,6 +172,33 @@ func newNodesCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
}
}
// modelStatus says what is happening to a model, in the words that decide what
// to do about it.
//
// The previous rendering distinguished "a replica is coming up" from "nothing
// could be placed" by a parenthetical — "0/1 ready (1 total)" against "0/1
// ready" — and those need opposite responses: one means wait, the other means
// look at explain-placement. On four models the difference is findable; on
// forty it is invisible, and a model that will never start looks exactly like
// one that is thirty seconds away.
//
// A model appearing here before it serves is deliberate: this is the operator's
// view, and a model coming up is precisely what someone wants to see. Clients
// are told separately and only when it is true — /v1/models lists what is
// actually served, never what is merely catalogued.
func modelStatus(m adminapi.ModelView) string {
switch {
case m.Min == 0 && m.Total == 0:
return "stopped"
case m.Ready >= m.Min && m.Min > 0:
return "ready"
case m.Total > m.Ready:
return "starting"
default:
return "not placed"
}
}
// idleColumn shows how long a replica has gone unused.
//
// "-" for a replica nothing has reported use of, which is emphatically not the
@@ -212,14 +239,15 @@ func newModelsCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
}
w := tab()
fmt.Fprintln(w, "MODEL\tREPLICAS\tALIASES\tPROFILES\tFALLBACKS")
fmt.Fprintln(w, "MODEL\tSERVING\tREPLICAS\tSTATUS\tALIASES\tPROFILES\tFALLBACKS")
for _, m := range models {
replicas := fmt.Sprintf("%d/%d ready", m.Ready, m.Min)
if m.Total > m.Ready {
replicas += fmt.Sprintf(" (%d total)", m.Total)
serving := "no"
if m.Ready > 0 {
serving = "yes"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
m.Name, replicas, join(m.Aliases), join(m.Profiles), join(m.Fallbacks))
fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\t%s\t%s\t%s\n",
m.Name, serving, m.Ready, m.Min, modelStatus(m),
join(m.Aliases), join(m.Profiles), join(m.Fallbacks))
}
return w.Flush()
},
+26
View File
@@ -3,6 +3,8 @@ package cli
import (
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
)
func TestBarStaysWithinItsWidth(t *testing.T) {
@@ -33,3 +35,27 @@ func TestIdleColumnDistinguishesUnknownFromIdle(t *testing.T) {
t.Errorf("idleColumn(90) = %q", got)
}
}
func TestModelStatusSeparatesWaitingFromBroken(t *testing.T) {
// These used to differ by a parenthetical — "0/1 ready (1 total)" against
// "0/1 ready" — and they need opposite responses: one means wait, the other
// means look at explain-placement. On forty models that difference is
// invisible.
for _, tc := range []struct {
name string
view adminapi.ModelView
want string
}{
{"serving what it asked for", adminapi.ModelView{Min: 2, Ready: 2, Total: 2}, "ready"},
{"a replica is coming up", adminapi.ModelView{Min: 1, Ready: 0, Total: 1}, "starting"},
{"nothing could be placed", adminapi.ModelView{Min: 1, Ready: 0, Total: 0}, "not placed"},
{"deliberately not running", adminapi.ModelView{Min: 0, Ready: 0, Total: 0}, "stopped"},
{"short but rebuilding", adminapi.ModelView{Min: 2, Ready: 1, Total: 2}, "starting"},
} {
t.Run(tc.name, func(t *testing.T) {
if got := modelStatus(tc.view); got != tc.want {
t.Errorf("modelStatus(%+v) = %q, want %q", tc.view, got, tc.want)
}
})
}
}