Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ MODEL_RUNNER_HOST=http://localhost:13434 ./model-cli list
- [Model Specification](https://github.com/docker/model-spec/blob/main/spec.md)
- [Community Slack Channel](https://dockercommunity.slack.com/archives/C09H9P5E57B)

### ModelPack Compatibility

Docker Model Runner supports both Docker model-spec artifacts and CNCF ModelPack artifacts stored in OCI registries.

For ModelPack images, Docker Model Runner accepts:

- config media type: `application/vnd.cncf.model.config.v1+json`
- weight layer media types, including:
- `application/vnd.cncf.model.weight.v1.gguf`
- `application/vnd.cncf.model.weight.v1.safetensors`
Comment thread
rishi-jat marked this conversation as resolved.

This means you can pull and run a ModelPack artifact with the same user workflow:

```bash
# Pull from any OCI-compliant registry
docker model pull <registry>/<namespace>/<model>:<tag>

# Run the model
docker model run <registry>/<namespace>/<model>:<tag> "Hello"
```

If you are publishing artifacts for compatibility across tooling, ensure your image config and layer media types follow the ModelPack spec so downstream clients can detect and use the correct format.

## Using the Makefile

This project includes a Makefile to simplify common development tasks. Docker targets require Docker Desktop >= 4.41.0.
Expand Down
208 changes: 208 additions & 0 deletions pkg/distribution/distribution/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,164 @@
"path/filepath"
"strings"
"testing"
"time"

"github.com/docker/model-runner/pkg/distribution/internal/mutate"
"github.com/docker/model-runner/pkg/distribution/internal/partial"
"github.com/docker/model-runner/pkg/distribution/internal/progress"
"github.com/docker/model-runner/pkg/distribution/internal/testutil"
"github.com/docker/model-runner/pkg/distribution/modelpack"
"github.com/docker/model-runner/pkg/distribution/oci"
"github.com/docker/model-runner/pkg/distribution/oci/reference"
"github.com/docker/model-runner/pkg/distribution/oci/remote"
mdregistry "github.com/docker/model-runner/pkg/distribution/registry"
"github.com/docker/model-runner/pkg/distribution/registry/testregistry"
"github.com/docker/model-runner/pkg/distribution/types"
"github.com/docker/model-runner/pkg/inference/platform"
"github.com/opencontainers/go-digest"
)

var (
testGGUFFile = filepath.Join("..", "assets", "dummy.gguf")
)

type modelPackTestArtifact struct {
rawConfig []byte
layers []oci.Layer
}

func (m *modelPackTestArtifact) Layers() ([]oci.Layer, error) {
return m.layers, nil
}

func (m *modelPackTestArtifact) MediaType() (oci.MediaType, error) {
manifest, err := m.Manifest()
if err != nil {
return "", err
}
return manifest.MediaType, nil
}

func (m *modelPackTestArtifact) Size() (int64, error) {
rawManifest, err := m.RawManifest()
if err != nil {
return 0, err
}
size := int64(len(rawManifest) + len(m.rawConfig))
for _, layer := range m.layers {
layerSize, err := layer.Size()
if err != nil {
return 0, err
}
size += layerSize
}
return size, nil
}

func (m *modelPackTestArtifact) ConfigName() (oci.Hash, error) {
hash, _, err := oci.SHA256(bytes.NewReader(m.rawConfig))
return hash, err
}

func (m *modelPackTestArtifact) ConfigFile() (*oci.ConfigFile, error) {
return nil, errors.New("invalid for model")
}

func (m *modelPackTestArtifact) RawConfigFile() ([]byte, error) {
return m.rawConfig, nil
}

func (m *modelPackTestArtifact) Digest() (oci.Hash, error) {
rawManifest, err := m.RawManifest()
if err != nil {
return oci.Hash{}, err
}
hash, _, err := oci.SHA256(bytes.NewReader(rawManifest))
return hash, err
}

func (m *modelPackTestArtifact) Manifest() (*oci.Manifest, error) {
return partial.ManifestForLayers(m)
}

func (m *modelPackTestArtifact) RawManifest() ([]byte, error) {
manifest, err := m.Manifest()
if err != nil {
return nil, err
}
return json.Marshal(manifest)
}

func (m *modelPackTestArtifact) LayerByDigest(hash oci.Hash) (oci.Layer, error) {
for _, layer := range m.layers {
layerDigest, err := layer.Digest()
if err != nil {
return nil, err
}
if layerDigest == hash {
return layer, nil
}
}
return nil, fmt.Errorf("layer with digest %s not found", hash)
}

func (m *modelPackTestArtifact) LayerByDiffID(hash oci.Hash) (oci.Layer, error) {
for _, layer := range m.layers {
layerDiffID, err := layer.DiffID()
if err != nil {
return nil, err
}
if layerDiffID == hash {
return layer, nil
}
}
return nil, fmt.Errorf("layer with diffID %s not found", hash)
}

func (m *modelPackTestArtifact) GetConfigMediaType() oci.MediaType {
return types.MediaTypeModelConfigV02
Comment thread
rishi-jat marked this conversation as resolved.
Outdated
}
Comment thread
rishi-jat marked this conversation as resolved.

func newModelPackTestArtifact(t *testing.T, modelFile string) *modelPackTestArtifact {
t.Helper()

layer, err := partial.NewLayer(modelFile, oci.MediaType(modelpack.MediaTypeWeightGGUF))
if err != nil {
t.Fatalf("Failed to create ModelPack layer: %v", err)
}

diffID, err := layer.DiffID()
if err != nil {
t.Fatalf("Failed to get layer DiffID: %v", err)
}

now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
mp := modelpack.Model{
Descriptor: modelpack.ModelDescriptor{
CreatedAt: &now,
Name: "dummy-modelpack",
},
Config: modelpack.ModelConfig{
Format: "gguf",
ParamSize: "8B",
},
ModelFS: modelpack.ModelFS{
Type: "layers",
DiffIDs: []digest.Digest{digest.Digest(diffID.String())},
},
}

rawConfig, err := json.Marshal(mp)
if err != nil {
t.Fatalf("Failed to marshal ModelPack config: %v", err)
}

return &modelPackTestArtifact{
rawConfig: rawConfig,
layers: []oci.Layer{layer},
}
}

// newTestClient creates a new client configured for testing with plain HTTP enabled.
func newTestClient(storeRootPath string) (*Client, error) {
return NewClient(
Expand Down Expand Up @@ -142,6 +284,72 @@
}
})

t.Run("pull modelpack artifact", func(t *testing.T) {
tempDir := t.TempDir()

testClient, err := newTestClient(tempDir)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

tag := registryHost + "/modelpack-test/model:v1.0.0"

Check failure on line 295 in pkg/distribution/distribution/client_test.go

View workflow job for this annotation

GitHub Actions / lint (darwin)

shadow: declaration of "tag" shadows declaration at line 209 (govet)
ref, err := reference.ParseReference(tag)
if err != nil {
t.Fatalf("Failed to parse reference: %v", err)
}

mpModel := newModelPackTestArtifact(t, testGGUFFile)
if err := remote.Write(ref, mpModel, nil, remote.WithPlainHTTP(true)); err != nil {
t.Fatalf("Failed to push ModelPack model: %v", err)
}

if err := testClient.PullModel(t.Context(), tag, nil); err != nil {
t.Fatalf("Failed to pull ModelPack model: %v", err)
}

pulledModel, err := testClient.GetModel(tag)
if err != nil {
t.Fatalf("Failed to get pulled model: %v", err)
}

ggufPaths, err := pulledModel.GGUFPaths()
if err != nil {
t.Fatalf("Failed to get GGUF paths: %v", err)
}
if len(ggufPaths) != 1 {
t.Fatalf("Unexpected number of GGUF files: %d", len(ggufPaths))
}

pulledContent, err := os.ReadFile(ggufPaths[0])
if err != nil {
t.Fatalf("Failed to read pulled GGUF file: %v", err)
}

originalContent, err := os.ReadFile(testGGUFFile)
if err != nil {
t.Fatalf("Failed to read source GGUF file: %v", err)
}

if string(pulledContent) != string(originalContent) {
t.Errorf("Pulled ModelPack model content doesn't match original")
}

cfg, err := pulledModel.Config()
if err != nil {
t.Fatalf("Failed to read pulled model config: %v", err)
}
if cfg.GetFormat() != "gguf" {
t.Errorf("Config format = %q, want %q", cfg.GetFormat(), "gguf")
}
if cfg.GetParameters() != "8B" {
t.Errorf("Config parameters = %q, want %q", cfg.GetParameters(), "8B")
}

if _, ok := cfg.(*modelpack.Model); !ok {
t.Errorf("Config type = %T, want *modelpack.Model", cfg)
}
})

t.Run("pull non-existent model", func(t *testing.T) {
tempDir := t.TempDir()

Expand Down
Loading