Files
foreman/internal/config/config.go
T
steve 7cd7eaff8b
CI / Tidy (push) Successful in 9m42s
CI / Build & Test (push) Successful in 10m28s
CI / Publish Docker Image (push) Successful in 21s
feat: add FOREMAN_KEEP_ALIVE config for worker model residency
Allow configuring how long the worker model stays resident on the Ollama
target after a request via FOREMAN_KEEP_ALIVE env var. Accepts Ollama
duration strings ("-1" forever, "0" unload, "15m", "1h", etc). Defaults
to "-1" (pin forever). The embedder warm-up is unaffected and always
uses keep_alive=-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:29:37 -04:00

113 lines
3.7 KiB
Go

// Package config loads foreman's runtime configuration from environment variables.
//
// Why: centralizes all env-based configuration into a single, validated struct so
// callers never read raw env vars directly.
// What: reads FOREMAN_* environment variables, applies defaults, and validates
// required values.
// Test: set required env vars, call Load(), assert fields match; omit required vars,
// assert Load() returns an error.
package config
import (
"fmt"
"os"
"strconv"
"time"
)
// Config holds all runtime configuration for the foreman daemon.
type Config struct {
// Addr is the listen address for the HTTP server (default ":8080").
Addr string
// OllamaURL is the base URL of the Ollama target (required).
OllamaURL string
// OllamaToken is an optional bearer token sent to the Ollama target.
OllamaToken string
// Token is an optional bearer token that callers must present to foreman.
Token string
// EmbedModel is the always-resident embedder model name (e.g. "nomic-embed-text").
EmbedModel string
// DBPath is the path to the SQLite database file (default "foreman.db").
DBPath string
// PollInterval controls how often the model poller hits the target (default 30s).
PollInterval time.Duration
// WebhookSecret is an optional HMAC key for signing webhook payloads.
WebhookSecret string
// MaxAttempts is the maximum number of retry attempts for a job before it is
// marked as failed (default 3).
MaxAttempts int
// JobTTL is how long terminal jobs are retained before the pruner deletes them
// (default 24h).
JobTTL time.Duration
// KeepAlive is the keep_alive value sent in outbound /api/chat requests to the
// Ollama target. It controls how long the worker model stays resident after a
// request. Accepts Ollama duration strings like "15m", "1h", "-1" (forever), or
// "0" (unload immediately). Default is "-1" (pin forever). This does NOT affect
// the embedder, which is always pinned with keep_alive=-1.
KeepAlive string
}
// Load reads configuration from environment variables and returns a validated Config.
//
// Why: provides a single entry point for configuration with sensible defaults.
// What: reads FOREMAN_* env vars, applies defaults, validates required fields.
// Test: call with FOREMAN_OLLAMA_URL set, assert success; call without it, assert error.
func Load() (Config, error) {
cfg := Config{
Addr: envOr("FOREMAN_ADDR", ":8080"),
OllamaURL: os.Getenv("FOREMAN_OLLAMA_URL"),
OllamaToken: os.Getenv("FOREMAN_OLLAMA_TOKEN"),
Token: os.Getenv("FOREMAN_TOKEN"),
EmbedModel: os.Getenv("FOREMAN_EMBED_MODEL"),
DBPath: envOr("FOREMAN_DB_PATH", "foreman.db"),
WebhookSecret: os.Getenv("FOREMAN_WEBHOOK_SECRET"),
KeepAlive: envOr("FOREMAN_KEEP_ALIVE", "-1"),
}
pollStr := envOr("FOREMAN_POLL_INTERVAL", "30s")
dur, err := time.ParseDuration(pollStr)
if err != nil {
return Config{}, fmt.Errorf("invalid FOREMAN_POLL_INTERVAL %q: %w", pollStr, err)
}
cfg.PollInterval = dur
maxAttemptsStr := envOr("FOREMAN_MAX_ATTEMPTS", "3")
maxAttempts, err := strconv.Atoi(maxAttemptsStr)
if err != nil {
return Config{}, fmt.Errorf("invalid FOREMAN_MAX_ATTEMPTS %q: %w", maxAttemptsStr, err)
}
cfg.MaxAttempts = maxAttempts
jobTTLStr := envOr("FOREMAN_JOB_TTL", "24h")
jobTTL, err := time.ParseDuration(jobTTLStr)
if err != nil {
return Config{}, fmt.Errorf("invalid FOREMAN_JOB_TTL %q: %w", jobTTLStr, err)
}
cfg.JobTTL = jobTTL
if cfg.OllamaURL == "" {
return Config{}, fmt.Errorf("FOREMAN_OLLAMA_URL is required")
}
return cfg, nil
}
// envOr returns the value of the environment variable named by key, or fallback
// if the variable is empty or unset.
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}