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
47 changes: 47 additions & 0 deletions commands/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package commands

import (
"fmt"

"github.com/docker/model-cli/commands/completion"
"github.com/docker/model-cli/desktop"
"github.com/spf13/cobra"
)

func newPushCmd(desktopClient *desktop.Client) *cobra.Command {
c := &cobra.Command{
Use: "push MODEL",
Short: "Upload a model",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf(
"'docker model push' requires 1 argument.\n\n" +
"Usage: docker model push MODEL\n\n" +
"See 'docker model push --help' for more information",
)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return pushModel(cmd, desktopClient, args[0])
},
ValidArgsFunction: completion.NoComplete,
}
return c
}

func pushModel(cmd *cobra.Command, desktopClient *desktop.Client, model string) error {
response, progressShown, err := desktopClient.Push(model, TUIProgress)

// Add a newline before any output (success or error) if progress was shown.
if progressShown {
cmd.Println()
}

if err != nil {
return handleNotRunningError(handleClientError(err, "Failed to push model"))
}

cmd.Println(response)
return nil
}
2 changes: 2 additions & 0 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ func NewRootCmd() *cobra.Command {
newVersionCmd(),
newStatusCmd(desktopClient),
newPullCmd(desktopClient),
newPushCmd(desktopClient),
newListCmd(desktopClient),
newLogsCmd(),
newRunCmd(desktopClient),
newRemoveCmd(desktopClient),
newInspectCmd(desktopClient),
newComposeCmd(desktopClient),
newTagCmd(desktopClient),
)
return rootCmd
}
49 changes: 49 additions & 0 deletions commands/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package commands

import (
"fmt"

"github.com/docker/model-cli/desktop"
"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
)

func newTagCmd(desktopClient *desktop.Client) *cobra.Command {
c := &cobra.Command{
Use: "tag SOURCE TARGET",
Short: "Tag a model",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf(
"'docker model tag' requires 2 arguments.\n\n" +
"Usage: docker model tag SOURCE TARGET\n\n" +
"See 'docker model tag --help' for more information",
)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return tagModel(cmd, desktopClient, args[0], args[1])
},
}
return c
}

func tagModel(cmd *cobra.Command, desktopClient *desktop.Client, source, target string) error {
// Parse the target to extract repo and tag
tag, err := name.NewTag(target)
if err != nil {
return fmt.Errorf("invalid tag: %w", err)
}
targetRepo := tag.Repository.String()
targetTag := tag.TagStr()

// Make the POST request
resp, err := desktopClient.Tag(source, targetRepo, targetTag)
if err != nil {
return fmt.Errorf("failed to tag model: %w", err)
}

cmd.Println(resp)
return nil
}
78 changes: 78 additions & 0 deletions desktop/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,56 @@ func (c *Client) Pull(model string, progress func(string)) (string, bool, error)
return "", progressShown, fmt.Errorf("unexpected end of stream while pulling model %s", model)
}

func (c *Client) Push(model string, progress func(string)) (string, bool, error) {
pushPath := inference.ModelsPrefix + "/" + model + "/push"
resp, err := c.doRequest(
http.MethodPost,
pushPath,
nil, // Assuming no body is needed for the push request
)
if err != nil {
return "", false, c.handleQueryError(err, pushPath)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", false, fmt.Errorf("pushing %s failed with status %s: %s", model, resp.Status, string(body))
}

progressShown := false

scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
progressLine := scanner.Text()
if progressLine == "" {
continue
}

// Parse the progress message
var progressMsg ProgressMessage
if err := json.Unmarshal([]byte(html.UnescapeString(progressLine)), &progressMsg); err != nil {
return "", progressShown, fmt.Errorf("error parsing progress message: %w", err)
}

// Handle different message types
switch progressMsg.Type {
case "progress":
progress(progressMsg.Message)
progressShown = true
case "error":
return "", progressShown, fmt.Errorf("error pushing model: %s", progressMsg.Message)
case "success":
return progressMsg.Message, progressShown, nil
default:
return "", progressShown, fmt.Errorf("unknown message type: %s", progressMsg.Type)
}
}

// If we get here, something went wrong
return "", progressShown, fmt.Errorf("unexpected end of stream while pushing model %s", model)
}

func (c *Client) List(jsonFormat, openai bool, quiet bool, model string) (string, error) {
modelsRoute := inference.ModelsPrefix
if openai {
Expand Down Expand Up @@ -433,3 +483,31 @@ func prettyPrintModels(models []Model) string {
table.Render()
return buf.String()
}

func (c *Client) Tag(source, targetRepo, targetTag string) (string, error) {
// Construct the URL with query parameters
tagPath := fmt.Sprintf("%s/%s/tag?repo=%s&tag=%s",
inference.ModelsPrefix,
source,
targetRepo,
targetTag,
)

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

if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("tagging failed with status %s: %s", resp.Status, string(body))
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}

return string(body), nil
}