Skip to content

Conversation

@ilopezluna
Copy link
Contributor

@ilopezluna ilopezluna commented Sep 30, 2025

Adds support for packaging Safetensors models as OCI Artifacts. It supports multiple Safetensors files (by naming convention).
Additional *.json files and merge.txt are included in a separate tar-packaged layer.
When the bundle is created, this layer is unpacked to restore the exact same folder structure.

Summary by Sourcery

Add comprehensive support for packaging and unpacking safetensors model artifacts alongside existing GGUF format.

New Features:

  • Enable CLI packaging of safetensors models via directory or single-file input with automatic config discovery and archiving.
  • Introduce internal safetensors package to build OCI artifacts from .safetensors files including shard auto-discovery and config archive layers.
  • Add builder.FromSafetensorsWithConfig and builder.WithConfigArchive to construct safetensors-based model artifacts.
  • Extend distribution types with MediaTypeSafetensors, MediaTypeVLLMConfigArchive, and FormatSafetensors.
  • Enhance bundle unpacker to detect model format and unpack safetensors files and config archives.

Enhancements:

  • Improve CLI usage messaging with examples for GGUF and safetensors workflows.
  • Automatically scan directories for .safetensors and config files, creating temporary tar archives for packaging.
  • Introduce helper functions detectModelFormat and hasLayerWithMediaType to streamline unpacking logic.

Tests:

  • Add unit tests for safetensors model creation, shard detection, and config metadata extraction.
  • Extend existing tests with SafetensorsPath and ConfigDir stubs in llmcpp backend tests.

Chores:

  • Update Makefile examples to include safetensors packaging command.

Copilot AI review requested due to automatic review settings September 30, 2025 14:56
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Sep 30, 2025

Reviewer's Guide

Introduce safetensors as a first-class OCI artifact by extending the CLI to package directory-based or file-based safetensors models with accompanying configs, adding builder and partial helpers for safetensors layers and config archives, augmenting distribution types and interfaces, updating unpack logic and bundle representation, and implementing a new safetensors package for model creation and metadata.

Sequence diagram for packaging a safetensors model with config archive

sequenceDiagram
    actor User
    participant CLI as "mdltool CLI"
    participant Safetensors as "safetensors package"
    participant Builder as "Builder"
    participant Registry as "Registry"
    User->>CLI: Run 'mdltool package ./model-dir --tag ...'
    CLI->>Safetensors: packageFromDirectory(model-dir)
    Safetensors->>CLI: Return safetensorsPaths, configArchive
    CLI->>Builder: FromSafetensorsWithConfig(safetensorsPaths, configArchive)
    Builder->>Registry: Build(ctx, target, os.Stdout)
    Registry-->>User: Model artifact pushed
Loading

Class diagram for updated Model interfaces and Bundle

classDiagram
    class Model {
        +ID() string
        +GGUFPaths() []string
        +SafetensorsPaths() []string
        +ConfigArchivePath() string
        +MMPROJPath() string
        +Config() Config
        +Tags() []string
        +ChatTemplatePath() string
    }
    class ModelBundle {
        +RootDir() string
        +GGUFPath() string
        +SafetensorsPath() string
        +ConfigDir() string
        +ChatTemplatePath() string
        +MMPROJPath() string
        +RuntimeConfig() Config
    }
    class Bundle {
        -dir string
        -mmprojPath string
        -ggufFile string
        -safetensorsFile string
        -configDir string
        -runtimeConfig Config
        -chatTemplatePath string
        +SafetensorsPath() string
        +ConfigDir() string
        +RuntimeConfig() Config
    }
    Model <|.. ModelBundle
    ModelBundle <|.. Bundle
Loading

File-Level Changes

Change Details Files
Extend CLI (cmdPackage) to detect and package safetensors models
  • Updated usage text and examples to accept file or directory paths
  • Validated source argument existence and type (file vs directory)
  • Scanned directories for .safetensors files and config files then created a temporary tar archive
  • Introduced isSafetensors flag and branched builder initialization accordingly
cmd/mdltool/main.go
Enhance builder and partial helpers to support safetensors and config archives
  • Added builder.FromSafetensorsWithConfig and builder.WithConfigArchive methods
  • Extended partial helper with SafetensorsPaths and ConfigArchivePath functions
  • Integrated config archive layer into model mutation chain
pkg/distribution/builder/builder.go
pkg/distribution/internal/partial/partial.go
Update distribution types and interfaces for safetensors and config archives
  • Defined MediaTypeSafetensors and MediaTypeVLLMConfigArchive and FormatSafetensors constants
  • Expanded Config struct with Safetensors metadata
  • Extended Model and ModelArtifact interfaces and implemented in internal store
  • Updated Makefile examples for directory-based packaging
pkg/distribution/types/config.go
pkg/distribution/types/model.go
pkg/distribution/internal/store/model.go
Makefile
Extend unpack and bundle logic to handle safetensors artifacts
  • Added detectModelFormat switch to choose between GGUF or safetensors unpack paths
  • Implemented unpackSafetensors and unpackConfigArchive with safe tar extraction
  • Updated Bundle struct to track safetensorsFile and configDir with accessors
pkg/distribution/internal/bundle/unpack.go
pkg/distribution/internal/bundle/bundle.go
Introduce internal safetensors package for model creation and metadata
  • Implemented NewModel and NewModelWithConfigArchive with shard auto-discovery and metadata extraction
  • Generated layers with appropriate media types and appended config archive
  • Provided Model artifact methods (Layers, Manifest, etc.)
  • Added unit tests for safetensors creation and config extraction
pkg/distribution/internal/safetensors/create.go
pkg/distribution/internal/safetensors/model.go
pkg/distribution/internal/safetensors/model_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
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

This PR introduces support for Safetensors model format as OCI artifacts, expanding the system beyond GGUF models. The implementation adds a parallel set of interfaces and handlers for Safetensors models while maintaining backward compatibility with existing GGUF functionality.

Key Changes:

  • Add Safetensors model format support with new media types and configuration
  • Implement auto-discovery of sharded Safetensors files and configuration archives
  • Extend CLI tooling to detect and package both GGUF and Safetensors models from directories

Reviewed Changes

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

Show a summary per file
File Description
pkg/distribution/types/model.go Add interface methods for Safetensors paths and config archive handling
pkg/distribution/types/config.go Define new media types and format constants for Safetensors
pkg/distribution/internal/safetensors/model.go Core Safetensors model implementation with OCI artifact interface
pkg/distribution/internal/safetensors/create.go Model creation logic with shard discovery and config extraction
pkg/distribution/internal/bundle/unpack.go Enhanced unpacking logic to handle both GGUF and Safetensors formats
cmd/mdltool/main.go CLI enhancements for directory-based packaging and format detection
pkg/distribution/builder/builder.go Builder pattern extension for Safetensors models with config archives

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@ilopezluna ilopezluna requested a review from Copilot October 1, 2025 13:09
Copy link
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

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copilot AI review requested due to automatic review settings October 1, 2025 13:58
Copy link
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

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copilot AI review requested due to automatic review settings October 1, 2025 14:25
Copy link
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

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

pkg/distribution/types/model.go:1

  • Adding required methods (SafetensorsPaths, ConfigArchivePath, SafetensorsPath, ConfigDir) to existing interfaces is a breaking change for all external implementations. Consider introducing a new extended interface (e.g., ModelWithSafetensors) or providing adapter shims to preserve backward compatibility.
package types

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copilot AI review requested due to automatic review settings October 1, 2025 14:56
Copy link
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

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copilot AI review requested due to automatic review settings October 1, 2025 15:27
Copy link
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

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

pkg/distribution/internal/bundle/unpack.go:1

  • Files from the config archive can overwrite previously unpacked model assets if the archive includes conflicting filenames (e.g., model.safetensors, model.gguf). Consider rejecting or renaming entries when a destination file already exists (using os.Lstat + fail) to prevent malicious or accidental overwrite of critical bundle contents.
package bundle

pkg/distribution/internal/bundle/unpack.go:1

  • Files from the config archive can overwrite previously unpacked model assets if the archive includes conflicting filenames (e.g., model.safetensors, model.gguf). Consider rejecting or renaming entries when a destination file already exists (using os.Lstat + fail) to prevent malicious or accidental overwrite of critical bundle contents.
package bundle

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

ilopezluna and others added 2 commits October 1, 2025 17:31
Copilot AI review requested due to automatic review settings October 1, 2025 15:33
Copy link
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

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +232 to +241
rel, err := filepath.Rel(absBaseDir, absTarget)
if err != nil {
return fmt.Errorf("compute relative path: %w", err)
}

// Use filepath.IsLocal() to check if the relative path is local (doesn't escape baseDir)
// This handles all edge cases including empty strings, ".", "..", symlinks, etc.
if !filepath.IsLocal(rel) {
return fmt.Errorf("invalid entry %q: path attempts to escape destination directory", targetPath)
}
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

The directory traversal check is ineffective: filepath.IsLocal(rel) returns true for paths beginning with "../", so entries like ../../etc/passwd would not be rejected. Replace the check with a robust containment test (e.g., ensure strings.HasPrefix(absTarget+string(os.PathSeparator), absBaseDir+string(os.PathSeparator)) or verify that rel does not start with ".."+path separator or equal "..") to properly prevent escaping the destination directory.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +90
// Add config archive layer
if configArchivePath != "" {
// Check if a config archive layer already exists
for _, layer := range model.layers {
mediaType, err := layer.MediaType()
if err == nil && mediaType == types.MediaTypeVLLMConfigArchive {
return nil, fmt.Errorf("model already has a config archive layer")
}
}
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Duplicate logic for detecting an existing config archive layer also appears in builder.WithConfigArchive; consider extracting a shared helper (e.g., hasConfigArchiveLayer(layers)) to reduce repetition and keep validation consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +94
// Check if config archive already exists
layers, err := b.model.Layers()
if err != nil {
return nil, fmt.Errorf("get model layers: %w", err)
}

for _, layer := range layers {
mediaType, err := layer.MediaType()
if err == nil && mediaType == types.MediaTypeVLLMConfigArchive {
return nil, fmt.Errorf("model already has a config archive layer")
}
}
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Logic duplicating the config archive presence check in safetensors.NewModelWithConfigArchive; factor into a shared utility to ensure consistent validation and simplify future changes.

Copilot uses AI. Check for mistakes.
@ilopezluna ilopezluna marked this pull request as ready for review October 1, 2025 15:39
@ilopezluna ilopezluna requested a review from a team October 1, 2025 15:39
@ilopezluna ilopezluna changed the title [WIP] Safetensors as OCI Artifact Safetensors as OCI Artifact Oct 1, 2025
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

We encountered an error and are unable to review this PR. We have been notified and are working to fix it.

You can try again by commenting this pull request with @sourcery-ai review, or contact us for help.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

We encountered an error and are unable to review this PR. We have been notified and are working to fix it.

You can try again by commenting this pull request with @sourcery-ai review, or contact us for help.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

We encountered an error and are unable to review this PR. We have been notified and are working to fix it.

You can try again by commenting this pull request with @sourcery-ai review, or contact us for help.

@ericcurtin ericcurtin merged commit de7e2d2 into main Oct 2, 2025
5 checks passed
@ericcurtin ericcurtin deleted the safetensors-as-oci-artifact branch October 2, 2025 08:33
Copy link
Contributor

@doringeman doringeman left a comment

Choose a reason for hiding this comment

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

LGTM!

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.

4 participants