diff --git a/server/layer.go b/manifest/layer.go similarity index 88% rename from server/layer.go rename to manifest/layer.go index 4baabe35c..82d44953a 100644 --- a/server/layer.go +++ b/manifest/layer.go @@ -1,4 +1,4 @@ -package server +package manifest import ( "crypto/sha256" @@ -14,7 +14,7 @@ type Layer struct { Size int64 `json:"size"` From string `json:"from,omitempty"` Name string `json:"name,omitempty"` // tensor name, e.g., "text_encoder/model.embed_tokens.weight" - status string + Status string `json:"-"` } const ( @@ -22,7 +22,7 @@ const ( ) func NewLayer(r io.Reader, mediatype string) (Layer, error) { - blobs, err := GetBlobsPath("") + blobs, err := BlobsPath("") if err != nil { return Layer{}, err } @@ -45,7 +45,7 @@ func NewLayer(r io.Reader, mediatype string) (Layer, error) { } digest := fmt.Sprintf("sha256:%x", sha256sum.Sum(nil)) - blob, err := GetBlobsPath(digest) + blob, err := BlobsPath(digest) if err != nil { return Layer{}, err } @@ -65,7 +65,7 @@ func NewLayer(r io.Reader, mediatype string) (Layer, error) { MediaType: mediatype, Digest: digest, Size: n, - status: fmt.Sprintf("%s %s", status, digest), + Status: fmt.Sprintf("%s %s", status, digest), }, nil } @@ -74,7 +74,7 @@ func NewLayerFromLayer(digest, mediatype, from string) (Layer, error) { return Layer{}, errors.New("creating new layer from layer with empty digest") } - blob, err := GetBlobsPath(digest) + blob, err := BlobsPath(digest) if err != nil { return Layer{}, err } @@ -89,7 +89,7 @@ func NewLayerFromLayer(digest, mediatype, from string) (Layer, error) { Digest: digest, Size: fi.Size(), From: from, - status: fmt.Sprintf("using existing layer %s", digest), + Status: fmt.Sprintf("using existing layer %s", digest), }, nil } @@ -98,7 +98,7 @@ func (l *Layer) Open() (io.ReadSeekCloser, error) { return nil, errors.New("opening layer with empty digest") } - blob, err := GetBlobsPath(l.Digest) + blob, err := BlobsPath(l.Digest) if err != nil { return nil, err } @@ -126,7 +126,7 @@ func (l *Layer) Remove() error { } } - blob, err := GetBlobsPath(l.Digest) + blob, err := BlobsPath(l.Digest) if err != nil { return err } diff --git a/server/manifest.go b/manifest/manifest.go similarity index 81% rename from server/manifest.go rename to manifest/manifest.go index da596f658..c0277e9a5 100644 --- a/server/manifest.go +++ b/manifest/manifest.go @@ -1,10 +1,9 @@ -package server +package manifest import ( "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "io" "log/slog" @@ -33,12 +32,38 @@ func (m *Manifest) Size() (size int64) { return } +func (m *Manifest) Digest() string { + return m.digest +} + +func (m *Manifest) FileInfo() os.FileInfo { + return m.fi +} + +// ReadConfigJSON reads and unmarshals a config layer as JSON. +func (m *Manifest) ReadConfigJSON(configPath string, v any) error { + for _, layer := range m.Layers { + if layer.MediaType == "application/vnd.ollama.image.json" && layer.Name == configPath { + blobPath, err := BlobsPath(layer.Digest) + if err != nil { + return err + } + data, err := os.ReadFile(blobPath) + if err != nil { + return err + } + return json.Unmarshal(data, v) + } + } + return fmt.Errorf("config %q not found in manifest", configPath) +} + func (m *Manifest) Remove() error { if err := os.Remove(m.filepath); err != nil { return err } - manifests, err := GetManifestPath() + manifests, err := Path() if err != nil { return err } @@ -70,11 +95,11 @@ func (m *Manifest) RemoveLayers() error { if _, used := inUse[layer.Digest]; used { continue } - blob, err := GetBlobsPath(layer.Digest) + blob, err := BlobsPath(layer.Digest) if err != nil { return err } - if err := os.Remove(blob); errors.Is(err, os.ErrNotExist) { + if err := os.Remove(blob); os.IsNotExist(err) { slog.Debug("layer does not exist", "digest", layer.Digest) } else if err != nil { return err @@ -89,7 +114,7 @@ func ParseNamedManifest(n model.Name) (*Manifest, error) { return nil, model.Unqualified(n) } - manifests, err := GetManifestPath() + manifests, err := Path() if err != nil { return nil, err } @@ -121,7 +146,7 @@ func ParseNamedManifest(n model.Name) (*Manifest, error) { } func WriteManifest(name model.Name, config Layer, layers []Layer) error { - manifests, err := GetManifestPath() + manifests, err := Path() if err != nil { return err } @@ -148,7 +173,7 @@ func WriteManifest(name model.Name, config Layer, layers []Layer) error { } func Manifests(continueOnError bool) (map[model.Name]*Manifest, error) { - manifests, err := GetManifestPath() + manifests, err := Path() if err != nil { return nil, err } diff --git a/server/manifest_test.go b/manifest/manifest_test.go similarity index 99% rename from server/manifest_test.go rename to manifest/manifest_test.go index d94deefb4..9eb83789e 100644 --- a/server/manifest_test.go +++ b/manifest/manifest_test.go @@ -1,4 +1,4 @@ -package server +package manifest import ( "encoding/json" diff --git a/manifest/paths.go b/manifest/paths.go new file mode 100644 index 000000000..4451c81aa --- /dev/null +++ b/manifest/paths.go @@ -0,0 +1,95 @@ +package manifest + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/ollama/ollama/envconfig" + "github.com/ollama/ollama/types/model" +) + +var ErrInvalidDigestFormat = errors.New("invalid digest format") + +func Path() (string, error) { + path := filepath.Join(envconfig.Models(), "manifests") + if err := os.MkdirAll(path, 0o755); err != nil { + return "", fmt.Errorf("%w: ensure path elements are traversable", err) + } + + return path, nil +} + +// PathForName returns the path to the manifest file for a specific model name. +func PathForName(n model.Name) (string, error) { + if !n.IsValid() { + return "", os.ErrNotExist + } + + manifests, err := Path() + if err != nil { + return "", err + } + + return filepath.Join(manifests, n.Filepath()), nil +} + +func BlobsPath(digest string) (string, error) { + // only accept actual sha256 digests + pattern := "^sha256[:-][0-9a-fA-F]{64}$" + re := regexp.MustCompile(pattern) + + if digest != "" && !re.MatchString(digest) { + return "", ErrInvalidDigestFormat + } + + digest = strings.ReplaceAll(digest, ":", "-") + path := filepath.Join(envconfig.Models(), "blobs", digest) + dirPath := filepath.Dir(path) + if digest == "" { + dirPath = path + } + + if err := os.MkdirAll(dirPath, 0o755); err != nil { + return "", fmt.Errorf("%w: ensure path elements are traversable", err) + } + + return path, nil +} + +// PruneDirectory removes empty directories recursively. +func PruneDirectory(path string) error { + info, err := os.Lstat(path) + if err != nil { + return err + } + + if info.IsDir() && info.Mode()&os.ModeSymlink == 0 { + entries, err := os.ReadDir(path) + if err != nil { + return err + } + + for _, entry := range entries { + if err := PruneDirectory(filepath.Join(path, entry.Name())); err != nil { + return err + } + } + + entries, err = os.ReadDir(path) + if err != nil { + return err + } + + if len(entries) > 0 { + return nil + } + + return os.Remove(path) + } + + return nil +} diff --git a/server/create.go b/server/create.go index 0944f7685..e3e87880a 100644 --- a/server/create.go +++ b/server/create.go @@ -28,6 +28,7 @@ import ( "github.com/ollama/ollama/format" ofs "github.com/ollama/ollama/fs" "github.com/ollama/ollama/fs/ggml" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/template" "github.com/ollama/ollama/types/errtypes" "github.com/ollama/ollama/types/model" @@ -90,7 +91,7 @@ func (s *Server) CreateHandler(c *gin.Context) { ch <- resp } - oldManifest, _ := ParseNamedManifest(name) + oldManifest, _ := manifest.ParseNamedManifest(name) var baseLayers []*layerGGML var err error @@ -123,9 +124,9 @@ func (s *Server) CreateHandler(c *gin.Context) { } if err == nil && !remote && (config.Renderer == "" || config.Parser == "" || config.Requires == "") { - manifest, mErr := ParseNamedManifest(fromName) - if mErr == nil && manifest.Config.Digest != "" { - configPath, pErr := GetBlobsPath(manifest.Config.Digest) + mf, mErr := manifest.ParseNamedManifest(fromName) + if mErr == nil && mf.Config.Digest != "" { + configPath, pErr := manifest.BlobsPath(mf.Config.Digest) if pErr == nil { if cfgFile, fErr := os.Open(configPath); fErr == nil { var baseConfig model.ConfigV2 @@ -342,7 +343,7 @@ func detectModelTypeFromFiles(files map[string]string) string { return "gguf" } else { // try to see if we can find a gguf file even without the file extension - blobPath, err := GetBlobsPath(files[fn]) + blobPath, err := manifest.BlobsPath(files[fn]) if err != nil { slog.Error("error getting blobs path", "file", fn) return "" @@ -394,7 +395,7 @@ func convertFromSafetensors(files map[string]string, baseLayers []*layerGGML, is return nil, fmt.Errorf("%w: %s: %s", errFilePath, err, fp) } - blobPath, err := GetBlobsPath(digest) + blobPath, err := manifest.BlobsPath(digest) if err != nil { return nil, err } @@ -432,7 +433,7 @@ func convertFromSafetensors(files map[string]string, baseLayers []*layerGGML, is return nil, err } - layer, err := NewLayer(t, mediaType) + layer, err := manifest.NewLayer(t, mediaType) if err != nil { return nil, err } @@ -465,7 +466,7 @@ func kvFromLayers(baseLayers []*layerGGML) (ofs.Config, error) { } func createModel(r api.CreateRequest, name model.Name, baseLayers []*layerGGML, config *model.ConfigV2, fn func(resp api.ProgressResponse)) (err error) { - var layers []Layer + var layers []manifest.Layer for _, layer := range baseLayers { if layer.GGML != nil { quantType := strings.ToUpper(cmp.Or(r.Quantize, r.Quantization)) @@ -550,13 +551,13 @@ func createModel(r api.CreateRequest, name model.Name, baseLayers []*layerGGML, } for _, layer := range layers { - if layer.status != "" { - fn(api.ProgressResponse{Status: layer.status}) + if layer.Status != "" { + fn(api.ProgressResponse{Status: layer.Status}) } } fn(api.ProgressResponse{Status: "writing manifest"}) - if err := WriteManifest(name, *configLayer, layers); err != nil { + if err := manifest.WriteManifest(name, *configLayer, layers); err != nil { return err } @@ -577,7 +578,7 @@ func quantizeLayer(layer *layerGGML, quantizeType string, fn func(resp api.Progr return nil, err } - blob, err := GetBlobsPath(layer.Digest) + blob, err := manifest.BlobsPath(layer.Digest) if err != nil { return nil, err } @@ -599,7 +600,7 @@ func quantizeLayer(layer *layerGGML, quantizeType string, fn func(resp api.Progr } temp.Seek(0, io.SeekStart) fn(api.ProgressResponse{Status: "verifying conversion"}) - newLayer, err := NewLayer(temp, layer.MediaType) + newLayer, err := manifest.NewLayer(temp, layer.MediaType) if err != nil { return nil, err } @@ -619,7 +620,7 @@ func ggufLayers(digest string, fn func(resp api.ProgressResponse)) ([]*layerGGML var layers []*layerGGML fn(api.ProgressResponse{Status: "parsing GGUF"}) - blobPath, err := GetBlobsPath(digest) + blobPath, err := manifest.BlobsPath(digest) if err != nil { return nil, err } @@ -654,7 +655,7 @@ func ggufLayers(digest string, fn func(resp api.ProgressResponse)) ([]*layerGGML mediatype = "application/vnd.ollama.image.projector" } - layer, err := NewLayerFromLayer(digest, mediatype, blob.Name()) + layer, err := manifest.NewLayerFromLayer(digest, mediatype, blob.Name()) if err != nil { slog.Debug("could not create new layer from layer", "error", err) return nil, err @@ -665,8 +666,8 @@ func ggufLayers(digest string, fn func(resp api.ProgressResponse)) ([]*layerGGML return detectChatTemplate(layers) } -func removeLayer(layers []Layer, mediatype string) []Layer { - return slices.DeleteFunc(layers, func(layer Layer) bool { +func removeLayer(layers []manifest.Layer, mediatype string) []manifest.Layer { + return slices.DeleteFunc(layers, func(layer manifest.Layer) bool { if layer.MediaType != mediatype { return false } @@ -680,7 +681,7 @@ func removeLayer(layers []Layer, mediatype string) []Layer { }) } -func setTemplate(layers []Layer, t string) ([]Layer, error) { +func setTemplate(layers []manifest.Layer, t string) ([]manifest.Layer, error) { layers = removeLayer(layers, "application/vnd.ollama.image.template") if _, err := template.Parse(t); err != nil { return nil, fmt.Errorf("%w: %s", errBadTemplate, err) @@ -690,7 +691,7 @@ func setTemplate(layers []Layer, t string) ([]Layer, error) { } blob := strings.NewReader(t) - layer, err := NewLayer(blob, "application/vnd.ollama.image.template") + layer, err := manifest.NewLayer(blob, "application/vnd.ollama.image.template") if err != nil { return nil, err } @@ -699,11 +700,11 @@ func setTemplate(layers []Layer, t string) ([]Layer, error) { return layers, nil } -func setSystem(layers []Layer, s string) ([]Layer, error) { +func setSystem(layers []manifest.Layer, s string) ([]manifest.Layer, error) { layers = removeLayer(layers, "application/vnd.ollama.image.system") if s != "" { blob := strings.NewReader(s) - layer, err := NewLayer(blob, "application/vnd.ollama.image.system") + layer, err := manifest.NewLayer(blob, "application/vnd.ollama.image.system") if err != nil { return nil, err } @@ -712,9 +713,9 @@ func setSystem(layers []Layer, s string) ([]Layer, error) { return layers, nil } -func setLicense(layers []Layer, l string) ([]Layer, error) { +func setLicense(layers []manifest.Layer, l string) ([]manifest.Layer, error) { blob := strings.NewReader(l) - layer, err := NewLayer(blob, "application/vnd.ollama.image.license") + layer, err := manifest.NewLayer(blob, "application/vnd.ollama.image.license") if err != nil { return nil, err } @@ -722,7 +723,7 @@ func setLicense(layers []Layer, l string) ([]Layer, error) { return layers, nil } -func setParameters(layers []Layer, p map[string]any) ([]Layer, error) { +func setParameters(layers []manifest.Layer, p map[string]any) ([]manifest.Layer, error) { if p == nil { p = make(map[string]any) } @@ -731,7 +732,7 @@ func setParameters(layers []Layer, p map[string]any) ([]Layer, error) { continue } - digestPath, err := GetBlobsPath(layer.Digest) + digestPath, err := manifest.BlobsPath(layer.Digest) if err != nil { return nil, err } @@ -765,7 +766,7 @@ func setParameters(layers []Layer, p map[string]any) ([]Layer, error) { if err := json.NewEncoder(&b).Encode(p); err != nil { return nil, err } - layer, err := NewLayer(&b, "application/vnd.ollama.image.params") + layer, err := manifest.NewLayer(&b, "application/vnd.ollama.image.params") if err != nil { return nil, err } @@ -773,7 +774,7 @@ func setParameters(layers []Layer, p map[string]any) ([]Layer, error) { return layers, nil } -func setMessages(layers []Layer, m []api.Message) ([]Layer, error) { +func setMessages(layers []manifest.Layer, m []api.Message) ([]manifest.Layer, error) { // this leaves the old messages intact if no new messages were specified // which may not be the correct behaviour if len(m) == 0 { @@ -786,7 +787,7 @@ func setMessages(layers []Layer, m []api.Message) ([]Layer, error) { if err := json.NewEncoder(&b).Encode(m); err != nil { return nil, err } - layer, err := NewLayer(&b, "application/vnd.ollama.image.messages") + layer, err := manifest.NewLayer(&b, "application/vnd.ollama.image.messages") if err != nil { return nil, err } @@ -794,7 +795,7 @@ func setMessages(layers []Layer, m []api.Message) ([]Layer, error) { return layers, nil } -func createConfigLayer(layers []Layer, config model.ConfigV2) (*Layer, error) { +func createConfigLayer(layers []manifest.Layer, config model.ConfigV2) (*manifest.Layer, error) { digests := make([]string, len(layers)) for i, layer := range layers { digests[i] = layer.Digest @@ -805,7 +806,7 @@ func createConfigLayer(layers []Layer, config model.ConfigV2) (*Layer, error) { if err := json.NewEncoder(&b).Encode(config); err != nil { return nil, err } - layer, err := NewLayer(&b, "application/vnd.docker.container.image.v1+json") + layer, err := manifest.NewLayer(&b, "application/vnd.docker.container.image.v1+json") if err != nil { return nil, err } diff --git a/server/create_test.go b/server/create_test.go index 061efb81a..0a9ac2d0a 100644 --- a/server/create_test.go +++ b/server/create_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ollama/ollama/api" + "github.com/ollama/ollama/manifest" ) func TestConvertFromSafetensors(t *testing.T) { @@ -17,7 +18,7 @@ func TestConvertFromSafetensors(t *testing.T) { // Helper function to create a new layer and return its digest makeTemp := func(content string) string { - l, err := NewLayer(strings.NewReader(content), "application/octet-stream") + l, err := manifest.NewLayer(strings.NewReader(content), "application/octet-stream") if err != nil { t.Fatalf("Failed to create layer: %v", err) } diff --git a/server/download.go b/server/download.go index 42d713c09..0019fa13a 100644 --- a/server/download.go +++ b/server/download.go @@ -24,6 +24,8 @@ import ( "github.com/ollama/ollama/api" "github.com/ollama/ollama/format" + "github.com/ollama/ollama/manifest" + "github.com/ollama/ollama/types/model" ) const maxRetries = 6 @@ -456,7 +458,7 @@ func (b *blobDownload) Wait(ctx context.Context, fn func(api.ProgressResponse)) } type downloadOpts struct { - mp ModelPath + n model.Name digest string regOpts *registryOptions fn func(api.ProgressResponse) @@ -465,10 +467,10 @@ type downloadOpts struct { // downloadBlob downloads a blob from the registry and stores it in the blobs directory func downloadBlob(ctx context.Context, opts downloadOpts) (cacheHit bool, _ error) { if opts.digest == "" { - return false, fmt.Errorf(("%s: %s"), opts.mp.GetNamespaceRepository(), "digest is empty") + return false, fmt.Errorf(("%s: %s"), opts.n.DisplayNamespaceModel(), "digest is empty") } - fp, err := GetBlobsPath(opts.digest) + fp, err := manifest.BlobsPath(opts.digest) if err != nil { return false, err } @@ -492,8 +494,8 @@ func downloadBlob(ctx context.Context, opts downloadOpts) (cacheHit bool, _ erro data, ok := blobDownloadManager.LoadOrStore(opts.digest, &blobDownload{Name: fp, Digest: opts.digest}) download := data.(*blobDownload) if !ok { - requestURL := opts.mp.BaseURL() - requestURL = requestURL.JoinPath("v2", opts.mp.GetNamespaceRepository(), "blobs", opts.digest) + requestURL := opts.n.BaseURL() + requestURL = requestURL.JoinPath("v2", opts.n.DisplayNamespaceModel(), "blobs", opts.digest) if err := download.Prepare(ctx, requestURL, opts.regOpts); err != nil { blobDownloadManager.Delete(opts.digest) return false, err diff --git a/server/images.go b/server/images.go index de795b20c..2955d26f7 100644 --- a/server/images.go +++ b/server/images.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -24,6 +23,7 @@ import ( "github.com/ollama/ollama/api" "github.com/ollama/ollama/envconfig" "github.com/ollama/ollama/fs/gguf" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/model/parsers" "github.com/ollama/ollama/parser" "github.com/ollama/ollama/template" @@ -274,44 +274,22 @@ func (m *Model) String() string { return modelfile.String() } -func GetManifest(mp ModelPath) (*Manifest, string, error) { - fp, err := mp.GetManifestPath() - if err != nil { - return nil, "", err - } - - f, err := os.Open(fp) - if err != nil { - return nil, "", err - } - defer f.Close() - - sha256sum := sha256.New() - - var manifest Manifest - if err := json.NewDecoder(io.TeeReader(f, sha256sum)).Decode(&manifest); err != nil { - return nil, "", err - } - - return &manifest, hex.EncodeToString(sha256sum.Sum(nil)), nil -} - func GetModel(name string) (*Model, error) { - mp := ParseModelPath(name) - manifest, digest, err := GetManifest(mp) + n := model.ParseName(name) + mf, err := manifest.ParseNamedManifest(n) if err != nil { return nil, err } - model := &Model{ - Name: mp.GetFullTagname(), - ShortName: mp.GetShortTagname(), - Digest: digest, + m := &Model{ + Name: n.String(), + ShortName: n.DisplayShortest(), + Digest: mf.Digest(), Template: template.DefaultTemplate, } - if manifest.Config.Digest != "" { - filename, err := GetBlobsPath(manifest.Config.Digest) + if mf.Config.Digest != "" { + filename, err := manifest.BlobsPath(mf.Config.Digest) if err != nil { return nil, err } @@ -322,29 +300,29 @@ func GetModel(name string) (*Model, error) { } defer configFile.Close() - if err := json.NewDecoder(configFile).Decode(&model.Config); err != nil { + if err := json.NewDecoder(configFile).Decode(&m.Config); err != nil { return nil, err } } - for _, layer := range manifest.Layers { - filename, err := GetBlobsPath(layer.Digest) + for _, layer := range mf.Layers { + filename, err := manifest.BlobsPath(layer.Digest) if err != nil { return nil, err } switch layer.MediaType { case "application/vnd.ollama.image.model": - model.ModelPath = filename - model.ParentModel = layer.From + m.ModelPath = filename + m.ParentModel = layer.From case "application/vnd.ollama.image.embed": // Deprecated in versions > 0.1.2 // TODO: remove this warning in a future version slog.Info("WARNING: model contains embeddings, but embeddings in modelfiles have been deprecated and will be ignored.") case "application/vnd.ollama.image.adapter": - model.AdapterPaths = append(model.AdapterPaths, filename) + m.AdapterPaths = append(m.AdapterPaths, filename) case "application/vnd.ollama.image.projector": - model.ProjectorPaths = append(model.ProjectorPaths, filename) + m.ProjectorPaths = append(m.ProjectorPaths, filename) case "application/vnd.ollama.image.prompt", "application/vnd.ollama.image.template": bts, err := os.ReadFile(filename) @@ -352,7 +330,7 @@ func GetModel(name string) (*Model, error) { return nil, err } - model.Template, err = template.Parse(string(bts)) + m.Template, err = template.Parse(string(bts)) if err != nil { return nil, err } @@ -362,7 +340,7 @@ func GetModel(name string) (*Model, error) { return nil, err } - model.System = string(bts) + m.System = string(bts) case "application/vnd.ollama.image.params": params, err := os.Open(filename) if err != nil { @@ -371,7 +349,7 @@ func GetModel(name string) (*Model, error) { defer params.Close() // parse model options parameters into a map so that we can see which fields have been specified explicitly - if err = json.NewDecoder(params).Decode(&model.Options); err != nil { + if err = json.NewDecoder(params).Decode(&m.Options); err != nil { return nil, err } case "application/vnd.ollama.image.messages": @@ -381,7 +359,7 @@ func GetModel(name string) (*Model, error) { } defer msgs.Close() - if err = json.NewDecoder(msgs).Decode(&model.Messages); err != nil { + if err = json.NewDecoder(msgs).Decode(&m.Messages); err != nil { return nil, err } case "application/vnd.ollama.image.license": @@ -389,11 +367,11 @@ func GetModel(name string) (*Model, error) { if err != nil { return nil, err } - model.License = append(model.License, string(bts)) + m.License = append(m.License, string(bts)) } } - return model, nil + return m, nil } func CopyModel(src, dst model.Name) error { @@ -408,7 +386,7 @@ func CopyModel(src, dst model.Name) error { return nil } - manifests, err := GetManifestPath() + manifests, err := manifest.Path() if err != nil { return err } @@ -437,7 +415,7 @@ func CopyModel(src, dst model.Name) error { func deleteUnusedLayers(deleteMap map[string]struct{}) error { // Ignore corrupt manifests to avoid blocking deletion of layers that are freshly orphaned - manifests, err := Manifests(true) + manifests, err := manifest.Manifests(true) if err != nil { return err } @@ -452,7 +430,7 @@ func deleteUnusedLayers(deleteMap map[string]struct{}) error { // only delete the files which are still in the deleteMap for k := range deleteMap { - fp, err := GetBlobsPath(k) + fp, err := manifest.BlobsPath(k) if err != nil { slog.Info(fmt.Sprintf("couldn't get file path for '%s': %v", k, err)) continue @@ -468,7 +446,7 @@ func deleteUnusedLayers(deleteMap map[string]struct{}) error { func PruneLayers() error { deleteMap := make(map[string]struct{}) - p, err := GetBlobsPath("") + p, err := manifest.BlobsPath("") if err != nil { return err } @@ -483,9 +461,9 @@ func PruneLayers() error { name := blob.Name() name = strings.ReplaceAll(name, "-", ":") - _, err := GetBlobsPath(name) + _, err := manifest.BlobsPath(name) if err != nil { - if errors.Is(err, ErrInvalidDigestFormat) { + if errors.Is(err, manifest.ErrInvalidDigestFormat) { // remove invalid blobs (e.g. partial downloads) if err := os.Remove(filepath.Join(p, blob.Name())); err != nil { slog.Error("couldn't remove blob", "blob", blob.Name(), "error", err) @@ -510,63 +488,30 @@ func PruneLayers() error { return nil } -func PruneDirectory(path string) error { - info, err := os.Lstat(path) - if err != nil { - return err - } - - if info.IsDir() && info.Mode()&os.ModeSymlink == 0 { - entries, err := os.ReadDir(path) - if err != nil { - return err - } - - for _, entry := range entries { - if err := PruneDirectory(filepath.Join(path, entry.Name())); err != nil { - return err - } - } - - entries, err = os.ReadDir(path) - if err != nil { - return err - } - - if len(entries) > 0 { - return nil - } - - return os.Remove(path) - } - - return nil -} - func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error { - mp := ParseModelPath(name) + n := model.ParseName(name) fn(api.ProgressResponse{Status: "retrieving manifest"}) - if mp.ProtocolScheme == "http" && !regOpts.Insecure { + if n.ProtocolScheme == "http" && !regOpts.Insecure { return errInsecureProtocol } - manifest, _, err := GetManifest(mp) + mf, err := manifest.ParseNamedManifest(n) if err != nil { fn(api.ProgressResponse{Status: "couldn't retrieve manifest"}) return err } - var layers []Layer - layers = append(layers, manifest.Layers...) - if manifest.Config.Digest != "" { - layers = append(layers, manifest.Config) + var layers []manifest.Layer + layers = append(layers, mf.Layers...) + if mf.Config.Digest != "" { + layers = append(layers, mf.Config) } // Use fast transfer for models with tensor layers (many small blobs) if hasTensorLayers(layers) { // Read raw manifest JSON to preserve tensor metadata fields - manifestPath, err := mp.GetManifestPath() + manifestPath, err := manifest.PathForName(n) if err != nil { return err } @@ -574,7 +519,7 @@ func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn fu if err != nil { return err } - if err := pushWithTransfer(ctx, mp, layers, manifestJSON, regOpts, fn); err != nil { + if err := pushWithTransfer(ctx, n, layers, manifestJSON, regOpts, fn); err != nil { return err } fn(api.ProgressResponse{Status: "success"}) @@ -582,17 +527,17 @@ func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn fu } for _, layer := range layers { - if err := uploadBlob(ctx, mp, layer, regOpts, fn); err != nil { + if err := uploadBlob(ctx, n, layer, regOpts, fn); err != nil { slog.Info(fmt.Sprintf("error uploading blob: %v", err)) return err } } fn(api.ProgressResponse{Status: "pushing manifest"}) - requestURL := mp.BaseURL() - requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag) + requestURL := n.BaseURL() + requestURL = requestURL.JoinPath("v2", n.DisplayNamespaceModel(), "manifests", n.Tag) - manifestJSON, err := json.Marshal(manifest) + manifestJSON, err := json.Marshal(mf) if err != nil { return err } @@ -611,44 +556,44 @@ func PushModel(ctx context.Context, name string, regOpts *registryOptions, fn fu } func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error { - mp := ParseModelPath(name) + n := model.ParseName(name) // build deleteMap to prune unused layers deleteMap := make(map[string]struct{}) - manifest, _, err := GetManifest(mp) + existingMf, err := manifest.ParseNamedManifest(n) if errors.Is(err, os.ErrNotExist) { // noop } else if err != nil { slog.Warn("pulling model with bad existing manifest", "name", name, "error", err) } else { - for _, l := range manifest.Layers { + for _, l := range existingMf.Layers { deleteMap[l.Digest] = struct{}{} } - if manifest.Config.Digest != "" { - deleteMap[manifest.Config.Digest] = struct{}{} + if existingMf.Config.Digest != "" { + deleteMap[existingMf.Config.Digest] = struct{}{} } } - if mp.ProtocolScheme == "http" && !regOpts.Insecure { + if n.ProtocolScheme == "http" && !regOpts.Insecure { return errInsecureProtocol } fn(api.ProgressResponse{Status: "pulling manifest"}) - manifest, err = pullModelManifest(ctx, mp, regOpts) + mf, err := pullModelManifest(ctx, n, regOpts) if err != nil { return fmt.Errorf("pull model manifest: %s", err) } - var layers []Layer - layers = append(layers, manifest.Layers...) - if manifest.Config.Digest != "" { - layers = append(layers, manifest.Config) + var layers []manifest.Layer + layers = append(layers, mf.Layers...) + if mf.Config.Digest != "" { + layers = append(layers, mf.Config) } // Use fast transfer for models with tensor layers (many small blobs) if hasTensorLayers(layers) { - if err := pullWithTransfer(ctx, mp, layers, manifest, regOpts, fn); err != nil { + if err := pullWithTransfer(ctx, n, layers, mf, regOpts, fn); err != nil { return err } fn(api.ProgressResponse{Status: "success"}) @@ -658,7 +603,7 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu skipVerify := make(map[string]bool) for _, layer := range layers { cacheHit, err := downloadBlob(ctx, downloadOpts{ - mp: mp, + n: n, digest: layer.Digest, regOpts: regOpts, fn: fn, @@ -677,7 +622,7 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu } if err := verifyBlob(layer.Digest); err != nil { if errors.Is(err, errDigestMismatch) { - fp, err := GetBlobsPath(layer.Digest) + fp, err := manifest.BlobsPath(layer.Digest) if err != nil { return err } @@ -692,16 +637,16 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu for _, layer := range layers { delete(deleteMap, layer.Digest) } - delete(deleteMap, manifest.Config.Digest) + delete(deleteMap, mf.Config.Digest) fn(api.ProgressResponse{Status: "writing manifest"}) - manifestJSON, err := json.Marshal(manifest) + manifestJSON, err := json.Marshal(mf) if err != nil { return err } - fp, err := mp.GetManifestPath() + fp, err := manifest.PathForName(n) if err != nil { return err } @@ -728,9 +673,9 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu } // hasTensorLayers checks if any layer has tensor media type. -func hasTensorLayers(layers []Layer) bool { +func hasTensorLayers(layers []manifest.Layer) bool { for _, layer := range layers { - if layer.MediaType == MediaTypeImageTensor { + if layer.MediaType == manifest.MediaTypeImageTensor { return true } } @@ -738,7 +683,7 @@ func hasTensorLayers(layers []Layer) bool { } // pullWithTransfer uses the simplified x/transfer package for downloading blobs. -func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifest *Manifest, regOpts *registryOptions, fn func(api.ProgressResponse)) error { +func pullWithTransfer(ctx context.Context, n model.Name, layers []manifest.Layer, mf *manifest.Manifest, regOpts *registryOptions, fn func(api.ProgressResponse)) error { blobs := make([]transfer.Blob, len(layers)) for i, layer := range layers { blobs[i] = transfer.Blob{ @@ -747,12 +692,12 @@ func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes } } - destDir, err := GetBlobsPath("") + destDir, err := manifest.BlobsPath("") if err != nil { return err } - base := mp.BaseURL() + base := n.BaseURL() if base.Scheme != "http" && regOpts != nil && regOpts.Insecure { base.Scheme = "http" } @@ -784,7 +729,7 @@ func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes Blobs: blobs, BaseURL: baseURL, DestDir: destDir, - Repository: mp.GetNamespaceRepository(), + Repository: n.DisplayNamespaceModel(), Progress: progress, Token: regOpts.Token, GetToken: getToken, @@ -795,12 +740,12 @@ func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes // Write manifest fn(api.ProgressResponse{Status: "writing manifest"}) - manifestJSON, err := json.Marshal(manifest) + manifestJSON, err := json.Marshal(mf) if err != nil { return err } - fp, err := mp.GetManifestPath() + fp, err := manifest.PathForName(n) if err != nil { return err } @@ -812,7 +757,7 @@ func pullWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes } // pushWithTransfer uses the simplified x/transfer package for uploading blobs and manifest. -func pushWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifestJSON []byte, regOpts *registryOptions, fn func(api.ProgressResponse)) error { +func pushWithTransfer(ctx context.Context, n model.Name, layers []manifest.Layer, manifestJSON []byte, regOpts *registryOptions, fn func(api.ProgressResponse)) error { blobs := make([]transfer.Blob, len(layers)) for i, layer := range layers { blobs[i] = transfer.Blob{ @@ -822,12 +767,12 @@ func pushWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes } } - srcDir, err := GetBlobsPath("") + srcDir, err := manifest.BlobsPath("") if err != nil { return err } - base := mp.BaseURL() + base := n.BaseURL() if base.Scheme != "http" && regOpts != nil && regOpts.Insecure { base.Scheme = "http" } @@ -864,13 +809,13 @@ func pushWithTransfer(ctx context.Context, mp ModelPath, layers []Layer, manifes GetToken: getToken, Logger: slog.Default(), Manifest: manifestJSON, - ManifestRef: mp.Tag, - Repository: mp.GetNamespaceRepository(), + ManifestRef: n.Tag, + Repository: n.DisplayNamespaceModel(), }) } -func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptions) (*Manifest, error) { - requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag) +func pullModelManifest(ctx context.Context, n model.Name, regOpts *registryOptions) (*manifest.Manifest, error) { + requestURL := n.BaseURL().JoinPath("v2", n.DisplayNamespaceModel(), "manifests", n.Tag) headers := make(http.Header) headers.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") @@ -880,7 +825,7 @@ func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptio } defer resp.Body.Close() - var m Manifest + var m manifest.Manifest if err := json.NewDecoder(resp.Body).Decode(&m); err != nil { return nil, err } @@ -1042,7 +987,7 @@ func parseRegistryChallenge(authStr string) registryChallenge { var errDigestMismatch = errors.New("digest mismatch, file must be downloaded again") func verifyBlob(digest string) error { - fp, err := GetBlobsPath(digest) + fp, err := manifest.BlobsPath(digest) if err != nil { return err } diff --git a/server/model.go b/server/model.go index 401547e4e..57190ffe0 100644 --- a/server/model.go +++ b/server/model.go @@ -13,6 +13,7 @@ import ( "github.com/ollama/ollama/api" "github.com/ollama/ollama/fs/ggml" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/template" "github.com/ollama/ollama/types/model" ) @@ -20,19 +21,19 @@ import ( var intermediateBlobs map[string]string = make(map[string]string) type layerGGML struct { - Layer + manifest.Layer *ggml.GGML } func parseFromModel(ctx context.Context, name model.Name, fn func(api.ProgressResponse)) (layers []*layerGGML, err error) { - m, err := ParseNamedManifest(name) + m, err := manifest.ParseNamedManifest(name) switch { case errors.Is(err, os.ErrNotExist): if err := PullModel(ctx, name.String(), ®istryOptions{}, fn); err != nil { return nil, err } - m, err = ParseNamedManifest(name) + m, err = manifest.ParseNamedManifest(name) if err != nil { return nil, err } @@ -41,7 +42,7 @@ func parseFromModel(ctx context.Context, name model.Name, fn func(api.ProgressRe } for _, layer := range m.Layers { - layer, err := NewLayerFromLayer(layer.Digest, layer.MediaType, name.DisplayShortest()) + layer, err := manifest.NewLayerFromLayer(layer.Digest, layer.MediaType, name.DisplayShortest()) if err != nil { return nil, err } @@ -50,7 +51,7 @@ func parseFromModel(ctx context.Context, name model.Name, fn func(api.ProgressRe case "application/vnd.ollama.image.model", "application/vnd.ollama.image.projector", "application/vnd.ollama.image.adapter": - blobpath, err := GetBlobsPath(layer.Digest) + blobpath, err := manifest.BlobsPath(layer.Digest) if err != nil { return nil, err } @@ -81,12 +82,12 @@ func detectChatTemplate(layers []*layerGGML) ([]*layerGGML, error) { if t, err := template.Named(s); err != nil { slog.Debug("template detection", "error", err, "template", s) } else { - layer, err := NewLayer(t.Reader(), "application/vnd.ollama.image.template") + layer, err := manifest.NewLayer(t.Reader(), "application/vnd.ollama.image.template") if err != nil { return nil, err } - layer.status = fmt.Sprintf("using autodetected template %s", t.Name) + layer.Status = fmt.Sprintf("using autodetected template %s", t.Name) layers = append(layers, &layerGGML{layer, nil}) if t.Parameters != nil { @@ -95,7 +96,7 @@ func detectChatTemplate(layers []*layerGGML) ([]*layerGGML, error) { return nil, err } - layer, err := NewLayer(&b, "application/vnd.ollama.image.params") + layer, err := manifest.NewLayer(&b, "application/vnd.ollama.image.params") if err != nil { return nil, err } diff --git a/server/modelpath.go b/server/modelpath.go deleted file mode 100644 index af82b8b3b..000000000 --- a/server/modelpath.go +++ /dev/null @@ -1,146 +0,0 @@ -package server - -import ( - "errors" - "fmt" - "io/fs" - "net/url" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/ollama/ollama/envconfig" - "github.com/ollama/ollama/types/model" -) - -type ModelPath struct { - ProtocolScheme string - Registry string - Namespace string - Repository string - Tag string -} - -const ( - DefaultRegistry = "registry.ollama.ai" - DefaultNamespace = "library" - DefaultTag = "latest" - DefaultProtocolScheme = "https" -) - -var ( - ErrInvalidImageFormat = errors.New("invalid image format") - ErrInvalidDigestFormat = errors.New("invalid digest format") - ErrInvalidProtocol = errors.New("invalid protocol scheme") - ErrInsecureProtocol = errors.New("insecure protocol http") - ErrModelPathInvalid = errors.New("invalid model path") -) - -func ParseModelPath(name string) ModelPath { - mp := ModelPath{ - ProtocolScheme: DefaultProtocolScheme, - Registry: DefaultRegistry, - Namespace: DefaultNamespace, - Repository: "", - Tag: DefaultTag, - } - - before, after, found := strings.Cut(name, "://") - if found { - mp.ProtocolScheme = before - name = after - } - - name = strings.ReplaceAll(name, string(os.PathSeparator), "/") - parts := strings.Split(name, "/") - switch len(parts) { - case 3: - mp.Registry = parts[0] - mp.Namespace = parts[1] - mp.Repository = parts[2] - case 2: - mp.Namespace = parts[0] - mp.Repository = parts[1] - case 1: - mp.Repository = parts[0] - } - - if repo, tag, found := strings.Cut(mp.Repository, ":"); found { - mp.Repository = repo - mp.Tag = tag - } - - return mp -} - -func (mp ModelPath) GetNamespaceRepository() string { - return fmt.Sprintf("%s/%s", mp.Namespace, mp.Repository) -} - -func (mp ModelPath) GetFullTagname() string { - return fmt.Sprintf("%s/%s/%s:%s", mp.Registry, mp.Namespace, mp.Repository, mp.Tag) -} - -func (mp ModelPath) GetShortTagname() string { - if mp.Registry == DefaultRegistry { - if mp.Namespace == DefaultNamespace { - return fmt.Sprintf("%s:%s", mp.Repository, mp.Tag) - } - return fmt.Sprintf("%s/%s:%s", mp.Namespace, mp.Repository, mp.Tag) - } - return fmt.Sprintf("%s/%s/%s:%s", mp.Registry, mp.Namespace, mp.Repository, mp.Tag) -} - -// GetManifestPath returns the path to the manifest file for the given model path, it is up to the caller to create the directory if it does not exist. -func (mp ModelPath) GetManifestPath() (string, error) { - name := model.Name{ - Host: mp.Registry, - Namespace: mp.Namespace, - Model: mp.Repository, - Tag: mp.Tag, - } - if !name.IsValid() { - return "", fs.ErrNotExist - } - return filepath.Join(envconfig.Models(), "manifests", name.Filepath()), nil -} - -func (mp ModelPath) BaseURL() *url.URL { - return &url.URL{ - Scheme: mp.ProtocolScheme, - Host: mp.Registry, - } -} - -func GetManifestPath() (string, error) { - path := filepath.Join(envconfig.Models(), "manifests") - if err := os.MkdirAll(path, 0o755); err != nil { - return "", fmt.Errorf("%w: ensure path elements are traversable", err) - } - - return path, nil -} - -func GetBlobsPath(digest string) (string, error) { - // only accept actual sha256 digests - pattern := "^sha256[:-][0-9a-fA-F]{64}$" - re := regexp.MustCompile(pattern) - - if digest != "" && !re.MatchString(digest) { - return "", ErrInvalidDigestFormat - } - - digest = strings.ReplaceAll(digest, ":", "-") - path := filepath.Join(envconfig.Models(), "blobs", digest) - dirPath := filepath.Dir(path) - if digest == "" { - dirPath = path - } - - if err := os.MkdirAll(dirPath, 0o755); err != nil { - return "", fmt.Errorf("%w: ensure path elements are traversable", err) - } - - return path, nil -} diff --git a/server/modelpath_test.go b/server/modelpath_test.go deleted file mode 100644 index 96429f958..000000000 --- a/server/modelpath_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package server - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetBlobsPath(t *testing.T) { - // GetBlobsPath expects an actual directory to exist - tempDir := t.TempDir() - - tests := []struct { - name string - digest string - expected string - err error - }{ - { - "empty digest", - "", - filepath.Join(tempDir, "blobs"), - nil, - }, - { - "valid with colon", - "sha256:456402914e838a953e0cf80caa6adbe75383d9e63584a964f504a7bbb8f7aad9", - filepath.Join(tempDir, "blobs", "sha256-456402914e838a953e0cf80caa6adbe75383d9e63584a964f504a7bbb8f7aad9"), - nil, - }, - { - "valid with dash", - "sha256-456402914e838a953e0cf80caa6adbe75383d9e63584a964f504a7bbb8f7aad9", - filepath.Join(tempDir, "blobs", "sha256-456402914e838a953e0cf80caa6adbe75383d9e63584a964f504a7bbb8f7aad9"), - nil, - }, - { - "digest too short", - "sha256-45640291", - "", - ErrInvalidDigestFormat, - }, - { - "digest too long", - "sha256-456402914e838a953e0cf80caa6adbe75383d9e63584a964f504a7bbb8f7aad9aaaaaaaaaa", - "", - ErrInvalidDigestFormat, - }, - { - "digest invalid chars", - "../sha256-456402914e838a953e0cf80caa6adbe75383d9e63584a964f504a7bbb8f7a", - "", - ErrInvalidDigestFormat, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Setenv("OLLAMA_MODELS", tempDir) - - got, err := GetBlobsPath(tc.digest) - - require.ErrorIs(t, tc.err, err, tc.name) - assert.Equal(t, tc.expected, got, tc.name) - }) - } -} - -func TestParseModelPath(t *testing.T) { - tests := []struct { - name string - arg string - want ModelPath - }{ - { - "full path https", - "https://example.com/ns/repo:tag", - ModelPath{ - ProtocolScheme: "https", - Registry: "example.com", - Namespace: "ns", - Repository: "repo", - Tag: "tag", - }, - }, - { - "full path http", - "http://example.com/ns/repo:tag", - ModelPath{ - ProtocolScheme: "http", - Registry: "example.com", - Namespace: "ns", - Repository: "repo", - Tag: "tag", - }, - }, - { - "no protocol", - "example.com/ns/repo:tag", - ModelPath{ - ProtocolScheme: "https", - Registry: "example.com", - Namespace: "ns", - Repository: "repo", - Tag: "tag", - }, - }, - { - "no registry", - "ns/repo:tag", - ModelPath{ - ProtocolScheme: "https", - Registry: DefaultRegistry, - Namespace: "ns", - Repository: "repo", - Tag: "tag", - }, - }, - { - "no namespace", - "repo:tag", - ModelPath{ - ProtocolScheme: "https", - Registry: DefaultRegistry, - Namespace: DefaultNamespace, - Repository: "repo", - Tag: "tag", - }, - }, - { - "no tag", - "repo", - ModelPath{ - ProtocolScheme: "https", - Registry: DefaultRegistry, - Namespace: DefaultNamespace, - Repository: "repo", - Tag: DefaultTag, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := ParseModelPath(tc.arg) - - if got != tc.want { - t.Errorf("got: %q want: %q", got, tc.want) - } - }) - } -} diff --git a/server/routes.go b/server/routes.go index f38259b5a..2ecf64869 100644 --- a/server/routes.go +++ b/server/routes.go @@ -39,6 +39,7 @@ import ( "github.com/ollama/ollama/fs/ggml" "github.com/ollama/ollama/llm" "github.com/ollama/ollama/logutil" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/middleware" "github.com/ollama/ollama/model/parsers" "github.com/ollama/ollama/model/renderers" @@ -974,7 +975,7 @@ func (s *Server) PushHandler(c *gin.Context) { // is. func getExistingName(n model.Name) (model.Name, error) { var zero model.Name - existing, err := Manifests(true) + existing, err := manifest.Manifests(true) if err != nil { return zero, err } @@ -1018,7 +1019,7 @@ func (s *Server) DeleteHandler(c *gin.Context) { return } - m, err := ParseNamedManifest(n) + m, err := manifest.ParseNamedManifest(n) if err != nil { switch { case os.IsNotExist(err): @@ -1080,7 +1081,7 @@ func (s *Server) ShowHandler(c *gin.Context) { func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { name := model.ParseName(req.Model) if !name.IsValid() { - return nil, ErrModelPathInvalid + return nil, model.Unqualified(name) } name, err := getExistingName(name) if err != nil { @@ -1112,7 +1113,7 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { // For safetensors LLM models (experimental), populate details from config.json if m.Config.ModelFormat == "safetensors" && slices.Contains(m.Config.Capabilities, "completion") { - if info, err := xserver.GetSafetensorsLLMInfo(name.String()); err == nil { + if info, err := xserver.GetSafetensorsLLMInfo(name); err == nil { if arch, ok := info["general.architecture"].(string); ok && arch != "" { modelDetails.Family = arch } @@ -1121,7 +1122,7 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { } } // Get torch_dtype directly from config.json for quantization level - if dtype, err := xserver.GetSafetensorsDtype(name.String()); err == nil && dtype != "" { + if dtype, err := xserver.GetSafetensorsDtype(name); err == nil && dtype != "" { modelDetails.QuantizationLevel = dtype } } @@ -1135,7 +1136,7 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { msgs[i] = api.Message{Role: msg.Role, Content: msg.Content} } - manifest, err := ParseNamedManifest(name) + mf, err := manifest.ParseNamedManifest(name) if err != nil { return nil, err } @@ -1147,7 +1148,7 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { Details: modelDetails, Messages: msgs, Capabilities: m.Capabilities(), - ModifiedAt: manifest.fi.ModTime(), + ModifiedAt: mf.FileInfo().ModTime(), Requires: m.Config.Requires, // Several integrations crash on a nil/omitempty+empty ModelInfo, so by // default we return an empty map. @@ -1214,7 +1215,7 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { if slices.Contains(m.Capabilities(), model.CapabilityImage) { // Populate tensor info if verbose if req.Verbose { - if tensors, err := xserver.GetSafetensorsTensorInfo(name.String()); err == nil { + if tensors, err := xserver.GetSafetensorsTensorInfo(name); err == nil { resp.Tensors = tensors } } @@ -1223,12 +1224,12 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) { // For safetensors LLM models (experimental), populate ModelInfo from config.json if m.Config.ModelFormat == "safetensors" && slices.Contains(m.Config.Capabilities, "completion") { - if info, err := xserver.GetSafetensorsLLMInfo(name.String()); err == nil { + if info, err := xserver.GetSafetensorsLLMInfo(name); err == nil { resp.ModelInfo = info } // Populate tensor info if verbose if req.Verbose { - if tensors, err := xserver.GetSafetensorsTensorInfo(name.String()); err == nil { + if tensors, err := xserver.GetSafetensorsTensorInfo(name); err == nil { resp.Tensors = tensors } } @@ -1285,7 +1286,7 @@ func getModelData(digest string, verbose bool) (ggml.KV, ggml.Tensors, error) { } func (s *Server) ListHandler(c *gin.Context) { - ms, err := Manifests(true) + ms, err := manifest.Manifests(true) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -1316,8 +1317,8 @@ func (s *Server) ListHandler(c *gin.Context) { RemoteModel: cf.RemoteModel, RemoteHost: cf.RemoteHost, Size: m.Size(), - Digest: m.digest, - ModifiedAt: m.fi.ModTime(), + Digest: m.Digest(), + ModifiedAt: m.FileInfo().ModTime(), Details: api.ModelDetails{ Format: cf.ModelFormat, Family: cf.ModelFamily, @@ -1376,7 +1377,7 @@ func (s *Server) CopyHandler(c *gin.Context) { } func (s *Server) HeadBlobHandler(c *gin.Context) { - path, err := GetBlobsPath(c.Param("digest")) + path, err := manifest.BlobsPath(c.Param("digest")) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -1392,7 +1393,7 @@ func (s *Server) HeadBlobHandler(c *gin.Context) { func (s *Server) CreateBlobHandler(c *gin.Context) { if ib, ok := intermediateBlobs[c.Param("digest")]; ok { - p, err := GetBlobsPath(ib) + p, err := manifest.BlobsPath(ib) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -1410,7 +1411,7 @@ func (s *Server) CreateBlobHandler(c *gin.Context) { } } - path, err := GetBlobsPath(c.Param("digest")) + path, err := manifest.BlobsPath(c.Param("digest")) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -1428,7 +1429,7 @@ func (s *Server) CreateBlobHandler(c *gin.Context) { return } - layer, err := NewLayer(c.Request.Body, "") + layer, err := manifest.NewLayer(c.Request.Body, "") if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -1628,7 +1629,7 @@ func Serve(ln net.Listener) error { slog.SetDefault(logutil.NewLogger(os.Stderr, envconfig.LogLevel())) slog.Info("server config", "env", envconfig.Values()) - blobsDir, err := GetBlobsPath("") + blobsDir, err := manifest.BlobsPath("") if err != nil { return err } @@ -1637,7 +1638,7 @@ func Serve(ln net.Listener) error { } if !envconfig.NoPrune() { - if _, err := Manifests(false); err != nil { + if _, err := manifest.Manifests(false); err != nil { slog.Warn("corrupt manifests detected, skipping prune operation. Re-pull or delete to clear", "error", err) } else { // clean up unused layers and manifests @@ -1645,12 +1646,12 @@ func Serve(ln net.Listener) error { return err } - manifestsPath, err := GetManifestPath() + manifestsPath, err := manifest.Path() if err != nil { return err } - if err := PruneDirectory(manifestsPath); err != nil { + if err := manifest.PruneDirectory(manifestsPath); err != nil { return err } } diff --git a/server/routes_create_test.go b/server/routes_create_test.go index 3d2ac3b5d..bfdf5f058 100644 --- a/server/routes_create_test.go +++ b/server/routes_create_test.go @@ -25,6 +25,7 @@ import ( "github.com/ollama/ollama/convert" "github.com/ollama/ollama/envconfig" "github.com/ollama/ollama/fs/ggml" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/types/model" ) @@ -223,15 +224,15 @@ func TestCreateFromModelInheritsRendererParser(t *testing.T) { t.Fatalf("expected status code 200, actual %d", w.Code) } - manifest, err := ParseNamedManifest(model.ParseName("child")) + mf, err := manifest.ParseNamedManifest(model.ParseName("child")) if err != nil { t.Fatalf("parse manifest: %v", err) } - if manifest.Config.Digest == "" { + if mf.Config.Digest == "" { t.Fatalf("unexpected empty config digest for child manifest") } - configPath, err := GetBlobsPath(manifest.Config.Digest) + configPath, err := manifest.BlobsPath(mf.Config.Digest) if err != nil { t.Fatalf("config blob path: %v", err) } diff --git a/server/routes_delete_test.go b/server/routes_delete_test.go index eb8c44320..a1a5f5424 100644 --- a/server/routes_delete_test.go +++ b/server/routes_delete_test.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/ollama/ollama/api" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/types/model" ) @@ -93,13 +94,13 @@ func TestDeleteDuplicateLayers(t *testing.T) { t.Fatal(err) } - config, err := NewLayer(&b, "application/vnd.docker.container.image.v1+json") + config, err := manifest.NewLayer(&b, "application/vnd.docker.container.image.v1+json") if err != nil { t.Fatal(err) } // create a manifest with duplicate layers - if err := WriteManifest(n, config, []Layer{config}); err != nil { + if err := manifest.WriteManifest(n, config, []manifest.Layer{config}); err != nil { t.Fatal(err) } diff --git a/server/upload.go b/server/upload.go index 2bd408d3f..35a32c679 100644 --- a/server/upload.go +++ b/server/upload.go @@ -21,12 +21,14 @@ import ( "github.com/ollama/ollama/api" "github.com/ollama/ollama/format" + "github.com/ollama/ollama/manifest" + "github.com/ollama/ollama/types/model" ) var blobUploadManager sync.Map type blobUpload struct { - Layer + manifest.Layer Total int64 Completed atomic.Int64 @@ -51,7 +53,7 @@ const ( ) func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *registryOptions) error { - p, err := GetBlobsPath(b.Digest) + p, err := manifest.BlobsPath(b.Digest) if err != nil { return err } @@ -59,7 +61,7 @@ func (b *blobUpload) Prepare(ctx context.Context, requestURL *url.URL, opts *reg if b.From != "" { values := requestURL.Query() values.Add("mount", b.Digest) - values.Add("from", ParseModelPath(b.From).GetNamespaceRepository()) + values.Add("from", model.ParseName(b.From).DisplayNamespaceModel()) requestURL.RawQuery = values.Encode() } @@ -128,7 +130,7 @@ func (b *blobUpload) Run(ctx context.Context, opts *registryOptions) { defer blobUploadManager.Delete(b.Digest) ctx, b.CancelFunc = context.WithCancel(ctx) - p, err := GetBlobsPath(b.Digest) + p, err := manifest.BlobsPath(b.Digest) if err != nil { b.err = err return @@ -364,9 +366,9 @@ func (p *progressWriter) Rollback() { p.written = 0 } -func uploadBlob(ctx context.Context, mp ModelPath, layer Layer, opts *registryOptions, fn func(api.ProgressResponse)) error { - requestURL := mp.BaseURL() - requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs", layer.Digest) +func uploadBlob(ctx context.Context, n model.Name, layer manifest.Layer, opts *registryOptions, fn func(api.ProgressResponse)) error { + requestURL := n.BaseURL() + requestURL = requestURL.JoinPath("v2", n.DisplayNamespaceModel(), "blobs", layer.Digest) resp, err := makeRequestWithRetry(ctx, http.MethodHead, requestURL, nil, nil, opts) switch { @@ -388,8 +390,8 @@ func uploadBlob(ctx context.Context, mp ModelPath, layer Layer, opts *registryOp data, ok := blobUploadManager.LoadOrStore(layer.Digest, &blobUpload{Layer: layer}) upload := data.(*blobUpload) if !ok { - requestURL := mp.BaseURL() - requestURL = requestURL.JoinPath("v2", mp.GetNamespaceRepository(), "blobs/uploads/") + requestURL := n.BaseURL() + requestURL = requestURL.JoinPath("v2", n.DisplayNamespaceModel(), "blobs/uploads/") if err := upload.Prepare(ctx, requestURL, opts); err != nil { blobUploadManager.Delete(layer.Digest) return err diff --git a/types/model/name.go b/types/model/name.go index a46f3e28d..311326d4c 100644 --- a/types/model/name.go +++ b/types/model/name.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log/slog" + "net/url" "path/filepath" "strings" ) @@ -35,22 +36,25 @@ func Unqualified(n Name) error { const MissingPart = "!MISSING!" const ( - defaultHost = "registry.ollama.ai" - defaultNamespace = "library" - defaultTag = "latest" + defaultHost = "registry.ollama.ai" + defaultNamespace = "library" + defaultTag = "latest" + defaultProtocolScheme = "https" ) // DefaultName returns a name with the default values for the host, namespace, -// and tag parts. The model and digest parts are empty. +// tag, and protocol scheme parts. The model and digest parts are empty. // // - The default host is ("registry.ollama.ai") // - The default namespace is ("library") // - The default tag is ("latest") +// - The default protocol scheme is ("https") func DefaultName() Name { return Name{ - Host: defaultHost, - Namespace: defaultNamespace, - Tag: defaultTag, + Host: defaultHost, + Namespace: defaultNamespace, + Tag: defaultTag, + ProtocolScheme: defaultProtocolScheme, } } @@ -87,10 +91,11 @@ func (k partKind) String() string { // It is not guaranteed to be valid. Use [Name.IsValid] to check if the name // is valid. type Name struct { - Host string - Namespace string - Model string - Tag string + Host string + Namespace string + Model string + Tag string + ProtocolScheme string } // ParseName parses and assembles a Name from a name string. The @@ -160,7 +165,9 @@ func ParseNameBare(s string) Name { } scheme, host, ok := strings.Cut(s, "://") - if !ok { + if ok { + n.ProtocolScheme = scheme + } else { host = scheme } n.Host = host @@ -189,12 +196,13 @@ func ParseNameFromFilepath(s string) (n Name) { return n } -// Merge merges the host, namespace, and tag parts of the two names, +// Merge merges the host, namespace, tag, and protocol scheme parts of the two names, // preferring the non-empty parts of a. func Merge(a, b Name) Name { a.Host = cmp.Or(a.Host, b.Host) a.Namespace = cmp.Or(a.Namespace, b.Namespace) a.Tag = cmp.Or(a.Tag, b.Tag) + a.ProtocolScheme = cmp.Or(a.ProtocolScheme, b.ProtocolScheme) return a } @@ -305,6 +313,23 @@ func (n Name) EqualFold(o Name) bool { strings.EqualFold(n.Tag, o.Tag) } +// BaseURL returns the base URL for the registry. +func (n Name) BaseURL() *url.URL { + return &url.URL{ + Scheme: n.ProtocolScheme, + Host: n.Host, + } +} + +// DisplayNamespaceModel returns the namespace and model joined by "/". +func (n Name) DisplayNamespaceModel() string { + var b strings.Builder + b.WriteString(n.Namespace) + b.WriteByte('/') + b.WriteString(n.Model) + return b.String() +} + func isValidLen(kind partKind, s string) bool { switch kind { case kindHost: diff --git a/types/model/name_test.go b/types/model/name_test.go index 794d14d79..056903728 100644 --- a/types/model/name_test.go +++ b/types/model/name_test.go @@ -32,10 +32,11 @@ func TestParseNameParts(t *testing.T) { { in: "scheme://host:port/namespace/model:tag", want: Name{ - Host: "host:port", - Namespace: "namespace", - Model: "model", - Tag: "tag", + Host: "host:port", + Namespace: "namespace", + Model: "model", + Tag: "tag", + ProtocolScheme: "scheme", }, wantFilepath: filepath.Join("host:port", "namespace", "model", "tag"), }, diff --git a/x/create/client/create.go b/x/create/client/create.go index 7729c6e5f..93ab481f2 100644 --- a/x/create/client/create.go +++ b/x/create/client/create.go @@ -12,8 +12,8 @@ import ( "fmt" "io" + "github.com/ollama/ollama/manifest" "github.com/ollama/ollama/progress" - "github.com/ollama/ollama/server" "github.com/ollama/ollama/types/model" "github.com/ollama/ollama/x/create" ) @@ -103,7 +103,7 @@ func CreateModel(opts CreateOptions, p *progress.Progress) error { // newLayerCreator returns a LayerCreator callback for creating config/JSON layers. func newLayerCreator() create.LayerCreator { return func(r io.Reader, mediaType, name string) (create.LayerInfo, error) { - layer, err := server.NewLayer(r, mediaType) + layer, err := manifest.NewLayer(r, mediaType) if err != nil { return create.LayerInfo{}, err } @@ -141,13 +141,13 @@ func createQuantizedLayers(r io.Reader, name, dtype string, shape []int32, quant } // Create layer for quantized weight - weightLayer, err := server.NewLayer(bytes.NewReader(qweightData), server.MediaTypeImageTensor) + weightLayer, err := manifest.NewLayer(bytes.NewReader(qweightData), manifest.MediaTypeImageTensor) if err != nil { return nil, err } // Create layer for scales - scalesLayer, err := server.NewLayer(bytes.NewReader(scalesData), server.MediaTypeImageTensor) + scalesLayer, err := manifest.NewLayer(bytes.NewReader(scalesData), manifest.MediaTypeImageTensor) if err != nil { return nil, err } @@ -169,7 +169,7 @@ func createQuantizedLayers(r io.Reader, name, dtype string, shape []int32, quant // Add qbiases layer if present (affine mode) if qbiasData != nil { - qbiasLayer, err := server.NewLayer(bytes.NewReader(qbiasData), server.MediaTypeImageTensor) + qbiasLayer, err := manifest.NewLayer(bytes.NewReader(qbiasData), manifest.MediaTypeImageTensor) if err != nil { return nil, err } @@ -186,7 +186,7 @@ func createQuantizedLayers(r io.Reader, name, dtype string, shape []int32, quant // createUnquantizedLayer creates a single tensor layer without quantization. func createUnquantizedLayer(r io.Reader, name string) ([]create.LayerInfo, error) { - layer, err := server.NewLayer(r, server.MediaTypeImageTensor) + layer, err := manifest.NewLayer(r, manifest.MediaTypeImageTensor) if err != nil { return nil, err } @@ -221,15 +221,15 @@ func newManifestWriter(opts CreateOptions, capabilities []string) create.Manifes } // Create config layer blob - configLayer, err := server.NewLayer(bytes.NewReader(configJSON), "application/vnd.docker.container.image.v1+json") + configLayer, err := manifest.NewLayer(bytes.NewReader(configJSON), "application/vnd.docker.container.image.v1+json") if err != nil { return fmt.Errorf("failed to create config layer: %w", err) } - // Convert LayerInfo to server.Layer - serverLayers := make([]server.Layer, 0, len(layers)) + // Convert LayerInfo to manifest.Layer + manifestLayers := make([]manifest.Layer, 0, len(layers)) for _, l := range layers { - serverLayers = append(serverLayers, server.Layer{ + manifestLayers = append(manifestLayers, manifest.Layer{ MediaType: l.MediaType, Digest: l.Digest, Size: l.Size, @@ -243,19 +243,19 @@ func newManifestWriter(opts CreateOptions, capabilities []string) create.Manifes if err != nil { return err } - serverLayers = append(serverLayers, modelfileLayers...) + manifestLayers = append(manifestLayers, modelfileLayers...) } - return server.WriteManifest(name, configLayer, serverLayers) + return manifest.WriteManifest(name, configLayer, manifestLayers) } } // createModelfileLayers creates layers for template, system, and license from Modelfile config. -func createModelfileLayers(mf *ModelfileConfig) ([]server.Layer, error) { - var layers []server.Layer +func createModelfileLayers(mf *ModelfileConfig) ([]manifest.Layer, error) { + var layers []manifest.Layer if mf.Template != "" { - layer, err := server.NewLayer(bytes.NewReader([]byte(mf.Template)), "application/vnd.ollama.image.template") + layer, err := manifest.NewLayer(bytes.NewReader([]byte(mf.Template)), "application/vnd.ollama.image.template") if err != nil { return nil, fmt.Errorf("failed to create template layer: %w", err) } @@ -263,7 +263,7 @@ func createModelfileLayers(mf *ModelfileConfig) ([]server.Layer, error) { } if mf.System != "" { - layer, err := server.NewLayer(bytes.NewReader([]byte(mf.System)), "application/vnd.ollama.image.system") + layer, err := manifest.NewLayer(bytes.NewReader([]byte(mf.System)), "application/vnd.ollama.image.system") if err != nil { return nil, fmt.Errorf("failed to create system layer: %w", err) } @@ -271,7 +271,7 @@ func createModelfileLayers(mf *ModelfileConfig) ([]server.Layer, error) { } if mf.License != "" { - layer, err := server.NewLayer(bytes.NewReader([]byte(mf.License)), "application/vnd.ollama.image.license") + layer, err := manifest.NewLayer(bytes.NewReader([]byte(mf.License)), "application/vnd.ollama.image.license") if err != nil { return nil, fmt.Errorf("failed to create license layer: %w", err) } diff --git a/x/server/show.go b/x/server/show.go index 8cadb2c62..dd95774d3 100644 --- a/x/server/show.go +++ b/x/server/show.go @@ -9,7 +9,8 @@ import ( "strings" "github.com/ollama/ollama/api" - "github.com/ollama/ollama/x/imagegen" + "github.com/ollama/ollama/manifest" + "github.com/ollama/ollama/types/model" ) // modelConfig represents the HuggingFace config.json structure @@ -35,22 +36,22 @@ type modelConfig struct { // GetSafetensorsLLMInfo extracts model information from safetensors LLM models. // It reads the config.json layer and returns a map compatible with GGML's KV format. -func GetSafetensorsLLMInfo(modelName string) (map[string]any, error) { - manifest, err := imagegen.LoadManifest(modelName) +func GetSafetensorsLLMInfo(name model.Name) (map[string]any, error) { + mf, err := manifest.ParseNamedManifest(name) if err != nil { return nil, fmt.Errorf("failed to load manifest: %w", err) } var config modelConfig - if err := manifest.ReadConfigJSON("config.json", &config); err != nil { + if err := mf.ReadConfigJSON("config.json", &config); err != nil { return nil, fmt.Errorf("failed to read config.json: %w", err) } // Calculate total tensor bytes from manifest layers var totalBytes int64 var tensorCount int64 - for _, layer := range manifest.Manifest.Layers { - if layer.MediaType == "application/vnd.ollama.image.tensor" { + for _, layer := range mf.Layers { + if layer.MediaType == manifest.MediaTypeImageTensor { totalBytes += layer.Size tensorCount++ } @@ -151,27 +152,30 @@ func buildModelInfo(config modelConfig, totalTensorBytes, tensorCount int64) map // GetSafetensorsTensorInfo extracts tensor information from safetensors model layers. // Each tensor is stored as a minimal safetensors file with an 88-byte header containing metadata. -func GetSafetensorsTensorInfo(modelName string) ([]api.Tensor, error) { - manifest, err := imagegen.LoadManifest(modelName) +func GetSafetensorsTensorInfo(name model.Name) ([]api.Tensor, error) { + mf, err := manifest.ParseNamedManifest(name) if err != nil { return nil, fmt.Errorf("failed to load manifest: %w", err) } - return getTensorInfoFromManifest(manifest) + return getTensorInfoFromManifest(mf) } // getTensorInfoFromManifest extracts tensor info from a manifest. // This is separated for testability. -func getTensorInfoFromManifest(manifest *imagegen.ModelManifest) ([]api.Tensor, error) { +func getTensorInfoFromManifest(mf *manifest.Manifest) ([]api.Tensor, error) { var tensors []api.Tensor - for _, layer := range manifest.Manifest.Layers { - if layer.MediaType != "application/vnd.ollama.image.tensor" { + for _, layer := range mf.Layers { + if layer.MediaType != manifest.MediaTypeImageTensor { continue } // Read the safetensors header from the blob - blobPath := manifest.BlobPath(layer.Digest) + blobPath, err := manifest.BlobsPath(layer.Digest) + if err != nil { + continue + } info, err := readSafetensorsHeader(blobPath) if err != nil { // Skip tensors we can't read @@ -197,15 +201,15 @@ func getTensorInfoFromManifest(manifest *imagegen.ModelManifest) ([]api.Tensor, // GetSafetensorsDtype returns the quantization type for a safetensors model. // If the model is quantized (has _scale tensors), returns the quantization type (e.g., "FP8"). // Otherwise returns the torch_dtype from config.json. -func GetSafetensorsDtype(modelName string) (string, error) { - manifest, err := imagegen.LoadManifest(modelName) +func GetSafetensorsDtype(name model.Name) (string, error) { + mf, err := manifest.ParseNamedManifest(name) if err != nil { return "", fmt.Errorf("failed to load manifest: %w", err) } // Check if model is quantized by looking for _scale tensors - for _, layer := range manifest.Manifest.Layers { - if layer.MediaType == "application/vnd.ollama.image.tensor" { + for _, layer := range mf.Layers { + if layer.MediaType == manifest.MediaTypeImageTensor { if strings.HasSuffix(layer.Name, "_scale") { // Model is quantized - return FP8 (affine quantization) return "FP8", nil @@ -217,7 +221,7 @@ func GetSafetensorsDtype(modelName string) (string, error) { var cfg struct { TorchDtype string `json:"torch_dtype"` } - if err := manifest.ReadConfigJSON("config.json", &cfg); err != nil { + if err := mf.ReadConfigJSON("config.json", &cfg); err != nil { return "", fmt.Errorf("failed to read config.json: %w", err) } diff --git a/x/server/show_test.go b/x/server/show_test.go index c510b0d54..be57758f8 100644 --- a/x/server/show_test.go +++ b/x/server/show_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/ollama/ollama/x/imagegen" + "github.com/ollama/ollama/manifest" ) func TestBuildModelInfo(t *testing.T) { @@ -451,8 +451,14 @@ func TestParseSafetensorsHeader_Errors(t *testing.T) { } func TestGetTensorInfoFromManifest(t *testing.T) { - // Create a temp directory for blobs + // Create a temp directory for blobs and set OLLAMA_MODELS tempDir := t.TempDir() + t.Setenv("OLLAMA_MODELS", tempDir) + + blobDir := filepath.Join(tempDir, "blobs") + if err := os.MkdirAll(blobDir, 0o755); err != nil { + t.Fatalf("failed to create blobs dir: %v", err) + } // Create test tensor blobs tensors := []struct { @@ -463,26 +469,26 @@ func TestGetTensorInfoFromManifest(t *testing.T) { }{ { name: "model.embed_tokens.weight", - digest: "sha256:abc123", + digest: "sha256:abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc0", dtype: "BF16", shape: []int64{262144, 2560}, }, { name: "model.layers.0.self_attn.q_proj.weight", - digest: "sha256:def456", + digest: "sha256:def456def456def456def456def456def456def456def456def456def456def0", dtype: "BF16", shape: []int64{2560, 2560}, }, { name: "model.norm.weight", - digest: "sha256:ghi789", + digest: "sha256:789789789789789789789789789789789789789789789789789789789789abc0", dtype: "F32", shape: []int64{2560}, }, } // Create blob files - var layers []imagegen.ManifestLayer + var layers []manifest.Layer for _, tensor := range tensors { // Create safetensors blob header := map[string]any{ @@ -498,15 +504,17 @@ func TestGetTensorInfoFromManifest(t *testing.T) { binary.Write(&buf, binary.LittleEndian, uint64(len(headerJSON))) buf.Write(headerJSON) - // Write blob file - blobName := "sha256-" + tensor.digest[7:] - blobPath := filepath.Join(tempDir, blobName) + // Write blob file using the digest format expected by GetBlobsPath + blobPath, err := manifest.BlobsPath(tensor.digest) + if err != nil { + t.Fatalf("failed to get blob path: %v", err) + } if err := os.WriteFile(blobPath, buf.Bytes(), 0o644); err != nil { t.Fatalf("failed to write blob: %v", err) } - layers = append(layers, imagegen.ManifestLayer{ - MediaType: "application/vnd.ollama.image.tensor", + layers = append(layers, manifest.Layer{ + MediaType: manifest.MediaTypeImageTensor, Digest: tensor.digest, Size: int64(buf.Len() + 1000), // header + fake data Name: tensor.name, @@ -514,21 +522,20 @@ func TestGetTensorInfoFromManifest(t *testing.T) { } // Add a non-tensor layer (should be skipped) - layers = append(layers, imagegen.ManifestLayer{ + layers = append(layers, manifest.Layer{ MediaType: "application/vnd.ollama.image.json", - Digest: "sha256:config", + Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000", Size: 100, Name: "config.json", }) - manifest := &imagegen.ModelManifest{ - Manifest: &imagegen.Manifest{ - Layers: layers, - }, - BlobDir: tempDir, + mf := &manifest.Manifest{ + SchemaVersion: 2, + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Layers: layers, } - result, err := getTensorInfoFromManifest(manifest) + result, err := getTensorInfoFromManifest(mf) if err != nil { t.Fatalf("getTensorInfoFromManifest() error = %v", err) }