The guardrails' judge has its own command, and its trouble is shown

llmgw ctl guard judge status lists each judge model the policies ask:
in the catalog, as a judge, replicas ready, its last failure to answer, and
what is wrong in words. enable serves the missing one through model serve -
Llama Guard 3 1B on CPUs, role guard, under the name the policies give it -
with --replace for a model of that name that is not a judge; the console has
it under Do. GET /api/v1/guard/judges, and the summary's judges, put the
problem on ctl status and the console's Overview.

A Llama Guard file served from the mirror page is added as a judge, under
the name the policies ask, rather than as a model clients use the policies
never find. ctl says plainly when the API answers with a page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-06 10:47:37 +02:00
1 parent 2a942409ea
commit a8bb417618
24 files changed
+667 -13

No files matched your search

+4 -2
View File
@@ -88,8 +88,10 @@ type AccountPages struct {
oidc *oidcLogin
// audit records every change made through these pages; nil records none.
audit audit.Sink
// hasGuard is set when /admin/guard is registered.
hasGuard bool
// hasGuard is set when /admin/guard is registered, and guardLibrary
// with it: the policies in force, whose judges the mirror page serves.
hasGuard bool
guardLibrary func() guard.Config
// guardFor names the guardrail policies in force for someone, and
// whether they keep nothing; nil when there are none.
guardFor func(guard.Subject) (string, bool)
+1 -1
View File
@@ -50,7 +50,7 @@ var guardWindows = map[string]time.Duration{"24h": 24 * time.Hour, "7d": 7 * 24
// RegisterAdminGuard mounts /admin/guard: what the guardrails did, where, by
// which policy — names and counts, never text.
func (p *AccountPages) RegisterAdminGuard(mux *http.ServeMux, s GuardAdminSource, library func() guard.Config) {
p.hasGuard = true
p.hasGuard, p.guardLibrary = true, library
mux.HandleFunc("GET /admin/guard", p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) {
ctx, l := r.Context(), langOf(r)
lib := library()
+15 -4
View File
@@ -324,10 +324,14 @@ func (p *AccountPages) RegisterAdminMirror(mux *http.ServeMux, d MirrorDeps) {
if err != nil {
return "", err
}
// A judge's weights are served as a judge, under the name the
// policies ask: served as a model clients use, the policies
// never find it and refuse every request they judge.
judge, renamed := p.asJudge(&model)
// The CPU while GPUs sit free is a choice to make on purpose:
// made by default, it was a model running at a tenth of the
// speed with nothing saying why.
if model.Profiles[0].CPUOnly() && r.FormValue("cpu_ok") == "" {
// speed with nothing saying why. A judge is meant for the CPU.
if !judge && model.Profiles[0].CPUOnly() && r.FormValue("cpu_ok") == "" {
if n := freeGPUs(p.fleet); n > 0 {
return "", errors.New(l.N("serve.err.cpu_with_free_gpus", int64(n), model.Name))
}
@@ -346,8 +350,15 @@ func (p *AccountPages) RegisterAdminMirror(mux *http.ServeMux, d MirrorDeps) {
}
// Placed on the next pass, not now: saying it is serving would
// be wrong for however long the weights take to reach a node.
return l.T("serve.msg.added", model.Name,
l.N("serve.replicas_n", int64(model.Scaling.Min)), describeServe(l, model)), nil
msg := l.T("serve.msg.added", model.Name,
l.N("serve.replicas_n", int64(model.Scaling.Min)), describeServe(l, model))
switch {
case renamed != "":
msg += " " + l.T("serve.msg.judge_renamed", renamed, model.Name)
case judge:
msg += " " + l.T("serve.msg.judge", model.Name)
}
return msg, nil
}))
}
+21
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
"gitea.mynodes.xyz/ilker/llmgw/internal/guard"
"io"
"log/slog"
"net/http"
@@ -745,3 +746,23 @@ func TestASafetensorsSetIsFetchedWithWhatTheEngineReadsBesideIt(t *testing.T) {
t.Errorf("the companions are not said; got %s", body)
}
}
func TestAJudgesWeightsAreServedAsTheJudgeThePoliciesAsk(t *testing.T) {
p := &AccountPages{guardLibrary: func() guard.Config {
return guard.Config{Policies: map[string]guard.Policy{
"baseline": {Judge: &guard.JudgePolicy{Model: "llama-guard-3-1b"}},
}}
}}
m := catalog.Model{Name: "llama-guard", Source: "http://10.0.0.146:8081/llama-guard/x/Llama-Guard-3-1B.Q4_K_M.gguf",
RoutingProfile: "chat"}
judge, renamed := p.asJudge(&m)
if !judge || renamed != "llama-guard" || m.Name != "llama-guard-3-1b" || !m.IsGuard() ||
m.Guard.Format != "llamaguard3" || m.RoutingProfile != "" {
t.Errorf("judge %v renamed %q: %+v", judge, renamed, m)
}
plain := catalog.Model{Name: "qwen", Source: "http://m/qwen.gguf", RoutingProfile: "chat"}
if judge, _ := p.asJudge(&plain); judge || plain.IsGuard() || plain.Name != "qwen" {
t.Errorf("a model clients use became a judge: %+v", plain)
}
}
+33
View File
@@ -585,3 +585,36 @@ func argInt(args []string, flag string) int {
}
return 0
}
// isJudgeWeights is whether a source is a guard model's weights: Llama
// Guard, which reads a conversation and says whether it is safe.
func isJudgeWeights(source string) bool {
return strings.Contains(strings.ToLower(source), "llama-guard")
}
// asJudge makes a model built from the serve form a judge when its weights
// are one's: role guard, Llama Guard's format, under the name the policies
// give their judge when they give one and the form gave another. It says
// whether it did, and the name the form gave when it was changed.
func (p *AccountPages) asJudge(m *catalog.Model) (judge bool, renamed string) {
if !isJudgeWeights(m.Source) {
return false, ""
}
m.Role, m.Guard, m.RoutingProfile = catalog.RoleGuard, &catalog.GuardModel{Format: "llamaguard3"}, ""
if p.guardLibrary == nil {
return true, ""
}
asked := map[string]bool{}
for _, pol := range p.guardLibrary().Policies {
if pol.Judge != nil && pol.Judge.Model != "" {
asked[pol.Judge.Model] = true
}
}
if asked[m.Name] || len(asked) != 1 {
return true, ""
}
for name := range asked {
renamed, m.Name = m.Name, name
}
return true, renamed
}
+3 -1
View File
@@ -77,5 +77,7 @@
"serve.err.cpu_with_free_gpus.other": "this fleet has %d GPUs free and %s was set to run on the CPU; choose a GPU placement, or tick \"run on the CPU anyway\"",
"serve.msg.added": "%s added to the catalog, %s: %s; it is placed on the next pass",
"serve.replicas_n.one": "%d replica",
"serve.replicas_n.other": "%d replicas"
"serve.replicas_n.other": "%d replicas",
"serve.msg.judge": "It is the guardrails' judge (role: guard): policies naming %s ask it; clients do not see it.",
"serve.msg.judge_renamed": "Its weights are a judge's, so it is the guardrails' judge (role: guard), named %[2]s as the policies ask rather than %[1]s."
}
+3 -1
View File
@@ -74,5 +74,7 @@
"serve.err.placement": "%q geçerli bir yerleşim değil",
"serve.err.cpu_with_free_gpus.other": "bu kümede %d GPU boşta, %s ise CPU üzerinde çalışacak biçimde ayarlandı; bir GPU yerleşimi seçin ya da \"yine de CPU üzerinde çalıştır\" kutusunu işaretleyin",
"serve.msg.added": "%s kataloğa eklendi, %s: %s; bir sonraki geçişte yerleştirilecek",
"serve.replicas_n.other": "%d replika"
"serve.replicas_n.other": "%d replika",
"serve.msg.judge": "Guardrail'lerin judge modeli olarak eklendi (role: guard): %s adını veren politikalar ona soruyor; istemciler görmüyor.",
"serve.msg.judge_renamed": "Ağırlıkları bir judge modelinin olduğu için guardrail judge'ı olarak eklendi (role: guard); adı %[1]s yerine politikaların istediği gibi %[2]s."
}