4.7 KiB
v2.1 task 02: router mode — loaded means loaded, context is per model
Branch: v2.1 (git switch v2.1; git status --short must be empty, otherwise stop)
Commit subject: Learn per-model context from /props?model=; only status "loaded" is loaded
Goal
crossbar's real upstreams are llama-server routers, not single servers, and v2's poller was
written against the single-server shape. On a router: /v1/models lists every configured model
with a status.value ("loaded", "unloaded", "loading"); the plain /props answers as the
router itself ("role":"router", n_ctx 0); and /props?model=X answers for X's child server —
and loads X if it is not loaded, which a health poll must never cause. After this task the
poller treats only "loaded" models as loaded, asks /props?model=X only for those, keeps the
answers per model, and the guard and the hosts view use the per-model figures. A plain single
server keeps working exactly as in v2.
Context
Verified on straylight's router on 2026-09-25: /v1/models entries carry
"status":{"value":"unloaded",...} for seven of eight models; plain /props returns
{"role":"router","model_alias":"llama-server","default_generation_settings":{"n_ctx":0}};
/props?model=ornith-1.5-35b-a3b (loaded) returns n_ctx 262144 and total_slots 4. With v2's
code every listed model counts as loaded and the guard learns nothing, so the "Context size has
been exceeded" failure crossbar exists to prevent still happens on a router.
Files
- Copy:
internal/health/props_router_test.go,internal/proxy/ctxguard_router_test.go,internal/admin/admin_models_test.go - Modify:
internal/health/health.go(a newinternal/health/props.gois allowed for the 400-line limit),internal/proxy/ctxguard.go,internal/admin/admin.go,docs/implementer-log.md
Interfaces
package health
// ModelCtx is what /props?model=X taught us about one loaded model.
type ModelCtx struct {
NCtx int `json:"n_ctx"`
Slots int `json:"slots"`
}
type Status struct {
// ... as v2 ...
Models map[string]ModelCtx `json:"models"` // per loaded model; never nil after a poll; copied by Get/All
}
// PerSlotCtxFor is the per-slot context for one model on this host: Models[model] when present
// (NCtx/Slots, 0 when either is 0); else, when model is in Loaded, the host-level PerSlotCtx();
// else 0 ("unknown" / not resident).
func (s Status) PerSlotCtxFor(model string) int
Poller rules:
/v1/models: an entry is loaded when it has nostatusorstatus.value == "loaded"; any other value ("unloaded","loading", …) is not loaded and does not appear inLoaded.- Plain
/props: when the body has"role":"router"the host-levelNCtx/Slotsstay 0 whatever else it says; otherwise as v2 (best effort, never a failure). - For each model in
Loaded,GET /props?model=<url.QueryEscape(id)>, decoded like the plain one, intoModels[id]. A failed or malformed answer leaves that id absent and the host healthy. Never ask for a model that is not inLoaded. Modelsis a fresh non-nil map on every successful poll;MarkDownleaves it as last seen.
Proxy (ctxguard.go): every use of PerSlotCtx() becomes PerSlotCtxFor(model) — the leased
host's figure, the candidates a prompt may move to, the wake path's "cannot serve it no matter
how it wakes" check, and largestSlotCtx, which now takes the model and so only counts hosts
that have it loaded. Admin (admin.go): HostView gains Models map[string]health.ModelCtx
(JSON models, an empty object never null).
Steps
- 1.
git switch v2.1; copy the three given tests. - 2. See them fail:
go test -race -count=1 ./internal/health/ ./internal/proxy/ ./internal/admin/— the new tests fail to compile until the names exist, then fail on behaviour. - 3.
healthfirst (rules 1–4,ModelCtx,PerSlotCtxFor);go test -race -count=1 ./internal/health/→ok. - 4.
ctxguard.go, thenadmin.go; each packageok.gofmt -w. - 5.
go test -race -count=1 ./...→ allok;make smoke→smoke: ok(the smoke's fake is a single server; nothing there changes). - 6.
make gate. 7. Rowv2.1/02-props-loaded-only; commit.
git add internal/health internal/proxy internal/admin docs/implementer-log.md
git commit
Done when
- All three given tests pass; every v2 health/guard/admin test still passes; gate and smoke ok; given files byte-identical; no file over 400 lines.
Stop and report if
- A v2 given test (
props_test.go,ctxguard_test.go,admin_test.go,health_test.go) needs changing to pass: quote it — that is the owner's test, not yours to edit.