Skip to content
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
67 changes: 52 additions & 15 deletions cmd/cli/commands/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,29 @@ import (
"github.com/docker/model-runner/cmd/cli/desktop"
)

// validateAbsolutePath validates that a path is absolute and returns the cleaned path
func validateAbsolutePath(path, name string) (string, error) {
if !filepath.IsAbs(path) {
return "", fmt.Errorf(
"%s path must be absolute.\n\n"+
"See 'docker model package --help' for more information",
name,
)
}
return filepath.Clean(path), nil
}

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

c := &cobra.Command{
Use: "package (--gguf <path> | --safetensors-dir <path> | --from <model>) [--license <path>...] [--context-size <tokens>] [--push] MODEL",
Use: "package (--gguf <path> | --safetensors-dir <path> | --from <model>) [--license <path>...] [--mmproj <path>] [--context-size <tokens>] [--push] MODEL",
Short: "Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact.",
Long: "Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified.\n" +
Long: "Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact, with optional licenses and multimodal projector. The package is sent to the model-runner, unless --push is specified.\n" +
"When packaging a sharded GGUF model, --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf).\n" +
"When packaging a Safetensors model, --safetensors-dir should point to a directory containing .safetensors files and config files (*.json, merges.txt). All files will be auto-discovered and config files will be packaged into a tar archive.\n" +
"When packaging from an existing model using --from, you can modify properties like context size to create a variant of the original model.",
"When packaging from an existing model using --from, you can modify properties like context size to create a variant of the original model.\n" +
"For multimodal models, use --mmproj to include a multimodal projector file.",
Args: func(cmd *cobra.Command, args []string) error {
if err := requireExactArgs(1, "package", "MODEL")(cmd, args); err != nil {
return err
Expand Down Expand Up @@ -66,13 +79,11 @@ func newPackagedCmd() *cobra.Command {

// Validate GGUF path if provided
if opts.ggufPath != "" {
if !filepath.IsAbs(opts.ggufPath) {
return fmt.Errorf(
"GGUF path must be absolute.\n\n" +
"See 'docker model package --help' for more information",
)
var err error
opts.ggufPath, err = validateAbsolutePath(opts.ggufPath, "GGUF")
if err != nil {
return err
}
opts.ggufPath = filepath.Clean(opts.ggufPath)
}

// Validate safetensors directory if provided
Expand Down Expand Up @@ -107,13 +118,29 @@ func newPackagedCmd() *cobra.Command {
}

for i, l := range opts.licensePaths {
if !filepath.IsAbs(l) {
return fmt.Errorf(
"license path must be absolute.\n\n" +
"See 'docker model package --help' for more information",
)
var err error
opts.licensePaths[i], err = validateAbsolutePath(l, "license")
if err != nil {
return err
}
}

// Validate chat template path if provided
if opts.chatTemplatePath != "" {
var err error
opts.chatTemplatePath, err = validateAbsolutePath(opts.chatTemplatePath, "chat template")
if err != nil {
return err
}
}

// Validate mmproj path if provided
if opts.mmprojPath != "" {
var err error
opts.mmprojPath, err = validateAbsolutePath(opts.mmprojPath, "mmproj")
if err != nil {
return err
}
opts.licensePaths[i] = filepath.Clean(l)
}

// Validate dir-tar paths are relative (not absolute)
Expand Down Expand Up @@ -146,6 +173,7 @@ func newPackagedCmd() *cobra.Command {
c.Flags().StringVar(&opts.chatTemplatePath, "chat-template", "", "absolute path to chat template file (must be Jinja format)")
c.Flags().StringArrayVarP(&opts.licensePaths, "license", "l", nil, "absolute path to a license file")
c.Flags().StringArrayVar(&opts.dirTarPaths, "dir-tar", nil, "relative path to directory to package as tar (can be specified multiple times)")
c.Flags().StringVar(&opts.mmprojPath, "mmproj", "", "absolute path to multimodal projector file")
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 @@ -159,6 +187,7 @@ type packageOptions struct {
fromModel string
licensePaths []string
dirTarPaths []string
mmprojPath string
push bool
tag string
}
Expand Down Expand Up @@ -305,6 +334,14 @@ func packageModel(ctx context.Context, cmd *cobra.Command, client *desktop.Clien
}
}

if opts.mmprojPath != "" {
cmd.PrintErrf("Adding multimodal projector file from %q\n", opts.mmprojPath)
pkg, err = pkg.WithMultimodalProjector(opts.mmprojPath)
if err != nil {
return fmt.Errorf("add multimodal projector file: %w", err)
}
}

// Check if we can use lightweight repackaging (config-only changes from existing model)
useLightweight := opts.fromModel != "" && pkg.HasOnlyConfigChanges()

Expand Down
14 changes: 12 additions & 2 deletions cmd/cli/docs/reference/docker_model_package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ command: docker model package
short: |
Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact.
long: |-
Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified.
Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact, with optional licenses and multimodal projector. The package is sent to the model-runner, unless --push is specified.
When packaging a sharded GGUF model, --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf).
When packaging a Safetensors model, --safetensors-dir should point to a directory containing .safetensors files and config files (*.json, merges.txt). All files will be auto-discovered and config files will be packaged into a tar archive.
When packaging from an existing model using --from, you can modify properties like context size to create a variant of the original model.
usage: docker model package (--gguf <path> | --safetensors-dir <path> | --from <model>) [--license <path>...] [--context-size <tokens>] [--push] MODEL
For multimodal models, use --mmproj to include a multimodal projector file.
usage: docker model package (--gguf <path> | --safetensors-dir <path> | --from <model>) [--license <path>...] [--mmproj <path>] [--context-size <tokens>] [--push] MODEL
pname: docker model
plink: docker_model.yaml
options:
Expand Down Expand Up @@ -69,6 +70,15 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: mmproj
value_type: string
description: absolute path to multimodal projector file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: push
value_type: bool
default_value: "false"
Expand Down
4 changes: 3 additions & 1 deletion cmd/cli/docs/reference/model_package.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# docker model package

<!---MARKER_GEN_START-->
Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact, with optional licenses. The package is sent to the model-runner, unless --push is specified.
Package a GGUF file, Safetensors directory, or existing model into a Docker model OCI artifact, with optional licenses and multimodal projector. The package is sent to the model-runner, unless --push is specified.
When packaging a sharded GGUF model, --gguf should point to the first shard. All shard files should be siblings and should include the index in the file name (e.g. model-00001-of-00015.gguf).
When packaging a Safetensors model, --safetensors-dir should point to a directory containing .safetensors files and config files (*.json, merges.txt). All files will be auto-discovered and config files will be packaged into a tar archive.
When packaging from an existing model using --from, you can modify properties like context size to create a variant of the original model.
For multimodal models, use --mmproj to include a multimodal projector file.

### Options

Expand All @@ -16,6 +17,7 @@ When packaging from an existing model using --from, you can modify properties li
| `--from` | `string` | | reference to an existing model to repackage |
| `--gguf` | `string` | | absolute path to gguf file |
| `-l`, `--license` | `stringArray` | | absolute path to a license file |
| `--mmproj` | `string` | | absolute path to multimodal projector file |
| `--push` | `bool` | | push to registry (if not set, the model is loaded into the Model Runner content store) |
| `--safetensors-dir` | `string` | | absolute path to directory containing safetensors files and config |

Expand Down
Loading