From d7501122ac4bc8d4247ce40898572fd03e0cffe9 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Sat, 21 Feb 2026 17:58:43 +0100 Subject: [PATCH] ai: refactor model runners (#1516) Signed-off-by: Abiola Ibrahim --- cmd/model.go | 45 +++++++++++++++----- model/ramalama.go | 82 ++++++----------------------------- model/runner.go | 106 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 151 insertions(+), 82 deletions(-) diff --git a/cmd/model.go b/cmd/model.go index eaef116..2ce9abc 100644 --- a/cmd/model.go +++ b/cmd/model.go @@ -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 }, } diff --git a/model/ramalama.go b/model/ramalama.go index ec6c61c..7d6172b 100644 --- a/model/ramalama.go +++ b/model/ramalama.go @@ -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 diff --git a/model/runner.go b/model/runner.go index d65c203..b0e6dec 100644 --- a/model/runner.go +++ b/model/runner.go @@ -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() +}