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 4 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
115 changes: 94 additions & 21 deletions commands/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ package commands

import (
"bufio"
"context"
"encoding/json"
"fmt"
"html"
"io"
"path/filepath"

"github.com/docker/model-cli/commands/completion"
"github.com/docker/model-cli/desktop"
"github.com/docker/model-distribution/builder"
"github.com/docker/model-distribution/registry"
"github.com/docker/model-distribution/tarball"
"github.com/docker/model-distribution/types"
"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"

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

func newPackagedCmd() *cobra.Command {
var opts packageOptions

c := &cobra.Command{
Use: "package --gguf <path> [--license <path>...] [--context-size <tokens>] --push TARGET",
Short: "Package a GGUF file into a Docker model OCI artifact, with optional licenses, and pushes it to the specified registry",
Use: "package --gguf <path> [--license <path>...] [--context-size <tokens>] [--push] TAG",
Short: "Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf(
Expand All @@ -30,12 +35,6 @@ func newPackagedCmd() *cobra.Command {
cmd.Use,
)
}
if opts.push != true {
return fmt.Errorf(
"This version of 'docker model package' requires --push and will write the resulting package directly to the registry.\n\n" +
"See 'docker model package --help' for more information",
)
}
if opts.ggufPath == "" {
return fmt.Errorf(
"GGUF path is required.\n\n" +
Expand All @@ -62,7 +61,8 @@ func newPackagedCmd() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := packageModel(cmd, args[0], opts); err != nil {
opts.tag = args[0]
if err := packageModel(cmd, opts); err != nil {
cmd.PrintErrln("Failed to package model")
return fmt.Errorf("package model: %w", err)
}
Expand All @@ -73,7 +73,7 @@ func newPackagedCmd() *cobra.Command {

c.Flags().StringVar(&opts.ggufPath, "gguf", "", "absolute path to gguf file (required)")
c.Flags().StringArrayVarP(&opts.licensePaths, "license", "l", nil, "absolute path to a license file")
c.Flags().BoolVar(&opts.push, "push", false, "push to registry (required)")
c.Flags().BoolVar(&opts.push, "push", false, "push to registry (if not set, the model is loaded into the Model Runner content store.")
c.Flags().Uint64Var(&opts.contextSize, "context-size", 0, "context size in tokens")
return c
}
Expand All @@ -83,13 +83,21 @@ type packageOptions struct {
licensePaths []string
push bool
contextSize uint64
tag string
}

func packageModel(cmd *cobra.Command, tag string, opts packageOptions) error {
cmd.PrintErrf("Packaging model %q\n", tag)
target, err := registry.NewClient(
registry.WithUserAgent("docker-model-cli/" + desktop.Version),
).NewTarget(tag)
func packageModel(cmd *cobra.Command, opts packageOptions) error {
var (
target builder.Target
err error
)
if opts.push {
target, err = registry.NewClient(
registry.WithUserAgent("docker-model-cli/" + desktop.Version),
).NewTarget(opts.tag)
} else {
target, err = newModelRunnerTarget(desktopClient, opts.tag)
}
if err != nil {
return err
}
Expand All @@ -116,8 +124,11 @@ func packageModel(cmd *cobra.Command, tag string, opts packageOptions) error {
}
}

// Write the artifact to the registry
cmd.PrintErrln("Pushing to registry...")
if opts.push {
cmd.PrintErrln("Pushing model to registry...")
} else {
cmd.PrintErrln("Loading model to Model Runner...")
}
pr, pw := io.Pipe()
done := make(chan error, 1)
go func() {
Expand Down Expand Up @@ -147,8 +158,70 @@ func packageModel(cmd *cobra.Command, tag string, opts packageOptions) error {
cmd.PrintErrln("Error streaming progress:", err)
}
if err := <-done; err != nil {
return fmt.Errorf("push: %w", err)
if opts.push {
return fmt.Errorf("failed to save packaged model: %w", err)
}
return fmt.Errorf("failed to load packaged model: %w", err)
}

if opts.push {
cmd.PrintErrln("Model pushed successfully")
} else {
cmd.PrintErrln("Model loaded successfully")
}
return nil
}

// modelRunnerTarget loads model to Docker Model Runner via models/load endpoint
type modelRunnerTarget struct {
client *desktop.Client
tag name.Tag
}

func newModelRunnerTarget(client *desktop.Client, tag string) (*modelRunnerTarget, error) {
target := &modelRunnerTarget{
client: client,
}
if tag != "" {
var err error
target.tag, err = name.NewTag(tag)
if err != nil {
return nil, fmt.Errorf("invalid tag: %w", err)
}
}
return target, nil
}

func (t *modelRunnerTarget) Write(ctx context.Context, mdl types.ModelArtifact, progressWriter io.Writer) error {
pr, pw := io.Pipe()
errCh := make(chan error, 1)
go func() {
defer pw.Close()
target, err := tarball.NewTarget(pw)
if err != nil {
errCh <- err
return
}
errCh <- target.Write(ctx, mdl, progressWriter)
}()

loadErr := t.client.LoadModel(ctx, pr)
writeErr := <-errCh

if loadErr != nil {
return fmt.Errorf("loading model archive: %w", loadErr)
}
if writeErr != nil {
return fmt.Errorf("writing model archive: %w", writeErr)
}
id, err := mdl.ID()
if err != nil {
return fmt.Errorf("get model ID: %w", err)
}
if t.tag.String() != "" {
if err := desktopClient.Tag(id, parseRepo(t.tag), t.tag.TagStr()); err != nil {
return fmt.Errorf("tag model: %w", err)
}
}
cmd.PrintErrln("Model pushed successfully")
return nil
}
23 changes: 23 additions & 0 deletions desktop/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package desktop
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"html"
Expand Down Expand Up @@ -704,3 +705,25 @@ func (c *Client) Tag(source, targetRepo, targetTag string) error {

return nil
}

func (c *Client) LoadModel(ctx context.Context, r io.Reader) error {
loadPath := fmt.Sprintf("%s/load", inference.ModelsPrefix)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.modelRunner.URL(loadPath), r)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/x-tar")
req.Header.Set("User-Agent", "docker-model-cli/"+Version)

resp, err := c.modelRunner.Client().Do(req)
if err != nil {
return c.handleQueryError(err, loadPath)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("load failed with status %s: %s", resp.Status, string(body))
}
return nil
}
9 changes: 5 additions & 4 deletions docs/reference/docker_model_package.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
command: docker model package
short: |
Package a GGUF file into a Docker model OCI artifact, with optional licenses, and pushes it to the specified registry
Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified
long: |
Package a GGUF file into a Docker model OCI artifact, with optional licenses, and pushes it to the specified registry
usage: docker model package --gguf <path> [--license <path>...] [--context-size <tokens>] --push TARGET
Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified
usage: docker model package --gguf <path> [--license <path>...] [--context-size <tokens>] [--push] TAG
pname: docker model
plink: docker_model.yaml
options:
Expand Down Expand Up @@ -40,7 +40,8 @@ options:
- option: push
value_type: bool
default_value: "false"
description: push to registry (required)
description: |
push to registry (if not set, the model is loaded into the Model Runner content store.
deprecated: false
hidden: false
experimental: false
Expand Down
36 changes: 18 additions & 18 deletions docs/reference/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ Docker Model Runner (EXPERIMENTAL)

### Subcommands

| Name | Description |
|:------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------|
| [`df`](model_df.md) | Show Docker Model Runner disk usage |
| [`inspect`](model_inspect.md) | Display detailed information on one model |
| [`install-runner`](model_install-runner.md) | Install Docker Model Runner (Docker Engine only) |
| [`list`](model_list.md) | List the models pulled to your local environment |
| [`logs`](model_logs.md) | Fetch the Docker Model Runner logs |
| [`package`](model_package.md) | Package a GGUF file into a Docker model OCI artifact, with optional licenses, and pushes it to the specified registry |
| [`ps`](model_ps.md) | List running models |
| [`pull`](model_pull.md) | Pull a model from Docker Hub or HuggingFace to your local environment |
| [`push`](model_push.md) | Push a model to Docker Hub |
| [`rm`](model_rm.md) | Remove local models downloaded from Docker Hub |
| [`run`](model_run.md) | Run a model and interact with it using a submitted prompt or chat mode |
| [`status`](model_status.md) | Check if the Docker Model Runner is running |
| [`tag`](model_tag.md) | Tag a model |
| [`uninstall-runner`](model_uninstall-runner.md) | Uninstall Docker Model Runner |
| [`unload`](model_unload.md) | Unload running models |
| [`version`](model_version.md) | Show the Docker Model Runner version |
| Name | Description |
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
| [`df`](model_df.md) | Show Docker Model Runner disk usage |
| [`inspect`](model_inspect.md) | Display detailed information on one model |
| [`install-runner`](model_install-runner.md) | Install Docker Model Runner (Docker Engine only) |
| [`list`](model_list.md) | List the models pulled to your local environment |
| [`logs`](model_logs.md) | Fetch the Docker Model Runner logs |
| [`package`](model_package.md) | Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified |
| [`ps`](model_ps.md) | List running models |
| [`pull`](model_pull.md) | Pull a model from Docker Hub or HuggingFace to your local environment |
| [`push`](model_push.md) | Push a model to Docker Hub |
| [`rm`](model_rm.md) | Remove local models downloaded from Docker Hub |
| [`run`](model_run.md) | Run a model and interact with it using a submitted prompt or chat mode |
| [`status`](model_status.md) | Check if the Docker Model Runner is running |
| [`tag`](model_tag.md) | Tag a model |
| [`uninstall-runner`](model_uninstall-runner.md) | Uninstall Docker Model Runner |
| [`unload`](model_unload.md) | Unload running models |
| [`version`](model_version.md) | Show the Docker Model Runner version |



Expand Down
14 changes: 7 additions & 7 deletions docs/reference/model_package.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# docker model package

<!---MARKER_GEN_START-->
Package a GGUF file into a Docker model OCI artifact, with optional licenses, and pushes it to the specified registry
Package a GGUF file into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified

### Options

| Name | Type | Default | Description |
|:------------------|:--------------|:--------|:--------------------------------------|
| `--context-size` | `uint64` | `0` | context size in tokens |
| `--gguf` | `string` | | absolute path to gguf file (required) |
| `-l`, `--license` | `stringArray` | | absolute path to a license file |
| `--push` | `bool` | | push to registry (required) |
| Name | Type | Default | Description |
|:------------------|:--------------|:--------|:---------------------------------------------------------------------------------------|
| `--context-size` | `uint64` | `0` | context size in tokens |
| `--gguf` | `string` | | absolute path to gguf file (required) |
| `-l`, `--license` | `stringArray` | | absolute path to a license file |
| `--push` | `bool` | | push to registry (if not set, the model is loaded into the Model Runner content store. |


<!---MARKER_GEN_END-->
Expand Down
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ go 1.24

toolchain go1.24.4

replace github.com/docker/model-runner v0.0.0-20250711130825-8907b3ddf82e => ../model-runner

replace github.com/docker/model-distribution v0.0.0-20250717222442-fb351a8da832 => ../model-distribution

require (
github.com/containerd/errdefs v1.0.0
github.com/docker/cli v28.3.0+incompatible
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/docker/model-distribution v0.0.0-20250710123110-a633223e127e
github.com/docker/model-runner v0.0.0-20250711130825-8907b3ddf82e
github.com/docker/model-distribution v0.0.0-20250724035854-a9454ee7284c
github.com/docker/model-runner v0.0.0-20250724025946-0dfa50af179a
github.com/google/go-containerregistry v0.20.6
github.com/mattn/go-isatty v0.0.17
github.com/nxadm/tail v1.4.8
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHz
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/model-distribution v0.0.0-20250710123110-a633223e127e h1:qBkjP4A20f3RXvtstitIPiStQ4p+bK8xcjosrXLBQZ0=
github.com/docker/model-distribution v0.0.0-20250710123110-a633223e127e/go.mod h1:dThpO9JoG5Px3i+rTluAeZcqLGw8C0qepuEL4gL2o/c=
github.com/docker/model-runner v0.0.0-20250711130825-8907b3ddf82e h1:oafd84kAFBgv/DAYgtXGLkC1KmRpDN+7G3be5+2+hA0=
github.com/docker/model-runner v0.0.0-20250711130825-8907b3ddf82e/go.mod h1:QmSoUNAbqolMY1Aq9DaC+sR/M/OPga0oCT/DBA1z9ow=
github.com/docker/model-distribution v0.0.0-20250724035854-a9454ee7284c h1:hkMon8WnL995BHacRCEsbtPEO2mYsUmdsohJva15QDU=
github.com/docker/model-distribution v0.0.0-20250724035854-a9454ee7284c/go.mod h1:dThpO9JoG5Px3i+rTluAeZcqLGw8C0qepuEL4gL2o/c=
github.com/docker/model-runner v0.0.0-20250724025946-0dfa50af179a h1:hbXJ8P/v7Dk+yvqeGsBAyHxIq/mjjhW5UiIP8i3CONo=
github.com/docker/model-runner v0.0.0-20250724025946-0dfa50af179a/go.mod h1:pmu0V0gxXVTzvjzkNm/jYDrIBccYTXBf8dL1zYm6sRQ=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
Expand Down
Loading