config: phraser block moves to its own file (V-410)
Its validate arm goes with it as validatePhraser. The doc drifted on every numeric field. It claimed NGpuLayers defaults to -1, NCtx to 2048 and Timeout to 30s. Nothing in this package defaults any of them: phraser.DefaultConfig sets -1 and has no callers, and cmd/mavend builds a phraser.Config literally. So an omitted n_gpu_layers reaches llama-server as -ngl 0, CPU-only, masked here only because deploy/mavend.json sets 99. Fixed the comment and left the code, and the discrepancy is filed.
This commit is contained in:
@@ -415,52 +415,6 @@ type EmailConfig struct {
|
|||||||
// DefaultEmailTimeout — extraction budget per message.
|
// DefaultEmailTimeout — extraction budget per message.
|
||||||
const DefaultEmailTimeout = 2 * time.Minute
|
const DefaultEmailTimeout = 2 * time.Minute
|
||||||
|
|
||||||
// PhraserConfig — the LLM-backed phraser seam. The daemon spawns llama-server
|
|
||||||
// as a managed subprocess and sends chat-completion requests to phrase nudge
|
|
||||||
// and reminder messages. nil ⇒ the template-based Stub is used instead.
|
|
||||||
//
|
|
||||||
// ModelPath is the only required field. The rest have sensible defaults:
|
|
||||||
// - BinPath defaults to "llama-server" (found via PATH at spawn time).
|
|
||||||
// - Listen defaults to "127.0.0.1:0" (random port, read from stderr).
|
|
||||||
// - NGpuLayers defaults to -1 (max, uses all available GPU layers).
|
|
||||||
// - NCtx defaults to 2048.
|
|
||||||
// - Timeout defaults to 30s per request.
|
|
||||||
type PhraserConfig struct {
|
|
||||||
ModelPath string `json:"model_path"`
|
|
||||||
BinPath string `json:"bin_path,omitempty"`
|
|
||||||
Listen string `json:"listen,omitempty"`
|
|
||||||
NGpuLayers int `json:"n_gpu_layers,omitempty"`
|
|
||||||
NCtx int `json:"n_ctx,omitempty"`
|
|
||||||
Timeout Duration `json:"timeout,omitempty"`
|
|
||||||
|
|
||||||
// CacheRAMMiB bounds llama-server's prompt cache. Omitted ⇒ 512 MiB, which
|
|
||||||
// is what keeps the resident model near 1 GB of RSS instead of the 7.9 GB
|
|
||||||
// measured on 2026-08-03. Set it to -1 to pass no flag at all and let the
|
|
||||||
// server apply its own 8 GiB default. See phraser.Config.CacheRAMMiB.
|
|
||||||
CacheRAMMiB int `json:"cache_ram_mib,omitempty"`
|
|
||||||
|
|
||||||
// LLMNudges — let the model word nudges again. Off by default: nudges are
|
|
||||||
// worded from hand-written Russian templates now (the model broke the
|
|
||||||
// persona and invented units). Chat, query and reminder phrasing always go
|
|
||||||
// through the model regardless. See phraser.Config.LLMNudges.
|
|
||||||
LLMNudges bool `json:"llm_nudges,omitempty"`
|
|
||||||
|
|
||||||
// SwapModels — the gguf files the running daemon is allowed to swap to
|
|
||||||
// without a restart (Vikunja #250). Empty (the default) means the swap
|
|
||||||
// capability does not exist: ipc.MethodSwapModel answers ErrUnknownMethod,
|
|
||||||
// exactly like an unconfigured weather or telegram block.
|
|
||||||
//
|
|
||||||
// It is an allowlist and not a directory on purpose. The request carries a
|
|
||||||
// path, and llama-server is started with it as `-m`; anything short of an
|
|
||||||
// exact match against a list a human wrote in this file would make "swap the
|
|
||||||
// model" mean "load a file of your choosing off my disk". ModelPath is
|
|
||||||
// always swappable back to whether or not it is listed.
|
|
||||||
//
|
|
||||||
// Paths must be absolute — the daemon's working directory is not the
|
|
||||||
// operator's, and a relative path here would resolve somewhere surprising.
|
|
||||||
SwapModels []string `json:"swap_models,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duration — a time.Duration that round-trips through JSON as a string
|
// Duration — a time.Duration that round-trips through JSON as a string
|
||||||
// ("60s", "5m", "1h30m"). Plain time.Duration marshals as a nanosecond int,
|
// ("60s", "5m", "1h30m"). Plain time.Duration marshals as a nanosecond int,
|
||||||
// which is unreadable in a config file; this wrapper uses ParseDuration.
|
// which is unreadable in a config file; this wrapper uses ParseDuration.
|
||||||
@@ -630,18 +584,8 @@ func (c *Config) applyDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) validate() error {
|
func (c *Config) validate() error {
|
||||||
if c.Phraser != nil {
|
if err := c.validatePhraser(); err != nil {
|
||||||
if c.Phraser.ModelPath == "" {
|
return err
|
||||||
return errors.New("phraser.model_path is required")
|
|
||||||
}
|
|
||||||
// A relative entry in the swap allowlist would resolve against the
|
|
||||||
// daemon's working directory, so the path a human reads in this file
|
|
||||||
// would not be the path llama-server is handed. Fail at startup.
|
|
||||||
for _, m := range c.Phraser.SwapModels {
|
|
||||||
if !filepath.IsAbs(m) {
|
|
||||||
return fmt.Errorf("phraser.swap_models: %q must be an absolute path", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// The update block is validated here even though mavend never acts on it: a
|
// The update block is validated here even though mavend never acts on it: a
|
||||||
// half-written update config that is only noticed by cmd/mavupdate is noticed
|
// half-written update config that is only noticed by cmd/mavupdate is noticed
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PhraserConfig — the LLM-backed phraser seam. The daemon spawns llama-server
|
||||||
|
// as a managed subprocess and sends chat-completion requests to phrase nudge
|
||||||
|
// and reminder messages. nil ⇒ the template-based Stub is used instead.
|
||||||
|
//
|
||||||
|
// ModelPath is the only required field. cmd/mavend fills BinPath
|
||||||
|
// ("llama-server", found via PATH at spawn time) and Listen ("127.0.0.1:0", a
|
||||||
|
// random port read back off stderr) when they are empty.
|
||||||
|
//
|
||||||
|
// Nothing in this package defaults the rest, and an omitted numeric field
|
||||||
|
// reaches llama-server as a zero. That matters most for NGpuLayers: see below.
|
||||||
|
type PhraserConfig struct {
|
||||||
|
ModelPath string `json:"model_path"`
|
||||||
|
BinPath string `json:"bin_path,omitempty"`
|
||||||
|
Listen string `json:"listen,omitempty"`
|
||||||
|
|
||||||
|
// NGpuLayers — layers offloaded to the GPU, passed straight through as
|
||||||
|
// `-ngl`. Omitted ⇒ 0, which is CPU-only inference.
|
||||||
|
//
|
||||||
|
// phraser.DefaultConfig says -1 (offload everything), but nothing calls it:
|
||||||
|
// cmd/mavend builds a phraser.Config literally and copies this field across.
|
||||||
|
// So the deploy config carries `"n_gpu_layers": 99` and must keep carrying
|
||||||
|
// it. Vikunja has the discrepancy; do not "fix" it by writing a default
|
||||||
|
// here, because that would change what a box without the key does.
|
||||||
|
NGpuLayers int `json:"n_gpu_layers,omitempty"`
|
||||||
|
|
||||||
|
// NCtx — context window, passed through as `-c`. Omitted ⇒ 0, which lets
|
||||||
|
// llama-server pick. The resident model is a Thinking variant and needs
|
||||||
|
// 4096; the deploy config sets it.
|
||||||
|
NCtx int `json:"n_ctx,omitempty"`
|
||||||
|
|
||||||
|
// Timeout — per-request budget. Omitted ⇒ phraser's own default.
|
||||||
|
Timeout Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
// CacheRAMMiB bounds llama-server's prompt cache. Omitted ⇒ 512 MiB, which
|
||||||
|
// is what keeps the resident model near 1 GB of RSS instead of the 7.9 GB
|
||||||
|
// measured on 2026-08-03. Set it to -1 to pass no flag at all and let the
|
||||||
|
// server apply its own 8 GiB default. See phraser.Config.CacheRAMMiB.
|
||||||
|
CacheRAMMiB int `json:"cache_ram_mib,omitempty"`
|
||||||
|
|
||||||
|
// LLMNudges — let the model word nudges again. Off by default: nudges are
|
||||||
|
// worded from hand-written Russian templates now (the model broke the
|
||||||
|
// persona and invented units). Chat, query and reminder phrasing always go
|
||||||
|
// through the model regardless. See phraser.Config.LLMNudges.
|
||||||
|
LLMNudges bool `json:"llm_nudges,omitempty"`
|
||||||
|
|
||||||
|
// SwapModels — the gguf files the running daemon is allowed to swap to
|
||||||
|
// without a restart (Vikunja #250). Empty (the default) means the swap
|
||||||
|
// capability does not exist: ipc.MethodSwapModel answers ErrUnknownMethod,
|
||||||
|
// exactly like an unconfigured weather or telegram block.
|
||||||
|
//
|
||||||
|
// It is an allowlist and not a directory on purpose. The request carries a
|
||||||
|
// path, and llama-server is started with it as `-m`; anything short of an
|
||||||
|
// exact match against a list a human wrote in this file would make "swap the
|
||||||
|
// model" mean "load a file of your choosing off my disk". ModelPath is
|
||||||
|
// always swappable back to whether or not it is listed.
|
||||||
|
//
|
||||||
|
// Paths must be absolute — the daemon's working directory is not the
|
||||||
|
// operator's, and a relative path here would resolve somewhere surprising.
|
||||||
|
SwapModels []string `json:"swap_models,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePhraser refuses a block with no model, and a swap allowlist entry
|
||||||
|
// that would resolve somewhere other than where a reader of this file expects.
|
||||||
|
func (c *Config) validatePhraser() error {
|
||||||
|
if c.Phraser == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Phraser.ModelPath == "" {
|
||||||
|
return errors.New("phraser.model_path is required")
|
||||||
|
}
|
||||||
|
// A relative entry in the swap allowlist would resolve against the
|
||||||
|
// daemon's working directory, so the path a human reads in this file
|
||||||
|
// would not be the path llama-server is handed. Fail at startup.
|
||||||
|
for _, m := range c.Phraser.SwapModels {
|
||||||
|
if !filepath.IsAbs(m) {
|
||||||
|
return fmt.Errorf("phraser.swap_models: %q must be an absolute path", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user