Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion commands/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func newInspectCmd() *cobra.Command {
}
model, err = client.List(false, openai, model)
if err != nil {
return fmt.Errorf("Failed to list models: %v\n", err)
err = handleClientError(err, "Failed to list models")
return handleNotRunningError(err)
}
cmd.Println(model)
return nil
Expand Down
3 changes: 2 additions & 1 deletion commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ func newListCmd() *cobra.Command {
}
models, err := client.List(jsonFormat, openai, "")
if err != nil {
return fmt.Errorf("Failed to list models: %v\n", err)
err = handleClientError(err, "Failed to list models")
return handleNotRunningError(err)
}
cmd.Println(models)
return nil
Expand Down
3 changes: 2 additions & 1 deletion commands/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ func newPullCmd() *cobra.Command {
}
response, err := client.Pull(model)
if err != nil {
return fmt.Errorf("Failed to pull model: %v\n", err)
err = handleClientError(err, "Failed to pull model")
return handleNotRunningError(err)
}
cmd.Println(response)
return nil
Expand Down
3 changes: 2 additions & 1 deletion commands/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ func newRemoveCmd() *cobra.Command {
}
response, err := client.Remove(model)
if err != nil {
return fmt.Errorf("Failed to remove model: %v\n", err)
err = handleClientError(err, "Failed to remove model")
return handleNotRunningError(err)
}
cmd.Println(response)
return nil
Expand Down
8 changes: 4 additions & 4 deletions commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ func newRunCmd() *cobra.Command {

if _, err := client.List(false, false, model); err != nil {
if !errors.Is(err, desktop.ErrNotFound) {
return fmt.Errorf("Failed to list model: %v\n", err)
return handleNotRunningError(handleClientError(err, "Failed to list models"))
}
cmd.Println("Unable to find model '" + model + "' locally. Pulling from the server.")
response, err := client.Pull(model)
if err != nil {
return fmt.Errorf("Failed to pull model: %v\n", err)
return handleNotRunningError(handleClientError(err, "Failed to pull model"))
}
cmd.Println(response)
}

if prompt != "" {
if err := client.Chat(model, prompt); err != nil {
return fmt.Errorf("Failed to generate a response: %v\n", err)
return handleClientError(err, "Failed to generate a response")
}
cmd.Println()
return nil
Expand All @@ -75,7 +75,7 @@ func newRunCmd() *cobra.Command {
}

if err := client.Chat(model, userInput); err != nil {
cmd.PrintErrf("Failed to generate a response: %v\n", err)
cmd.PrintErr(handleClientError(err, "Failed to generate a response"))
cmd.Print("> ")
continue
}
Expand Down
5 changes: 4 additions & 1 deletion commands/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package commands

import (
"fmt"
"os"

"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/model-cli/desktop"
"github.com/spf13/cobra"
"os"
)

func newStatusCmd() *cobra.Command {
Expand All @@ -24,6 +26,7 @@ func newStatusCmd() *cobra.Command {
cmd.Println("Docker Model Runner is running")
} else {
cmd.Println("Docker Model Runner is not running")
hooks.PrintNextSteps(os.Stdout, []string{enableViaCLI, enableViaGUI})
os.Exit(1)
}

Expand Down
34 changes: 34 additions & 0 deletions commands/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package commands

import (
"bytes"
"fmt"
"strings"

"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/model-cli/desktop"
"github.com/pkg/errors"
)

const (
enableViaCLI = "Enable Docker Model Runner via the CLI → docker desktop enable model-runner"
enableViaGUI = "Enable Docker Model Runner via the GUI → Go to Settings->Features in development->Enable Docker Model Runner"
)

var notRunningErr = fmt.Errorf("Docker Model Runner is not running. Please start it and try again.\n")

func handleClientError(err error, message string) error {
if errors.Is(err, desktop.ErrServiceUnavailable) {
return notRunningErr
}
return errors.Wrap(err, message)
}

func handleNotRunningError(err error) error {
if errors.Is(err, notRunningErr) {
var buf bytes.Buffer
hooks.PrintNextSteps(&buf, []string{enableViaCLI, enableViaGUI})
return fmt.Errorf("%w\n%s", err, strings.TrimRight(buf.String(), "\n"))
}
return err
}
72 changes: 53 additions & 19 deletions desktop/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import (
"go.opentelemetry.io/otel"
)

var ErrNotFound = errors.New("model not found")
var (
ErrNotFound = errors.New("model not found")
ErrServiceUnavailable = errors.New("service unavailable")
)

type otelErrorSilencer struct{}

Expand Down Expand Up @@ -79,13 +82,14 @@ func (c *Client) Pull(model string) (string, error) {
return "", fmt.Errorf("error marshaling request: %w", err)
}

resp, err := c.dockerClient.HTTPClient().Post(
url(inference.ModelsPrefix+"/create"),
"application/json",
createPath := inference.ModelsPrefix + "/create"
resp, err := c.doRequest(
http.MethodPost,
createPath,
bytes.NewReader(jsonData),
)
if err != nil {
return "", fmt.Errorf("error querying %s: %w", inference.ModelsPrefix+"/create", err)
return "", c.handleQueryError(err, createPath)
}
defer resp.Body.Close()

Expand Down Expand Up @@ -118,17 +122,20 @@ func (c *Client) List(jsonFormat, openai bool, model string) (string, error) {
}
modelsRoute += "/" + model
}
resp, err := c.dockerClient.HTTPClient().Get(url(modelsRoute))

resp, err := c.doRequest(http.MethodGet, modelsRoute, nil)
if err != nil {
return "", err
return "", c.handleQueryError(err, modelsRoute)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
if model != "" && resp.StatusCode == http.StatusNotFound {
return "", errors.Wrap(ErrNotFound, model)
}
return "", fmt.Errorf("failed to list models: %s", resp.Status)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
Expand Down Expand Up @@ -188,13 +195,14 @@ func (c *Client) Chat(model, prompt string) error {
return fmt.Errorf("error marshaling request: %w", err)
}

resp, err := c.dockerClient.HTTPClient().Post(
url(inference.InferencePrefix+"/v1/chat/completions"),
"application/json",
chatCompletionsPath := inference.InferencePrefix + "/v1/chat/completions"
resp, err := c.doRequest(
http.MethodPost,
chatCompletionsPath,
bytes.NewReader(jsonData),
)
if err != nil {
return fmt.Errorf("error querying %s: %w", inference.InferencePrefix+"/v1/chat/completions", err)
return c.handleQueryError(err, chatCompletionsPath)
}
defer resp.Body.Close()

Expand Down Expand Up @@ -239,18 +247,14 @@ func (c *Client) Chat(model, prompt string) error {
}

func (c *Client) Remove(model string) (string, error) {
req, err := http.NewRequest(http.MethodDelete, url(inference.ModelsPrefix+"/"+model), nil)
removePath := inference.ModelsPrefix + "/" + model
resp, err := c.doRequest(http.MethodDelete, removePath, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %w", err)
}

resp, err := c.dockerClient.HTTPClient().Do(req)
if err != nil {
return "", fmt.Errorf("error querying %s: %w", inference.ModelsPrefix+"/"+model, err)
return "", c.handleQueryError(err, removePath)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK { // from common/pkg/inference/models/manager.go
if resp.StatusCode != http.StatusOK {
var bodyStr string
body, err := io.ReadAll(resp.Body)
if err != nil {
Expand All @@ -268,6 +272,36 @@ func url(path string) string {
return fmt.Sprintf("http://localhost" + inference.ExperimentalEndpointsPrefix + path)
}

// doRequest is a helper function that performs HTTP requests and handles 503 responses
func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, url(path), body)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}

resp, err := c.dockerClient.HTTPClient().Do(req)
if err != nil {
return nil, err
}

if resp.StatusCode == http.StatusServiceUnavailable {
resp.Body.Close()
return nil, ErrServiceUnavailable
}

return resp, nil
}

func (c *Client) handleQueryError(err error, path string) error {
if errors.Is(err, ErrServiceUnavailable) {
return ErrServiceUnavailable
}
return fmt.Errorf("error querying %s: %w", path, err)
}

func prettyPrintModels(models []Model) string {
var buf bytes.Buffer
table := tablewriter.NewWriter(&buf)
Expand Down