Skip to content
Open
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
34 changes: 33 additions & 1 deletion cmd/cli/commands/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextS

// Set context size if specified
if contextSize != nil {
pkg = pkg.WithContextSize(*contextSize)
pkg, err = pkg.WithContextSize(*contextSize)
require.NoError(t, err)
}

// Construct the full reference with the local registry host for pushing from test host
Expand Down Expand Up @@ -1053,6 +1054,7 @@ func TestIntegration_PackageModel(t *testing.T) {
opts := packageOptions{
ggufPath: absPath,
tag: targetTag,
format: "docker",
}

// Execute the package command using the helper function with test client
Expand Down Expand Up @@ -1088,6 +1090,7 @@ func TestIntegration_PackageModel(t *testing.T) {
ggufPath: absPath,
tag: targetTag,
contextSize: 4096,
format: "docker",
}

// Create a command for context
Expand Down Expand Up @@ -1120,6 +1123,7 @@ func TestIntegration_PackageModel(t *testing.T) {
opts := packageOptions{
ggufPath: absPath,
tag: targetTag,
format: "docker",
}

// Create a command for context
Expand All @@ -1142,6 +1146,34 @@ func TestIntegration_PackageModel(t *testing.T) {
require.NoError(t, err, "Failed to remove model")
})

// Test case 4: Package with CNCF format
t.Run("package GGUF with CNCF format", func(t *testing.T) {
targetTag := "ai/packaged-cncf:latest"

// Create package options with CNCF format
opts := packageOptions{
ggufPath: absPath,
tag: targetTag,
format: "cncf",
}

// Execute the package command using the helper function with test client
t.Logf("Packaging GGUF file as CNCF format %s", targetTag)
err := packageModel(env.ctx, newPackagedCmd(), env.client, opts)
require.NoError(t, err, "Failed to package GGUF model with CNCF format")

// Verify the model was loaded and tagged
model, err := env.client.Inspect(targetTag, false)
require.NoError(t, err, "Failed to inspect CNCF packaged model")
require.Contains(t, model.Tags, normalizeRef(t, targetTag), "Model should have the expected tag")

t.Logf("✓ Successfully packaged model with CNCF format: %s (ID: %s)", targetTag, model.ID[7:19])

// Cleanup
err = removeModel(env.client, model.ID, true)
require.NoError(t, err, "Failed to remove model")
})

// Verify all models are cleaned up
models, err = listModels(false, env.client, true, false, "")
require.NoError(t, err)
Expand Down
53 changes: 38 additions & 15 deletions cmd/cli/commands/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ Packaging behavior:
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")
c.Flags().StringVar(&opts.format, "format", "docker",
"output artifact format: \"docker\" (default) or \"cncf\" (CNCF ModelPack spec)")
return c
}

Expand All @@ -222,21 +224,30 @@ type packageOptions struct {
mmprojPath string
push bool
tag string
format string // "docker" (default) or "cncf"
}

// builderInitResult contains the result of initializing a builder from various sources
// builderInitResult contains the result of initializing a builder from
// various sources.
type builderInitResult struct {
builder *builder.Builder
distClient *distribution.Client // Only set when building from existing model
cleanupFunc func() // Optional cleanup function for temporary files
distClient *distribution.Client // Only set when building from existing model.
cleanupFunc func() // Optional cleanup function for temporary files.
}

// initializeBuilder creates a package builder from GGUF, Safetensors, DDUF, or existing model
// initializeBuilder creates a package builder from GGUF, Safetensors, DDUF,
// or existing model.
func initializeBuilder(ctx context.Context, cmd *cobra.Command, client *desktop.Client, opts packageOptions) (*builderInitResult, error) {
result := &builderInitResult{}

// Map the CLI format string to a BuildFormat constant.
buildFmt := builder.BuildFormatDocker
if opts.format == "cncf" {
buildFmt = builder.BuildFormatCNCF
}

if opts.fromModel != "" {
// Get the model store path
// Get the model store path.
userHomeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("get user home directory: %w", err)
Expand All @@ -246,14 +257,14 @@ func initializeBuilder(ctx context.Context, cmd *cobra.Command, client *desktop.
modelStorePath = envPath
}

// Create a distribution client to access the model store
// Create a distribution client to access the model store.
distClient, err := distribution.NewClient(distribution.WithStoreRootPath(modelStorePath))
if err != nil {
return nil, fmt.Errorf("create distribution client: %w", err)
}
result.distClient = distClient

// Package from existing model
// Package from existing model.
cmd.PrintErrf("Reading model from store: %q\n", opts.fromModel)

mdl, err := distClient.GetModel(opts.fromModel)
Expand All @@ -266,35 +277,36 @@ func initializeBuilder(ctx context.Context, cmd *cobra.Command, client *desktop.
}
}

// Type assert to ModelArtifact - the Model from store implements both interfaces
// Type assert to ModelArtifact.
modelArtifact, ok := mdl.(types.ModelArtifact)
if !ok {
return nil, fmt.Errorf("model does not implement ModelArtifact interface")
}

cmd.PrintErrf("Creating builder from existing model\n")
result.builder, err = builder.FromModel(modelArtifact)
result.builder, err = builder.FromModel(modelArtifact, builder.WithFormat(buildFmt))
if err != nil {
return nil, fmt.Errorf("create builder from model: %w", err)
}
} else if opts.ggufPath != "" {
cmd.PrintErrf("Adding GGUF file from %q\n", opts.ggufPath)
pkg, err := builder.FromPath(opts.ggufPath)
pkg, err := builder.FromPath(opts.ggufPath, builder.WithFormat(buildFmt))
if err != nil {
return nil, fmt.Errorf("add gguf file: %w", err)
}
result.builder = pkg
} else if opts.ddufPath != "" {
cmd.PrintErrf("Adding DDUF file from %q\n", opts.ddufPath)
pkg, err := builder.FromPath(opts.ddufPath)
pkg, err := builder.FromPath(opts.ddufPath, builder.WithFormat(buildFmt))
if err != nil {
return nil, fmt.Errorf("add dduf file: %w", err)
}
result.builder = pkg
} else if opts.safetensorsDir != "" {
// Safetensors model from directory — uses V0.2 layer-per-file packaging
// Safetensors model from directory — uses V0.2 layer-per-file packaging.
cmd.PrintErrf("Scanning directory %q for safetensors model...\n", opts.safetensorsDir)
pkg, err := builder.FromDirectory(opts.safetensorsDir)
pkg, err := builder.FromDirectory(opts.safetensorsDir,
builder.WithOutputFormat(buildFmt))
if err != nil {
return nil, fmt.Errorf("create safetensors model from directory: %w", err)
}
Expand Down Expand Up @@ -344,9 +356,17 @@ func fetchModelFromDaemon(ctx context.Context, cmd *cobra.Command, client *deskt
}

func packageModel(ctx context.Context, cmd *cobra.Command, client *desktop.Client, opts packageOptions) error {
// Use daemon-side repackaging for simple config-only changes (no new layers)
// Validate format flag.
if opts.format != "docker" && opts.format != "cncf" {
return fmt.Errorf("invalid --format value %q: must be \"docker\" or \"cncf\"", opts.format)
}

// Use daemon-side repackaging for simple config-only changes (no new
// layers). Disabled for CNCF format because the daemon produces
// Docker-format artifacts.
canUseDaemonRepackage := opts.fromModel != "" &&
!opts.push &&
opts.format != "cncf" &&
len(opts.licensePaths) == 0 &&
opts.chatTemplatePath == "" &&
opts.mmprojPath == "" &&
Expand Down Expand Up @@ -408,7 +428,10 @@ func packageModel(ctx context.Context, cmd *cobra.Command, client *desktop.Clien
// Set context size
if cmd.Flags().Changed("context-size") {
cmd.PrintErrf("Setting context size %d\n", opts.contextSize)
pkg = pkg.WithContextSize(int32(opts.contextSize))
pkg, err = pkg.WithContextSize(int32(opts.contextSize))
if err != nil {
return err
}
}

// Add license files
Expand Down
11 changes: 11 additions & 0 deletions cmd/cli/docs/reference/docker_model_package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: format
value_type: string
default_value: docker
description: |
output artifact format: "docker" (default) or "cncf" (CNCF ModelPack spec)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: from
value_type: string
description: reference to an existing model to repackage
Expand Down
23 changes: 12 additions & 11 deletions cmd/cli/docs/reference/model_package.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,18 @@ Packaging behavior:

### Options

| Name | Type | Default | Description |
|:--------------------|:--------------|:--------|:---------------------------------------------------------------------------------------|
| `--chat-template` | `string` | | absolute path to chat template file (must be Jinja format) |
| `--context-size` | `uint64` | `0` | context size in tokens |
| `--dduf` | `string` | | absolute path to DDUF archive file (Diffusers Unified Format) |
| `--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 |
| Name | Type | Default | Description |
|:--------------------|:--------------|:---------|:---------------------------------------------------------------------------------------|
| `--chat-template` | `string` | | absolute path to chat template file (must be Jinja format) |
| `--context-size` | `uint64` | `0` | context size in tokens |
| `--dduf` | `string` | | absolute path to DDUF archive file (Diffusers Unified Format) |
| `--format` | `string` | `docker` | output artifact format: "docker" (default) or "cncf" (CNCF ModelPack spec) |
| `--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 |


<!---MARKER_GEN_END-->
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/moby/moby/api v1.54.1
github.com/moby/moby/client v0.4.0
github.com/moby/term v0.5.2
github.com/modelpack/model-spec v0.0.7
github.com/muesli/termenv v0.16.0
github.com/nxadm/tail v1.4.11
github.com/olekukonko/tablewriter v1.1.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelpack/model-spec v0.0.7 h1:3fAxau4xUqF0Pf1zzFC5lItF0gEaiXLxaCcPAH8PW8I=
github.com/modelpack/model-spec v0.0.7/go.mod h1:5Go37og1RmvcTdVI5Remd+PpQRNLlKSNwSNbXmEqu50=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
Loading
Loading