diff --git a/cmd/cli/commands/package.go b/cmd/cli/commands/package.go index 1d6a1f2f..b98bf422 100644 --- a/cmd/cli/commands/package.go +++ b/cmd/cli/commands/package.go @@ -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 | --safetensors-dir | --from ) [--license ...] [--context-size ] [--push] MODEL", + Use: "package (--gguf | --safetensors-dir | --from ) [--license ...] [--mmproj ] [--context-size ] [--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 @@ -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 @@ -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) @@ -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 @@ -159,6 +187,7 @@ type packageOptions struct { fromModel string licensePaths []string dirTarPaths []string + mmprojPath string push bool tag string } @@ -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() diff --git a/cmd/cli/docs/reference/docker_model_package.yaml b/cmd/cli/docs/reference/docker_model_package.yaml index b0166ee3..d59835ce 100644 --- a/cmd/cli/docs/reference/docker_model_package.yaml +++ b/cmd/cli/docs/reference/docker_model_package.yaml @@ -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 | --safetensors-dir | --from ) [--license ...] [--context-size ] [--push] MODEL + For multimodal models, use --mmproj to include a multimodal projector file. +usage: docker model package (--gguf | --safetensors-dir | --from ) [--license ...] [--mmproj ] [--context-size ] [--push] MODEL pname: docker model plink: docker_model.yaml options: @@ -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" diff --git a/cmd/cli/docs/reference/model_package.md b/cmd/cli/docs/reference/model_package.md index 49234cee..eaf3da29 100644 --- a/cmd/cli/docs/reference/model_package.md +++ b/cmd/cli/docs/reference/model_package.md @@ -1,10 +1,11 @@ # docker model package -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 @@ -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 |