Repository: ollama/ollama
Pull request: check server is running before running command

Check if the server is running rather than returning confusing "connection refused" errors. 

If the mac app is available start it, otherwise tell the user to run `ollama serve`.

resolves #47 

Changed files:
filename: api/client.go
patch: @@ -223,3 +223,10 @@ func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error {
 	}
 	return nil
 }
+
+func (c *Client) Heartbeat(ctx context.Context) error {
+	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
+		return err
+	}
+	return nil
+}

filename: cmd/cmd.go
patch: @@ -10,7 +10,9 @@ import (
 	"net"
 	"net/http"
 	"os"
+	"os/exec"
 	"path/filepath"
+	"runtime"
 	"strings"
 	"time"
 
@@ -425,7 +427,6 @@ func generateInteractive(cmd *cobra.Command, model string) error {
 							usage()
 							continue
 						}
-
 					} else {
 						usage()
 						continue
@@ -519,6 +520,54 @@ func RunServer(_ *cobra.Command, _ []string) error {
 	return server.Serve(ln)
 }
 
+func startMacApp(client *api.Client) error {
+	exe, err := os.Executable()
+	if err != nil {
+		return err
+	}
+	link, err := os.Readlink(exe)
+	if err != nil {
+		return err
+	}
+	if !strings.Contains(link, "Ollama.app") {
+		return fmt.Errorf("could not find ollama app")
+	}
+	path := strings.Split(link, "Ollama.app")
+	if err := exec.Command("/usr/bin/open", "-a", path[0]+"Ollama.app").Run(); err != nil {
+		return err
+	}
+	// wait for the server to start
+	timeout := time.After(5 * time.Second)
+	tick := time.Tick(500 * time.Millisecond)
+	for {
+		select {
+		case <-timeout:
+			return errors.New("timed out waiting for server to start")
+		case <-tick:
+			if err := client.Heartbeat(context.Background()); err == nil {
+				return nil // server has started
+			}
+		}
+	}
+}
+
+func checkServerHeartbeat(_ *cobra.Command, _ []string) error {
+	client := api.NewClient()
+	if err := client.Heartbeat(context.Background()); err != nil {
+		if !strings.Contains(err.Error(), "connection refused") {
+			return err
+		}
+		if runtime.GOOS == "darwin" {
+			if err := startMacApp(client); err != nil {
+				return fmt.Errorf("could not connect to ollama app, is it running?")
+			}
+		} else {
+			return fmt.Errorf("could not connect to ollama server, run 'ollama serve' to start it")
+		}
+	}
+	return nil
+}
+
 func NewCLI() *cobra.Command {
 	log.SetFlags(log.LstdFlags | log.Lshortfile)
 
@@ -534,19 +583,21 @@ func NewCLI() *cobra.Command {
 	cobra.EnableCommandSorting = false
 
 	createCmd := &cobra.Command{
-		Use:   "create MODEL",
-		Short: "Create a model from a Modelfile",
-		Args:  cobra.MinimumNArgs(1),
-		RunE:  CreateHandler,
+		Use:     "create MODEL",
+		Short:   "Create a model from a Modelfile",
+		Args:    cobra.MinimumNArgs(1),
+		PreRunE: checkServerHeartbeat,
+		RunE:    CreateHandler,
 	}
 
 	createCmd.Flags().StringP("file", "f", "Modelfile", "Name of the Modelfile (default \"Modelfile\")")
 
 	runCmd := &cobra.Command{
-		Use:   "run MODEL [PROMPT]",
-		Short: "Run a model",
-		Args:  cobra.MinimumNArgs(1),
-		RunE:  RunHandler,
+		Use:     "run MODEL [PROMPT]",
+		Short:   "Run a model",
+		Args:    cobra.MinimumNArgs(1),
+		PreRunE: checkServerHeartbeat,
+		RunE:    RunHandler,
 	}
 
 	runCmd.Flags().Bool("verbose", false, "Show timings for response")
@@ -559,19 +610,21 @@ func NewCLI() *cobra.Command {
 	}
 
 	pullCmd := &cobra.Command{
-		Use:   "pull MODEL",
-		Short: "Pull a model from a registry",
-		Args:  cobra.MinimumNArgs(1),
-		RunE:  PullHandler,
+		Use:     "pull MODEL",
+		Short:   "Pull a model from a registry",
+		Args:    cobra.MinimumNArgs(1),
+		PreRunE: checkServerHeartbeat,
+		RunE:    PullHandler,
 	}
 
 	pullCmd.Flags().Bool("insecure", false, "Use an insecure registry")
 
 	pushCmd := &cobra.Command{
-		Use:   "push MODEL",
-		Short: "Push a model to a registry",
-		Args:  cobra.MinimumNArgs(1),
-		RunE:  PushHandler,
+		Use:     "push MODEL",
+		Short:   "Push a model to a registry",
+		Args:    cobra.MinimumNArgs(1),
+		PreRunE: checkServerHeartbeat,
+		RunE:    PushHandler,
 	}
 
 	pushCmd.Flags().Bool("insecure", false, "Use an insecure registry")
@@ -580,21 +633,24 @@ func NewCLI() *cobra.Command {
 		Use:     "list",
 		Aliases: []string{"ls"},
 		Short:   "List models",
+		PreRunE: checkServerHeartbeat,
 		RunE:    ListHandler,
 	}
 
 	copyCmd := &cobra.Command{
-		Use:   "cp",
-		Short: "Copy a model",
-		Args:  cobra.MinimumNArgs(2),
-		RunE:  CopyHandler,
+		Use:     "cp",
+		Short:   "Copy a model",
+		Args:    cobra.MinimumNArgs(2),
+		PreRunE: checkServerHeartbeat,
+		RunE:    CopyHandler,
 	}
 
 	deleteCmd := &cobra.Command{
-		Use:   "rm",
-		Short: "Remove a model",
-		Args:  cobra.MinimumNArgs(1),
-		RunE:  DeleteHandler,
+		Use:     "rm",
+		Short:   "Remove a model",
+		Args:    cobra.MinimumNArgs(1),
+		PreRunE: checkServerHeartbeat,
+		RunE:    DeleteHandler,
 	}
 
 	rootCmd.AddCommand(

filename: server/routes.go
patch: @@ -318,6 +318,9 @@ func Serve(ln net.Listener) error {
 	r.GET("/", func(c *gin.Context) {
 		c.String(http.StatusOK, "Ollama is running")
 	})
+	r.HEAD("/", func(c *gin.Context) {
+		c.Status(http.StatusOK)
+	})
 
 	r.POST("/api/pull", PullModelHandler)
 	r.POST("/api/generate", GenerateHandler)