ai: refactor model runners (#1516)

Signed-off-by: Abiola Ibrahim <git@abiosoft.com>
This commit is contained in:
Abiola Ibrahim
2026-02-21 17:58:43 +01:00
committed by GitHub
parent 8ae20c1223
commit d7501122ac
3 changed files with 151 additions and 82 deletions
+35 -10
View File
@@ -90,19 +90,44 @@ var modelSetupCmd = &cobra.Command{
return err
}
// Build header for alternate screen
var header string
separator := "────────────────────────────────────────"
if runner.Name() == model.RunnerDocker {
header = fmt.Sprintf("Colima - Docker Model Runner Setup\n%s", separator)
} else {
header = fmt.Sprintf("Colima - Ramalama Setup\n%s", separator)
// Check if setup is needed (on primary screen)
status, err := runner.CheckSetup()
if err != nil {
return err
}
// Run in alternate screen with header
return terminal.WithAltScreen(func() error {
// Print version info on primary screen
fmt.Println(runner.DisplayName())
if status.CurrentVersion != "" {
fmt.Printf("current: %s\n", status.CurrentVersion)
}
if status.LatestVersion != "" {
fmt.Printf("latest: %s\n", status.LatestVersion)
}
if !status.NeedsSetup {
fmt.Println()
fmt.Println("Already up to date")
return nil
}
// Build header for alternate screen
separator := "────────────────────────────────────────"
header := fmt.Sprintf("Colima - %s Setup\n%s", runner.DisplayName(), separator)
// Run setup in alternate screen
if err := terminal.WithAltScreen(func() error {
return runner.Setup()
}, header)
}, header); err != nil {
return err
}
// Print new version on primary screen after update
if newVersion := runner.GetCurrentVersion(); newVersion != "" {
fmt.Printf("updated: %s\n", newVersion)
}
return nil
},
}
+12 -70
View File
@@ -10,80 +10,15 @@ import (
"github.com/abiosoft/colima/environment/host"
"github.com/abiosoft/colima/environment/vm/lima"
"github.com/abiosoft/colima/store"
"github.com/coreos/go-semver/semver"
log "github.com/sirupsen/logrus"
)
const ramalamaReleasesURL = "https://api.github.com/repos/containers/ramalama/releases/latest"
// SetupOrUpdateRamalama handles both fresh installs and updates with version checking.
// SetupOrUpdateRamalama installs or updates ramalama.
// Call CheckSetup() first to determine if setup is needed and display version info.
func SetupOrUpdateRamalama() error {
s, _ := store.Load()
// Fresh install - no version check needed
if !s.RamalamaProvisioned {
if err := ProvisionRamalama(); err != nil {
return err
}
// Print installed version
if version := GetRamalamaVersion(); version != "" {
fmt.Println("AI model runner")
fmt.Printf("version: %s", version)
fmt.Println()
}
return nil
}
// Update - check versions first
currentVersion := GetRamalamaVersion()
if currentVersion == "" {
// Can't determine current version, proceed with update
log.Debug("could not determine current ramalama version, proceeding with update")
return ProvisionRamalama()
}
latestVersion, err := getLatestRamalamaVersion()
if err != nil {
log.Debugf("could not fetch latest ramalama version: %v", err)
return fmt.Errorf("could not check for updates: %w", err)
}
// Compare versions
current, err := semver.NewVersion(currentVersion)
if err != nil {
log.Debugf("could not parse current version %q: %v", currentVersion, err)
return ProvisionRamalama()
}
latest, err := semver.NewVersion(latestVersion)
if err != nil {
log.Debugf("could not parse latest version %q: %v", latestVersion, err)
return ProvisionRamalama()
}
// Show version info
fmt.Println("AI model runner")
fmt.Printf("current: %s", currentVersion)
fmt.Println()
fmt.Printf("latest: %s", latestVersion)
fmt.Println()
if current.Compare(*latest) >= 0 {
fmt.Println()
fmt.Println("Already up to date")
return nil
}
if err := ProvisionRamalama(); err != nil {
return err
}
// Print new version
if newVersion := GetRamalamaVersion(); newVersion != "" {
fmt.Printf("updated: %s", newVersion)
fmt.Println()
}
return nil
return ProvisionRamalama()
}
// GetRamalamaVersion returns the currently installed ramalama version in the VM.
@@ -216,8 +151,15 @@ func ProvisionRamalama() error {
script := `set -e
export PATH="$HOME/.local/bin:$PATH"
# install ramalama
curl -fsSL https://ramalama.ai/install.sh | bash
# ensure pipx is available
sudo apt-get update -y && sudo apt-get install -y pipx
# install ramalama via pipx; upgrade if ramalama is already installed
if command -v ramalama >/dev/null 2>&1; then
pipx upgrade ramalama
else
pipx install ramalama
fi
# pull ramalama container images
docker pull quay.io/ramalama/ramalama
+104 -2
View File
@@ -15,6 +15,9 @@ import (
"github.com/abiosoft/colima/environment/vm/lima/limaconfig"
"github.com/abiosoft/colima/store"
"github.com/abiosoft/colima/util"
"github.com/abiosoft/colima/util/terminal"
"github.com/coreos/go-semver/semver"
log "github.com/sirupsen/logrus"
)
// RunnerType represents the type of AI model runner.
@@ -25,10 +28,22 @@ const (
RunnerRamalama RunnerType = "ramalama"
)
// SetupStatus contains the result of checking if setup is needed.
type SetupStatus struct {
// NeedsSetup indicates whether setup/update is required.
NeedsSetup bool
// CurrentVersion is the currently installed version (empty if not installed).
CurrentVersion string
// LatestVersion is the latest available version (empty if not checked).
LatestVersion string
}
// Runner defines the interface for AI model runners.
type Runner interface {
// Name returns the runner type name.
Name() RunnerType
// DisplayName returns a human-readable name for the runner.
DisplayName() string
// ValidatePrerequisites checks runner-specific requirements.
ValidatePrerequisites(a app.App) error
// EnsureProvisioned ensures the runner is set up (no-op for docker).
@@ -43,8 +58,14 @@ type Runner interface {
// This is a blocking call that runs until interrupted.
// The model should already be available (call EnsureModel first).
Serve(model string, port int) error
// CheckSetup checks if setup/update is needed and returns version info.
// This should be called before Setup() to display version info on primary screen.
CheckSetup() (SetupStatus, error)
// Setup installs or updates the runner.
// Call CheckSetup() first to determine if setup is needed.
Setup() error
// GetCurrentVersion returns the currently installed version.
GetCurrentVersion() string
}
// GetRunner returns the appropriate Runner based on type.
@@ -101,6 +122,10 @@ func (d *dockerRunner) Name() RunnerType {
return RunnerDocker
}
func (d *dockerRunner) DisplayName() string {
return "Docker Model Runner"
}
func (d *dockerRunner) ValidatePrerequisites(a app.App) error {
return validateCommonPrerequisites(a)
}
@@ -255,10 +280,22 @@ func normalizeModelName(name string) string {
return name
}
func (d *dockerRunner) CheckSetup() (SetupStatus, error) {
// Docker Model Runner always reinstalls; no version comparison
return SetupStatus{
NeedsSetup: true,
CurrentVersion: GetDockerModelVersion(),
}, nil
}
func (d *dockerRunner) Setup() error {
return SetupOrUpdateDocker()
}
func (d *dockerRunner) GetCurrentVersion() string {
return GetDockerModelVersion()
}
// gpuSubcommands are ramalama subcommands that need GPU device passthrough.
var gpuSubcommands = map[string]bool{
"run": true,
@@ -275,6 +312,10 @@ func (r *ramalamaRunner) Name() RunnerType {
return RunnerRamalama
}
func (r *ramalamaRunner) DisplayName() string {
return "Ramalama"
}
func (r *ramalamaRunner) ValidatePrerequisites(a app.App) error {
return validateCommonPrerequisites(a)
}
@@ -285,11 +326,15 @@ func (r *ramalamaRunner) EnsureProvisioned() error {
return nil
}
if !cli.Prompt("AI model support requires initial setup (this may take a few minutes depending on internet connection speed). Continue") {
prompt := fmt.Sprintf("%s requires initial setup (this may take a few minutes depending on internet connection speed). Continue", r.DisplayName())
if !cli.Prompt(prompt) {
return fmt.Errorf("setup cancelled")
}
return ProvisionRamalama()
separator := "────────────────────────────────────────"
header := fmt.Sprintf("Colima - %s Setup\n%s", r.DisplayName(), separator)
return terminal.WithAltScreen(ProvisionRamalama, header)
}
func (r *ramalamaRunner) BuildArgs(args []string) ([]string, error) {
@@ -333,6 +378,63 @@ func (r *ramalamaRunner) buildRamalamaArgs(args []string) []string {
return ramalamaArgs
}
func (r *ramalamaRunner) CheckSetup() (SetupStatus, error) {
s, _ := store.Load()
// Fresh install - no version check needed
if !s.RamalamaProvisioned {
return SetupStatus{NeedsSetup: true}, nil
}
// Get current version
currentVersion := GetRamalamaVersion()
if currentVersion == "" {
// Can't determine current version, proceed with update
log.Debug("could not determine current ramalama version, proceeding with update")
return SetupStatus{NeedsSetup: true}, nil
}
// Fetch latest version
latestVersion, err := getLatestRamalamaVersion()
if err != nil {
log.Debugf("could not fetch latest ramalama version: %v", err)
return SetupStatus{}, fmt.Errorf("could not check for updates: %w", err)
}
// Compare versions
current, err := semver.NewVersion(currentVersion)
if err != nil {
log.Debugf("could not parse current version %q: %v", currentVersion, err)
return SetupStatus{
NeedsSetup: true,
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
}, nil
}
latest, err := semver.NewVersion(latestVersion)
if err != nil {
log.Debugf("could not parse latest version %q: %v", latestVersion, err)
return SetupStatus{
NeedsSetup: true,
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
}, nil
}
needsSetup := current.Compare(*latest) < 0
return SetupStatus{
NeedsSetup: needsSetup,
CurrentVersion: currentVersion,
LatestVersion: latestVersion,
}, nil
}
func (r *ramalamaRunner) Setup() error {
return SetupOrUpdateRamalama()
}
func (r *ramalamaRunner) GetCurrentVersion() string {
return GetRamalamaVersion()
}