123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- 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
- }
|