Documentation
¶
Overview ¶
Package localmodel manages the lifecycle of the local LLM server (cmd/llm_server) that runs on Apple Silicon via MLX. It handles:
- Detecting whether a server is already running on the configured port
- Spawning a new server as a detached background process
- Downloading models from HuggingFace with progress reporting
- Selecting the best model for the machine's RAM
The package is the glue between sprout's provider system (which treats sprout-local as just another OpenAI-compatible endpoint) and the actual Go MLX server binary. When a user selects the "Local" provider in onboarding, this package ensures the server is running with the right model before the provider is used.
Index ¶
- Constants
- Variables
- func EnsureModel(ctx context.Context, status ModelStatus, progressFn ProgressCallback) (string, error)
- func EnsureServerForProvider(ctx context.Context, providerID string) error
- func EnsureServerForProviderWithCheck(ctx context.Context, providerID string) error
- func EnsureServerForProviderWithCheckAndModel(ctx context.Context, providerID, model string) error
- func EnsureServerHealth(ctx context.Context, modelDir string) error
- func EnsureServerHealthWithBackend(ctx context.Context, modelDir, backend string) error
- func HasInstalledModel() bool
- func IsRunning() bool
- func IsServerPresent() bool
- func LastActivity() time.Time
- func PlatformSupported() bool
- func ResetIdleForTest()
- func ServerModelDir() string
- func StopServer(port int) error
- func TieredModelInfos(ram uint64) []api.ModelInfo
- func TotalSystemRAM() uint64
- func TouchActivity()
- type LocalProvider
- func (p *LocalProvider) CheckConnection() error
- func (p *LocalProvider) Close() error
- func (p *LocalProvider) GetAverageTPS() float64
- func (p *LocalProvider) GetLastTPS() float64
- func (p *LocalProvider) GetModel() string
- func (p *LocalProvider) GetModelContextLimit() (int, error)
- func (p *LocalProvider) GetProvider() string
- func (p *LocalProvider) GetTPSStats() map[string]float64
- func (p *LocalProvider) GetVisionModel() string
- func (p *LocalProvider) ListModels(ctx context.Context) ([]api.ModelInfo, error)
- func (p *LocalProvider) ResetTPSStats()
- func (p *LocalProvider) SendChatRequest(ctx context.Context, messages []api.Message, tools []api.Tool, ...) (*api.ChatResponse, error)
- func (p *LocalProvider) SendChatRequestStream(ctx context.Context, messages []api.Message, tools []api.Tool, ...) (*api.ChatResponse, error)
- func (p *LocalProvider) SendVisionRequest(ctx context.Context, messages []api.Message, tools []api.Tool, ...) (*api.ChatResponse, error)
- func (p *LocalProvider) SetDebug(bool)
- func (p *LocalProvider) SetModel(string) error
- func (p *LocalProvider) SupportsConversationalVision() bool
- func (p *LocalProvider) SupportsVision() bool
- func (p *LocalProvider) VisionCapabilities() api.VisionCapabilities
- type ModelStatus
- type ProgressCallback
- type ServerStatus
Constants ¶
const DefaultPort = 18081
DefaultPort is the port the local LLM server listens on (for the standalone HTTP server mode). The in-process provider doesn't use it.
Variables ¶
var DefaultModelsDir = resolveDefaultModelsDir()
DefaultModelsDir is where downloaded LLM (chat) model weights are stored — XDG-style, honoring $SPROUT_DATA_DIR/$XDG_DATA_HOME like sprout's other data, under the SAME models/ root pkg/embedding's DefaultModelDir uses (<DataDir>/models/embedding for embedding models), but its own "llm" subdirectory — one shared, discoverable "models" root with a subdirectory per kind, rather than two same-purpose-sounding but unrelated top-level directories under DataDir. $SPROUT_MODELS_DIR is already pkg/embedding's env var for its own directory, so this uses a distinct name.
~/dev/llm-models was the prior hardcoded default here: a personal dev-machine convention ("~/dev/...") baked in as if it were universal, not a real per-user default — most tools use a dot-folder or XDG path for downloaded model weights.
Resolution: $SPROUT_LLM_MODELS_DIR → $SPROUT_DATA_DIR/models/llm → $XDG_DATA_HOME/sprout/models/llm → $HOME/.local/share/sprout/models/llm. Falls back to the legacy ~/dev/llm-models location only when it already has content and the new location doesn't, so installations that downloaded models before this fix keep working without needing to re-download or manually migrate anything. New installs go straight to the proper location.
Functions ¶
func EnsureModel ¶
func EnsureModel(ctx context.Context, status ModelStatus, progressFn ProgressCallback) (string, error)
EnsureModel downloads a model if it's not already installed.
func EnsureServerForProvider ¶
EnsureServerForProvider ensures the local model is loaded and ready. On Apple Silicon with MLX, and on Linux ARM64 with the GGML backend, this loads the model in-process (direct compute call, no HTTP server). On other platforms it returns an error.
Safe to call on every request — short-circuits when the model is already loaded.
func EnsureServerForProviderWithCheck ¶
EnsureServerForProviderWithCheck loads the local model in-process. No HTTP server is spawned — the model runs directly via MLX. This is the proactive pre-load path called when the user switches to sprout-local. Returns nil if the model is already loaded.
Equivalent to EnsureServerForProviderWithCheckAndModel with no model hint — auto-selects by RAM tier. Kept as a separate name since most callers (the local-provider recovery hook, onboarding) have no specific model in mind and auto-selection is exactly right for them.
func EnsureServerForProviderWithCheckAndModel ¶
EnsureServerForProviderWithCheckAndModel is EnsureServerForProviderWithCheck, but preloads the given model (a catalog Name or installed directory basename — see ResolveModelID) instead of the RAM-tier auto-selected one, when model is non-empty.
This matters for callers preloading BEFORE the real agent (and its config-driven model resolution) exists yet — cmd/agent_command.go's createChatAgent preloads here to warm up the model while the rest of startup proceeds, but without this, that preload had no way to know the user's persisted model choice and always auto-selected instead. If that choice differs from the RAM-tier default (e.g. the user explicitly picked the larger "stretch" tier model), the auto-selected preload loads the WRONG model, and the real agent creation moments later corrects it with SetModel + a second full reload — silently doubling startup latency with an 8+ second load nobody asked for. A bad model hint (unknown ID, stale/uninstalled choice) degrades gracefully to auto-selection, same as SetModel's other callers.
func EnsureServerHealth ¶
EnsureServerHealth checks if the server is running and starts it if not, using the given model directory. Convenience wrapper for agent integration.
func EnsureServerHealthWithBackend ¶
EnsureServerHealthWithBackend is like EnsureServerHealth but specifies the server backend ("gomlx" or "mlx_lm").
func HasInstalledModel ¶
func HasInstalledModel() bool
HasInstalledModel reports whether any model is installed locally.
func IsServerPresent ¶
func IsServerPresent() bool
IsServerPresent reports whether any local model backend is available. Used by provider readiness checks.
func LastActivity ¶
LastActivity returns the timestamp of the last request, or zero if the model has never been used.
func PlatformSupported ¶
func PlatformSupported() bool
PlatformSupported reports whether the local LLM engine is supported on this platform (Linux ARM64, or Apple Silicon).
func ServerModelDir ¶
func ServerModelDir() string
ServerModelDir returns the model directory of the loaded model, or empty string if no model is loaded.
func StopServer ¶
func TieredModelInfos ¶
TieredModelInfos builds the RAM-tier catalog matrix as api.ModelInfo entries for a given RAM budget — see llm.TieredCatalogForRAM. Every tier is included (so /model shows the full roadmap, not just what's pickable today), but only the suggested and stretch tiers carry EligibleRoles; blocked tiers get an explanatory Description and no eligible/recommended roles, matching existing picker conventions for "not really pickable". Actual selection is enforced separately in LocalProvider.SetModel — this only informs the picker.
func TotalSystemRAM ¶
func TotalSystemRAM() uint64
TotalSystemRAM returns this machine's physical RAM in bytes, for callers outside this package that need it for RAM-tier gate checks (e.g. the /model CLI command, deciding whether to download a selection before LocalProvider.SetModel's own gate would reject it).
func TouchActivity ¶
func TouchActivity()
TouchActivity records that the local model is in use, resetting the idle timer. Called before each request to the local provider.
Types ¶
type LocalProvider ¶
type LocalProvider struct{}
LocalProvider is a stub on platforms without MLX. All methods return errors indicating local LLM is unavailable. This allows the rest of sprout to compile and run on non-Apple-Silicon platforms.
func GetLocalProvider ¶
func GetLocalProvider() *LocalProvider
func (*LocalProvider) CheckConnection ¶
func (p *LocalProvider) CheckConnection() error
func (*LocalProvider) Close ¶
func (p *LocalProvider) Close() error
func (*LocalProvider) GetAverageTPS ¶
func (p *LocalProvider) GetAverageTPS() float64
func (*LocalProvider) GetLastTPS ¶
func (p *LocalProvider) GetLastTPS() float64
func (*LocalProvider) GetModel ¶
func (p *LocalProvider) GetModel() string
func (*LocalProvider) GetModelContextLimit ¶
func (p *LocalProvider) GetModelContextLimit() (int, error)
func (*LocalProvider) GetProvider ¶
func (p *LocalProvider) GetProvider() string
func (*LocalProvider) GetTPSStats ¶
func (p *LocalProvider) GetTPSStats() map[string]float64
func (*LocalProvider) GetVisionModel ¶
func (p *LocalProvider) GetVisionModel() string
func (*LocalProvider) ListModels ¶
func (*LocalProvider) ResetTPSStats ¶
func (p *LocalProvider) ResetTPSStats()
func (*LocalProvider) SendChatRequest ¶
func (*LocalProvider) SendChatRequestStream ¶
func (p *LocalProvider) SendChatRequestStream(ctx context.Context, messages []api.Message, tools []api.Tool, reasoning string, disableThinking bool, callback api.StreamCallback) (*api.ChatResponse, error)
func (*LocalProvider) SendVisionRequest ¶
func (*LocalProvider) SetDebug ¶
func (p *LocalProvider) SetDebug(bool)
func (*LocalProvider) SetModel ¶
func (p *LocalProvider) SetModel(string) error
func (*LocalProvider) SupportsConversationalVision ¶
func (p *LocalProvider) SupportsConversationalVision() bool
func (*LocalProvider) SupportsVision ¶
func (p *LocalProvider) SupportsVision() bool
func (*LocalProvider) VisionCapabilities ¶
func (p *LocalProvider) VisionCapabilities() api.VisionCapabilities
type ModelStatus ¶
type ModelStatus struct {
Name string `json:"name"`
Dir string `json:"dir"`
HFRepo string `json:"hf_repo"`
HFInclude string `json:"hf_include,omitempty"`
// MinRAM is the minimum RAM to select this model at all (with a warning
// if below MinRAMSuggested). 0 for the entry-level model.
MinRAM uint64 `json:"min_ram_gb"`
// MinRAMSuggested is the RAM at which this model becomes the unwarned
// default. Capped display-side for the top-of-line model, whose real
// threshold is intentionally unbounded (it never becomes an unwarned
// default — see catalog.go) rather than showing a nonsensical value.
MinRAMSuggested uint64 `json:"min_ram_suggested_gb,omitempty"`
Installed bool `json:"installed"`
Size int64 `json:"size_bytes"`
IsTuned bool `json:"is_tuned"`
ParamSize string `json:"param_size"`
QuantBits string `json:"quant_bits"`
ServerBackend string `json:"server_backend,omitempty"`
}
ModelStatus describes a model from the user's perspective — either a downloadable catalog entry or an installed model discovered on disk. Thresholds here are RAM-agnostic (the raw catalog values); classifying a model as suggested/stretch/blocked for a specific machine is done separately via llm.TieredCatalogForRAM, which needs actual RAM in hand.
func ListModels ¶
func ListModels() []ModelStatus
ListModels returns all models: installed variants discovered on disk (including sprout-tuned, different quant levels) plus downloadable catalog entries that aren't installed. Installed models are listed first, sorted by param size (largest first).
func RecommendedModel ¶
func RecommendedModel(ramBytes uint64) *ModelStatus
RecommendedModel returns the best model for the machine's RAM that is already installed, preferring sprout-tuned variants. Delegates to llm.SelectModelForRAM so this shares the same RAM-gate and quant preference (mlx-q5 > q5 > q8 > unquantized) logic as onboarding and the standalone server — see preferTunedQuant.
func ResolveModelID ¶
func ResolveModelID(id string) (*ModelStatus, error)
ResolveModelID finds the ModelStatus for a stable model ID — either a catalog tier Name (e.g. "qwen3.5-9b", preferring an installed sprout-tuned variant of the same size, matching SelectModelForRAM's own preference) or an installed directory's exact basename. Returns an error if unknown.
type ProgressCallback ¶
type ProgressCallback func(downloaded, total int64)
ProgressCallback is called during model download with bytes downloaded and total bytes (0 if unknown).
type ServerStatus ¶
type ServerStatus struct {
Running bool `json:"running"`
Port int `json:"port"`
Model string `json:"model"`
Healthy bool `json:"healthy"`
URL string `json:"url"`
PID int `json:"pid,omitempty"`
Error string `json:"error,omitempty"`
}
ServerStatus describes the state of the local LLM server.
func EnsureServer ¶
EnsureServer checks if the local LLM server is running and healthy on the given port. If not, it spawns a new server with the given model directory. Returns the server status when the server is healthy, or an error if it couldn't be started within the timeout.
The modelDir must be an absolute path to a directory containing a valid model (config.json, tokenizer.json, *.safetensors).
func EnsureServerWithBackend ¶
func EnsureServerWithBackend(ctx context.Context, port int, modelDir, backend string) (*ServerStatus, error)
EnsureServerWithBackend is like EnsureServer but lets the caller specify which server backend to use: "gomlx" (default, Go-native) or "mlx_lm" (Python mlx_lm.server, needed for models without a Go architecture).
func HealthCheck ¶
func HealthCheck(port int) (*ServerStatus, error)
HealthCheck pings the server's /health endpoint. Returns nil if the server is healthy and running. Returns a non-nil error otherwise.