config: a workstation block, dropped when it names no address (V-485)
Health defaults to the supervisor's /health rather than llama-server's, because mavgpud is what answers 503 while the card is held.
This commit is contained in:
@@ -224,6 +224,11 @@ type Config struct {
|
|||||||
// See SearchConfig.
|
// See SearchConfig.
|
||||||
Search *SearchConfig `json:"search,omitempty"`
|
Search *SearchConfig `json:"search,omitempty"`
|
||||||
|
|
||||||
|
// Workstation — the big model on the owner's desktop, preferred over the
|
||||||
|
// resident one when its GPU is free. nil / absent / url empty ⇒ homesrv
|
||||||
|
// behaves exactly as it does today. See WorkstationConfig.
|
||||||
|
Workstation *WorkstationConfig `json:"workstation,omitempty"`
|
||||||
|
|
||||||
// Praxis — the ecosystem attention-state service. When configured, maven
|
// Praxis — the ecosystem attention-state service. When configured, maven
|
||||||
// calls the Praxis HTTP tools API for attention listing and item lifecycle.
|
// calls the Praxis HTTP tools API for attention listing and item lifecycle.
|
||||||
// Maven never touches Praxis's database directly (ecosystem invariant: no
|
// Maven never touches Praxis's database directly (ecosystem invariant: no
|
||||||
@@ -1105,6 +1110,44 @@ const (
|
|||||||
DefaultKiwixSnippetRunes = 1500
|
DefaultKiwixSnippetRunes = 1500
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WorkstationConfig — the big model on the owner's desktop (bugmachine, a
|
||||||
|
// 7900 GRE with 16GB), fronted by mavgpud.
|
||||||
|
//
|
||||||
|
// homesrv cannot grow a GPU, so the resident Qwen3-1.7B is the floor and this
|
||||||
|
// is the preferred model above it (owner's call, 2026-08-02, docs/offload.md).
|
||||||
|
// The workstation is never assumed up: its card is often held by a CPT run and
|
||||||
|
// the machine sleeps. No block, or an empty URL, and homesrv behaves exactly as
|
||||||
|
// it does today.
|
||||||
|
//
|
||||||
|
// Only the prompt crosses the LAN, and the workstation is not "the box". The
|
||||||
|
// rules in CLAUDE.md about what may leave still apply.
|
||||||
|
type WorkstationConfig struct {
|
||||||
|
// URL — where mavgpud listens, e.g. "http://192.168.1.105:8080". Empty ⇒
|
||||||
|
// the whole block is normalised to nil and nothing probes anything.
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
|
||||||
|
// Health — the admission endpoint. Empty ⇒ URL + "/health", which is what
|
||||||
|
// mavgpud serves. It answers 503 while the card is held, and that is the
|
||||||
|
// signal, so it must be the supervisor's endpoint and not llama-server's.
|
||||||
|
Health string `json:"health,omitempty"`
|
||||||
|
|
||||||
|
// Probe — how often admission is re-checked. 0 ⇒ DefaultWorkstationProbe.
|
||||||
|
// Nothing on the hot path waits for it: the answer is cached and read
|
||||||
|
// atomically, so this only sets how late Maven notices the card came back.
|
||||||
|
Probe Duration `json:"probe,omitempty"`
|
||||||
|
|
||||||
|
// Timeout — the per-request budget for a completion on the workstation.
|
||||||
|
// 0 ⇒ DefaultWorkstationTimeout. A big model on a LAN host is slower than
|
||||||
|
// the resident one, and a request that overruns falls back to the floor.
|
||||||
|
Timeout Duration `json:"timeout,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workstation defaults, applied in Normalise.
|
||||||
|
const (
|
||||||
|
DefaultWorkstationProbe = 15 * time.Second
|
||||||
|
DefaultWorkstationTimeout = 90 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// SearchConfig — the self-hosted SearXNG instance she searches with.
|
// SearchConfig — the self-hosted SearXNG instance she searches with.
|
||||||
//
|
//
|
||||||
// External search is allowed and off unless configured (CLAUDE.md). Configuring
|
// External search is allowed and off unless configured (CLAUDE.md). Configuring
|
||||||
@@ -1500,6 +1543,24 @@ func (c *Config) applyDefaults() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No address, no preferred model. An unconfigured workstation is the
|
||||||
|
// default deploy and must be indistinguishable from today.
|
||||||
|
if c.Workstation != nil && strings.TrimSpace(c.Workstation.URL) == "" {
|
||||||
|
c.Workstation = nil
|
||||||
|
}
|
||||||
|
if c.Workstation != nil {
|
||||||
|
w := c.Workstation
|
||||||
|
if strings.TrimSpace(w.Health) == "" {
|
||||||
|
w.Health = strings.TrimRight(w.URL, "/") + "/health"
|
||||||
|
}
|
||||||
|
if w.Probe <= 0 {
|
||||||
|
w.Probe = Duration(DefaultWorkstationProbe)
|
||||||
|
}
|
||||||
|
if w.Timeout <= 0 {
|
||||||
|
w.Timeout = Duration(DefaultWorkstationTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c.Voice != nil {
|
if c.Voice != nil {
|
||||||
if c.Voice.RouterThreshold <= 0 {
|
if c.Voice.RouterThreshold <= 0 {
|
||||||
c.Voice.RouterThreshold = DefaultRouterThreshold
|
c.Voice.RouterThreshold = DefaultRouterThreshold
|
||||||
|
|||||||
@@ -413,3 +413,56 @@ func TestNormaliseFillsKiwixDefaults(t *testing.T) {
|
|||||||
t.Error("rewrite: false was not honoured")
|
t.Error("rewrite: false was not honoured")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A workstation with no address is not a workstation. The unconfigured deploy
|
||||||
|
// must be indistinguishable from today, so the block is dropped rather than
|
||||||
|
// left to fail one probe at a time.
|
||||||
|
func TestNormaliseDropsAddresslessWorkstation(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
in *WorkstationConfig
|
||||||
|
}{
|
||||||
|
{"no url", &WorkstationConfig{Probe: Duration(time.Second)}},
|
||||||
|
{"blank url", &WorkstationConfig{URL: " "}},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c := &Config{Workstation: tc.in}
|
||||||
|
c.applyDefaults()
|
||||||
|
if c.Workstation != nil {
|
||||||
|
t.Errorf("kept an unusable workstation block: %+v", c.Workstation)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The health endpoint defaults to the supervisor's, not llama-server's: mavgpud
|
||||||
|
// answers 503 while the card is held, and that refusal is the whole signal.
|
||||||
|
func TestNormaliseFillsWorkstationDefaults(t *testing.T) {
|
||||||
|
c := &Config{Workstation: &WorkstationConfig{URL: "http://192.168.1.105:8080/"}}
|
||||||
|
c.applyDefaults()
|
||||||
|
if c.Workstation == nil {
|
||||||
|
t.Fatal("dropped a usable workstation block")
|
||||||
|
}
|
||||||
|
if got, want := c.Workstation.Health, "http://192.168.1.105:8080/health"; got != want {
|
||||||
|
t.Errorf("Health = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if time.Duration(c.Workstation.Probe) != DefaultWorkstationProbe {
|
||||||
|
t.Errorf("Probe = %s, want %s", time.Duration(c.Workstation.Probe), DefaultWorkstationProbe)
|
||||||
|
}
|
||||||
|
if time.Duration(c.Workstation.Timeout) != DefaultWorkstationTimeout {
|
||||||
|
t.Errorf("Timeout = %s, want %s", time.Duration(c.Workstation.Timeout), DefaultWorkstationTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An explicit health URL is left alone: the supervisor may sit behind something
|
||||||
|
// that does not put /health at the root.
|
||||||
|
func TestNormaliseKeepsExplicitWorkstationHealth(t *testing.T) {
|
||||||
|
c := &Config{Workstation: &WorkstationConfig{
|
||||||
|
URL: "http://192.168.1.105:8080",
|
||||||
|
Health: "http://192.168.1.105:9000/ready",
|
||||||
|
}}
|
||||||
|
c.applyDefaults()
|
||||||
|
if got, want := c.Workstation.Health, "http://192.168.1.105:9000/ready"; got != want {
|
||||||
|
t.Errorf("Health = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user