From edeef19ff0c13d11e1b504d46db4aa209286fb3c Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 22:42:09 +0400 Subject: [PATCH 1/5] 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. --- internal/config/config.go | 61 ++++++++++++++++++++++++++++++++++ internal/config/config_test.go | 53 +++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index a95bf77..b152b14 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -224,6 +224,11 @@ type Config struct { // See SearchConfig. 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 // calls the Praxis HTTP tools API for attention listing and item lifecycle. // Maven never touches Praxis's database directly (ecosystem invariant: no @@ -1105,6 +1110,44 @@ const ( 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. // // 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.RouterThreshold <= 0 { c.Voice.RouterThreshold = DefaultRouterThreshold diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fe275dd..2e315d3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -413,3 +413,56 @@ func TestNormaliseFillsKiwixDefaults(t *testing.T) { 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) + } +} From 92d5fd580c7456bdfd0abcb882f4bcd708a60e73 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 22:42:09 +0400 Subject: [PATCH 2/5] mavend: route and reply through the workstation when its card is free (V-485) modelSeam builds an llm.Pair when a workstation is configured and hands it to the router and the replier. Both are the silent half of the degradation rule: the big model is only better there, and he is never told which model answered. No block, no probe, and the box behaves exactly as it did. --- cmd/mavend/modelseam_test.go | 91 ++++++++++++++++++++++++++++++++++++ cmd/mavend/voicewire.go | 57 ++++++++++++++++++++-- 2 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 cmd/mavend/modelseam_test.go diff --git a/cmd/mavend/modelseam_test.go b/cmd/mavend/modelseam_test.go new file mode 100644 index 0000000..553b8ac --- /dev/null +++ b/cmd/mavend/modelseam_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/kami/maven/internal/config" + "github.com/kami/maven/internal/llm" +) + +// No `workstation` block is the shipping deploy. The seam must then be the +// resident client itself, with nothing probing anything. +func TestModelSeamUnconfiguredIsResidentOnly(t *testing.T) { + resident := llm.New("http://127.0.0.1:1", time.Second) + hot, pair := modelSeam(&config.Config{}, resident) + if pair != nil { + t.Error("built a pair with no workstation configured") + } + if hot == nil { + t.Fatal("no seam at all, so the cascade would route with the classifier") + } +} + +// A workstation with no resident model behind it has no floor, and a Pair with +// no floor is a configuration mistake rather than a degraded mode. +func TestModelSeamWithoutResidentIsNil(t *testing.T) { + cfg := &config.Config{Workstation: &config.WorkstationConfig{URL: "http://127.0.0.1:1"}} + cfg.Workstation.Health = strings.TrimRight(cfg.Workstation.URL, "/") + "/health" + hot, pair := modelSeam(cfg, nil) + if hot != nil || pair != nil { + t.Errorf("built a seam with no floor: hot=%v pair=%v", hot, pair) + } +} + +// The configured case: the seam is the pair, and the pair notices a workstation +// that answers /health. +func TestModelSeamPrefersAnAnsweringWorkstation(t *testing.T) { + up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer up.Close() + + cfg := &config.Config{Workstation: &config.WorkstationConfig{ + URL: up.URL, + Probe: config.Duration(10 * time.Millisecond), + }} + cfg.Workstation.Health = strings.TrimRight(cfg.Workstation.URL, "/") + "/health" + + hot, pair := modelSeam(cfg, llm.New("http://127.0.0.1:1", time.Second)) + if pair == nil || hot == nil { + t.Fatal("no pair built for a configured workstation") + } + defer pair.Stop() + + deadline := time.Now().Add(2 * time.Second) + for !pair.Available() && time.Now().Before(deadline) { + time.Sleep(5 * time.Millisecond) + } + if !pair.Available() { + t.Fatal("the pair never saw a workstation that answers /health") + } +} + +// A card held by a CPT run answers 503, and that must read as unavailable +// rather than as an error a turn has to handle. +func TestModelSeamHeldCardIsUnavailable(t *testing.T) { + busy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "model not loaded", http.StatusServiceUnavailable) + })) + defer busy.Close() + + cfg := &config.Config{Workstation: &config.WorkstationConfig{ + URL: busy.URL, + Probe: config.Duration(10 * time.Millisecond), + }} + cfg.Workstation.Health = strings.TrimRight(cfg.Workstation.URL, "/") + "/health" + + _, pair := modelSeam(cfg, llm.New("http://127.0.0.1:1", time.Second)) + if pair == nil { + t.Fatal("no pair built for a configured workstation") + } + defer pair.Stop() + + time.Sleep(50 * time.Millisecond) + if pair.Available() { + t.Error("a 503 from the supervisor read as available") + } +} diff --git a/cmd/mavend/voicewire.go b/cmd/mavend/voicewire.go index f382829..41a6911 100644 --- a/cmd/mavend/voicewire.go +++ b/cmd/mavend/voicewire.go @@ -48,7 +48,11 @@ type voiceWiring struct { // mcp — the MCP client, nil unless the `mcp` block configures an enabled // server (Vikunja #251). Its tools land in the same allowlist as every // other act, so nothing else here has to know about it. - mcp *mcpWiring + // pair — the workstation model with the resident one as the floor, nil + // unless a `workstation` block names an address. Held here only so the + // prober is stopped on shutdown; callers were handed it at build time. + pair *llm.Pair + mcp *mcpWiring // home — the Home Assistant client, nil unless the `smarthome` block is // enabled (Vikunja #256). Its devices land in the same allowlist as every // other act, so nothing else here has to know about it. @@ -76,6 +80,9 @@ func (w *voiceWiring) close() { if w.ttsClient != nil { _ = w.ttsClient.Close() } + if w.pair != nil { + w.pair.Stop() + } w.mcp.close() } @@ -188,6 +195,11 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem // the new llama-server when the resident model is swapped (Vikunja #250). llmClient = llmClientFor(lp, 60*time.Second) } + // The workstation model sits above that one when it is configured and its + // card is free. hot is what the router and the replier complete through: + // either the pair, or the resident client alone, or nothing at all. + hot, pair := modelSeam(cfg, llmClient) + w.pair = pair // ----- router (the cascade; floor examples seed the classifier) ----- // The act matcher's allowlist is exactly the enabled tool names — the // router only matches acts the executor can run (one source of truth). @@ -199,7 +211,7 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem // against the classifier's 50.0%, at about 1s a turn instead of 30ms (see // config.VoiceConfig.LLMRouter). The classifier always stays wired as the // fallback, so a model error never breaks a turn. - rtr := buildRouter(emb, matcher, threshold, pickLLMRouter(cfg.Voice.UseLLMRouter(), llmClient)) + rtr := buildRouter(emb, matcher, threshold, pickLLMRouter(cfg.Voice.UseLLMRouter(), hot)) // ----- sessions registry (shared with voicesink) ----- sessions := voice.NewSessions() @@ -233,8 +245,8 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem // ----- replier (LLM-backed when the engine is on, Stub floor otherwise) ----- replier := voice.Replier(voice.NewStubReplier()) - if llmClient != nil { - replier = newLLMReplier(llmClient, contextBlockFn(cfg, time.Now)) + if hot != nil { + replier = newLLMReplier(hot, contextBlockFn(cfg, time.Now)) } // ----- the handler (the reactive path; closes over stt / tts / router / coreAPI / memory) ----- @@ -291,7 +303,42 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem // pickLLMRouter returns the LLM router when the operator asked for it and there // is a llama-server to talk to, and nil otherwise. nil is safe: the cascade then // routes with the classifier, so an unusable setting costs accuracy, not turns. -func pickLLMRouter(enabled bool, c *llm.Client) *router.LLMRouter { +// modelSeam builds the completion seam the hot paths use: routing and replies. +// +// With no `workstation` block it is the resident client and nothing probes +// anything, which is today's deploy exactly. With one, it is an llm.Pair that +// prefers the workstation and falls back to the resident model silently — the +// silent half of the degradation rule (docs/offload.md), because the big model +// is only better here and the 1.7B is today's shipping quality. He is never +// told which of the two phrased his reply. +// +// A nil resident client means the phraser is not an LLM phraser. There is then +// no floor, and a Pair with no floor is a configuration mistake rather than a +// degraded mode, so the seam is nil and the cascade routes with the classifier. +func modelSeam(cfg *config.Config, resident *llm.Client) (router.Completer, *llm.Pair) { + if resident == nil { + if cfg.Workstation != nil { + log.Printf("voice: a workstation is configured but there is no resident model to floor it with — ignoring the block") + } + return nil, nil + } + if cfg.Workstation == nil { + return resident, nil + } + ws := cfg.Workstation + pair := llm.NewPair( + llm.New(ws.URL, time.Duration(ws.Timeout)), + resident, + ws.Health, + time.Duration(ws.Probe), + ) + pair.Start(context.Background()) + log.Printf("voice: workstation model at %s, probed every %s, resident model as the floor", + ws.URL, time.Duration(ws.Probe)) + return pair, pair +} + +func pickLLMRouter(enabled bool, c router.Completer) *router.LLMRouter { if !enabled { return nil } From 2db59d52a727da482f6b44e11906cf9f5fe6a5ca Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 22:42:09 +0400 Subject: [PATCH 3/5] deploy, docs: point homesrv at bugmachine and say what is still unwired (V-485) --- deploy/mavend.json | 17 +++++++++++++++++ docs/offload.md | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/deploy/mavend.json b/deploy/mavend.json index 501e2dc..246b69f 100644 --- a/deploy/mavend.json +++ b/deploy/mavend.json @@ -39,6 +39,23 @@ "proxy": "socks5://192.168.240.1:10808" }, + "//workstation": [ + "The big model on the desk PC (bugmachine, 7900 GRE 16GB), fronted by", + "mavgpud on port 8080. It runs gemma-4-12b and it is preferred over the", + "resident Qwen3-1.7B for routing and replies whenever the card is free.", + "The machine is never assumed up: it sleeps, and the card is often held by", + "a CPT run, in which case mavgpud answers 503 and Maven falls back to the", + "resident model without saying so. Deleting this block restores exactly", + "the behaviour homesrv had before it existed.", + "Addressed by LAN address, not container name: mavgpud runs on another", + "machine and there is no shared docker network to name it on." + ], + "workstation": { + "url": "http://192.168.1.105:8080", + "probe": "15s", + "timeout": "90s" + }, + "//search": [ "The live web, searched after his own notes and before Kiwix. Only the", "query string leaves the box — never a note, a fact, the persona block or", diff --git a/docs/offload.md b/docs/offload.md index 4c6a6c8..32291fe 100644 --- a/docs/offload.md +++ b/docs/offload.md @@ -125,7 +125,12 @@ Then the embedder above, **whisper.cpp** in `mavsttd`, and **piper** in `mavttsd `internal/netaddr` landed in PR #92. A seam address now carries its own scheme, and a scheme-less one is still unix. A tcp seam requires a shared token, because the filesystem permission that authenticated the unix socket is gone. -2. **The resident model** (#485). Biggest quality delta. A 16GB card runs a 7-14B, +2. **The resident model** (#485). Half wired, 02-08-2026. A `workstation` block + builds an `llm.Pair` in `modelSeam` (`cmd/mavend/voicewire.go`), and routing + and replies complete through it. Both are the silent half of the rule. The + naming half is not wired. A world question still goes to the resident model + through `PhraseQuery`, and the fixture measurement has not been run. + Biggest quality delta. A 16GB card runs a 7-14B, which fixes what the 1.7B gets wrong: world knowledge, and the persona the CPT targets. The degradation path is already written and measured, since the classifier scores 68.8% full accuracy at p50 16.6µs on its own. From 774217199e35aeee4b458c26eb9eca10d5e90154 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 22:51:47 +0400 Subject: [PATCH 4/5] docs: measure gemma-4-12b on the workstation against the resident model (V-485) Both fixtures, run from homesrv across the LAN with the proxy env stripped. Routing: 84.4% full / 93.5% intent-only at p50 329ms through the cascade, against 72.7% / 77.9% at p50 0.80-1.04s for Qwen3-1.7B. Talk: 25/27 against 20/27, with knowledge 9/9. Nudges 15/15. Settles #485's first assumption by measurement. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NJYcaBiuny9UGSpFweQVQ1 --- .../2026-08-02-workstation-gemma4-12b.md | 82 +++++++++++++++++++ docs/offload.md | 7 +- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 docs/evals/2026-08-02-workstation-gemma4-12b.md diff --git a/docs/evals/2026-08-02-workstation-gemma4-12b.md b/docs/evals/2026-08-02-workstation-gemma4-12b.md new file mode 100644 index 0000000..725de3c --- /dev/null +++ b/docs/evals/2026-08-02-workstation-gemma4-12b.md @@ -0,0 +1,82 @@ +# gemma-4-12b on the workstation, against the resident Qwen3-1.7B + +Measured 2026-08-02 on the fixtures as they stand. Dated file: it is not edited +after today, and a newer number is a new file. + +Vikunja #485's first assumption was that a 7-14B measurably beats Qwen3-1.7B on +the 77-case RU routing fixture and the 27-case talk fixture. It does, on both, +and it is also faster. + +## The setup + +`gemma-4-12B-it-qat-UD-Q4_K_XL` with the `mtp-gemma-4-12B-it-BF16` draft model, +served by `llama-server` b10220 on bugmachine (AMD 7900 GRE, 16GB), fronted by +`mavgpud` on `192.168.1.105:8080`. Thinking is off through +`--chat-template-kwargs '{"enable_thinking":false}'`, speculative decoding is +`--spec-type draft-mtp --spec-draft-n-max 2`, context 32768. The exact line is +`deploy/mavgpud.json`. + +Every number below crossed the LAN from homesrv. Note the trap: homesrv's shell +exports `HTTP_PROXY`, Go honours it, and the runs need +`env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy`. + +## Routing, 77-case RU fixture + +| | full | intent-only | p50 | p95 | +|---|---|---|---|---| +| classifier alone (02-08) | 68.8% | — | 16.6µs | — | +| Qwen3-1.7B through the cascade (31-07, 02-08) | 72.7% | 77.9% | 0.80-1.04s | — | +| **gemma-4-12b through the cascade** | **84.4%** | **93.5%** | **329ms** | 429ms | +| gemma-4-12b alone, no cascade | 55.8% | 85.7% | 335ms | 436ms | + +The workstation buys 11.7 points of full accuracy over the resident model. It +buys 15.6 points of intent-only, at a third of the latency. The router's p50 was +never the model's fault, which the 02-08 contention finding already said. A 12B +on a free 16GB card answers a routing turn in a third of a second. + +Two things the table hides. + +The alone-versus-cascade gap is slots, not intents. gemma reads the intent right +85.7% of the time on its own. It loses full accuracy on seven fact keys +(`вода` instead of `water`, `ужин` instead of `meal`) and on six reminder times +with no time slot. Stage 0 and the daemon's own extractor repair +both, which is why the cascade is 28 points higher. The lesson is that the +cascade earns its keep even under a much better model, not that it is scaffolding +to remove. + +`errors: 6` in the alone row are declines on single-token and ambiguous +utterances, all of which the cascade caught. The remaining defects through the +cascade are three `query→fact` confusions, one `chat→query`, and one false +clarify. + +## Talk, 27-case conversational fixture + +| | pass | notes | +|---|---|---| +| Qwen3-1.7B (31-07) | 20/27 | 11-17/27 for the 0.8B before it | +| **gemma-4-12b** | **25/27 (92.6%)** | chat 8/9, knowledge 9/9, query 8/9 | + +Knowledge is the interesting column: 9/9, in Russian, with real answers about +Rayleigh scattering, SSD versus HDD and thunder delay. That is the case the +1.7B cannot do at all and the reason the naming half of the degradation rule +exists. + +Two failures, and one of them is the persona defect the CPT (#122) targets: +`query-notes-do-not-answer` wrote `заплатил` where Maven needs the feminine +form. The other is `chat-joke`, where the model told a joke without using any of +the words the check looks for. Run-to-run variance is about one case: a second +run scored 24/27 with `chat-followup-server` also off-topic. + +## Nudge phrasing, 15-case fixture + +15/15, every check, no errors. `mood`, `lang`, `length`, `feminine`, +`hisgender`, `address`, `cringe` and `ontopic` all clean. + +## What this settles and what it does not + +Settled: the size question. A 12B on the workstation beats the resident model on +every fixture we have, and it is faster. The offload argument holds. + +Not settled: how often the card is free. That is #485's second assumption and +only the `mavgpud` log answers it, after a week of the owner's normal work. A +model that is better whenever it is up is worth little if it is never up. diff --git a/docs/offload.md b/docs/offload.md index 32291fe..4971447 100644 --- a/docs/offload.md +++ b/docs/offload.md @@ -129,8 +129,11 @@ Then the embedder above, **whisper.cpp** in `mavsttd`, and **piper** in `mavttsd builds an `llm.Pair` in `modelSeam` (`cmd/mavend/voicewire.go`), and routing and replies complete through it. Both are the silent half of the rule. The naming half is not wired. A world question still goes to the resident model - through `PhraseQuery`, and the fixture measurement has not been run. - Biggest quality delta. A 16GB card runs a 7-14B, + through `PhraseQuery`. + Measured, `docs/evals/2026-08-02-workstation-gemma4-12b.md`: gemma-4-12b + through the cascade scores 84.4% full accuracy at p50 329ms. The resident + model scores 72.7% at p50 0.80-1.04s. On the talk fixture it is 25/27 + against 20/27. Biggest quality delta. A 16GB card runs a 7-14B, which fixes what the 1.7B gets wrong: world knowledge, and the persona the CPT targets. The degradation path is already written and measured, since the classifier scores 68.8% full accuracy at p50 16.6µs on its own. From 4fae13af75f8313e5745fe67588471daebc5e3b2 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 2 Aug 2026 22:53:13 +0400 Subject: [PATCH 5/5] docs: record the workstation routing numbers where the router is documented (V-485) CLAUDE.md carried only the homesrv figures, which now read as the whole story. Also points offload.md's order at #490 for the naming half. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NJYcaBiuny9UGSpFweQVQ1 --- CLAUDE.md | 10 +++++++++- docs/offload.md | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 51952b5..a9bfc65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,7 +143,15 @@ seed additions, both of which now score inside the classifier baseline. Qwen3-1. not a doubling, and the trade is worth re-arguing rather than assuming. **The ≈2.7s figure that stood here until 2026-08-02 was contention, not the model.** See `docs/evals/2026-07-31-routing.md` line 61, which measures the LLM router at p50 825ms / p95 1.2s / max 3.0s and the full cascade at p50 0.80-1.04s. Do not plan latency -work off the bakeoff table. `Confidence: 1.0` used to be hardcoded in `llmrouter.go`, so the LLM +work off the bakeoff table. + +**The numbers above are the homesrv floor, not the ceiling.** With the workstation up, routing +completes through `llm.Pair` against gemma-4-12b and scores **84.4% full / 93.5% intent-only at +p50 329ms** — better than the resident model and about 2.5× faster (`docs/evals/2026-08-02-workstation-gemma4-12b.md`, +Vikunja #485). The workstation is never assumed up, so both sets of numbers are live. Judge a +routing change against the classifier and the resident model, since those are what always answer. + +`Confidence: 1.0` used to be hardcoded in `llmrouter.go`, so the LLM path could never ask for clarification (6/6 refusal cases missed on the fixture) — Vikunja #359. Fixed 31-07-2026 with structural signal (single-token utterance, keyless fact, act with no allowlisted fn) feeding the same stage-3 gate the classifier path already had — see diff --git a/docs/offload.md b/docs/offload.md index 4971447..034faa3 100644 --- a/docs/offload.md +++ b/docs/offload.md @@ -129,7 +129,7 @@ Then the embedder above, **whisper.cpp** in `mavsttd`, and **piper** in `mavttsd builds an `llm.Pair` in `modelSeam` (`cmd/mavend/voicewire.go`), and routing and replies complete through it. Both are the silent half of the rule. The naming half is not wired. A world question still goes to the resident model - through `PhraseQuery`. + through `PhraseQuery`. That, and the four callers 485 did not reach, are #490. Measured, `docs/evals/2026-08-02-workstation-gemma4-12b.md`: gemma-4-12b through the cascade scores 84.4% full accuracy at p50 329ms. The resident model scores 72.7% at p50 0.80-1.04s. On the talk fixture it is 25/27