210 lines
5.4 KiB
Go
210 lines
5.4 KiB
Go
package build
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/ollama/ollama/x/build/internal/blobstore"
|
|
"github.com/ollama/ollama/x/model"
|
|
)
|
|
|
|
// Errors
|
|
var (
|
|
ErrIncompleteRef = errors.New("unqualified ref")
|
|
ErrBuildPresentInRef = errors.New("build present in ref")
|
|
ErrUnsupportedModelFormat = errors.New("unsupported model format")
|
|
ErrMissingFileType = errors.New("missing 'general.file_type' key")
|
|
ErrNotFound = errors.New("not found")
|
|
)
|
|
|
|
type mediaType string
|
|
|
|
// Known media types
|
|
const (
|
|
mediaTypeModel mediaType = "application/vnd.ollama.image.model"
|
|
)
|
|
|
|
type Server struct {
|
|
st *blobstore.Store
|
|
}
|
|
|
|
// Open starts a new build server that uses dir as the base directory for all
|
|
// build artifacts. If dir is empty, DefaultDir is used.
|
|
//
|
|
// It returns an error if the provided or default dir cannot be initialized.
|
|
func Open(dir string) (*Server, error) {
|
|
if dir == "" {
|
|
var err error
|
|
dir, err = DefaultDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
st, err := blobstore.Open(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Server{st: st}, nil
|
|
}
|
|
|
|
func (s *Server) Build(ref string, f model.File) error {
|
|
mp := model.ParseName(ref)
|
|
if !mp.CompleteWithoutBuild() {
|
|
return fmt.Errorf("%w: %q", ErrIncompleteRef, ref)
|
|
}
|
|
|
|
// 1. Resolve FROM
|
|
// a. If it's a local file (gguf), hash it and add it to the store.
|
|
// c. If it's a remote file (http), refuse.
|
|
// 2. Turn other pragmas into layers, and add them to the store.
|
|
// 3. Create a manifest from the layers.
|
|
// 4. Store the manifest in the manifest cache
|
|
// 5. Done.
|
|
|
|
if f.From == "" {
|
|
return &model.FileError{Pragma: "FROM", Message: "missing"}
|
|
}
|
|
|
|
var layers []layerJSON
|
|
|
|
id, info, size, err := s.importModel(f.From)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
layers = append(layers, layerJSON{
|
|
ID: id,
|
|
MediaType: mediaTypeModel,
|
|
Size: size,
|
|
})
|
|
|
|
id, size, err = blobstore.PutString(s.st, f.License)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
layers = append(layers, layerJSON{
|
|
ID: id,
|
|
MediaType: "text/plain",
|
|
Size: size,
|
|
})
|
|
|
|
data, err := json.Marshal(manifestJSON{Layers: layers})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.setManifestData(
|
|
mp.WithBuild(info.FileType.String()),
|
|
data,
|
|
)
|
|
}
|
|
|
|
func (s *Server) LayerFile(digest string) (string, error) {
|
|
fileName := s.st.OutputFilename(blobstore.ParseID(digest))
|
|
_, err := os.Stat(fileName)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return "", fmt.Errorf("%w: %q", ErrNotFound, digest)
|
|
}
|
|
return fileName, nil
|
|
}
|
|
|
|
func (s *Server) ManifestData(ref string) ([]byte, error) {
|
|
data, _, err := s.resolve(model.ParseName(ref))
|
|
return data, err
|
|
}
|
|
|
|
// WeightFile returns the absolute path to the weights file for the given model ref.
|
|
func (s *Server) WeightsFile(ref string) (string, error) {
|
|
m, err := s.getManifest(model.ParseName(ref))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, l := range m.Layers {
|
|
if l.MediaType == mediaTypeModel {
|
|
return s.st.OutputFilename(l.ID), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("missing weights layer for %q", ref)
|
|
}
|
|
|
|
// resolve returns the data for the given ref, if any.
|
|
//
|
|
// TODO: This should ideally return an ID, but the current on
|
|
// disk layout is that the actual manifest is stored in the "ref" instead of
|
|
// a pointer to a content-addressed blob. I (bmizerany) think we should
|
|
// change the on-disk layout to store the manifest in a content-addressed
|
|
// blob, and then have the ref point to that blob. This would simplify the
|
|
// code, allow us to have integrity checks on the manifest, and clean up
|
|
// this interface.
|
|
func (s *Server) resolve(ref model.Name) (data []byte, fileName string, err error) {
|
|
fileName, err = s.refFileName(ref)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
data, err = os.ReadFile(fileName)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, "", fmt.Errorf("%w: %q", ErrNotFound, ref)
|
|
}
|
|
if err != nil {
|
|
// do not wrap the error here, as it is likely an I/O error
|
|
// and we want to preserve the absraction since we may not
|
|
// be on disk later.
|
|
return nil, "", fmt.Errorf("manifest read error: %v", err)
|
|
}
|
|
return data, fileName, nil
|
|
}
|
|
|
|
func (s *Server) SetManifestData(ref string, data []byte) error {
|
|
return s.setManifestData(model.ParseName(ref), data)
|
|
}
|
|
|
|
// Set sets the data for the given ref.
|
|
func (s *Server) setManifestData(mp model.Name, data []byte) error {
|
|
path, err := s.refFileName(mp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(path, data, 0666); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) refFileName(mp model.Name) (string, error) {
|
|
if !mp.Complete() {
|
|
return "", fmt.Errorf("ref not fully qualified: %q", mp)
|
|
}
|
|
return filepath.Join(s.st.Dir(), "manifests", filepath.Join(mp.Parts()...)), nil
|
|
}
|
|
|
|
type manifestJSON struct {
|
|
// Layers is the list of layers in the manifest.
|
|
Layers []layerJSON `json:"layers"`
|
|
}
|
|
|
|
// Layer is a layer in a model manifest.
|
|
type layerJSON struct {
|
|
// ID is the ID of the layer.
|
|
ID blobstore.ID `json:"digest"`
|
|
MediaType mediaType `json:"mediaType"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func (s *Server) getManifest(ref model.Name) (manifestJSON, error) {
|
|
data, path, err := s.resolve(ref)
|
|
if err != nil {
|
|
return manifestJSON{}, err
|
|
}
|
|
var m manifestJSON
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return manifestJSON{}, &fs.PathError{Op: "unmarshal", Path: path, Err: err}
|
|
}
|
|
return m, nil
|
|
}
|