Skip to content

Commit 6674c88

Browse files
committed
feat(unpack): enhance unpacking logic to support V0.2 layer-per-file packaging
1 parent 3f8b58b commit 6674c88

5 files changed

Lines changed: 62 additions & 4 deletions

File tree

pkg/distribution/builder/from_directory.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func FromDirectory(dirPath string, opts ...DirectoryOption) (*Builder, error) {
190190
// TODO: Extract additional metadata from weight files if needed
191191
// For safetensors, we might want to read config.json from the directory
192192

193-
// Build the model
193+
// Build the model with V0.2 config (layer-per-file with annotations)
194194
created := time.Now()
195195
mdl := &partial.BaseModel{
196196
ModelConfigFile: types.ConfigFile{
@@ -203,7 +203,8 @@ func FromDirectory(dirPath string, opts ...DirectoryOption) (*Builder, error) {
203203
DiffIDs: diffIDs,
204204
},
205205
},
206-
LayerList: layers,
206+
LayerList: layers,
207+
ConfigMediaType: types.MediaTypeModelConfigV02, // V0.2: layer-per-file with filepath annotations
207208
}
208209

209210
return &Builder{

pkg/distribution/internal/bundle/unpack.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,33 @@ import (
1414
)
1515

1616
// Unpack creates and return a Bundle by unpacking files and config from model into dir.
17+
// It auto-detects the packaging version:
18+
// - V0.2 (layer-per-file with annotations): Uses UnpackFromLayers for full path preservation
19+
// - V0.1 (legacy): Uses the original unpacking logic based on GGUFPaths(), SafetensorsPaths(), etc.
1720
func Unpack(dir string, model types.Model) (*Bundle, error) {
21+
// Check if the model uses V0.2 packaging (layer-per-file with annotations)
22+
if artifact, ok := model.(types.ModelArtifact); ok {
23+
if isV02Model(artifact) {
24+
return UnpackFromLayers(dir, artifact)
25+
}
26+
}
27+
28+
// V0.1 legacy unpacking
29+
return unpackLegacy(dir, model)
30+
}
31+
32+
// isV02Model checks if the model was packaged using V0.2 format (layer-per-file with annotations).
33+
// It does this by checking the config media type in the manifest.
34+
func isV02Model(model types.ModelArtifact) bool {
35+
manifest, err := model.Manifest()
36+
if err != nil {
37+
return false
38+
}
39+
return manifest.Config.MediaType == types.MediaTypeModelConfigV02
40+
}
41+
42+
// unpackLegacy is the original V0.1 unpacking logic that uses model.GGUFPaths(), model.SafetensorsPaths(), etc.
43+
func unpackLegacy(dir string, model types.Model) (*Bundle, error) {
1844
bundle := &Bundle{
1945
dir: dir,
2046
}

pkg/distribution/internal/partial/model.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414
type BaseModel struct {
1515
ModelConfigFile types.ConfigFile
1616
LayerList []oci.Layer
17+
// ConfigMediaType specifies the media type for the config descriptor.
18+
// If empty, defaults to MediaTypeModelConfigV01 for backward compatibility.
19+
// Set to MediaTypeModelConfigV02 for layer-per-file packaging (FromDirectory).
20+
ConfigMediaType oci.MediaType
1721
}
1822

1923
var _ types.ModelArtifact = &BaseModel{}
@@ -125,3 +129,9 @@ func (m *BaseModel) Config() (types.ModelConfig, error) {
125129
func (m *BaseModel) Descriptor() (types.Descriptor, error) {
126130
return Descriptor(m)
127131
}
132+
133+
// GetConfigMediaType returns the config media type for the model.
134+
// If not set, returns empty string and ManifestForLayers will default to V0.1.
135+
func (m *BaseModel) GetConfigMediaType() oci.MediaType {
136+
return m.ConfigMediaType
137+
}

pkg/distribution/internal/partial/partial.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ func matchesMediaType(layerMT, targetMT oci.MediaType) bool {
191191
}
192192
}
193193

194+
// WithConfigMediaType provides access to the config media type version.
195+
type WithConfigMediaType interface {
196+
GetConfigMediaType() oci.MediaType
197+
}
198+
194199
func ManifestForLayers(i WithLayers) (*oci.Manifest, error) {
195200
raw, err := i.RawConfigFile()
196201
if err != nil {
@@ -200,8 +205,17 @@ func ManifestForLayers(i WithLayers) (*oci.Manifest, error) {
200205
if err != nil {
201206
return nil, fmt.Errorf("compute config hash: %w", err)
202207
}
208+
209+
// Use the config media type from the model if available, otherwise default to V0.1
210+
configMediaType := types.MediaTypeModelConfigV01
211+
if cmt, ok := i.(WithConfigMediaType); ok {
212+
if mt := cmt.GetConfigMediaType(); mt != "" {
213+
configMediaType = mt
214+
}
215+
}
216+
203217
cfgDsc := oci.Descriptor{
204-
MediaType: types.MediaTypeModelConfigV01,
218+
MediaType: configMediaType,
205219
Size: int64(len(raw)),
206220
Digest: cfgHash,
207221
}

pkg/distribution/types/config.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import (
1010
type MediaType = oci.MediaType
1111

1212
const (
13-
// MediaTypeModelConfigV01 is the media type for the model config json.
13+
// MediaTypeModelConfigV01 is the media type for the model config json (legacy format).
14+
// V0.1 uses model.GGUFPaths(), model.SafetensorsPaths(), etc. for unpacking.
1415
MediaTypeModelConfigV01 MediaType = "application/vnd.docker.ai.model.config.v0.1+json"
1516

17+
// MediaTypeModelConfigV02 is the media type for models using the layer-per-file approach.
18+
// V0.2 packages each file as an individual layer with filepath annotations.
19+
// This format preserves nested directory structure (e.g., text_encoder/model.safetensors).
20+
// Used by builder.FromDirectory.
21+
MediaTypeModelConfigV02 MediaType = "application/vnd.docker.ai.model.config.v0.2+json"
22+
1623
// MediaTypeGGUF indicates a file in GGUF version 3 format, containing a tensor model.
1724
MediaTypeGGUF MediaType = "application/vnd.docker.ai.gguf.v3"
1825

0 commit comments

Comments
 (0)