-
Notifications
You must be signed in to change notification settings - Fork 174
fix: generate Kitfile for ModelPack if missing from manifest #1142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Copyright 2026 The KitOps Authors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // you may not use this file except in compliance with the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // You may obtain a copy of the License at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Unless required by applicable law or agreed to in writing, software | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // See the License for the specific language governing permissions and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // limitations under the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package util | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/kitops-ml/kitops/pkg/artifact" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/kitops-ml/kitops/pkg/lib/constants/mediatype" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelspecv1 "github.com/modelpack/model-spec/specs-go/v1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GenerateKitfileForModelPack generates a minimal Kitfile for a ModelPack manifest. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This fallback is suitable for unpack and inspect workflows when no Kitfile is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // available in annotations. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func GenerateKitfileForModelPack(manifest *ocispec.Manifest) (*artifact.KitFile, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if format, err := mediatype.ModelFormatForManifest(manifest); err != nil || format != mediatype.ModelPackFormat { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("not a modelpack artifact") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kf := &artifact.KitFile{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Model: &artifact.Model{}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, desc := range manifest.Layers { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Mar 25, 2026
There was a problem hiding this comment.
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.
| 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
AI
Mar 25, 2026
There was a problem hiding this comment.
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.
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Copyright 2026 The KitOps Authors. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package util | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/kitops-ml/kitops/pkg/lib/constants/mediatype" | ||
| modelspecv1 "github.com/modelpack/model-spec/specs-go/v1" | ||
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| 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) | ||
| } | ||
|
Comment on lines
+28
to
+72
|
||
|
|
||
| func TestGenerateKitfileForModelPackMissingPathAnnotation(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(), | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| _, err := GenerateKitfileForModelPack(manifest) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), modelspecv1.AnnotationFilepath) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -84,16 +84,15 @@ func GetKitfileForManifest(ctx context.Context, store oras.ReadOnlyTarget, manif | |||||
| case mediatype.KitFormat: | ||||||
| return GetConfig(ctx, store, manifest.Config) | ||||||
| case mediatype.ModelPackFormat: | ||||||
| // TODO: can we (try to) generate a Kitfile from a ModelPack manifest? | ||||||
| if manifest.Annotations == nil || manifest.Annotations[constants.KitfileJsonAnnotation] == "" { | ||||||
| return nil, ErrNoKitfile | ||||||
| if manifest.Annotations != nil && manifest.Annotations[constants.KitfileJsonAnnotation] != "" { | ||||||
| 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) | ||||||
|
||||||
| return nil, fmt.Errorf("failed to parse config: %w", err) | |
| return nil, fmt.Errorf("failed to parse Kitfile JSON annotation: %w", err) |
There was a problem hiding this comment.
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.