795ecf67a5
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.
89 lines
3.9 KiB
Go
89 lines
3.9 KiB
Go
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
|
|
}
|