From d07dddb1bb4cf86e64706aaf02abccf29c409899 Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Thu, 17 Sep 2026 06:34:04 +0200 Subject: [PATCH] Say plainly whether a model is serving, and why not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- internal/cli/ctl.go | 40 +++++++++++++++++++++++++++++++------ internal/cli/status_test.go | 26 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/internal/cli/ctl.go b/internal/cli/ctl.go index c88b3e3..cce20d3 100644 --- a/internal/cli/ctl.go +++ b/internal/cli/ctl.go @@ -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() }, diff --git a/internal/cli/status_test.go b/internal/cli/status_test.go index 7c297ef..0e4f007 100644 --- a/internal/cli/status_test.go +++ b/internal/cli/status_test.go @@ -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) + } + }) + } +}