Skip to content

fix: generate Kitfile for ModelPack if missing from manifest#1142

Open
AdeshDeshmukh wants to merge 1 commit intokitops-ml:mainfrom
AdeshDeshmukh:fix/modelpack-kitfile-fallback
Open

fix: generate Kitfile for ModelPack if missing from manifest#1142
AdeshDeshmukh wants to merge 1 commit intokitops-ml:mainfrom
AdeshDeshmukh:fix/modelpack-kitfile-fallback

Conversation

@AdeshDeshmukh
Copy link
Copy Markdown

Use fallback generation for missing Kitfile in ModelPacks.

Description

This PR introduces a fallback mechanism to generate a Kitfile in-memory when a ModelPack manifest does not contain one in its annotations.

Previously, if the org.cncf.model.kitfile annotation was missing, GetKitfileForManifest would return ErrNoKitfile. This change adds GenerateKitfileForModelPack to reconstruct the Kitfile from layer metadata (filepaths and media types), allowing tools to inspect and unpack these artifacts seamlessy.

Validation:

  • Added new unit tests in pkg/lib/repo/util/modelpack_test.go
  • Verified existing logic in pkg/lib/filesystem/unpack/core.go now uses the shared utility.

Linked issues

Fixes missing Kitfile handling for ModelPacks.

This commit introduces a fallback mechanism to generate a Kitfile in-memory
when a ModelPack manifest does not contain one in its annotations. This
allows tools to inspect and unpack ModelPack artifacts that are missing
an explicit Kitfile, by reconstructing it from layer metadata (filepaths
and media types).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a fallback path for ModelPack artifacts to synthesize a minimal in-memory Kitfile when the Kitfile JSON annotation is missing, and centralizes the generation logic in pkg/lib/repo/util.

Changes:

  • Update GetKitfileForManifest to parse the Kitfile annotation when present, otherwise generate a Kitfile for ModelPacks.
  • Introduce GenerateKitfileForModelPack utility and reuse it from unpack logic (removing the local implementation).
  • Add unit tests for the ModelPack Kitfile generation utility.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
pkg/lib/repo/util/reference.go Uses ModelPack annotation Kitfile when available; otherwise calls the shared generator.
pkg/lib/repo/util/modelpack.go New shared utility to synthesize a minimal Kitfile from ModelPack layer metadata.
pkg/lib/repo/util/modelpack_test.go New unit tests validating basic generation and missing-path error behavior.
pkg/lib/filesystem/unpack/core.go Switches unpack fallback logic to call the shared ModelPack generator and removes the local generator.
Comments suppressed due to low confidence (1)

pkg/lib/filesystem/unpack/core.go:102

  • With GetKitfileForManifest now generating a Kitfile for ModelPacks when the annotation is missing, the ErrNoKitfile branch here is unlikely to run for ModelPacks anymore. That means the warning logs and the explicit fallback path may become dead/duplicated logic and the synthesized Kitfile will flow through the "legitimate Kitfile" path (including potentially unpacking a generated Kitfile to disk). Consider simplifying this block, or explicitly detecting the "generated" case so unpack can still warn/avoid writing a synthesized Kitfile if that’s not desired.
		output.Logf(output.LogLevelWarn, "Could not get Kitfile: %s", err)
		output.Logf(output.LogLevelWarn, "Functionality may be impacted")
		// TODO: we can probably _also_ handle getting the model-spec config and using it here
		genconfig, err := util.GenerateKitfileForModelPack(manifest)
		if err != nil {
			return fmt.Errorf("could not process manifest: %w", err)
		}
		config = genconfig

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return nil, fmt.Errorf("not a modelpack artifact")
}
kf := &artifact.KitFile{
Model: &artifact.Model{},
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated Kitfile leaves ManifestVersion empty. Since ManifestVersion is not omitempty, this will serialize as an empty string when unpack writes a Kitfile to disk (and may trigger validation warnings elsewhere). Set a default manifestVersion (e.g. "1.0.0") on the synthesized Kitfile to keep it consistent with normal Kitfiles.

Suggested change
Model: &artifact.Model{},
ManifestVersion: "1.0.0",
Model: &artifact.Model{},

Copilot uses AI. Check for mistakes.
kfstring := manifest.Annotations[constants.KitfileJsonAnnotation]
kitfile := &artifact.KitFile{}
if err := json.Unmarshal([]byte(kfstring), kitfile); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message returned when unmarshalling the Kitfile JSON annotation says "failed to parse config", but this path is parsing the Kitfile stored in a manifest annotation (not the OCI config blob). Updating the message to reference the Kitfile annotation will make failures easier to diagnose.

Suggested change
return nil, fmt.Errorf("failed to parse config: %w", err)
return nil, fmt.Errorf("failed to parse Kitfile JSON annotation: %w", err)

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +72
func TestGenerateKitfileForModelPack(t *testing.T) {
manifest := &ocispec.Manifest{
ArtifactType: mediatype.ArtifactTypeModelManifest,
Config: ocispec.Descriptor{
MediaType: mediatype.ModelPackConfigMediaType.String(),
},
Layers: []ocispec.Descriptor{
{
MediaType: mediatype.New(mediatype.ModelPackFormat, mediatype.ModelBaseType, mediatype.TarFormat, mediatype.GzipCompression).String(),
Annotations: map[string]string{
modelspecv1.AnnotationFilepath: "models/main.gguf",
},
},
{
MediaType: mediatype.New(mediatype.ModelPackFormat, mediatype.CodeBaseType, mediatype.TarFormat, mediatype.GzipCompression).String(),
Annotations: map[string]string{
modelspecv1.AnnotationFilepath: "src/app.py",
},
},
{
MediaType: mediatype.New(mediatype.ModelPackFormat, mediatype.DatasetBaseType, mediatype.TarFormat, mediatype.GzipCompression).String(),
Annotations: map[string]string{
modelspecv1.AnnotationFilepath: "data/train.csv",
},
},
{
MediaType: mediatype.New(mediatype.ModelPackFormat, mediatype.DocsBaseType, mediatype.TarFormat, mediatype.GzipCompression).String(),
Annotations: map[string]string{
modelspecv1.AnnotationFilepath: "docs/readme.md",
},
},
},
}

kf, err := GenerateKitfileForModelPack(manifest)
require.NoError(t, err)
require.NotNil(t, kf.Model)
require.Equal(t, "models/main.gguf", kf.Model.Path)
require.Len(t, kf.Code, 1)
require.Equal(t, "src/app.py", kf.Code[0].Path)
require.Len(t, kf.DataSets, 1)
require.Equal(t, "data/train.csv", kf.DataSets[0].Path)
require.Len(t, kf.Docs, 1)
require.Equal(t, "docs/readme.md", kf.Docs[0].Path)
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current tests cover basic layer-to-Kitfile mapping and missing filepath, but they don’t cover prompt subtypes (code layer with ml.kitops.modelkit.layer-subtype=prompt) or manifests containing ConfigBaseType/unknown layers that should be skipped. Adding tests for these cases will help prevent regressions and catch the unpack indexing/panic scenario.

Copilot uses AI. Check for mistakes.
case mediatype.ModelPartBaseType:
kf.Model.Parts = append(kf.Model.Parts, artifact.ModelPart{Path: filepath})
case mediatype.CodeBaseType:
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenerateKitfileForModelPack treats every code layer as regular code. Unpack logic differentiates prompt layers via the ml.kitops.modelkit.layer-subtype annotation and indexes into config.Prompts for those; with the current generator this can lead to an index-out-of-range panic (Prompts stays empty) when a ModelPack includes prompt layers. Handle the prompt subtype annotation here by populating KitFile.Prompts (and leaving KitFile.Code for non-prompt code layers).

Suggested change
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
subtype := ""
if desc.Annotations != nil {
subtype = desc.Annotations["ml.kitops.modelkit.layer-subtype"]
}
if subtype == "prompt" {
kf.Prompts = append(kf.Prompts, artifact.Prompt{Path: filepath})
} else {
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
}

Copilot uses AI. Check for mistakes.
case mediatype.DatasetBaseType:
kf.DataSets = append(kf.DataSets, artifact.DataSet{Path: filepath})
case mediatype.DocsBaseType:
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenerateKitfileForModelPack errors on any layer whose parsed base type isn't model/modelpart/code/dataset/docs. ModelPacks can include config-type layers (and unpack already explicitly expects to skip ConfigBaseType layers), so returning an error here can prevent unpack/inspect from working even though the extra layer could be safely ignored. Consider explicitly skipping ConfigBaseType layers (and potentially other non-content layers) instead of treating them as fatal.

Suggested change
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
case mediatype.ConfigBaseType:
// Config layers do not contribute to the Kitfile contents; safely ignore.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +59
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
mt, err := mediatype.ParseMediaType(desc.MediaType)
if err != nil {
return nil, err
}
switch mt.Base() {
case mediatype.ModelBaseType:
kf.Model.Path = filepath
case mediatype.ModelPartBaseType:
kf.Model.Parts = append(kf.Model.Parts, artifact.ModelPart{Path: filepath})
case mediatype.CodeBaseType:
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
case mediatype.DatasetBaseType:
kf.DataSets = append(kf.DataSets, artifact.DataSet{Path: filepath})
case mediatype.DocsBaseType:
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
default:
return nil, fmt.Errorf("unknown layer type: %s", mt)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenerateKitfileForModelPack requires org.cncf.model.filepath on every layer before even checking the layer media type. If the manifest contains config layers or any layer types you intend to skip, this will return an error unnecessarily. Reorder the logic to parse/recognize the media type first (and skip config/unknown layers) before enforcing the filepath annotation requirement.

Suggested change
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
mt, err := mediatype.ParseMediaType(desc.MediaType)
if err != nil {
return nil, err
}
switch mt.Base() {
case mediatype.ModelBaseType:
kf.Model.Path = filepath
case mediatype.ModelPartBaseType:
kf.Model.Parts = append(kf.Model.Parts, artifact.ModelPart{Path: filepath})
case mediatype.CodeBaseType:
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
case mediatype.DatasetBaseType:
kf.DataSets = append(kf.DataSets, artifact.DataSet{Path: filepath})
case mediatype.DocsBaseType:
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
default:
return nil, fmt.Errorf("unknown layer type: %s", mt)
mt, err := mediatype.ParseMediaType(desc.MediaType)
if err != nil {
return nil, err
}
switch mt.Base() {
case mediatype.ModelBaseType:
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
kf.Model.Path = filepath
case mediatype.ModelPartBaseType:
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
kf.Model.Parts = append(kf.Model.Parts, artifact.ModelPart{Path: filepath})
case mediatype.CodeBaseType:
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
kf.Code = append(kf.Code, artifact.Code{Path: filepath})
case mediatype.DatasetBaseType:
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
kf.DataSets = append(kf.DataSets, artifact.DataSet{Path: filepath})
case mediatype.DocsBaseType:
if desc.Annotations == nil || desc.Annotations[modelspecv1.AnnotationFilepath] == "" {
return nil, fmt.Errorf("unknown file path for layer: no %s annotation", modelspecv1.AnnotationFilepath)
}
filepath := desc.Annotations[modelspecv1.AnnotationFilepath]
kf.Docs = append(kf.Docs, artifact.Docs{Path: filepath})
default:
// Skip layers that are not recognized as modelpack content (e.g., config or unknown types).
continue

Copilot uses AI. Check for mistakes.
@gorkem
Copy link
Copy Markdown
Member

gorkem commented Mar 26, 2026

Can you add the sign-off messages to your commits. Click on the failing DCO check above for how to instructions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants